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

  1. Create the filecontent/blog/<slug>.mdx where <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>.
  2. 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/malformed category → falls back to Uncategorized. Missing/invalid publishedAt → logs a warning and uses "now".

  3. 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).

  4. 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:

![Before](/blog/how-we-cut-lcp/before.png)

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 import statements inside MDX. The runtime compile doesn't evaluate them. If you need a custom component, pass it via the components prop on <MDXContent /> in src/app/blog/[slug]/page.tsx first.

  • 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 an id via rehype-slug. The sidebar TOC uses github-slugger under 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:

  1. Filename ends in .mdx (not .md.mdx, not uppercase).
  2. Slug matches /^[a-z0-9][a-z0-9-]*$/.
  3. 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 .mdx files.
  • Loader: src/lib/blog/mdx.ts (filesystem + gray-matter, validated, React-cached).
  • Render: src/app/blog/[slug]/page.tsx compiles MDX at request time via @mdx-js/mdx with remark-gfm + rehype-slug.

Full architectural doc: BLUEPRINT-BLOG.md at the repo root.