Skip to content

Add Keystatic CMS integration - #74

Open
github-actions[bot] wants to merge 6 commits into
mainfrom
i73-20260625-1040
Open

Add Keystatic CMS integration#74
github-actions[bot] wants to merge 6 commits into
mainfrom
i73-20260625-1040

Conversation

@github-actions

@github-actions github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

1. Target

2. Specification / Test Plan

Adds Keystatic as a CMS option alongside microCMS.

Specification

  • Available on Astro bases only (astro, astro-react, astro-vue); excluded from Nuxt/Vite via the registry's excludeGroups (hidden in the wizard, warns + falls back to none in the CLI).
  • Generates a root keystatic.config.ts using local storage (content as files in the repo), with a commented GitHub-mode block + docs link for switching later.
  • The keystatic() integration is mounted in dev only...(process.env.SKIP_KEYSTATIC ? [] : [keystatic()]). The admin UI is served at /keystatic during dev; the production build script sets SKIP_KEYSTATIC=true so the site stays fully static and needs no server adapter. This means the existing Cloudflare deploy path is untouched.
  • The React renderer (@astrojs/react + react/react-dom + types) is auto-injected, since Keystatic's admin UI is React-based — even on astro and astro-vue bases.
  • Pinned versions verified against npm/Keystatic for the repo's Astro 6 / React 19 stack: @keystatic/astro@5.1.0, @keystatic/core@0.5.50, cross-env@^10.1.0.

Implementation notes

  • CMS templates re-keyed by provider: templates/cms/{cms}/{group} (existing microCMS files renamed under microcms/, history preserved via git mv).
  • scaffold.go: hardcoded "no CMS on vite" check replaced with a generic registry-driven excludeGroups guard.
  • New CMS.IsValidForBase helper + create-command validation/fallback.
  • astro.config.mjs.tmpl reworked for the new integration combinations (also fixes a latent missing-comma bug in the generated config).

Test Plan

Automated (go test ./..., all green):

  • keystatic on plain astro injects the React renderer + cross-env/types.
  • keystatic on astro-vue adds react() alongside vue().
  • keystatic on astro-react does not duplicate React deps.
  • build script = cross-env SKIP_KEYSTATIC=true astro build.
  • CMS.IsValidForBase matrix (astro* → true, nuxt/vite → false).

Manual (scaffolded real projects with the built binary):

  • plain astro / astro-vue / astro-react + keystatic → correct deps, integrations, and keystatic.config.ts; all generated astro.config.mjs pass node --check.
  • nuxt + keystatic → warns and falls back to none (no keystatic files emitted).
  • microCMS (astro + nuxt) regression → still scaffolds correctly after the template move.

3. Additional Instructions / Notes for Shipping (optional)

Keystatic + Cloudflare-managed deploy is intentionally not a special case — because production builds are static, the existing Cloudflare Pages/Workers flow works as-is. GitHub/Cloud storage modes are out of scope for this PR (documented switch path in the generated config).

4. Check before Review Request

  • Self Review : I reviewed changes by myself and approved them.
    • Ensure there is no sensitive information, typos, unrelated changes, or debugging code.
  • Evidence : I attached evidences to prove the changes.
    • Record and attach a demo video. For minor changes, attaching an image is also acceptable.
    • Evidences should be updated to the latest version when further changes are made.

5. Evidence

(Attach here before request review)

Add Keystatic as a CMS option alongside microCMS, scoped to Astro bases
(astro, astro-react, astro-vue) and excluded from Nuxt/Vite.

- Generate keystatic.config.ts with local storage (commented GitHub block)
- Mount the keystatic() integration in dev only; production build sets
  SKIP_KEYSTATIC=true so the site stays fully static and needs no adapter
- Auto-inject the React renderer (Keystatic's admin UI is React-based)
- Re-key CMS templates by provider: templates/cms/{cms}/{group}
- Replace the hardcoded vite CMS guard with a registry-driven excludeGroups
  check; add CMS.IsValidForBase and create-command validation/fallback
@VibratingKoala

Copy link
Copy Markdown
Collaborator

Code review — Keystatic feature (commit 9ec3861)

Reviewed for correctness bugs, broken combinations, template-rendering edge cases, and microCMS regressions. Build/vet/tests pass; all generated astro.config.mjs combinations were verified valid via node --check, and microCMS (astro + nuxt) still scaffolds correctly after the template move. Findings below, triaged by materiality.

🟡 Worth fixing

  1. React versions hardcoded in Go duplicate the registrypkg/packagejson.go
    The Keystatic cross-cutting rule hardcodes @astrojs/react / react / react-dom / @types/* versions that already live in the astro-react base entry. Fix: move the React package set and the build script override into the registry keystatic entry, then delete the Go rule. The cms merge already overwrites build (it runs after the base), so cross-env SKIP_KEYSTATIC=true astro build works from the registry. Makes Keystatic a near-pure registry addition (per the "registry is the single source of truth" convention) and removes ~12 lines of Go.

  2. GetGroup lacks a nil-checkpkg/config.go:44 (latent)
    Sibling methods (IsAstro/IsVite) guard entry == nil; GetGroup dereferences entry.Group directly. Not reachable today (CLI validates Base first), but the new scaffold.go call site newly exercises it. 3-line defensive fix.

🔵 By design — not bugs

  1. No SSR adapter; admin only works in dev — intentional dev-only/static design (admin in dev, production build stays static via SKIP_KEYSTATIC=true, no adapter, existing Cloudflare deploy untouched). One fair sub-point: the commented github-mode block in keystatic.config.ts could read as "uncomment to deploy the admin" — deploying the admin also needs an adapter. Worth a one-line doc clarification, not a code change.

🟢 Latent / low priority

  1. Removed the unconditional "no CMS on Vite" guard — now relies on each CMS declaring excludeGroups. A future CMS added without excludeGroups:["vite"] would fail obscurely. No live bug (both current CMSes declare it). Mitigation: a clean existence-check error or document the convention.

  2. CMS.IsValidForBase only checks excludeGroups, not requiresIntegration — the TUI checks both. No live bug (no CMS uses requiresIntegration), but CLI/TUI could diverge if one ever does.

Scope question (not a defect)

  • No content-consumption pipeline. We scaffold keystatic.config.ts + an empty src/content/posts/ but no src/content.config.ts or sample page, so content authored in /keystatic has no wired render path. This was explicitly scoped out of v1 in the issue ("content-reader example pages"), so it's a known cut. Options: leave as-is (admin + storage work; rendering documented as the user's step) or add a minimal content.config.ts + sample page so getCollection('posts') works out of the box.

Suggested action: apply #1 + #2 + the #3 doc clarification (clear, low-risk wins); decide separately on the content pipeline.

Move the React renderer packages and the SKIP_KEYSTATIC build-script
override into the keystatic registry entry, and delete the Go
cross-cutting rule. The cms merge runs after the base, so the build
override applies via the registry. Keeps package versions in the
registry single source of truth instead of duplicated in Go.
GetBase returns nil for an unrecognized base; dereferencing entry.Group
would panic. Return an error instead, matching the nil-registry guard
already in this method and the entry-nil checks in IsAstro/IsVite.
@VibratingKoala

Copy link
Copy Markdown
Collaborator

Review follow-up

Fixed (two commits):

  • bf0cf6c — moved Keystatic's React packages + SKIP_KEYSTATIC build override into the registry entry and deleted the Go cross-cutting rule (item Fix template issue #1). Verified behavior-preserving across all three Astro bases; vite/nuxt still fall back to none so the build override can't leak onto a non-Astro base.
  • 9c5350b — added the entry == nil guard in GetGroup (item Fix template issue #2).

Acknowledged, deferred (not fixing in this PR):

  • Vite-CMS guard is now data-dependent — replacing the hardcoded IsVite() rejection with the registry excludeGroups check means "no CMS on Vite" holds only while every CMS entry lists excludeGroups: ["vite"]. Both current CMSes do. A future CMS added without it would fail with an obscure error instead of a clear message. Possible later hardening: a templates/cms/{cms}/{group} existence check that returns a friendly "not supported" error for any combo.
  • CMS.IsValidForBase only checks excludeGroups, not requiresIntegration — the TUI checks both. No divergence today (no CMS uses requiresIntegration); revisit if/when a CMS gains an integration requirement.
  • build script override replaces rather than wraps — now flows through the standard script merge (consistent with every other option). Latent only if a future option also contributes a build step.

All three are future-maintainer concerns with no current reachable path; tracking here rather than fixing now.

The jsx/jsxImportSource compiler options were gated on Base==astro-react,
but Keystatic pulls in the React renderer on plain astro and astro-vue
too. Gate on React being present (astro-react OR keystatic) so .tsx and
astro check type-check correctly. Mirrors the astro.config integration
condition.
@VibratingKoala

Copy link
Copy Markdown
Collaborator

Re-review (full diff vs main, tip 9c5350b → now e81260e)

Independent high-effort re-review of the whole feature, including the two prior fixes. One new correctness bug found and fixed; everything else is already-tracked or non-issues.

🔴 Fixed — new bug

JSX tsconfig options gated on the wrong conditionconfig/templates/base/astro/tsconfig.json.tmpl
jsx/jsxImportSource were gated on Base == "astro-react", but Keystatic adds the React renderer on plain astro and astro-vue too. Those projects shipped React deps but no JSX config, so .tsx files (and astro check/editor) failed type-checking. Same class as the astro.config.mjs.tmpl integration injection — the tsconfig was missed.

Fixed in e81260e: gate on or (eq .Base "astro-react") (eq .CMS "keystatic"). Verified:

  • astro + keystatic → jsx: react-jsx ✓ (was missing)
  • astro-vue + keystatic → jsx: react-jsx ✓ (was missing)
  • astro-react (no cms) → unchanged ✓
  • astro (no cms) → no jsx ✓ (no regression)
  • all generated tsconfig.json valid JSON ✓

Already known / non-issues

  • Build override no longer guarded by IsAstro at the builder level — same as deferred (a)/(c). Verified unreachable: vite/nuxt + keystatic fall back to none via create.go before BuildPackageJSON.
  • No fs.Stat existence guard on the CMS template block — this is the hardening already noted for deferred low-pri Fix template issue #1.
  • React versions duplicated across the astro-react base and keystatic entries — accepted tradeoff of moving packaging into the registry.
  • GitHub-mode repo: {{.ProjectName}} could be a path — cosmetic; commented-out and matches the existing wrangler.jsonc pattern.

Verifier refuted the higher-severity candidates: fields.markdoc needing @markdoc/markdoc (it's self-contained), the peer-version mismatch, and "corrupt package.json left on disk."

Scaffold already fetches and nil-guards the base entry at the top, so
the CMS block can use entry.Group directly instead of re-deriving it via
GetGroup — removing a redundant registry scan and an unreachable error
path.
@VibratingKoala

Copy link
Copy Markdown
Collaborator

Re-review #3 (full diff vs main, tip e81260e → now c5db3aa)

Independent high-effort pass over the whole feature. No new correctness bugs. One small cleanup applied; everything else is by-design or already-tracked.

Applied — cleanup

scaffold.go re-derived the base group via GetGroup when Scaffold already holds the nil-guarded base entry. Switched the CMS block to use entry.Group directly — removes a redundant registry scan and an unreachable error path. No behavior change. (c5db3aa)

Reviewed — not a bug (by design)

astro-vue + keystatic registers both vue() and react() (flagged CONFIRMED by the reviewer, which lacked design context). This is the agreed scope: Keystatic supports all Astro bases, and its admin UI is React-based, so React is auto-injected even on Vue/vanilla Astro. Astro is multi-framework, so this is coherent. excludeGroups: [nuxt, vite] works as intended. No action.

Maps to already-deferred items

  • No IsValidForBase guard inside BuildPackageJSON (Form/Query/State have one) → deferred (d); verified unreachable via CLI/TUI fallback.
  • Guard skips when GetCMS returns nil for an unknown CMS value → deferred (g).
  • src/content/posts/ is Astro's reserved content dir with no content.config.ts → facet of deferred (f). Noting one nuance to weigh when that's picked up: a generated project may emit content-collection warnings on dev/build.

Refuted (good signals)

Verifier refuted the version-mismatch candidates again: @astrojs/react@^5.0.3 on Astro 6, and @keystatic/astro@5.1.0 / @keystatic/core@0.5.50 peer-deps — all compatible.

Verdict: PR is solid; no outstanding correctness issues. Remaining items are the intentionally-deferred low-priority notes and the content-pipeline scope decision.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Keystatic CMS integration

2 participants