Add a blog to an existing Next.js site

This is for a different situation than the rest of these guides: you already have a Next.js website — your own layout, header, footer, design system — and you just want /blog and /blog/[slug] added to it. You are not adopting the framework's docs shell, sidebar, or theming. You're using two standalone pieces — MDX-with-frontmatter loading, and MDX rendering — and wiring them into pages that look like the rest of your site.

What you're adding

  • @inkform/framework/content — reads content/blog/*.mdx (frontmatter + body) off disk at build time. No database, no CMS required.
  • @inkform/framework/mdx — renders a post's MDX body to React, with syntax highlighting and the framework's built-in content blocks (<Callout>, <Card>, <Steps>, <Tabs>, <CodeGroup>, <Accordion>, and more).

Nothing else. DocsShell, the sidebar, search, and the theme tokens are a separate, optional layer for people building a full docs site — skip all of it here.

1. Install

npm install @inkform/framework

It expects the same major versions of react, react-dom, and next your project already has (React 19, Next 16) — no separate copies get pulled in.

2. Point Next at the package

Add transpilePackages to your existing next.config.ts — don't replace the file, just merge this key in:

import type { NextConfig } from 'next';
 
const nextConfig: NextConfig = {
  // ...whatever you already have
  transpilePackages: ['@inkform/framework'],
};
 
export default nextConfig;

The package ships TypeScript/JSX source rather than pre-compiled output, so Next needs to know to transpile it.

3. Add your content folder

content/
  blog/
    my-first-post.mdx
    another-post.mdx

By default this is read from <project root>/content/blog. If you'd rather keep content somewhere else in your repo, set DOCS_CONTENT_ROOT (env var) to that directory instead — everything downstream still just calls loadBlogPosts()/loadBlogPost().

Each file needs frontmatter:

---
title: My First Post
date: 2026-01-15
author: Jane Doe
description: A one-line summary for the list page.
---
 
Post body in MDX. Regular markdown, plus `<Callout>`, `<Card>`, fenced code
blocks with syntax highlighting, etc.

See the Blog guide for the full frontmatter reference (tags, series, scheduled/draft posts via status, cover images) — it's the same loader, so every field there works here too.

4. Register your MDX component map

// mdx-components.tsx
import { mdxComponents } from '@inkform/framework/components';
 
export const blogMdxComponents = mdxComponents();

If you want to use a custom React component inside post MDX (a chart, an embed, anything project-specific), pass a { Name: Component } map into mdxComponents() and it merges with the built-ins.

5. The list page — app/blog/page.tsx

Use your own page shell here (whatever wraps the rest of your site's pages) instead of the framework's DocsShell:

import Link from 'next/link';
import { loadBlogPosts } from '@inkform/framework/content';
import { SiteLayout } from '@/components/site-layout'; // your own layout
 
export const metadata = { title: 'Blog' };
 
export default function BlogIndexPage() {
  const posts = loadBlogPosts();
 
  return (
    <SiteLayout>
      <h1>Blog</h1>
      <ul>
        {posts.map((p) => (
          <li key={p.slug}>
            <Link href={`/blog/${p.slug}`}>{p.title}</Link>
            <p>{p.date} · {p.readingTime} min read{p.author ? ` · ${p.author}` : ''}</p>
            {p.description ? <p>{p.description}</p> : null}
          </li>
        ))}
      </ul>
    </SiteLayout>
  );
}

6. The post page — app/blog/[slug]/page.tsx

import { notFound } from 'next/navigation';
import { Mdx } from '@inkform/framework/mdx';
import { loadBlogPosts, loadBlogPost } from '@inkform/framework/content';
import { blogMdxComponents } from '@/mdx-components';
import { SiteLayout } from '@/components/site-layout';
 
export const dynamicParams = false;
 
export function generateStaticParams() {
  return loadBlogPosts().map((p) => ({ slug: p.slug }));
}
 
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  return { title: loadBlogPost(slug)?.title ?? 'Blog' };
}
 
export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = loadBlogPost(slug);
  if (!post) notFound();
 
  return (
    <SiteLayout>
      <h1>{post.title}</h1>
      <p>{post.date} · {post.readingTime} min read{post.author ? ` · ${post.author}` : ''}</p>
      <Mdx source={post.content} components={blogMdxComponents} />
    </SiteLayout>
  );
}

Mdx is an async server component — it compiles and highlights the MDX at request/build time, so nothing extra to await at the call site.

7. Styling

Mdx outputs plain semantic HTML (h1h6, p, pre/code, ul/ol, blockquote, table, plus the block components from step 4) — no required class names, so it inherits your site's existing global styles/Tailwind typography plugin for free in most cases. Two options:

  • Fastest: also import @inkform/framework/styles.css and wrap the rendered post in <div className="fw-prose">. You get the framework's full typography/code-block/callout styling with zero CSS of your own, but it brings its own --fw-* design tokens (defaults are set, but for a matching look you'd override them — see any templates/*/app/theme.css in the framework repo for the token list).
  • Better fit for an existing design system: skip the framework stylesheet entirely and style the plain HTML output the way you already style everything else on your site (a Tailwind prose class from @tailwindcss/typography, your own CSS, etc.).

8. Verify

npm run build

generateStaticParams prerenders every post at build time, so a successful build means every post in content/blog/ compiled and rendered without errors. Visit /blog and /blog/<slug> to confirm.

What you didn't need

No docs.json, no DocsShell, no sidebar, no search setup, no theme tokens, no CLI scaffold. Those all exist for people building a complete docs site on the framework — a blog bolted onto an existing site is a much smaller slice of it: two loader functions and a renderer.