Writing a blog post
All posts live in this directory as .mdx files. Drop a new file, commit, deploy — that's the whole flow. No CMS, no admin UI.
Quick start
-
Create the file —
content/blog/<slug>.mdxwhere<slug>is the URL path.- Lowercase letters, digits, hyphens only (
/^[a-z0-9][a-z0-9-]*$/). Anything else is silently rejected by the loader. - The URL becomes
https://siteaudit.lambdaflows.com/blog/<slug>.
- Lowercase letters, digits, hyphens only (
-
Add frontmatter (YAML between
---fences at the very top):--- title: "How We Cut LCP by 40% in One Afternoon" description: "Short SEO/OG summary — shown on the listing card, in search results, and as the <p> lead under the title. 140–160 chars is ideal." publishedAt: "2026-05-01" image: "/blog/how-we-cut-lcp/hero.png" authors: - name: "Sam Safaei" image: "/team/sam.png" category: title: "Performance" slug: "performance" ---All fields are validated at load time. Missing/malformed
authors→ empty list. Missing/malformedcategory→ falls back toUncategorized. Missing/invalidpublishedAt→ logs a warning and uses "now". -
Write the body in Markdown + JSX. Use
##and###for the on-page TOC — H2 and H3 are auto-extracted and shown in the sticky sidebar. H1 is reserved for the post title (rendered automatically from frontmatter). -
Ship it — commit and push. The detail page is statically generated on build (
generateStaticParams), and the sitemap picks up the new URL.
Images
Images live under public/blog/<slug>/ and are referenced with an absolute path starting with /blog/<slug>/...:
public/blog/
how-we-cut-lcp/
hero.png ← 1200×675 — powers OG, Twitter card, and the in-article hero
before.png
after.png
Then in MDX:

Or for Next.js optimization:
<Image
src="/blog/how-we-cut-lcp/before.png"
alt="Before"
width={1200}
height={630}
/>
Hero image convention: 1200×675 (16:9). Same file powers the OG card, the Twitter card, and the in-article hero above the body.
If you skip image: (or set it to ""), the detail page simply omits the hero block and the grid card falls back to a text-only layout. No broken images.
Categories
Pick a category.slug that's lowercase with hyphens. Any new slug automatically becomes a new filter chip on /blog and a new page at /blog/category/<slug>. Reuse existing slugs for consistency — currently in use:
announcements- (add yours here as you ship)
MDX tips
-
No
importstatements inside MDX. The runtime compile doesn't evaluate them. If you need a custom component, pass it via thecomponentsprop on<MDXContent />insrc/app/blog/[slug]/page.tsxfirst. -
Don't put text on its own line inside a
<p>tag — MDX will render it as a nested paragraph and trigger a hydration error. Keep content on the same line as the opening tag:<!-- BAD --> <p className="lead">See how we do it.</p> <!-- GOOD --> <p className="lead">See how we do it.</p> -
Custom JSX islands leak prose styles. Wrap CTA cards / callouts in
not-prose:<div className="not-prose bg-muted rounded-xl p-6">...</div> -
GFM is on — tables, strikethrough, task lists, autolinks all work.
-
Heading anchors — every
##/###gets anidviarehype-slug. The sidebar TOC usesgithub-sluggerunder the hood, so anchors match exactly.
Previewing locally
npm run dev
# http://localhost:3010/blog
Changes hot-reload. If you add a file and it doesn't appear, check:
- Filename ends in
.mdx(not.md.mdx, not uppercase). - Slug matches
/^[a-z0-9][a-z0-9-]*$/. - Frontmatter fences (
---) are on their own lines.
What gets generated
Per post you ship:
/blog/<slug>— the detail page (statically generated)- Appears on
/blog(paginated, newest-first) - Appears on
/blog/category/<category-slug> - Entry in
/sitemap.xml - OG + Twitter Card meta tags pointing at
image: - Canonical link →
https://siteaudit.lambdaflows.com/blog/<slug>
Architecture (in 3 lines)
- Content: this directory, plain
.mdxfiles. - Loader:
src/lib/blog/mdx.ts(filesystem +gray-matter, validated, React-cached). - Render:
src/app/blog/[slug]/page.tsxcompiles MDX at request time via@mdx-js/mdxwithremark-gfm+rehype-slug.
Full architectural doc: BLUEPRINT-BLOG.md at the repo root.