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)

1. Install OctoCMS#

bash
npm 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-devtools panel 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#

bash
npx octocms init

The CLI asks three questions (press Enter to accept the defaults):

PromptDefaultDescription
Project nameMy CMSShown in the admin header
Git base branchmainBranch for content reads and PR targets
Use a separate published pointer branch?NoWhen yes, per-build pointer files under cms/pointers/ live on that branch so Publish doesn't touch a protected base

3. What gets created#

PathPurpose
cms/octocms.config.tsSchema — collections and fields
next.config.tsThin wrapper that calls withOctoCMS()
app/layout.tsxRoot layout — imports configInit so every route initializes the config
app/cms/layout.tsxAdmin layout (re-exports AdminLayout + metadata from octocms/admin)
app/cms/[[...path]]/page.tsxCatch-all route — re-exports AdminApp from octocms/admin. Package owns dispatch + per-component Suspense + skeletons.
app/cms/error.tsxRe-exports AdminError from octocms/admin (client component).
app/api/auth/[...nextauth]/route.tsNextAuth GitHub handler
cms/content/helloPage/helloPage-0000.jsonDemo content entry
app/hello/page.tsxDemo public page that queries the content
cms/__generated__/Auto-generated types and query binding
.env.localEnvironment 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#

bash
npm install octocms

2. Create the schema#

Create cms/octocms.config.ts:

typescript
import 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#

typescript
import 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 throw OctoCMS config not initialized. The admin layout imports it too, but Next.js server-action POST bundles may not load layout.tsx, so the admin catch-all cms/[[...path]]/page.tsx must also side-effect-import configInit (see octocms init / octocms update templates).

5. Create admin routes#

app/cms/layout.tsx

typescript
import '../../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 filesoctocms init scaffolds three files in app/cms/. The package owns everything else.

app/cms/layout.tsx

tsx
import 'octocms/globals.css';
import '@mdxeditor/editor/style.css';

export { AdminLayout as default, metadata } from 'octocms/admin';

app/cms/[[...path]]/page.tsx

tsx
export { 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

typescript
import 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 in public/ on the running instance. The route handler bridges that gap: in dev it reads from local public/media/; in production it streams the binary from the GitHub Contents API. next/image and bare <img> tags both work transparently with no remotePatterns config 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#

bash
mkdir -p cms/content public/media

10. Generate types#

bash
npx 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#

  1. Go to GitHub Settings > Developer settings > GitHub Apps > New GitHub App
  2. Fill in:
FieldValue
GitHub App namee.g. My CMS Local (must be globally unique)
Homepage URLhttp://localhost:3000
Callback URLhttp://localhost:3000/api/auth/callback/github
Expire user authorization tokensChecked (default)
Request user authorization (OAuth) during installationChecked
WebhookUncheck "Active"
PermissionsNo special permissions needed for login
  1. Click Create GitHub App
  2. Copy the Client ID (starts with Iv)
  3. 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:

  1. Install the app on your repository
  2. Set repository permissions to Contents: Read and write
  3. 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:

bash
openssl rand -base64 32

Required variables#

VariableDescription
GITHUB_IDClient ID from your GitHub App (starts with Iv)
GITHUB_SECRETClient secret generated on the GitHub App page
NEXTAUTH_SECRETRandom string for encrypting sessions
NEXTAUTH_URLBase URL of your app (http://localhost:3000 for dev)

Production variables#

Add these when deploying (e.g. in Vercel Environment Variables):

VariableDescription
GITHUB_REPO_OWNERGitHub username or organization that owns the repo
GITHUB_REPO_NAMERepository name

Optional variables#

VariableDescription
CMS_GITHUB_TOKENStatic GitHub token for private repos or higher API rate limits
SITE_TIMEZONEIANA time zone for displaying dates on public pages (default: America/New_York)

Note: Git branch names (baseBranch, publishedPointerBranch) are configured in cms/octocms.config.ts, not as environment variables.

For production deployment details, see Deployment errors.


Start the Dev Server#

bash
npm run dev

Or use the OctoCMS dev command, which watches your config and auto-regenerates types on changes:

bash
npx octocms dev

Sign In#

  1. Navigate to http://localhost:3000/cms
  2. Click "Sign in with GitHub"
  3. GitHub asks you to authorize the app — click Authorize
  4. 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#