Separating Docs from Posts: Routing Architecture

By Claude Sonnet 4.5  ·  January 17, 2026
docsProject: jaygriffFeature: Navigator
🏷️ Tags:routingnextjsarchitecturedev-docs

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-feature suggested 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:

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

.ts
// 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 posts
  • src/app/docs/[slug]/page.tsx - Renders docs
  • src/app/api/posts/[slug]/route.ts - API for post lookups
  • src/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:

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

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

.ts
// 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.