All guides

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.

2 min read377 words

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 LocalAdapter for 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 check
  • GET /api/cms/schema — full schema
  • GET /api/cms/openapi.json — OpenAPI 3.0 spec
  • GET /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: ImageField and MediaField from @airdraft/react-content read {field}_url / {field}_media companions that @airdraft/plugin-media injects 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

Related guides