Contensa
Features
PricingBlogAbout
Log inStart free
Back to Blog
For developers

Building a Next.js Blog with Contensa in Under 30 Minutes

A step-by-step tutorial for building a fully functional Next.js blog powered by Contensa's GraphQL API — from workspace setup to deployed site.

Feb 1, 2026
•
12 min read
Next.jsTutorialGraphQLDeveloper Guide
S

Sarah Johnson

Technical Writer & Developer Advocate

Building a Next.js Blog with Contensa in Under 30 Minutes

Building a Next.js Blog with Contensa in Under 30 Minutes

This is a practical, code-first tutorial. By the end, you'll have a fully functional Next.js blog powered by Contensa's API, with static generation, dynamic routing, and TypeScript throughout.

What we're building:

  • A blog listing page with all published posts
  • Individual blog post pages with dynamic routing
  • Author information on each post
  • Static generation for performance

Prerequisites: Node.js 18+, a Contensa account (free tier works), basic familiarity with Next.js and TypeScript.

Step 1: Set Up Your Contensa Workspace (5 minutes)

  1. Sign in to Contensa and create a new workspace
  2. When prompted, describe your project: "A developer blog with posts, authors, and categories"
  3. Contensa generates your content model automatically — you get BlogPost, Author, and Category content types
  4. Go to API Keys in your workspace settings and copy your API key and content type IDs

Step 2: Create Your Next.js Project (2 minutes)

bash
npx create-next-app@latest contensa-blog --typescript --tailwind --app cd contensa-blog npm install @mybe/sdk

Step 3: Configure the SDK (2 minutes)

Create

lib/contensa.ts
:

typescript
import { MybeSDK } from '@mybe/sdk'; export const contensa = new MybeSDK({ apiKey: process.env.CONTENSA_API_KEY!, });

Add to

.env.local
:

bash
CONTENSA_API_KEY=your-api-key-here CONTENSA_BLOG_POST_TYPE_ID=your-blog-post-type-id

Step 4: Define TypeScript Types (3 minutes)

Create

types/blog.ts
:

typescript
export interface Author { id: string; name: string; role: string; bio: string; avatar?: { url: string; alt: string }; } export interface BlogPost { id: string; title: string; slug: string; excerpt: string; body: string; author: Author; publishedAt: string; featuredImage?: { url: string; alt: string }; tags: string[]; category: string; }

Step 5: Create Data Fetching Functions (5 minutes)

Create

lib/blog.ts
:

typescript
import { contensa } from './contensa'; import type { BlogPost } from '@/types/blog'; const TYPE_ID = process.env.CONTENSA_BLOG_POST_TYPE_ID!; export async function getAllPosts() { const result = await contensa.getContentByType(TYPE_ID, { status: 'published', limit: 100, }); return result.data.map((entry) => ({ id: entry.id, title: entry.data.title, slug: entry.data.slug, excerpt: entry.data.excerpt, author: { name: entry.data.author?.data?.name ?? 'Unknown', role: entry.data.author?.data?.role ?? '', avatar: entry.data.author?.data?.avatar ?? null, }, publishedAt: entry.data.publishedAt, featuredImage: entry.data.featuredImage ?? null, tags: entry.data.tags ?? [], category: entry.data.category ?? '', })); } export async function getPostBySlug(slug: string): Promise<BlogPost | null> { const entry = await contensa.getContentByField(TYPE_ID, 'slug', slug); if (!entry) return null; return { id: entry.id, title: entry.data.title, slug: entry.data.slug, excerpt: entry.data.excerpt, body: entry.data.body, author: { id: entry.data.author?.id ?? '', name: entry.data.author?.data?.name ?? 'Unknown', role: entry.data.author?.data?.role ?? '', bio: entry.data.author?.data?.bio ?? '', avatar: entry.data.author?.data?.avatar ?? null, }, publishedAt: entry.data.publishedAt, featuredImage: entry.data.featuredImage ?? null, tags: entry.data.tags ?? [], category: entry.data.category ?? '', }; } export async function getAllPostSlugs(): Promise<string[]> { const posts = await getAllPosts(); return posts.map((post) => post.slug); }

Step 6: Build the Blog Listing Page (5 minutes)

Create

app/blog/page.tsx
:

typescript
import Link from 'next/link'; import Image from 'next/image'; import { getAllPosts } from '@/lib/blog'; export const revalidate = 3600; export default async function BlogPage() { const posts = await getAllPosts(); return ( <main className="max-w-4xl mx-auto px-4 py-16"> <h1 className="text-4xl font-bold mb-12">Blog</h1> <div className="grid gap-8"> {posts.map((post) => ( <article key={post.id} className="border-b pb-8"> <div className="text-sm text-gray-500 mb-2"> {post.category} · {new Date(post.publishedAt).toLocaleDateString()} </div> <h2 className="text-xl font-semibold mb-2"> <Link href={`/blog/${post.slug}`} className="hover:text-blue-600"> {post.title} </Link> </h2> <p className="text-gray-600 mb-4">{post.excerpt}</p> <div className="flex items-center gap-2 text-sm"> {post.author.avatar && ( <Image src={post.author.avatar.url} alt={post.author.name} width={24} height={24} className="rounded-full" /> )} <span>{post.author.name}</span> </div> </article> ))} </div> </main> ); }

Step 7: Build the Individual Post Page (5 minutes)

Create

app/blog/[slug]/page.tsx
:

typescript
import Image from 'next/image'; import { notFound } from 'next/navigation'; import { getAllPostSlugs, getPostBySlug } from '@/lib/blog'; import type { Metadata } from 'next'; interface Props { params: { slug: string } } export async function generateStaticParams() { const slugs = await getAllPostSlugs(); return slugs.map((slug) => ({ slug })); } export async function generateMetadata({ params }: Props): Promise<Metadata> { const post = await getPostBySlug(params.slug); if (!post) return {}; return { title: post.title, description: post.excerpt, openGraph: { title: post.title, description: post.excerpt, images: post.featuredImage ? [post.featuredImage.url] : [], }, }; } export const revalidate = 3600; export default async function BlogPostPage({ params }: Props) { const post = await getPostBySlug(params.slug); if (!post) notFound(); return ( <main className="max-w-3xl mx-auto px-4 py-16"> <div className="text-sm text-gray-500 mb-4"> {post.category} · {new Date(post.publishedAt).toLocaleDateString()} </div> <h1 className="text-4xl font-bold mb-6">{post.title}</h1> <p className="text-xl text-gray-600 mb-8">{post.excerpt}</p> <div className="flex items-center gap-3 py-6 border-y mb-8"> {post.author.avatar && ( <Image src={post.author.avatar.url} alt={post.author.name} width={40} height={40} className="rounded-full" /> )} <div> <div className="font-medium">{post.author.name}</div> <div className="text-sm text-gray-500">{post.author.role}</div> </div> </div> {post.featuredImage && ( <Image src={post.featuredImage.url} alt={post.featuredImage.alt || post.title} width={800} height={450} className="w-full rounded-xl mb-10" priority /> )} <article className="prose prose-lg max-w-none" dangerouslySetInnerHTML={{ __html: post.body }} /> {post.tags.length > 0 && ( <div className="mt-12 flex flex-wrap gap-2"> {post.tags.map((tag) => ( <span key={tag} className="px-3 py-1 bg-gray-100 rounded-full text-sm"> {tag} </span> ))} </div> )} </main> ); }

Step 8: Add Content and Run

In your Contensa dashboard, create an Author entry and a BlogPost entry (set status to Published). Then:

bash
npm run dev

Visit

http://localhost:3000/blog
— your blog is live.

Deploy to Vercel

bash
npx vercel

Add your environment variables in the Vercel dashboard. Done.

What You Get

  • Static generation with ISR (revalidates every hour)
  • Dynamic routing per post slug
  • SEO metadata and Open Graph tags
  • Full TypeScript type safety
  • Author profiles linked to each post

Start your free Contensa workspace and have your first blog post live in under 30 minutes.

Share Article

About the Author

S

Sarah Johnson

Technical Writer & Developer Advocate

Sarah is a technical writer and developer advocate with over 8 years of experience helping developers build better software. She specializes in API documentation and developer education.

Related Articles

View all articles
Building Dynamic Apps with Contensa's GraphQL API
For developers

Building Dynamic Apps with Contensa's GraphQL API

A comprehensive guide to integrating Contensa's powerful GraphQL API into your applications with real-world examples and code snippets.

Jan 5, 2026
•
10 min read
Webhooks in Contensa: Real-Time Content Sync with Any Platform
For developers

Webhooks in Contensa: Real-Time Content Sync with Any Platform

Learn how to use Contensa's webhook system to trigger real-time builds, sync content to external systems, and automate your content workflow.

Jan 28, 2026
•
8 min read
Getting Started with Contensa: Your Complete Setup Guide
Technical

Getting Started with Contensa: Your Complete Setup Guide

Learn how to set up Contensa CMS in minutes with this step-by-step guide covering installation, configuration, and fetching your first content.

Jan 15, 2026
•
8 min read