webhouse.appwebhouse.appdocs

How to read CMS content in Next.js — loader functions, pages, static generation, and metadata.

Reading content

All content is read server-side using fs:

typescript
// lib/content.ts
import { readFileSync, readdirSync, existsSync } from 'fs';
import { join } from 'path';

const CONTENT = join(process.cwd(), 'content');

export function getCollection(name: string) {
  const dir = join(CONTENT, name);
  if (!existsSync(dir)) return [];
  return readdirSync(dir)
    .filter(f => f.endsWith('.json'))
    .map(f => JSON.parse(readFileSync(join(dir, f), 'utf-8')))
    .filter(d => d.status === 'published');
}

export function getDocument(collection: string, slug: string) {
  const file = join(CONTENT, collection, `${slug}.json`);
  if (!existsSync(file)) return null;
  return JSON.parse(readFileSync(file, 'utf-8'));
}

Blog listing page

typescript
// app/blog/page.tsx
import { getCollection } from '@/lib/content';

export default function BlogPage() {
  const posts = getCollection('posts')
    .sort((a, b) => (b.data.date ?? '').localeCompare(a.data.date ?? ''));

  return (
    <div>
      <h1>Blog</h1>
      {posts.map(post => (
        <a key={post.slug} href={`/blog/${post.slug}`}>
          <h2>{post.data.title}</h2>
          <p>{post.data.excerpt}</p>
        </a>
      ))}
    </div>
  );
}

Dynamic page with static generation

typescript
// app/blog/[slug]/page.tsx
import { getCollection, getDocument } from '@/lib/content';
import { notFound } from 'next/navigation';

export function generateStaticParams() {
  return getCollection('posts').map(d => ({ slug: d.slug }));
}

export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const doc = getDocument('posts', slug);
  if (!doc) return {};
  return {
    title: doc.data._seo?.metaTitle ?? doc.data.title,
    description: doc.data._seo?.metaDescription ?? doc.data.excerpt,
  };
}

export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const doc = getDocument('posts', slug);
  if (!doc) notFound();

  return (
    <article>
      <h1>{doc.data.title}</h1>
      {/* Render doc.data.content with react-markdown */}
    </article>
  );
}

Key patterns

  1. Server Components only — content reads happen at build/request time
  2. generateStaticParams — pre-generate all pages at build time
  3. generateMetadata — SEO metadata from CMS _seo fields
  4. Always filter publishedstatus === 'published' to skip drafts
  5. Never hardcode content — everything from CMS JSON files
Previous
Blocks
Next
Internationalization (i18n)