Separating Docs from Posts: Routing Architecture
How documentation pages are separated from blog posts using type-based routing, allowing docs to live at /docs/ while posts stay at /posts/.
This site serves two types of content: blog posts and technical documentation. While they share the same file structure in the codebase (both live in src/posts/), they need different URL paths: posts at /posts/ and docs at /docs/.
The Problem
Originally, all content was served from /posts/ regardless of whether it was a blog post or technical documentation. This was confusing because:
- Documentation isn't really a "post" - it's reference material
- URLs like
/posts/navigator-featuresuggested blog content when it was actually dev docs - No clear separation between narrative blog posts and technical documentation
The Solution
We added a type field to the PostMeta interface that allows each file to declare whether it's a post or a doc:
export interface PostMeta {
title: string;
slug: string;
date: string;
description: string;
type?: 'post' | 'doc'; // Default is 'post'
// ...other fields
}Files with type: 'doc' are routed to /docs/[slug], while files without a type (or with type: 'post') continue to be served at /posts/[slug].
Implementation Details
1. Type Field in Metadata
Each content file declares its type in the metadata export:
// This is a doc
export const metadata: PostMeta = {
title: 'Building the Navigator',
slug: 'navigator-feature',
type: 'doc', // Routes to /docs/navigator-feature
// ...
};
// This is a post (type omitted, defaults to 'post')
export const metadata: PostMeta = {
title: 'Why Posts Are Programs',
slug: 'programs-not-documents',
// Routes to /posts/programs-not-documents
// ...
};2. Separate Route Handlers
Two parallel route structures exist:
src/app/posts/[slug]/page.tsx- Renders postssrc/app/docs/[slug]/page.tsx- Renders docssrc/app/api/posts/[slug]/route.ts- API for post lookupssrc/app/api/docs/[slug]/route.ts- API for doc lookups
Both route handlers work identically - they load the TSX file from src/posts/and render it. The only difference is which files they filter for.
3. Filtering Logic in posts.ts
The getAllPosts() and getAllDocs() functions filter based on type:
export async function getAllPosts(): Promise<Post[]> {
// Load all .tsx files from src/posts/
const allContent = await loadAllContentFiles();
// Filter to only return actual posts (not docs)
return allContent.filter((p) => p.metadata.type !== 'doc');
}
export async function getAllDocs(): Promise<Post[]> {
// Load all .tsx files from src/posts/
const allContent = await loadAllContentFiles();
// Filter to only return docs
return allContent.filter((d) => d.metadata.type === 'doc');
}4. Sitemap Generation
The sitemap generates URLs based on type:
const posts = await getAllPosts();
const docs = await getAllDocs();
const postUrls = posts.map((post) => ({
url: `${baseUrl}/posts/${post.metadata.slug}`,
lastModified: post.metadata.updated ?? post.metadata.date,
}));
const docUrls = docs.map((doc) => ({
url: `${baseUrl}/docs/${doc.metadata.slug}`,
lastModified: doc.metadata.updated ?? doc.metadata.date,
}));5. Navigator Search Integration
The Navigator automatically picks up both posts and docs from the sitemap. Routes at /posts/ and /docs/ are treated the same in the search interface - users just see titles and descriptions.
Why This Approach?
This design keeps all content files in one directory (src/posts/) while allowing different URL routing. Benefits:
- Single source of truth - All content lives in one place
- Same authoring experience - Posts and docs use identical components
- Flexible routing - URLs reflect content type without moving files
- No duplication - Shared rendering logic, just different filters
- Easy migration - Change one field to move content between /posts/ and /docs/
Creating New Docs
To create a new doc page, just add type: 'doc' to the metadata:
// src/posts/my-new-doc.tsx
import { Heading, Paragraph } from '@/components/Primitives';
import type { PostMeta } from '@/types/post';
export const metadata: PostMeta = {
title: 'My Documentation Page',
slug: 'my-new-doc',
date: '2026-01-17T00:00:00Z',
type: 'doc', // This makes it route to /docs/my-new-doc
description: 'A new documentation page',
topics: ['dev-docs'],
tags: ['documentation'],
};
const MyNewDoc = () => {
return (
<article>
<Heading level={1}>My Documentation Page</Heading>
<Paragraph>Content goes here...</Paragraph>
</article>
);
};
export default MyNewDoc;That's it. The file will automatically:
- Be served at
/docs/my-new-doc - Appear in the sitemap at the correct URL
- Show up in Navigator search results
- Be excluded from the posts list
Key Takeaway
URLs are infrastructure, not part of the authoring experience. By keeping all content in one place but routing based on metadata, we get clean separation without complexity. The type field is the single source of truth for how content gets served.