Using Airdraft in Next.js
A complete guide to integrating Airdraft into a Next.js App Router project — from the API route to RSC data fetching to the admin UI.
This guide walks through a complete Airdraft integration in a Next.js 14+ App Router project — covering the config file, API route, server-side data fetching, React hooks, and the drop-in admin UI.
Install packages
npm install @airdraft/next @airdraft/core @airdraft/plugin-auth @airdraft/plugin-media \
@airdraft/react @airdraft/react-ui @airdraft/react-content @airdraft/content
1. Create airdraft.config.ts
The config file wires together the storage adapter, plugins, and schema. It's imported by both the API route and the server client.
// airdraft.config.ts
import { defineConfig, LocalAdapter } from '@airdraft/core'
import { withAutoAuth } from '@airdraft/plugin-auth'
import { withAutoMedia } from '@airdraft/plugin-media'
import { withSeo } from '@airdraft/plugin-seo'
const adapter = new LocalAdapter({ root: './content' })
const media = await withAutoMedia({ storageAdapter: adapter })
const auth = withAutoAuth() // returns null if AIRDRAFT_JWT_SECRET is not set
export default defineConfig({
adapter,
schemaPath: 'airdraft.schema.json',
plugins: [
media,
withSeo(),
...(auth ? [auth] : []),
],
})
Tip: Swap
LocalAdapterfor a database adapter (@airdraft/db-adapter-postgres,@airdraft/db-adapter-mongodb, or@airdraft/db-adapter-sqlite) in production. See Connecting a Database.
2. Set up the API route
Create a catch-all route that handles all CMS requests:
// app/api/cms/[...cms]/route.ts
import { createCmsHandler } from '@airdraft/next'
import airdraft from '@/airdraft.config'
export const { GET, POST, PATCH, PUT, DELETE } = createCmsHandler(airdraft)
That's it. The handler auto-generates:
- Full CRUD routes for every collection
GET /api/cms/health— health checkGET /api/cms/schema— full schemaGET /api/cms/openapi.json— OpenAPI 3.0 specGET /api/cms/docs— Scalar API reference UI
3. Run database migration on startup
If you're using a database adapter, run migrate() on server start via Next.js instrumentation.ts:
// instrumentation.ts
import airdraft from '@/airdraft.config'
import { BaseDatabaseAdapter } from '@airdraft/db-adapter'
export async function register() {
if (airdraft.adapter instanceof BaseDatabaseAdapter) {
await airdraft.adapter.migrate()
}
}
4. Required environment variables
# .env.local
# JWT secret — generate with: npx airdraft generate-secret
AIRDRAFT_JWT_SECRET=your-jwt-secret-here
# Used by the admin UI and client-side hooks
NEXT_PUBLIC_CMS_API_URL=http://localhost:3000/api/cms
5. Create the first admin user
npx airdraft create-user
6. Server-side data fetching (RSC)
Use createCmsClient from @airdraft/next in Server Components and generateStaticParams. It calls the CMS engine directly — no HTTP, no latency.
// lib/cms.ts
import { createCmsClient } from '@airdraft/next'
import airdraft from '@/airdraft.config'
export const cms = createCmsClient(airdraft)
// app/blog/page.tsx
import { cms } from '@/lib/cms'
export default async function BlogPage() {
const { entries } = await cms.listEntries('posts', {
status: 'published',
limit: 10,
sort: { field: 'publishedAt', order: 'desc' },
})
return (
<ul>
{entries.map((post) => (
<li key={post.slug}>
<a href={`/blog/${post.slug}`}>{post.data.title as string}</a>
</li>
))}
</ul>
)
}
// app/blog/[slug]/page.tsx
import { cms } from '@/lib/cms'
import { BodyField, DateField } from '@airdraft/react-content'
import { notFound } from 'next/navigation'
export async function generateStaticParams() {
const { entries } = await cms.listEntries('posts', { status: 'published' })
return entries.map((e) => ({ slug: e.slug }))
}
export default async function PostPage({ params }: { params: { slug: string } }) {
const entry = await cms.getEntry('posts', params.slug)
if (!entry) notFound()
return (
<article>
<h1>{entry.data.title as string}</h1>
<DateField value={entry.data.publishedAt as string} />
<BodyField value={entry.data.body as string} />
</article>
)
}
Note:
ImageFieldandMediaFieldfrom@airdraft/react-contentread{field}_url/{field}_mediacompanions that@airdraft/plugin-mediainjects automatically. No manual URL resolution needed.
7. Client-side hooks
Wrap your layout with AirdraftProvider:
// app/layout.tsx
import { AirdraftProvider } from '@airdraft/react'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AirdraftProvider apiUrl={process.env.NEXT_PUBLIC_CMS_API_URL!}>
{children}
</AirdraftProvider>
</body>
</html>
)
}
// components/PostList.tsx
'use client'
import { useEntries } from '@airdraft/react'
export function PostList() {
const { entries, loading, error } = useEntries('posts', {
status: 'published',
limit: 10,
})
if (loading) return <p>Loading…</p>
if (error) return <p>Something went wrong.</p>
return (
<ul>
{entries.map((post) => (
<li key={post.slug}>{post.data.title as string}</li>
))}
</ul>
)
}
8. Add the admin UI
Mount CMSAdmin at a protected route:
// app/admin/[[...segments]]/page.tsx
import { CMSAdmin } from '@airdraft/react-ui'
import '@airdraft/react-ui/styles.css'
export default function AdminPage() {
return (
<CMSAdmin
basePath="/admin"
apiUrl={process.env.NEXT_PUBLIC_CMS_API_URL!}
/>
)
}
The [[...segments]] catch-all lets the admin handle its own internal navigation.
Protect the admin route with middleware
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
export function middleware(req: NextRequest) {
const isAdmin = req.nextUrl.pathname.startsWith('/admin')
const token = req.cookies.get('airdraft_session')?.value
if (isAdmin && !token) {
return NextResponse.redirect(new URL('/admin/login', req.url))
}
return NextResponse.next()
}
export const config = { matcher: ['/admin/:path*'] }
9. On-demand ISR revalidation
Trigger Next.js cache revalidation when content is published:
// app/api/revalidate/route.ts
import { createRevalidationHandler } from '@airdraft/next'
export const POST = createRevalidationHandler({
secret: process.env.REVALIDATION_SECRET!,
tags: ['posts', 'authors'],
})
In the Airdraft dashboard, go to Settings → Webhooks and add a webhook pointing to https://yoursite.com/api/revalidate with the matching secret.
10. Next.js config wrapper
// next.config.mjs
import { withAirdraft } from '@airdraft/next/config'
export default withAirdraft({
// your existing Next.js config
})
SEO metadata example
// app/blog/[slug]/page.tsx
import { resolveMediaUrl, resolveImageDimensions } from '@airdraft/content'
export async function generateMetadata({ params }) {
const entry = await cms.getEntry('posts', params.slug)
if (!entry) return {}
const ogImage = resolveMediaUrl('cover', entry.data)
const dims = resolveImageDimensions('cover', entry.data)
return {
title: (entry.data.seo as any)?.title ?? (entry.data.title as string),
description: (entry.data.seo as any)?.description,
openGraph: ogImage
? { images: [{ url: ogImage, ...dims }] }
: undefined,
}
}
Quick-reference: package responsibilities
| Package | Where used | npm |
|---|---|---|
@airdraft/next |
API route, server client, ISR, config wrapper | npm |
@airdraft/core |
Config, defineConfig, types, LocalAdapter |
npm |
@airdraft/plugin-auth |
withAutoAuth — login, sessions, invite |
npm |
@airdraft/plugin-media |
withAutoMedia — file upload, URL injection |
npm |
@airdraft/plugin-seo |
withSeo — SEO field injection |
npm |
@airdraft/plugin-audit-log |
withAuditLog — structured audit events |
npm |
@airdraft/client |
HTTP client for external apps or CI | npm |
@airdraft/react |
Client hooks (useEntries, useAuth, useMedia) |
npm |
@airdraft/react-ui |
CMSAdmin, EntryEditor, MediaManager |
npm |
@airdraft/react-content |
RSC display components (BodyField, ImageField) |
npm |
@airdraft/content |
Pure helpers (resolveMediaUrl, parseMarkdown) |
npm |
Resources
- GitHub — aevrHQ/airdraft
- API reference — auto-generated Scalar docs from your running instance