phage-cms
v0.1.10
Published
Astro integration: Phage CMS (Cloudflare KV/R2, block editor, API routes)
Maintainers
Readme
phage-cms
Astro integration for Phage: block CMS editor, Cloudflare KV + R2 APIs, /phage admin UI, and published-content helpers.
- Runtime: Workers (SSR) via
@astrojs/cloudflare - UI: React islands (
client:only="react"editor) - Host app: You provide
src/blocks,src/collections,src/site-options,src/taxonomies(minimal stubs are enough to boot).
Install from npm: npm install phage-cms
Requirements
| Requirement | Notes |
|-------------|--------|
| Node.js | >= 22.12 |
| Astro | v6.1+ recommended, output: 'server' |
| @astrojs/cloudflare | With KV + R2 bindings in wrangler.toml |
| @astrojs/react + react + react-dom | v19 |
| vite + wrangler | v6 / v4 (peer ranges in package.json) |
Quick start (blank Astro + Phage)
1) Create the project and add adapters
npm create astro@latest my-phage-site
cd my-phage-siteChoose TypeScript, and a template that is not “static only” (or start empty and configure below).
Add Cloudflare + React:
npx astro add cloudflare react2) Install Phage and peers
npm install phage-cmsIf npm does not already install peers, add explicitly:
npm install @astrojs/cloudflare @astrojs/react astro react react-dom vite wrangler3) Root wrangler.toml
Bindings must be named exactly KV and R2_BUCKET. Create wrangler.toml at the project root, for example:
name = "my-phage-site"
compatibility_date = "2024-05-01"
[[kv_namespaces]]
binding = "KV"
id = "<your-kv-namespace-id>"
[[r2_buckets]]
binding = "R2_BUCKET"
bucket_name = "<your-r2-bucket-name>"
remote = trueCreate namespaces with wrangler kv namespace create / wrangler r2 bucket create, then paste IDs / names.
4) astro.config.mjs
Use server output, Cloudflare adapter, and phageCms(). Remote R2 in dev, TipTap / use-sync-external-store shims, picomatch prebundle hints, and server.fs.allow for linked installs are applied inside phageCms()—you should not need extra vite boilerplate in the host.
// @ts-check
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import cloudflare from '@astrojs/cloudflare';
import phageCms, {
phageCloudflareRemoteBindingsForAstro,
viteReactPluginExcludeAllowPhageCms
} from 'phage-cms';
export default defineConfig({
output: 'server',
adapter: cloudflare({ remoteBindings: phageCloudflareRemoteBindingsForAstro(import.meta) }),
integrations: [react({ exclude: viteReactPluginExcludeAllowPhageCms }), phageCms()],
devToolbar: { enabled: false }
});viteReactPluginExcludeAllowPhageCms keeps the React plugin from skipping node_modules/phage-cms. If you add custom vite.resolve.alias, merge additively (do not replace the whole alias array or you can wipe Phage’s entries).
Advanced: use phageCloudflareRemoteBindings(projectRoot) if you already have an absolute project root string instead of import.meta.
Leave PHAGE_REMOTE_R2 unset for local Miniflare R2 during astro dev.
5) team.json (project root)
Editable allowlist for auth (not inside node_modules):
{
"members": [{ "email": "[email protected]", "role": "admin" }],
"linkExpiryHours": 24,
"sessionDays": 30
}6) .env.local (project root, do not commit)
Minimum for local editor:
PHAGE_ENV=local
PHAGE_JWT_SECRET=<long-random-string>Add PHAGE_REMOTE_R2=1 only if you intentionally want dev to hit real R2.
7) Minimal src/ layout Phage expects
Under src/ (or set phageCms({ srcDir: 'app' }) if your tree lives elsewhere):
| Path | Purpose |
|------|---------|
| src/blocks/index.ts | Re-export everything in PhageHostBlocksIndex (see phage-cms/host-blocks): builtinBlocks, blockPresets, phageBlockChromeSiteDefaults, registeredBlocks, blockRenderEntries, plus create page helpers below |
| src/blocks/config.ts | registeredBlocks (each with a preset), derived builtinBlocks / blockPresets / blockRenderEntries, and the Pages → Create page triplet |
Presets and new-page starters — Default field values for the editor and the “Create page” dialog are not defined inside phage-cms. Put them on each registeredBlocks entry as preset, expose a blockPresets map (type Record<string, Record<string, unknown>>), and wire the Pages list modal:
| Export | Purpose |
|--------|---------|
| pageCreateStartingOptions | { value, label }[] for the create-page segmented control |
| buildInitialPageBlocks(start) | Returns the initial blocks array for the new-page POST (usually { type, _id, ...clonedPreset }[]; 'blank' → []) |
| PageCreateStartId (type export) | String union of the values above; import it from @blocks/index in your app only if you want the same union in host code |
Import PhageHostBlocksIndex from phage-cms/host-blocks if you want a structural check that your config module exports everything Phage expects.
| src/collections/index.ts | Wire registeredCollections with createCollectionBindings from phage-cms/collections/content |
| src/collections/config.ts | registeredCollections array (can be [] for an empty CMS) |
| src/site-options/index.ts | registeredOptionPages, cloneOptionDefaults, getOptionsPageLabel, getOptionsPageDef |
| src/taxonomies/index.ts | registeredVocabularies, isRegisteredVocabularyId, re-export taxonomy types from phage-cms/taxonomies as needed |
The integration registers Vite aliases (@blocks, @collections, …) so imports from inside node_modules/phage-cms resolve to your src/.
8) tsconfig.json paths (recommended)
So astro check / the editor agree with Vite, extend compilerOptions:
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*", "node_modules/phage-cms/types/user-team.d.ts"],
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react",
"baseUrl": ".",
"paths": {
"@blocks": ["src/blocks/index.ts"],
"@blocks/*": ["src/blocks/*"],
"@collections": ["src/collections/index.ts"],
"@collections/*": ["src/collections/*"],
"@site-options": ["src/site-options/index.ts"],
"@site-options/*": ["src/site-options/*"],
"@taxonomies": ["src/taxonomies/index.ts"],
"@taxonomies/*": ["src/taxonomies/*"],
"@templates/*": ["src/collections/*"],
"@phage/schema": ["node_modules/phage-cms/schema.ts"],
"@phage/blockChrome": ["node_modules/phage-cms/blockChrome.ts"],
"@phage/linkField": ["node_modules/phage-cms/linkField.ts"],
"@phage/mediaUrl": ["node_modules/phage-cms/mediaUrl.ts"],
"@phage/pagePath": ["node_modules/phage-cms/pagePath.ts"],
"@phage/content": ["node_modules/phage-cms/collections/content.ts"],
"@phage/taxonomies": ["node_modules/phage-cms/taxonomies/index.ts"],
"@phage/forms": ["node_modules/phage-cms/forms/index.ts"],
"@phage/user-team": ["./team.json"],
"@phage/utils/markdown": ["node_modules/phage-cms/utils/markdown.ts"],
"@phage/utils/richTextLinks": ["node_modules/phage-cms/utils/richTextLinks.ts"],
"@phage/*": ["node_modules/phage-cms/*"]
}
}
}If you use pnpm, dependency paths can differ; point node_modules/phage-cms/... at the real install path or use workspace: / file: while developing the package.
9) Run
npm run devOpen /phage. With PHAGE_ENV=local, the middleware uses the local auth bypass.
Integration options
phageCms({
/** Folder under project root containing blocks, collections, site-options, taxonomies. Default `"src"`. */
srcDir: 'src',
/** Allowlist JSON path relative to project root. Default `team.json`. */
teamJsonFile: 'team.json'
})Publishing (maintainers)
From the monorepo root:
npm publish -w phage-cms --access publicpackage.json "files" controls the tarball; do not commit stray *.tgz inside phage-cms/.
Support
For a full reference site (pages, blocks, collections, options), use this repo’s src/ tree as a template alongside this README.
