Add a blog to a Next.js site
Two pages in the App Router: a list at /blog and a post at /blog/[slug]. Both render on the server, so every post is in the HTML that Google reads. About twenty minutes.
Before you start
- A Next.js app on the App Router
- A project API key from Developer → API keys. It is read-only and serves published posts only
- At least one published post
Install and connect
npm install cmskite marked.env.local
CMSKITE_API_KEY=csk_live_...lib/cms.ts
import { createCMSKite } from 'cmskite'
export const cms = createCMSKite({ apiKey: process.env.CMSKITE_API_KEY! })The key has no NEXT_PUBLIC_ prefix, so it stays on the server. Use the prefixed name only for code that runs in the browser.
The list page
app/blog/page.tsx
import Link from 'next/link'
import { cms } from '@/lib/cms'
export default async function Blog() {
const { items } = await cms.posts.list({ limit: 20 })
return (
<ul>
{items.map((post) => (
<li key={post.id}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
<p>{post.excerpt}</p>
</li>
))}
</ul>
)
}The post page
app/blog/[slug]/page.tsx
import { marked } from 'marked'
import { notFound, redirect } from 'next/navigation'
import { cms } from '@/lib/cms'
export default async function Post({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await cms.posts.findBySlug(slug)
if (!post) notFound()
// An old address: send readers and search engines to the new one.
if (post.slugRedirectedFrom) redirect(`/blog/${post.slug}`)
const html = post.bodyFormat === 'markdown' ? await marked.parse(post.body) : post.body
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: html }} />
</article>
)
}Titles, descriptions and the canonical address for generateMetadata are in Making your posts findable.
Show new posts without a rebuild
If your pages are cached, a new post waits for the cache to expire. Instead, add a webhook for post.published and post.updated that calls a route which runs revalidatePath('/blog'). The post is live seconds after you press Publish.
Count views
Server-rendered pages do not report views by themselves. Add the three-line hook from Why your views are zero.
Check that it works
- Publish a short test post in the dashboard
- Open /blog on your deployed site in a private window and reload. The post should be in the list within a minute
- Open the post. Its address should be your site's, not cmskite.com
- Delete or unpublish the test post when you are done







