Show navigationHide navigation
Getting Started
This guide walks you through adding OctoCMS to a Next.js project — either with the CLI (recommended) or by hand.
Prerequisites#
- Node.js 18+ and npm
- A Next.js project (App Router). If you don't have one yet:
bash
npx create-next-app@latest my-site cd my-site - A GitHub account (used for CMS authentication)
Option A: CLI Setup (recommended)#
1. Install OctoCMS#
bashnpm install octocms
Want React Query introspection during dev? Use the React DevTools browser extension↗ — its Components tab can drill into the
<QueryClientProvider>and surface the cached query state. The standalone@tanstack/react-query-devtoolspanel is not wired into OctoCMS (consumer-build and Turbopack-chunking constraints make a clean integration impossible — see CLAUDE.md "Package boundary" for the failure modes that were tried).
2. Initialize#
bashnpx octocms init
The CLI asks three questions (press Enter to accept the defaults):
| Prompt | Default | Description |
|---|---|---|
| Project name | My CMS | Shown in the admin header |
| Git base branch | main | Branch for content reads and PR targets |
| Use a separate published pointer branch? | No | When yes, per-build pointer files under cms/pointers/ live on that branch so Publish doesn't touch a protected base |
3. What gets created#
| Path | Purpose |
|---|---|
cms/octocms.config.ts | Schema — collections and fields |
next.config.ts | Thin wrapper that calls withOctoCMS() |
app/layout.tsx | Root layout — imports configInit so every route initializes the config |
app/cms/layout.tsx | Admin layout (re-exports AdminLayout + metadata from octocms/admin) |
app/cms/[[...path]]/page.tsx | Catch-all route — re-exports AdminApp from octocms/admin. Package owns dispatch + per-component Suspense + skeletons. |
app/cms/error.tsx | Re-exports AdminError from octocms/admin (client component). |
app/api/auth/[...nextauth]/route.ts | NextAuth GitHub handler |
cms/content/helloPage/helloPage-0000.json | Demo content entry |
app/hello/page.tsx | Demo public page that queries the content |
cms/__generated__/ | Auto-generated types and query binding |
.env.local | Environment variable template (if missing) |
Tip: Pass
--yes(or-y) to skip prompts and accept all defaults.
For the full CLI reference, see CLI docs.
Option B: Manual Setup#
Use this if you prefer setting up each file yourself.
1. Install#
bashnpm install octocms
2. Create the schema#
Create cms/octocms.config.ts:
typescriptimport type { Config } from 'octocms/types'; import { defineConfig } from 'octocms/config'; const _typedConfigOctoCMS = defineConfig({ projectName: 'My Site', git: { baseBranch: 'main' }, contentFolder: 'cms/content', mediaFolder: 'public/media', mediaAllowedFormats: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'avif'], collections: { helloPage: { label: 'Hello Page', fields: { title: { label: 'Title', format: 'string', entryTitle: true, required: true }, description: { label: 'Description', format: 'text' }, }, }, }, }); export const configOctoCMS: Config = _typedConfigOctoCMS; export type OctoConfig = typeof _typedConfigOctoCMS;
3. Create next.config.ts#
typescriptimport type { NextConfig } from 'next'; import { withOctoCMS } from 'octocms/config'; import { configOctoCMS } from './cms/octocms.config'; export { configOctoCMS } from './cms/octocms.config'; export type { OctoConfig } from './cms/octocms.config'; const nextConfig: NextConfig = {}; export default withOctoCMS(nextConfig, configOctoCMS);
4. Add configInit to your root layout#
OctoCMS stores its runtime config in a module-level singleton. In serverless environments (Vercel, etc.) each route runs in its own function instance. Importing configInit in the root layout ensures the singleton is initialized for every route — public pages and admin alike.
If app/layout.tsx already exists, add the import at the top:
typescript// app/layout.tsx import '../cms/__generated__/configInit'; // ← add this line import type { Metadata } from 'next'; export const metadata: Metadata = { title: 'My App' }; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body>{children}</body> </html> ); }
Why this matters: Without the root import, any public page that calls
query()on a cold-start serverless instance can throwOctoCMS config not initialized. The admin layout imports it too, but Next.js server-actionPOSTbundles may not loadlayout.tsx, so the admin catch-allcms/[[...path]]/page.tsxmust also side-effect-importconfigInit(seeoctocms init/octocms updatetemplates).
5. Create admin routes#
app/cms/layout.tsx
typescriptimport '../../cms/__generated__/configInit'; import 'octocms/globals.css'; import '@mdxeditor/editor/style.css'; export { AdminLayout as default, metadata } from 'octocms/admin/pages/AdminLayout';
Three thin re-export files — octocms init scaffolds three files in app/cms/. The package owns everything else.
app/cms/layout.tsx
tsximport 'octocms/globals.css'; import '@mdxeditor/editor/style.css'; export { AdminLayout as default, metadata } from 'octocms/admin';
app/cms/[[...path]]/page.tsx
tsxexport { AdminApp as default } from 'octocms/admin';
app/cms/error.tsx
tsx'use client'; export { AdminError as default } from 'octocms/admin';
AdminApp is an async server component that awaits params, dispatches by URL segment, and wraps each branch in its own <Suspense fallback={<MatchingSkeleton/>}>. The package owns routing, skeletons, and Suspense boundaries — see docs/admin-routing.md for the full picture.
6. Create the NextAuth route#
app/api/auth/[...nextauth]/route.ts
typescriptimport NextAuth from 'next-auth'; import { authOptions } from 'octocms/admin/auth'; const handler = NextAuth(authOptions); export { handler as GET, handler as POST };
7. Create the media proxy route#
app/media/[...slug]/route.ts (under src/app/... if your project uses the src/ layout):
typescript// Side-effect import: registers `configOctoCMS` into the runtime store so // `getConfig()` resolves on cold start. Route Handlers don't run // `app/layout.tsx`, so this import has to live here. import '../../../cms/__generated__/configInit'; export { mediaRoute as GET } from 'octocms/admin/mediaRoute';
Adjust the .. segments in the import to reach your project root: depth 3 for app/media/[...slug]/route.ts, depth 4 for src/app/media/[...slug]/route.ts.
Why this route exists: images uploaded through the CMS are committed to GitHub at
public/media/<uuid>.<ext>, but on Vercel (and most serverless hosts) the deployed filesystem is immutable after build — anything committed after the deploy never lands inpublic/on the running instance. The route handler bridges that gap: in dev it reads from localpublic/media/; in production it streams the binary from the GitHub Contents API.next/imageand bare<img>tags both work transparently with noremotePatternsconfig because the URL is on your own origin.
8. Update tsconfig.json#
Add path aliases under compilerOptions.paths:
json{ "compilerOptions": { "paths": { "cms/__generated__": ["./cms/__generated__/index.ts"], "cms/__generated__/*": ["./cms/__generated__/*"], "@/*": ["./src/*"] } } }
9. Create content and media directories#
bashmkdir -p cms/content public/media
10. Generate types#
bashnpx octocms types:gen
This reads your schema and writes TypeScript types, enums, and a typed query() binding to cms/__generated__/.
Configure GitHub Authentication#
The CMS uses a GitHub App (not an OAuth App) for sign-in via NextAuth.
Create a GitHub App#
| Field | Value |
|---|---|
| GitHub App name | e.g. My CMS Local (must be globally unique) |
| Homepage URL | http://localhost:3000 |
| Callback URL | http://localhost:3000/api/auth/callback/github |
| Expire user authorization tokens | Checked (default) |
| Request user authorization (OAuth) during installation | Checked |
| Webhook | Uncheck "Active" |
| Permissions | No special permissions needed for login |
- Click Create GitHub App
- Copy the Client ID (starts with
Iv) - Under Client secrets, click Generate a new client secret — copy it immediately
For production#
Create a separate GitHub App with your production domain:
- Homepage URL:
https://your-domain.com - Callback URL:
https://your-domain.com/api/auth/callback/github
For production saves (commits via the GitHub API), the app also needs:
- Install the app on your repository
- Set repository permissions to Contents: Read and write
- After changing permissions, reinstall/update the app installation and sign out + back in
Environment Variables#
Create .env.local in your project root (the CLI creates a template for you):
bash# GitHub App credentials (required) GITHUB_ID=Iv23li... GITHUB_SECRET=877cfb... # NextAuth (required) NEXTAUTH_SECRET=<random-secret> NEXTAUTH_URL=http://localhost:3000
Generate NEXTAUTH_SECRET with:
bashopenssl rand -base64 32
Required variables#
| Variable | Description |
|---|---|
GITHUB_ID | Client ID from your GitHub App (starts with Iv) |
GITHUB_SECRET | Client secret generated on the GitHub App page |
NEXTAUTH_SECRET | Random string for encrypting sessions |
NEXTAUTH_URL | Base URL of your app (http://localhost:3000 for dev) |
Production variables#
Add these when deploying (e.g. in Vercel Environment Variables):
| Variable | Description |
|---|---|
GITHUB_REPO_OWNER | GitHub username or organization that owns the repo |
GITHUB_REPO_NAME | Repository name |
Optional variables#
| Variable | Description |
|---|---|
CMS_GITHUB_TOKEN | Static GitHub token for private repos or higher API rate limits |
SITE_TIMEZONE | IANA time zone for displaying dates on public pages (default: America/New_York) |
Note: Git branch names (
baseBranch,publishedPointerBranch) are configured incms/octocms.config.ts, not as environment variables.
For production deployment details, see Deployment errors.
Start the Dev Server#
bashnpm run dev
Or use the OctoCMS dev command, which watches your config and auto-regenerates types on changes:
bashnpx octocms dev
- Public site: http://localhost:3000↗
- CMS admin: http://localhost:3000/cms↗
Sign In#
- Navigate to http://localhost:3000/cms↗
- Click "Sign in with GitHub"
- GitHub asks you to authorize the app — click Authorize
- You are redirected back to the CMS editor
Any GitHub user who completes the OAuth flow can sign in. To restrict access (e.g. by email or org membership), add authorization logic in the NextAuth callbacks.
Next Steps#
- Tutorial: Your first content page — create a schema, add content, render it on a public page
- Content model — how entries, collections, and singletons work
- Field types — all available field formats