Working on the core
Keep the docs current
Section titled “Keep the docs current”Update the docs in the same commit as the change:
CORE.md: what the core does and where. Sites read it, so describe the core, never a particular site.AGENTS.md: how to work on the core.README.md: setting up a new site and taking core updates.docs-site/: this site.
Where things go
Section titled “Where things go”- Nothing site-specific in core code. Core code reads a site only through
src/lib/site. - Sites keep their own blocks, patterns and templates where those are their design; only general features belong in the core.
- The example site follows the same layout as every site and must keep working with every change.
- Styles use role tokens only, never a site’s palette names.
- Name code, data, URLs and classes for what they are here, not after another system’s names.
Adding a block
Section titled “Adding a block”- Renderer in
src/render/c/: a string of HTML withc-*classes, added tosrc/render/blocks/index.ts. - Data it needs: a
caseinwalk()insrc/render/prepare.ts. Load intoctx.data; add page scripts toctx.scripts/ctx.headScripts, vendor CSS throughctx.assets. - Editor: a panel in
CONTENT_PANELSand a label inCONTENT_LABELS(src/puck/panels.tsx), its category inbuildConfig(src/puck/config.tsx). Containers go inCONTENT_CONTAINERS. - Browser behaviour: a script in
public/assets/js/that enhances the server markup. - Styles in
src/styles/site.css, and a section inCORE.mdand these docs.
Changing stored content
Section titled “Changing stored content”Content changes the core needs (renamed props, moved settings) ship as a core content migration
(scripts/migrate-content.ts; migrateCollectionLayout in src/lib/content/collections.ts is one)
or a SQL migration (db/migrations/) that edits the JSON.
- Walk the JSON properly (a content migration in TypeScript). The key order of stored blocks varies,
so text patterns miss some. The query layer fills
body_textwhen content is written, so changes to entries’ content belong in content migrations. - Keep updated dates: drop the table’s
*_touchtrigger before the update and create it again after (as*_init.sqlhas it). It fires whenever a write leavesupdated_atas it was, setting it to itself included. - Name migrations with the time they are written (
YYYYMMDDHHMMSS_name.sql).
When a site migration switches block types or settings, the site’s code and its database change must
land together: push the code, wait for the deploy, then npx wrangler d1 migrations apply DB --remote at once.
Workflow
Section titled “Workflow”- Change and commit in
cfpuck-cms.npm run checkandnpm testmust pass (a fix comes with a test intests/that fails without it). - Test on the example site and, for anything real content exercises, in a site: merge the core change there without committing and test.
- Push
cfpuck-cms(it deploys nothing), then in each sitegit fetch upstream && git merge upstream/mainand commit. The site’s owner pushes the site, which deploys it.
npm test runs the unit tests in tests/ with Vitest. A test imports the code
it tests and calls it; nothing needs a dev server. Each test file gets a fresh database made from
db/migrations, so tests that read and write through the query layer (siteDb(), or sessionDb() to
act as an editor or admin) start from empty tables and see real SQLite.
Every statement a test sends is checked against D1’s limits before it runs: 32 arguments per SQL function, LIKE and GLOB patterns of 50 bytes, 100 bound values, 100 KB of SQL and 2 MB per value. Local SQLite allows more, so without this a query that works on your machine can fail on the first deploy. A test that breaks a limit fails with “D1 would refuse this statement” and the reason.
Put a test beside the others of its area (tests/db-policies.test.ts, tests/security.test.ts…),
and give every fix one that fails without it. CI runs npm test on every push.
Checking changes
Section titled “Checking changes”- Render the affected pages before and after and diff the HTML (ignore cache-busting
?v=values and shuffled collections). Screenshot intended changes on desktop and phone. Lazy-loaded images need the page scrolled before a full-page screenshot. - Browser tests use Playwright from a script inside the project, signing in as
LOCAL_ADMIN_EMAIL/LOCAL_ADMIN_PASSWORD. POSTs to Astro need anOriginheader (CSRF check). The editor canvas isiframe#preview-frame; blocks that can’t be dragged report as disabled, so tests click them withforce. - Checks that need one site’s content live in that site’s
src/site/scripts/.