
Tony Spiro
May 15, 2026
Building a blog with React and Next.js is one of the most common use cases for modern web development. But where you store and manage your content matters just as much as the frontend you choose. In this tutorial, you'll learn how to build a fast, production-ready React blog using the Next.js App Router and Cosmic as your headless CMS.
Want to skip ahead? Grab the free Simple React Blog template and deploy in minutes.
Why Use a Headless CMS for a React Blog?
Managing blog content directly in your codebase works fine for a handful of posts, but it breaks down fast. Markdown files scattered across a repo, no visual editing interface, and every content update requiring a developer and a deployment are all pain points that accumulate quickly.
A headless CMS solves this by separating your content layer from your presentation layer. You manage posts, authors, and categories in a structured dashboard. Your Next.js app fetches that content via the REST API at build time or on demand. Non-technical editors can publish without touching code, and developers stay focused on the frontend. Cosmic gives you a clean API, a TypeScript SDK, a generous free tier, and zero infrastructure to manage. It's purpose-built for exactly this kind of project.
Prerequisites
Before you start, you'll need:
- Node.js 18 or later
- A free Cosmic account
- Basic familiarity with React and TypeScript
Step 1: Set Up Your Cosmic Bucket and Content Model
After signing up for a free Cosmic account, create a new bucket. You can start from the Simple React Blog template to get a pre-configured bucket with sample content and a ready-to-deploy Next.js app, or follow along manually.
Creating a Blog Posts Object Type
In your Cosmic dashboard, navigate to Object Types and create a new type called Posts (slug: posts). Add the following metafields:
| Field | Type | Key |
|---|---|---|
| Title | Text | title (built-in) |
| Slug | Text | slug (built-in) |
| Cover Image | File (image) | cover_image |
| Excerpt | Textarea | excerpt |
| Content | Markdown | content |
| Published Date | Date | published_date |
| Author | Text | author |
Once saved, go ahead and create a few sample posts so you have content to query during development.
Get Your API Credentials
Go to Settings > API Keys in your bucket. You'll need:
- Bucket Slug (e.g.
my-react-blog) - Read Key (for public content fetching)
Save these as environment variables. You'll use them in the next step.
Step 2: Create a New Next.js App
Scaffold a new Next.js project with TypeScript:
npx create-next-app@latest my-react-blog --typescript --app --tailwind cd my-react-blog
Install the Cosmic SDK:
npm install @cosmicjs/sdk
Create a .env.local file at the root of your project:
COSMIC_BUCKET_SLUG=your-bucket-slug COSMIC_READ_KEY=your-read-key
Step 3: Initialize the Cosmic Client
Create a shared Cosmic client so you're not re-initializing it in every file. Add a new file at lib/cosmic.ts:
// lib/cosmic.ts import { createBucketClient } from '@cosmicjs/sdk'; export const cosmic = createBucketClient({ bucketSlug: process.env.COSMIC_BUCKET_SLUG as string, readKey: process.env.COSMIC_READ_KEY as string, });
The @cosmicjs/sdk package is fully typed, so you get autocomplete and type safety throughout your app.
Step 4: Define Your TypeScript Types
Create a types/post.ts file to keep things clean:
// types/post.ts export interface Post { id: string; title: string; slug: string; metadata: { cover_image?: { imgix_url: string; }; excerpt?: string; content?: string; published_date?: string; author?: string; }; }
Step 5: Fetch Posts from the Cosmic REST API
With the SDK initialized, fetching content is straightforward. The SDK wraps the Cosmic REST API with a clean, promise-based interface.
Fetch All Posts (Blog Index)
// lib/posts.ts import { cosmic } from './cosmic'; import type { Post } from '@/types/post'; export async function getAllPosts(): Promise<Post[]> { const { objects } = await cosmic.objects .find({ type: 'posts' }) .props(['id', 'title', 'slug', 'metadata']) .sort('-created_at'); return objects as Post[]; }
Fetch a Single Post by Slug
export async function getPostBySlug(slug: string): Promise<Post> { const { object } = await cosmic.objects .findOne({ type: 'posts', slug, }) .props(['id', 'title', 'slug', 'metadata']); return object as Post; }
Note: The .props() method limits the fields returned, which keeps payloads lean and improves performance.
Step 6: Build the Blog with Next.js App Router
Next.js 13+ uses the App Router with React Server Components by default. This is a great fit for a Cosmic-powered blog: data fetching happens on the server, HTML is pre-rendered, and pages are fast.
Blog Index Page
// app/blog/page.tsx import Link from 'next/link'; import { getAllPosts } from '@/lib/posts'; import type { Post } from '@/types/post'; export const metadata = { title: 'Blog | My React Blog', description: 'Thoughts, tutorials, and updates.', }; export default async function BlogPage() { const posts = await getAllPosts(); return ( <main className="max-w-3xl mx-auto px-4 py-12"> <h1 className="text-4xl font-bold mb-8">Blog</h1> <div className="space-y-8"> {posts.map((post: Post) => ( <article key={post.id}> {post.metadata.cover_image?.imgix_url && ( <img src={`${post.metadata.cover_image.imgix_url}?w=800&auto=format`} alt={post.title} className="w-full rounded-lg mb-4" /> )} <h2 className="text-2xl font-semibold"> <Link href={`/blog/${post.slug}`}>{post.title}</Link> </h2> {post.metadata.excerpt && ( <p className="text-gray-600 mt-2">{post.metadata.excerpt}</p> )} {post.metadata.published_date && ( <p className="text-sm text-gray-400 mt-1"> {new Date(post.metadata.published_date).toLocaleDateString()} </p> )} <Link href={`/blog/${post.slug}`} className="inline-block mt-3 text-blue-600 hover:underline" > Read more → </Link> </article> ))} </div> </main> ); }
Dynamic Post Page
// app/blog/[slug]/page.tsx import { getPostBySlug, getAllPosts } from '@/lib/posts'; import type { Post } from '@/types/post'; type Props = { params: { slug: string }; }; export async function generateStaticParams() { const posts = await getAllPosts(); return posts.map((post: Post) => ({ slug: post.slug })); } export async function generateMetadata({ params }: Props) { const post = await getPostBySlug(params.slug); return { title: post.title, description: post.metadata.excerpt ?? '', openGraph: { images: post.metadata.cover_image?.imgix_url ? [post.metadata.cover_image.imgix_url] : [], }, }; } export default async function PostPage({ params }: Props) { const post = await getPostBySlug(params.slug); return ( <main className="max-w-3xl mx-auto px-4 py-12"> <h1 className="text-4xl font-bold mb-4">{post.title}</h1> {post.metadata.published_date && ( <p className="text-sm text-gray-400 mb-8"> {new Date(post.metadata.published_date).toLocaleDateString()} </p> )} {post.metadata.cover_image?.imgix_url && ( <img src={`${post.metadata.cover_image.imgix_url}?w=1200&auto=format`} alt={post.title} className="w-full rounded-lg mb-8" /> )} <div className="prose prose-lg" dangerouslySetInnerHTML={{ __html: post.metadata.content ?? '' }} /> </main> ); }
generateStaticParams pre-renders all post pages at build time, which means your blog is fast and SEO-friendly out of the box.
Step 7: Optimize Images with Imgix
Cosmic stores all media through Imgix, a powerful image CDN. You can append transformation parameters directly to any media URL:
// Resize and auto-format a cover image const optimizedUrl = `${post.metadata.cover_image.imgix_url}?w=800&h=400&fit=crop&auto=format,compress`;
Common params:
?w=800sets width?auto=formatserves WebP to supported browsers?auto=compressreduces file size automatically?fit=cropcrops to exact dimensions
No additional image optimization setup needed.
Step 8: Deploy to Vercel
Vercel is the natural home for Next.js apps. Deploying is a one-command operation once you've pushed your project to GitHub.
- Push your project to a GitHub repository
- Go to vercel.com and import the repo
- Add your environment variables under Settings > Environment Variables:
COSMIC_BUCKET_SLUGCOSMIC_READ_KEY
- Click Deploy
Vercel will detect Next.js automatically, build your app, and give you a live URL in under a minute.
Enable On-Demand Revalidation (Optional)
For a production blog, you'll want new Cosmic posts to appear without a full redeploy. Add a revalidation route:
// app/api/revalidate/route.ts import { revalidatePath } from 'next/cache'; import { NextRequest, NextResponse } from 'next/server'; export async function POST(request: NextRequest) { const secret = request.nextUrl.searchParams.get('secret'); if (secret !== process.env.REVALIDATION_SECRET) { return NextResponse.json({ message: 'Invalid secret' }, { status: 401 }); } revalidatePath('/blog'); return NextResponse.json({ revalidated: true }); }
Then configure a Cosmic webhook to call this endpoint whenever a post is published. In Cosmic, go to Settings > Webhooks and add your Vercel deployment URL with the /api/revalidate?secret=your_secret path.
What You've Built
Here's what your React blog now has:
- A Next.js App Router project with TypeScript
- A Cosmic bucket with a structured
Postscontent model - Server-side data fetching via the
@cosmicjs/sdk - Static pre-rendering with
generateStaticParams - Imgix-powered image optimization
- Live on Vercel with optional on-demand revalidation
Your editors can log into the Cosmic dashboard and publish new posts without touching your codebase. Your readers get a fast, statically rendered experience. And your SEO improves because every post is pre-rendered HTML at build time.
Start with the Free Template
Don't want to build from scratch? The Simple React Blog template gives you everything in this tutorial, already wired up and ready to deploy with one click.
Get the free React blog template →
Or jump straight in:
Have questions or want to talk through your project? Book a quick intro call with the Cosmic team.
FAQ
Can I use this with the Next.js Pages Router instead of App Router?
Yes. The @cosmicjs/sdk works the same way. Replace async function Page() server components with getStaticProps and getStaticPaths, and the data fetching calls are identical.
Does Cosmic support Markdown for blog content?
Yes. Cosmic has a native Markdown metafield type. The SDK returns the raw Markdown string, so you can render it with a library like react-markdown or convert it to HTML server-side with remark.
Is the Cosmic free tier enough for a personal blog?
The free plan includes 1 Bucket, 1,000 Objects, and 2 team members, which is plenty for most personal or small team blogs. Paid plans start at $99/month for more objects and additional buckets.
Does Cosmic offer a REST API directly, without the SDK?
Yes. The SDK wraps the Cosmic REST API, but you can call the REST endpoints directly using fetch if you prefer. The SDK is recommended for TypeScript projects because of the built-in types.






