Astro + TinaCMS site, pnpm. Visual editing branch of tina-astro-starter.
Work flows through GitHub, not direct local commits to main. For each task:
- Start from latest
main(git checkout main && git pull --ff-only) - Create a branch with a typed prefix:
feature/...,fix/...,chore/...,docs/... - Make the change, commit, push the branch
- Open a PR against
mainwithgh pr create --base main --head <branch> --title "…" --body-file <path> - Wait for automated reviews (Copilot, CodeRabbit) and address their feedback in follow-up commits on the same branch
- Treat the task as done only when the user merges the PR in GitHub
- After merge,
git checkout main && git pull && git branch -d <branch>before starting the next task
Don't bundle unrelated changes into one PR. If you spot something worth fixing while doing task A, open a separate branch + PR for it (or note it as a follow-up).
The only exception is hotfixes the user explicitly authorises.
Every piece of content on every page must be editable via the Tina editor (/admin/). Never hardcode user-visible text, images, links, or list items into .astro files. If you're about to type a sentence the user might want to change later, stop and model it as a Tina field first.
This means:
- New section → new fields in a Tina collection, new data in the matching content file, then read it in the component
- New page → new collection (or new file in an existing collection) before any markup
- Every rendered text node should have a
data-tina-field={tinaField(parent, 'fieldName')}so inline editing works - Lists render from
data.collection.items.map(...), not from JSX literals — and each item's editable fields get their owntinaField()
The only things that are OK to hardcode are decorative chrome (icons, dots, divider lines) and structural CSS classes.
- Astro 5 — file-based routing under
src/pages/, components insrc/components/ - TinaCMS 3 (visual-editing canary) —
@tinacms/astrointegration, schema intina/, content insrc/content/ - pnpm 10 (pinned via
packageManager) — package manager; native build approvals inpnpm.onlyBuiltDependencies(package.json) - Font Awesome 6 for icons, Inter variable font, design tokens in
public/assets/colors_and_type.css - Tina's local backend uses
better-sqlite3(native);sharppowers Astro images. Both pre-approved inpnpm.onlyBuiltDependencies(package.json).
tina/
config.ts # Register collections here
collections/ # One file per content type
src/
content/
landing/home.json # Homepage data — every section editable
page/*.mdx # Other pages (rich-text body)
blog/*.mdx # Blog posts
config/config.json # Global site config
components/islands/ # Editable regions — re-rendered live by Tina bridge
lib/
data.ts # requestWithMetadata-wrapped Tina queries
islands.ts # Island registry (one entry per editable region)
pages/ # Astro routes
styles/ # Global CSS
public/
assets/ # Logos, fonts, design tokens CSS
admin/ # Tina admin UI (built artifact + bridge.js)
- Schema: add fields to the relevant collection in
tina/collections/*.ts(useobject,list: true,ui.itemPropsfor list labels) - Content: add the corresponding data to the matching file in
src/content/... - Render: in the island component, map over
data.section.itemsand wrap each editable text withdata-tina-field={tinaField(item, 'fieldName')} - No new collection needed? Skip to step 2-3
- New collection? Create the file, import it in
tina/config.ts, add aget<Name>query insrc/lib/data.ts, and an island entry insrc/lib/islands.ts
pnpm install # native builds run automatically (pnpm.onlyBuiltDependencies)
pnpm run dev # Tina dev server (port 4001 GraphQL) + Astro (port 4321)
pnpm run build # production build (Tina + Astro); needs Tina cloud creds
pnpm run build:local # fully local build, no cloud creds# Serve the built site in the real Workers runtime (workerd) via wrangler dev —
# exercises asset serving, the Tina SSR edit endpoint and bindings like CF does.
pnpm run build:local && pnpm run preview:cf # http://localhost:8787Visit /admin/index.html to open the Tina editor. Inline visual editing works on any element with a data-tina-field attribute.
- Keep islands lean — they SSR and re-render on edits, so heavy logic belongs upstream in
lib/ - For list items, pass the item object (not the parent + index) into
tinaField()so the editor can scope edits correctly - Don't break the homepage rule by adding a "for now" hardcoded string. Add the field.
- Use Font Awesome class names as strings in content (
"fa-robot") — icons render with<i class={\fa-solid ${icon}`}>` - Use US English, sentence case, Inter font, brand red
#cc4141. No emoji in UI copy.
After any content-structure change, restart the dev server so Tina regenerates tina/__generated__/{client,types}.ts. Without a restart, the GraphQL client and TypeScript types lag behind the schema.
When verifying inline editing, open /admin/ and confirm: (a) the new fields appear in the sidebar, (b) edits propagate live to the page preview, (c) saving writes the expected JSON/MDX on disk.
-
Every page must include
<ClientRouter />in the<head>. The Tina bridge listens onastro:page-loadto re-sync forms; without it, the initial prime succeeds but live edits in the sidebar never reach the iframe.Base.astroalready includes it viaBaseHead.astro— pages that bypass Base (like a custom shell) must addimport { ClientRouter } from 'astro:transitions'and render<ClientRouter />themselves. -
Tina Cloud's schema lags the repo. Adding a new field to
tina/collections/*.tsonly takes effect locally withpnpm run build:local(uses--local, in-process datalayer against the repo). Cloudflare'spnpm run buildscript uses--content=local --skip-cloud-checks, but the generated GraphQL client still issues queries against Tina Cloud's schema at prerender time. If Tina Cloud doesn't know about the field, the query fails:[@tinacms/astro] client query failed Cannot query field "<newField>" on type "<...>".getLanding(...)returns null, the route'sif (!data) return new Response('Not Found', { status: 404 })fires, and the 9-byte "Not Found" body gets prerendered asdist/<route>/index.html. The deploy "succeeds" but the URL serves "Not Found".Symptoms to recognise: the page returns HTTP 200 with
content-type: text/htmland a body that's literallyNot Found. Other routes that don't query the new field still work, so it looks like a routing bug — but it's actually a Tina-side schema mismatch.Before pushing a PR that adds a new schema field:
- Reproduce the prod build locally —
rm -rf dist && SITE_URL=https://... pnpm run build— and checkdist/<affected-route>/index.htmlis real HTML, not the 9-byte body - If it errors with "Cannot query field", trigger a Tina Cloud schema sync (Settings → Rebuild on app.tina.io) before the Cloudflare deploy goes out
- Or split it into two PRs: schema-only first (so Tina Cloud rebuilds), then a follow-up that reads the field
This bit us in PR #14 → #15 on 2026-05-22. See PR #15's description for the full incident.
- Reproduce the prod build locally —