Show navigationHide navigation
Deployment errors and troubleshooting
This page describes how the CMS fails fast when configuration is wrong, and how to fix common GitHub-related errors on the public site.
Environment validation#
When NODE_ENV is production, the app checks required variables while loading next.config.ts and again when the Node server starts (next start, including Vercel).
If something is missing, next build or next start stops with an error like:
Missing required environment variables for production: … See README.md (Production requirements) and docs/deployment-errors.md.
Always required in production#
NEXTAUTH_SECRETGITHUB_IDGITHUB_SECRETNEXTAUTH_URL(use your real public URL in production, notlocalhost)
Also required when public content is read from GitHub#
Public routes use the GitHub API when either:
NODE_ENV=production, orCMS_FORCE_GITHUB_API=trueis set (forces GitHub reads whileNODE_ENVis not production).
In that case you must also set:
GITHUB_REPO_OWNERGITHUB_REPO_NAME
For private repositories, set CMS_GITHUB_TOKEN with Contents: Read (and write access for CMS saves). See Installation.
"OctoCMS config not initialized" (500 on public pages)#
Error message:
Error: OctoCMS config not initialized. Make sure withOctoCMS(nextConfig, config) is called in next.config.ts and cms/__generated__/configInit.ts is imported in your admin layout.
Cause: OctoCMS stores its runtime config in a module-level singleton (octocms/lib/configStore). In serverless deployments each route runs in its own function instance. If a public page cold-starts without having loaded configInit, the singleton is empty and every query() call throws.
The admin layout (app/cms/layout.tsx) already imports configInit, so /cms/* routes are fine. Public routes are affected only when they cold-start in a separate function instance that never loaded the admin layout.
Fix: Import configInit in your root layout so it runs for every route, and keep the admin catch-all page (app/cms/[[...path]]/page.tsx or src/app/cms/[[...path]]/page.tsx) side-effect-importing the same module. Some Next.js server action bundles do not load layout.tsx for the POST that carries the action; without the catch-all import, getConfig() can throw even when /cms works in the browser.
typescript// app/layout.tsx (or src/app/layout.tsx) import '../cms/__generated__/configInit'; // ← add this line
The relative path depends on where your root layout lives:
| Root layout path | Import path |
|---|---|
app/layout.tsx | '../cms/__generated__/configInit' |
src/app/layout.tsx | '../../cms/__generated__/configInit' |
| Admin catch-all page | Typical import path |
|---|---|
app/cms/[[...path]]/page.tsx | '../../../cms/__generated__/configInit' |
src/app/cms/[[...path]]/page.tsx | '../../../../cms/__generated__/configInit' |
If you used npx octocms init, the CLI wires the catch-all page and adds the cms/__generated__/* TypeScript path. Run npx octocms update to refresh admin route stubs on an existing project (it migrates legacy one-line re-exports).
TypeScript path: Admin server actions side-import cms/__generated__/configInit via octocms/admin/actions/registerConfig.ts. Your app’s tsconfig.json must map that specifier (for example "cms/__generated__/*": ["./cms/__generated__/*"]). Without it, next build can fail to resolve the import even though relative imports in layouts work.
Public site: content could not be loaded#
If GitHub is unreachable, rate-limited, or the token cannot read the repo, the App Router shows a “Something went wrong” page with a short explanation and Try again / Home.
Typical causes:
| Situation | What to do |
|---|---|
| Private repo, no token | Set CMS_GITHUB_TOKEN with access to the content repo. |
| 403 / permission errors | Ensure the token or app installation has Contents: Read on the repository. |
| Rate limit (429) | Wait and retry; reduce traffic or use a token for higher limits. |
| Wrong branch / missing files | Confirm git.baseBranch and optional git.publishedPointerBranch in your config match your repo; ensure per-build pointer files under cms/pointers/ (and CMS_BRANCH if used) and content paths exist on the ref you read. See docs/multi-deploy.md. |
CMS_BRANCH set but site still uses baseBranch | That name must exist on GitHub (heads/<name>). If it does not, resolution falls back to git.baseBranch with no error page. |
| No pointer file yet | Until you Publish once, switch the header branch off the default in production, or add cms/pointers/<build-id>.json on the pointer ref, public reads use CMS_BRANCH when set and valid, otherwise git.baseBranch. See Multi-deploy. |
| GitHub outage / network | Retry later; check GitHub Status↗. |
CMS admin: POST /cms/content/... returns 500 after Save#
What you see: The browser Network tab shows 500 on a POST to the same URL as the entry editor (for example /cms/content/homePage/homePage-0000). That request carries the Server Action payload (save plus any refetches), not a missing route.
Common causes:
-
GitHub write failure — Missing or under-scoped installation token, branch protection, or wrong branch. The action usually returns
{ success: false, error: "…" }with a toast; only uncaught errors become 500. Check Vercel Functions logs for the stack trace. -
Refetch after save hit a code path that read the repo from local disk — On Vercel the deployed app does not ship your Git working tree under
process.cwd()the way dev does. IfgetFilefell through tofs.readFileafter the in-memory store missed on a cold serverless instance,readFilethrew and Next surfaced 500. OctoCMS now retriesreadGitHubFilePublicin production and never uses local FS for missing entries in production; upgrade if you still see this on an older build. -
Production without a feature branch — Saves are rejected when the
cms-active-branchcookie is unset (Create or select a branch before editing.). That path returns a structured failure, not 500, unless something else throws afterward.
What to do: Open Vercel → Project → Logs (or Runtime Logs) for the failing timestamp and read the server error message. Confirm CMS_GITHUB_TOKEN / GitHub App permissions, that you are on a writable CMS branch from the header, and that configInit is imported from the root layout (see above).