iTranslated by AI
Introduction to Practical Headless CMS Development with Next.js (App Router) and microCMS
Introduction
Using Next.js (App Router) and microCMS, I have implemented a "simple Headless CMS architecture ready for production use."
In this article, I will use this implementation as a subject to organize and explain the following points:
- How to approach CMS architecture in the App Router era
- How to design microCMS for easy maintenance
- How to choose between SSG and ISR
...based on the actual implementation.
What we will build
- A CMS frontend using Next.js (App Router)
- Article management using microCMS
- Article index and detail pages
- High-speed delivery using SSG / ISR
The completed version will look like a minimal blog/media site, suitable for:
- Personal blogs
- Technical blogs
- Small-scale owned media
- "News" features on corporate sites
Target Audience
This article is intended for those who:
- Have started using Next.js App Router but are unsure about the design
- Are using microCMS "just because" but don't know the best practices
- Want to introduce and operate a Headless CMS in a professional environment
- Want to properly understand the differences between SSG and ISR
Assumes basic knowledge of Next.js / React.
Sample Code
The implementation explained in this article is available in the following GitHub repository:
Following along with the code will make it easier to understand.
Overall Architecture and Data Flow
First, let's get a rough overview of the architecture.
System Diagram
Key Points
- Users only access Next.js
- microCMS is API-only (no frontend UI)
- Next.js handles data fetching and screen rendering
- Displays are fast due to SSG / ISR
Data Flow (Page Rendering Process)
Notes
- Only accesses microCMS during initial build or revalidation
- No API traffic during the cache period
- An architecture well-suited for CMS content with low update frequency
Technologies and Design Policy
Technologies Used
- Next.js (App Router)
- TypeScript
- microCMS
- SSG / ISR
- Vercel (Deployment target)
Why use App Router?
Since Next.js 13, the App Router has become the standard.
App Router offers benefits such as:
- Server Components by default
- Tightly integrated data fetching and rendering
- Natural support for SSG / ISR
The point is that it is highly compatible with CMS.
Why use microCMS?
microCMS is characterized by:
- Extensive Japanese documentation
- Simple management console
- Easy-to-understand APIs
- Suitability for small to medium-scale sites
It is a service that is easy to handle even for those "new to Headless CMS."
What this article does not cover
This article will not cover the following:
- Authentication / Login features
- Customizing the management console
- Preview features
- Advanced SEO tuning
We will focus on the "minimal architecture for reliable operations."
Setting up microCMS
Creating a Service
Log in to microCMS and create a new service.
Designing the Content Model
We will prepare the following two models:
posts
- title (text)
- content (rich editor)
- category (reference)
- publishedAt (publication date)
categories
- name (text)
- slug (text)
Note: We will use slug for the URL.
API Settings
- Issue a Read API key
- Configure settings to retrieve only published content
👉 We will only use the Read API from the Next.js side.
Next.js Project Creation and Structure
Creating the Project
npx create-next-app@latest
- Select App Router
- Enable TypeScript
Directory Structure
src/
├─ app/
│ ├─ layout.tsx
│ ├─ page.tsx // Article index
│ └─ posts/
│ └─ [slug]/
│ └─ page.tsx // Article detail
│
├─ libs/
│ └─ microcms.ts // microCMS client
│
├─ types/
│ └─ microcms.ts // Type definitions
We will proceed with the implementation based on this structure.
Implementing the microCMS Client
First, we will implement the client to access microCMS from the Next.js side.
Installing the microCMS SDK
npm install microcms-js-sdk
Defining the Client
Create libs/microcms.ts.
import { createClient } from "microcms-js-sdk";
if (!process.env.MICROCMS_SERVICE_DOMAIN) {
throw new Error("MICROCMS_SERVICE_DOMAIN is required");
}
if (!process.env.MICROCMS_API_KEY) {
throw new Error("MICROCMS_API_KEY is required");
}
export const client = createClient({
serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN,
apiKey: process.env.MICROCMS_API_KEY,
});
Key Points
- Always perform environment variable checks.
- It makes it easier to notice configuration errors since you can throw errors during build time.
- Since Server Components are the default in App Router, this implementation is sufficient.
Type Definitions
Prepare types/microcms.ts.
import type { MicroCMSDate, MicroCMSImage } from "microcms-js-sdk";
export type Category = {
name: string;
slug: string;
};
export type Post = {
title: string;
content: string;
category: Category;
} & MicroCMSDate;
👉 It is easier to manage microCMS types by starting with the "minimum necessary and adding more as needed."
Implementing the Article Index Page (SSG / ISR)
On the top page, we will display the article index.
Data Fetching Logic
Implement this in app/page.tsx.
import { client } from "@/libs/microcms";
import type { Post } from "@/types/microcms";
export const revalidate = 60;
export default async function Home() {
const data = await client.getList<Post>({
endpoint: "posts",
});
return (
<main>
<h1>Article Index</h1>
<ul>
{data.contents.map((post) => (
<li key={post.id}>
<a href={`/posts/${post.id}`}>{post.title}</a>
</li>
))}
</ul>
</main>
);
}
Thinking about SSG / ISR
-
revalidate = 60
→ ISR that regenerates every 60 seconds. - Completely statically generated during the initial build.
- No access to microCMS during the cache period.
Guidelines for production use
| Update Frequency | revalidate |
|---|---|
| Rarely updated | Several hours to 1 day |
| Occasional updates | Several minutes to tens of minutes |
| Frequent updates | 60 seconds or less |
Implementing the Article Detail Page
Next, we will create the article detail page.
Directory Structure
app/posts/[slug]/page.tsx
Static Path Generation
import { client } from "@/libs/microcms";
import type { Post } from "@/types/microcms";
export async function generateStaticParams() {
const data = await client.getList<Post>({
endpoint: "posts",
});
return data.contents.map((post) => ({
slug: post.id,
}));
}
Fetching and Displaying Article Details
export const revalidate = 60;
export default async function PostPage({
params,
}: {
params: { slug: string };
}) {
const post = await client.get<Post>({
endpoint: "posts",
contentId: params.slug,
});
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
Points to note:
-
dangerouslySetInnerHTMLassumes you are using the microCMS rich editor. - Consider sanitization if handling external inputs.
Category Design and Expansion
When using categories, the following design is generally used.
URL Design Example
- /categories/[slug]
- /posts/[slug]
Fetching Articles by Category (Example)
await client.getList<Post>({
endpoint: "posts",
queries: {
filters: `category.slug[equals]${slug}`,
},
});
Key Point:
- Always consider URL design and CMS design as a set.
- Changing them later will have a significant impact on SEO.
Common Pitfalls in App Router
Difference between Server and Client Components
- Need
useState/useEffect? → Client Component - Data fetching only? → Server Component
👉 It is simpler to put CMS integration parts into Server Components.
Causes of Build Errors
- Unconfigured environment variables
- Incorrect return value from
generateStaticParams - Type mismatches
👉 Check the build logs first.
Use Cases in Production
This architecture can be used directly for:
- Technical blogs
- Corporate websites
- News / Announcement pages
- Small-scale owned media
Ideas for Future Expansion
- Preview features
- Draft support
- Pagination
- SEO (Metadata API)
- Management console integration
Conclusion
By combining Next.js (App Router) and microCMS, you can build a Headless CMS architecture that is:
- Fast
- Simple
- Easy to maintain
My recommendation is to start with a minimal implementation and expand as needed.
Discussion