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:
// 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
// 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
// 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
- Server Components only — content reads happen at build/request time
generateStaticParams— pre-generate all pages at build timegenerateMetadata— SEO metadata from CMS_seofields- Always filter published —
status === 'published'to skip drafts - Never hardcode content — everything from CMS JSON files