
Cosmic AI
May 4, 2026
Next.js 16 shipped in October 2025 with Turbopack stable, React Compiler support, Cache Components, and a redesigned caching model built around use cache. If you are starting a new blog or content site today, this is the stack to use.
This tutorial walks you through building a production-ready blog from scratch using Next.js 16 and Cosmic as your headless CMS. You will set up a content model in Cosmic, fetch posts with the TypeScript SDK, render them in Next.js 16 App Router server components, and deploy the whole thing to Vercel. The estimated time is 30 minutes.
What you will build: A blog with a homepage listing posts, individual post pages, and tag filtering — all powered by Cosmic's REST API and the @cosmicjs/sdk.
Prerequisites
- Node.js 20+
- A free Cosmic account (no credit card required)
- A Vercel account for deployment (free tier is fine)
- Basic familiarity with React and TypeScript
Step 1: Create Your Cosmic Bucket
Log into Cosmic and create a new bucket. Give it a name like my-nextjs-blog.
Once inside your bucket, you will see the dashboard. Cosmic organizes content into Object Types (your content models) and Objects (individual content entries). For this tutorial, you need one Object Type: blog-posts.
Set Up the Blog Post Content Model
In your bucket, go to Object Types and create a new type called Blog Posts. Add the following metafields:
| Field | Type | Notes |
|---|---|---|
title | Text | Built-in, always present |
slug | Text | Built-in, auto-generated |
content | Markdown | Long-form post body |
excerpt | Textarea | Short summary, 160 chars max |
cover_image | File (image) | Featured image |
published_date | Date | Post date |
tags | Text | Comma-separated tags |
Save the Object Type. Now create 2–3 sample blog posts so you have content to fetch. Publish them.
Get Your API Keys
In your Cosmic dashboard, go to Bucket Settings > API Keys. You need:
- Bucket Slug — e.g.,
my-nextjs-blog-production - Read Key — used for fetching published content on the frontend
Keep these handy. You will add them to your .env.local in a moment.
Step 2: Scaffold a Next.js 16 App
Open your terminal and run:
npx create-next-app@latest my-cosmic-blog --typescript --tailwind --app cd my-cosmic-blog
When prompted:
- Use the App Router (say yes)
- TypeScript: yes
- Tailwind CSS: yes (optional, but the examples below use it)
Next.js 16 uses the App Router by default. The app/ directory is your routing root. All components in this directory are React Server Components by default, which means they can fetch data directly on the server with no client-side waterfall.
Install the Cosmic TypeScript SDK
npm install @cosmicjs/sdk
This is the official SDK. Do not use the older cosmicjs package.
Configure Environment Variables
Create a .env.local file in the project root:
COSMIC_BUCKET_SLUG=your-bucket-slug COSMIC_READ_KEY=your-read-key
Add .env.local to your .gitignore if it is not already there.
Step 3: Create the Cosmic Client
Create a file at 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, });
That is your entire data layer. The createBucketClient function returns a typed client scoped to your bucket. All fetch methods are available on it.
Step 4: Define Your TypeScript Types
Create types/post.ts:
export type Post = { id: string; title: string; slug: string; metadata: { content: string; excerpt: string; cover_image?: { imgix_url: string; }; published_date: string; tags?: string; }; };
This mirrors the shape Cosmic returns for your blog post objects.
Step 5: Fetch Posts from Cosmic
Create 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: 'blog-posts' }) .props('id,title,slug,metadata') .sort('-metadata.published_date') .status('published'); return objects as Post[]; } export async function getPostBySlug(slug: string): Promise<Post | null> { try { const { object } = await cosmic.objects .findOne({ type: 'blog-posts', slug }) .props('id,title,slug,metadata') .status('published'); return object as Post; } catch { return null; } }
A few things to note:
.props()limits the fields returned, keeping API responses lean..sort('-metadata.published_date')sorts newest first. The-prefix means descending..status('published')ensures you only fetch published posts (not drafts).- The SDK is fully typed. Your editor will autocomplete method chains.
Step 6: Build the Homepage — Post Listing
Replace the contents of app/page.tsx:
import Link from 'next/link'; import Image from 'next/image'; import { getAllPosts } from '@/lib/posts'; import type { Post } from '@/types/post'; export const revalidate = 60; // ISR: revalidate every 60 seconds export default async function HomePage() { 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-10"> {posts.map((post: Post) => ( <article key={post.id} className="border-b pb-10"> {post.metadata.cover_image && ( <div className="relative w-full h-48 mb-4 rounded-lg overflow-hidden"> <Image src={`${post.metadata.cover_image.imgix_url}?w=800&auto=format`} alt={post.title} fill className="object-cover" /> </div> )} <h2 className="text-2xl font-semibold mb-2"> <Link href={`/blog/${post.slug}`} className="hover:underline"> {post.title} </Link> </h2> <p className="text-gray-600 mb-3">{post.metadata.excerpt}</p> <p className="text-sm text-gray-400"> {new Date(post.metadata.published_date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', })} </p> </article> ))} </div> </main> ); }
Key Next.js 16 patterns here:
export const revalidate = 60enables Incremental Static Regeneration (ISR). The page is statically generated at build time and refreshed every 60 seconds in the background. No need forgetStaticPropsorgetServerSideProps.- The component is
asyncandawaits data directly. NouseEffect, no loading states on the server. Imagefromnext/imagehandles optimization automatically. The?w=800&auto=formatquery appended to Cosmic's imgix URL tells imgix to resize and optimize on the fly.
Step 7: Build the Individual Post Page
Create app/blog/[slug]/page.tsx:
import { notFound } from 'next/navigation'; import Image from 'next/image'; import ReactMarkdown from 'react-markdown'; import { getAllPosts, getPostBySlug } from '@/lib/posts'; type Props = { params: Promise<{ slug: string }>; // Next.js 16: params is a Promise }; // Generate static paths at build time export async function generateStaticParams() { const posts = await getAllPosts(); return posts.map((post) => ({ slug: post.slug })); } // Generate page metadata dynamically export async function generateMetadata({ params }: Props) { const { slug } = await params; const post = await getPostBySlug(slug); if (!post) return {}; return { title: post.title, description: post.metadata.excerpt, openGraph: { title: post.title, description: post.metadata.excerpt, images: post.metadata.cover_image ? [{ url: `${post.metadata.cover_image.imgix_url}?w=1200&auto=format` }] : [], }, }; } export default async function PostPage({ params }: Props) { const { slug } = await params; const post = await getPostBySlug(slug); if (!post) notFound(); return ( <main className="max-w-3xl mx-auto px-4 py-12"> <h1 className="text-4xl font-bold mb-4">{post.title}</h1> <p className="text-sm text-gray-400 mb-8"> {new Date(post.metadata.published_date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', })} </p> {post.metadata.cover_image && ( <div className="relative w-full h-64 mb-8 rounded-xl overflow-hidden"> <Image src={`${post.metadata.cover_image.imgix_url}?w=1200&auto=format`} alt={post.title} fill className="object-cover" priority /> </div> )} <article className="prose prose-lg max-w-none"> <ReactMarkdown>{post.metadata.content}</ReactMarkdown> </article> </main> ); }
Install react-markdown for rendering your Markdown content:
npm install react-markdown
Important Next.js 16 note: In Next.js 16, params in dynamic route pages is a Promise. You must await params before accessing its properties. This is a breaking change from Next.js 14. The @next/codemod tool can migrate your existing code automatically.
generateStaticParams tells Next.js which slugs to pre-render at build time. Any slug not in the list will be rendered on-demand (ISR fallback).
Step 8: On-Demand Revalidation (Webhook)
When an editor publishes or updates a post in Cosmic, you want your site to reflect those changes quickly. Next.js 16 supports on-demand cache revalidation via its Cache API.
Create app/api/revalidate/route.ts:
import { revalidateTag } from 'next/cache'; import { NextRequest, NextResponse } from 'next/server'; export async function POST(request: NextRequest) { const secret = request.headers.get('x-revalidate-secret'); if (secret !== process.env.REVALIDATE_SECRET) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } revalidateTag('blog-posts'); return NextResponse.json({ revalidated: true, timestamp: Date.now() }); }
Add REVALIDATE_SECRET=your-secret-here to your .env.local.
Then, in Cosmic, go to Bucket Settings > Webhooks and add a webhook pointing to https://your-vercel-domain.vercel.app/api/revalidate. Send the secret in a header: x-revalidate-secret: your-secret-here. Trigger it on object.published events.
Now when you publish a post in Cosmic, your Next.js site will revalidate within seconds.
Step 9: Deploy to Vercel
Vercel is the easiest way to deploy Next.js apps. If you haven't already:
npm install -g vercel vercel
Or push your repo to GitHub and import it at vercel.com.
Add your environment variables in Vercel:
- Go to your project in the Vercel dashboard.
- Open Settings > Environment Variables.
- Add:
COSMIC_BUCKET_SLUGCOSMIC_READ_KEYREVALIDATE_SECRET
Trigger a new deployment. Vercel will build your app, run generateStaticParams, and pre-render all your blog posts as static HTML. Your blog is now live.
Step 10: Preview Mode for Drafts (Bonus)
Cosmic supports previewing draft content before publishing. To enable this, set .status('any') on the Cosmic client:
export async function getPreviewPost(slug: string) { const previewClient = createBucketClient({ bucketSlug: process.env.COSMIC_BUCKET_SLUG as string, readKey: process.env.COSMIC_READ_KEY as string, }); const { object } = await previewClient.objects .findOne({ type: 'blog-posts', slug }) .props('id,title,slug,metadata') .status('any'); // fetch drafts too return object; }
Wire this up to a Next.js Draft Mode route handler to give your editors a live preview experience.
What You Built
Here is what you have at this point:
- A Next.js 16 blog with App Router and server components
- Content managed in Cosmic (no database required)
- Static generation with ISR for fast page loads
- On-demand revalidation via webhooks
- Automatic image optimization via Cosmic's imgix CDN
- Deployed to Vercel in minutes
The full source for this tutorial is structured to extend easily. Add a tag filter, an author page, or a search endpoint using Cosmic's REST API — the SDK supports filtering, sorting, and full-text search out of the box.
Why Cosmic for Next.js 16
Cosmic pairs well with Next.js 16 for a few concrete reasons:
Sub-100ms API responses. Cosmic's CDN caches your content globally. Your server components fetch fast, which keeps your Time to First Byte (TTFB) low even at high traffic.
Structured content model. You define the schema. No rigid blog templates. Your Object Types map cleanly to TypeScript interfaces, which makes type-safe data fetching straightforward.
No backend to maintain. Cosmic handles hosting, scaling, and backups. Your team writes content; your Next.js app delivers it.
Free tier to start. The free plan includes 1 bucket, 1,000 objects, and 100K cached API requests per month. No credit card required. You can go from this tutorial to a live blog without spending anything.
Next Steps
- Sign up for Cosmic free and scaffold your bucket
- Check the Cosmic documentation for advanced SDK usage
- Browse Cosmic's community projects for a head start
- Want a guided walkthrough? Book a demo with Tony
Ready to build? Start for free — no credit card required.







