Skip to content

feat: deploy Keystatic admin UI to Netlify (SSR adapter + GitHub App redirect) #12

@rathermercurial

Description

@rathermercurial

Goal

Deploy the Keystatic admin UI to Netlify so team members can edit content at
https://docs.bread.coop/keystatic without running anything locally.

Prerequisite: #11 (switch Keystatic from Cloud → GitHub mode, dev-only) must be complete first.

Background

Currently the admin UI is dev-only — Keystatic is conditionally excluded from production builds:

// astro.config.mjs
...(process.env.NODE_ENV !== 'production' ? [keystatic()] : []),

Keystatic needs server-side code to handle the GitHub OAuth callback and API requests
(/keystatic/api/* routes). Since the project currently builds as a static site
(no adapter), we need to add an SSR adapter to deploy these routes.

Architectural change

Before After
Output mode static (implicit default) hybrid (most pages static, Keystatic API SSR)
Adapter None @astrojs/netlify
Keystatic in production Excluded Included
Deploy target dist/ (static files) Netlify Functions + static files
Admin UI URL localhost:4321/keystatic https://docs.bread.coop/keystatic

Steps

1. Install the Netlify adapter

npx astro add netlify

Or manually:

npm install @astrojs/netlify

This adds the adapter to astro.config.mjs:

import netlify from '@astrojs/netlify';

export default defineConfig({
  output: 'hybrid',  // or 'server' — see note below
  adapter: netlify(),
  // ...
});

2. Choose output mode

Two approaches:

Option A — output: 'hybrid' (recommended)

  • Most pages pre-rendered at build time (same as today's static behavior)
  • Only Keystatic API routes (/keystatic/api/*) run as Netlify Functions
  • Minimal change to existing page rendering
  • May need export const prerender = false on any non-API Keystatic page

Option B — output: 'server'

  • All pages rendered on-demand by default
  • Must explicitly add export const prerender = true to static pages
  • More Netlify Function invocations = higher cost/latency
  • Not recommended unless Option A proves insufficient

Start with Option A.

3. Include Keystatic in production builds

Remove the production guard in astro.config.mjs:

-  ...(process.env.NODE_ENV !== 'production' ? [keystatic()] : []),
+  keystatic(),

Alternatively, keep the guard but add a separate env var for toggling the admin UI:

...(process.env.ENABLE_KEYSTATIC === 'true' || process.env.NODE_ENV !== 'production' ? [keystatic()] : []),

4. Configure GitHub App redirect URI

After deployment is working, add the production callback URL to the GitHub App
(created in #11):

  1. Go to the BreadchainCoop GitHub App settings
    or personal app settings
  2. Select the app created in feat: switch Keystatic from Cloud to GitHub mode #11 (e.g. "Bread Docs CMS")
  3. Under "Callback URL" → "Add Callback URL":
    https://docs.bread.coop/keystatic/api/auth/callback
    
  4. Save

Without this, GitHub OAuth will fail in production.

5. Set env vars on Netlify

The four env vars from .env (generated in #11) must be set in the Netlify dashboard
or netlify.toml. The Netlify site manager needs to add:

KEYSTATIC_GITHUB_CLIENT_ID      = <value from .env>
KEYSTATIC_GITHUB_CLIENT_SECRET  = <value from .env>
KEYSTATIC_SECRET                = <value from .env>
PUBLIC_KEYSTATIC_GITHUB_APP_SLUG = <value from .env>

⚠️ Do NOT commit .env to the repo. These are already in .gitignore.

How to set on Netlify:
Netlify dashboard → Site settings → Environment variables → Add variables
Or via CLI: netlify env:set KEYSTATIC_GITHUB_CLIENT_ID "<value>"

6. Update netlify.toml (if needed)

The current netlify.toml is minimal:

[build]
  command = "npm run build"
  publish = "dist"

With the adapter, astro build outputs to both dist/ (static) and
.netlify/ (functions). The default publish = "dist" should still work.
No changes expected, but verify after astro build.

7. Build and deploy

npm run build

Verify:

  • dist/ contains static HTML (as before)
  • .netlify/functions-internal/ contains Keystatic API function(s)
  • No build errors
  • npm run preview works locally (simulates SSR)

Deploy to Netlify (via git push or CLI):

netlify deploy --prod   # or push to main for auto-deploy

8. Smoke test the deployed admin UI

  • Visit https://docs.bread.coop/keystatic
  • Login with GitHub works (OAuth redirects correctly)
  • Admin UI loads with collections visible
  • Create a test entry, save — commit appears on GitHub
  • Edit an existing page, save — commit appears on GitHub
  • All static pages still render correctly (/, /about/, etc.)
  • Redirects still work (/token/about/bread-token, etc.)

Netlify manager action items

The following must be done by someone with Netlify dashboard access.
Fill in the actual values after #11 is complete.

  • Add env vars to Netlify dashboard:

    Variable Value (from .env after feat: switch Keystatic from Cloud to GitHub mode #11)
    KEYSTATIC_GITHUB_CLIENT_ID <TBD — from .env>
    KEYSTATIC_GITHUB_CLIENT_SECRET <TBD — from .env>
    KEYSTATIC_SECRET <TBD — from .env>
    PUBLIC_KEYSTATIC_GITHUB_APP_SLUG <TBD — from .env>
  • Verify Netlify site is on a plan that supports Functions (Functions are included on all Netlify plans, including Starter)

  • Verify Netlify Functions timeout is adequate — default 10s should be fine for Keystatic API calls (max: 26s on Starter, 30s on Pro)

  • Netlify site ID: <TBD> (needed for netlify link if running CLI locally)

  • Netlify team/account: <TBD>

Rollback plan

If the SSR deployment causes issues:

  1. Revert the astro.config.mjs change (re-add the production guard)
  2. Set ENABLE_KEYSTATIC=false in Netlify env vars
  3. Redeploy — site returns to fully static mode
  4. Admin UI remains available via local dev only

Reference

Metadata

Metadata

Labels

enhancementNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions