How this blog works
Write markdown, get real URLs, working link previews, and a two-minute path to Substack. A working note to myself.
This post exists to document the thing it’s written in, and to show what the typography actually looks like. Delete it whenever.
Writing a post
Drop a file in src/content/posts/. The filename becomes the URL, so energy-transition.md
publishes at /posts/energy-transition/. Every post needs a few lines of frontmatter:
---
title: 'The title, shown on the page and in link previews'
description: 'One or two sentences. This is the grey text under the title in an X or LinkedIn preview.'
date: 2026-07-14
tags: ['optional']
---
Add draft: true and the post renders while you’re running the dev server but is excluded
from the published site, the RSS feed, and the sitemap. That’s how you park something
half-finished without it leaking.
The
descriptiondoes double duty — it’s the standfirst on the page and the preview text every social platform shows. Write it like ad copy, not like a summary.
Margin notes
Save a file as .mdx instead of .md and you can use <Side>…</Side> anywhere in the text
to push a note into the margin — for a citation, a caveat, an aside that would otherwise
break the paragraph’s spine. There’s one to the right of this sentence.
Plain .md files still work exactly as before; you only need .mdx if you want components.
Why the link previews work now
The old site was a single-page React app using HashRouter, which meant every article lived
at a URL like 0xjeffery.com/#/notes-energy. Crawlers throw away everything after the #,
so X and LinkedIn only ever saw the homepage — and since a single-page app ships one empty
HTML shell, there was nothing for them to read anyway. Every share looked identical.
Now each post is pre-rendered to its own HTML file with its own Open Graph tags baked in. There’s no JavaScript to execute, so the crawlers just read them. Three things follow:
- Real URLs.
/posts/how-this-works/— no hash, shareable, linkable, indexable. - A preview card per post, generated at build time from the title and description, in the same two typefaces as this page.
- An RSS feed at
/rss.xml, carrying the full text of every post.
Sharing
Every post ends with a row of buttons. x and linkedin open a pre-filled composer with the
link in it — the preview card is rendered by them, pulled from this page’s tags.
Two things learned the hard way. Previews cannot be tested locally: the crawler runs on
X’s servers, so localhost means its machine, not yours. It has to be a public URL. And
both platforms cache previews aggressively — publish, share, then fix a typo in the
title, and the stale card can persist for days.
Worth knowing separately: X’s algorithm demotes posts containing external links. That’s a reach problem, not a preview problem, and it applies to Substack links too.
Cross-posting to Substack
Substack has no public write API and no ongoing RSS sync, so there’s no way to make this fully automatic. There is, however, a good manual path, and it’s about two minutes:
- Hit
copy for substackat the bottom of the post. It lifts just the article body — no navigation, no footer, no share buttons — onto the clipboard as rich HTML, rewriting every link and image to an absolute URL so nothing breaks on their domain. - Paste into a new Substack draft. Headings, bold, italics, links, lists, and blockquotes all survive, because Substack’s editor accepts rich text.
- Re-enter the title and subtitle (their editor keeps those separate from the body), and publish.
Code blocks and footnotes are the two things that travel badly — Substack’s support for both is weak. For a prose post, the paste lands clean.
For the initial migration, don’t do this by hand. Substack’s importer takes an RSS feed
under Settings → Import, and /rss.xml carries the full body of every post, so the entire
back catalogue lands as drafts in one shot. It’s a one-time tool, not a sync — but you only
need it once.
The site is the canonical home; Substack is just the email pipe. Write once, here.