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 description does 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.

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:

  1. Hit copy for substack at 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.
  2. Paste into a new Substack draft. Headings, bold, italics, links, lists, and blockquotes all survive, because Substack’s editor accepts rich text.
  3. 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.

sharexlinkedin