TransmissionArchive

Typesetting a blog nobody asked for

Why this site has no utility framework, how unlayered CSS beats @layer utilities without !important, and every component you can use in a post.

Every technical blog looks the same now, and the reason is not laziness. It is that the default path — a docs theme, a utility framework, a component library — produces a specific visual result, and that result has been the default for long enough that it reads as “generated” whether or not it was.

This post is the build log for going the other way, and it doubles as a reference for everything you can put in a post here.

The layering trick

nextra-theme-blog ships a compiled Tailwind v4 bundle. Every utility is prefixed x: and — this is the useful part — wrapped in @layer utilities:

nextra-theme-blog/dist/style.css
@layer theme, base, components, utilities; @layer utilities { .x\:prose { max-width: 65ch; /* … */ } .x\:container { width: 100%; /* … */ } }

CSS cascade layers sort below unlayered author styles. Not by specificity — by origin. An unlayered article { max-width: none } beats a layered .x\:prose.x\:container[data-x] every time, because layered styles lose to unlayered ones before specificity is even consulted.

So the entire design sits on top of the theme with no !important anywhere:

app/styles/nextra.css
/* Unlayered. Wins over @layer utilities regardless of specificity. */ article[data-pagefind-body] { max-width: none; padding: 0; }

The one thing to be deliberate about is where prose styling comes from. Rather than fight Tailwind Typography rule by rule, every custom subtree carries not-prose — which is the plugin’s own opt-out — and app/styles/prose.css is then the only file describing the reading experience.

Three voices, kept apart

The typography is doing one job: separating the machine from the human.

Type assignment

Display
Space Grotesk — headings, wordmark, blockquotes
Reading
Newsreader — body copy only
Apparatus
JetBrains Mono — labels, dates, tags, code, all chrome
Measure
68ch
Leading
1.68 body / 1.18 headings

A serif for body copy in a cyberpunk-adjacent design sounds wrong until you consider what the register actually is: this is a printed field manual that leaked, not a neon street scene. Dossiers are set in serif. The mono is the stamp on top.

Headings hang their numbers

Section numbers sit in the left margin above 68rem and fold inline below it, which is a counter() and a position: absolute — no JavaScript, no plugin.

app/styles/prose.css
.prose-body { counter-reset: h2; } .prose-body h2 { counter-increment: h2; position: relative; } .prose-body h2::before { content: '§' counter(h2); position: absolute; left: -4.25rem; color: var(--acid); }

Authoring surface

Everything below works in any .mdx file under app/blog/, with no imports.

Callouts

GitHub alert syntax is mapped onto Nextra’s Callout. One gotcha, and it cost me twenty minutes: the marker has to be its own paragraph, so you need the bare > line after it.

Correct
> [!NOTE] > > Body goes here.
Silently renders as a plain blockquote
> [!NOTE] > Body goes here.

Nextra’s withGitHubAlert matches /^\s*\[!(?<name>.*?)]\s*$/ against the first paragraph’s entire text content. Without the blank line the paragraph is [!NOTE]\nBody goes here., the $ never matches, and you get a blockquote with a visible [!NOTE] in it. No warning, no error.

Note

Five types are wired up: NOTE, TIP, IMPORTANT, WARNING, CAUTION.

Warning

Each gets a different accent on the left rule. Nothing gets a pastel pill.

Code blocks

Fenced blocks take filename, line highlights in {}, and word highlights in //:

dataframe/concat.go
func Concat(frames ...*DataFrame) (*DataFrame, error) { if len(frames) == 0 { return nil, ErrNoFrames } schema := frames[0].Schema() for _, f := range frames[1:] { if !schema.Equal(f.Schema()) { return nil, fmt.Errorf("concat: %w", ErrSchemaMismatch) } } return concatChecked(frames), nil }

Add showLineNumbers to the meta string when the line numbers are part of the point.

Tables

ApproachHit ratep99Notes
Raw SQL string11%4.2msFormatting kills it
Normalised AST60%4.4msParse cost is real but bounded
AST + param split71%4.4msWhere we stopped

Lists

Ordered lists use zero-padded mono counters:

  1. Parse the statement to an AST.
  2. Canonicalise predicate order and literal formatting.
  3. Hash the canonical form, not the text.
  4. Cache on that key.

Unordered lists get an acid marker:

  • The eviction policy mattered less than the key.
  • The key mattered less than agreeing on what “the same query” means.
  • Nobody agreed on that for three weeks.

Inline apparatus

Inline code is a bordered chip, not a colour change. Keyboard shortcuts render as ⌘ K. Footnotes work1. Some things are under NDA.

Blockquotes

Plain blockquotes are pulled into the display face, at size, with an acid rule:

The fastest way to make a system understandable is to delete something.

What this cost

Weight

Stylesheet
~1,300 lines, hand-written
Utility framework
None
Component library
None
Runtime JS for search
0 — Pagefind indexes at build
Client components
Two: the theme switch and the prev/next rail

Which is the actual argument. Not that hand-written CSS is faster — it is roughly a wash — but that when something looks wrong you can open one file and find the rule that did it.

Footnotes

  1. And land in their own hairline-ruled block at the end of the post. ↩

End of transmission