Start here

Arrows to move, enter to open, escape to close.

Ship/Now

What is wired · 3 min

Blog

Posts are MDX files, not database rows, which means the blog stays up when the database does not.

content/blog/hello.mdx:

---
title: Why we rebuilt onboarding
description: It took four screens. Now it takes one.
date: 2026-09-01
tags: [product]
---

The first version asked for a company name before it asked for anything
useful.

That is a published post. No admin screen, no editor, no row to insert.

Why files

A blog backed by the database goes down when the database does. It also cannot be reviewed in a pull request, cannot be reverted with git revert, and cannot be edited by the AI tools you already have pointed at this repo.

The trade is that publishing means deploying. For a product blog that ships a post a week, that is not a real cost.

Reading them

import { allPosts, postBySlug } from "@/lib/blog";

const posts = await allPosts();          // newest first, sorted on date
const post = await postBySlug("hello");  // null if there is no such file

allPosts reads the directory once and caches. Files cannot change at runtime on a serverless host, so there is nothing to invalidate.

Rendering

The route compiles MDX at build time and maps the elements to your components, so a post inherits the site's typography instead of arriving with browser defaults.

export const dynamicParams = false;

export async function generateStaticParams() {
  return (await allPosts()).map((post) => ({ slug: post.slug }));
}

dynamicParams = false makes an unknown slug a 404 rather than an attempt to render a file that is not there.

Adding syntax highlighting

If your posts contain code, add Shiki as a rehype plugin and emit both themes as CSS variables, so highlighted code follows the light and dark switch without shipping a highlighter to the browser:

rehypePlugins: [[rehypeShiki, {
  themes: { light: "vitesse-light", dark: "vitesse-dark" },
  defaultColor: false,
}]]

That is exactly what these docs do.

The feed

/blog/feed.xml is generated from the same posts the pages render, so it cannot drift from them, and force-static means it is built once rather than on every request. The blog page advertises it in its metadata, so a reader that lands there finds it without being told.

Change NEXT_PUBLIC_SITE_URL and every link in the feed follows; RSS needs absolute URLs and a relative one silently breaks in half the readers.

Something wrong or missing on this page? Tell us.