diff --git a/.github/workflows/playwright-react-kitchen-sink.yml b/.github/workflows/playwright-react-kitchen-sink.yml new file mode 100644 index 0000000000..3fcd0eebaf --- /dev/null +++ b/.github/workflows/playwright-react-kitchen-sink.yml @@ -0,0 +1,38 @@ +name: Playwright — React kitchen-sink + +on: + pull_request: + branches: + - main + paths: + - 'examples/react/kitchen-sink/**' + - '.github/workflows/playwright-react-kitchen-sink.yml' + +jobs: + e2e: + name: react-kitchen-sink-e2e + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Node.js environment + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + package_json_file: package.json + run_install: false + - name: Install dependencies + run: pnpm install + - name: Build workspace packages + run: pnpm build + - name: Install Playwright browsers + working-directory: examples/react/kitchen-sink + run: npx playwright install --with-deps chromium + - name: Run React kitchen-sink Playwright tests + working-directory: examples/react/kitchen-sink + run: npx playwright test + env: + CI: true diff --git a/AGENTS.md b/AGENTS.md index 824efe0609..a27961974d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,6 +40,7 @@ tests/ # Build verification tests - **Linting/Formatting:** Biome (`biome.json` at root). Example apps extend with `"extends": ["../../../biome.json"]` - **TypeScript:** Base config at `base.tsconfig.json`. Examples extend it. Strict mode enabled. - **Package manager:** pnpm only. Never use npm or yarn. +- **`CLAUDE.md` files** are git symlinks to the sibling `AGENTS.md`. On Windows without Developer Mode, if `git status` shows `TT` typechanges on them, run `git config --local core.symlinks false` — git then materialises them as regular pointer files. Linux/macOS clones get real symlinks automatically. ## Kitchen-Sink Examples diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 47dc3e3d86..0000000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -AGENTS.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/examples/AGENTS.md b/examples/AGENTS.md new file mode 100644 index 0000000000..b80bac6032 --- /dev/null +++ b/examples/AGENTS.md @@ -0,0 +1,123 @@ +# TinaCMS Examples + +Framework example apps that demonstrate TinaCMS against the same content model. Each app is independently runnable and has its own AGENTS.md covering framework-specific patterns. This file documents everything **shared** across them — per SSW, agents auto-inject every parent AGENTS.md up the path, so children should stay delta-only. + +## Apps + +- [next/kitchen-sink](next/kitchen-sink/AGENTS.md) — Next.js 15 App Router with `useTina()` visual editing. +- [next/tina-self-hosted-demo](next/tina-self-hosted-demo/AGENTS.md) — Next.js Pages Router with self-hosted backend (MongoDB, GitHub, `tinacms-authjs`). **Does not consume shared content** — keeps its own `content/`. +- [astro/kitchen-sink](astro/kitchen-sink/AGENTS.md) — Astro 5 static site with zero-JS production output. +- [hugo/kitchen-sink](hugo/kitchen-sink/AGENTS.md) — Hugo Extended static site, admin-panel editing only. +- [react/kitchen-sink](react/kitchen-sink/AGENTS.md) — Vite + React 18 SPA with runtime Tina client queries. + +Shared content lives under [shared/](shared/) (no AGENTS.md — its contract is documented here). + +## Shared Content Contract + +Content files and shared media live in [shared/](shared/) as a single source of truth. Every kitchen-sink app reads the same files. The self-hosted demo does NOT — it keeps its own content for the auth/DB walkthrough. + +### How each app consumes shared content + +| App | Content | Static assets | +|-----|---------|---------------| +| Next / Astro / React | `localContentPath: '../../../shared'` in `tina/config.tsx` | `public/uploads` and `public/blocks` symlinks to `../../../shared/public/*` | +| Hugo | `[[module.mounts]]` in `hugo.toml` (NOT symlinks — Hugo's Go file walker doesn't follow directory symlinks on Windows) | `static/uploads` and `static/blocks` symlinks to `../../shared/public/*` | + +TinaCMS's `assertWithinBase` security check rejects symlinks that resolve outside the project root, which is why content is accessed via `localContentPath` (TinaCMS-aware) or Hugo module mounts instead of filesystem symlinks. Only static asset directories are symlinked — TinaCMS doesn't walk those. + +### Shared directory layout + +``` +shared/ +├── content/ +│ ├── authors/ # .md — markdown frontmatter with avatar, hobbies +│ ├── blogs/ # .md — hero image, dates, author ref, body +│ ├── global/ # .json — header nav, footer links, theme settings +│ ├── pages/ # .md — block-based (hero, features, cta, testimonial, content) +│ ├── posts/ # .md — author ref, tags, nested folders allowed +│ └── tags/ # .json — simple tag records +├── public/ +│ ├── blocks/ # Block preview images used in the admin UI +│ └── uploads/ # User-uploaded media +└── .gitignore # Ignores /content/**/e2e-* +``` + +## Shared TinaCMS Patterns + +The kitchen-sink apps (everything except the self-hosted demo) share these TinaCMS conventions. Children document only their framework-specific wrapping. + +### `useTina()` + `tinaField()` + +Canonical client pattern: feed the query result into `useTina()`, then attach `data-tina-field={tinaField(node, 'fieldName')}` to any element that should be click-to-edit in the admin iframe. + +```tsx +const { data } = useTina({ data, query, variables }); +

{data.post.title}

+``` + +Per-framework specifics (Next App Router server/client split, Astro `client:tina` directive, React `useTinaQuery` hook) live in each app's AGENTS.md. Hugo doesn't use this — it's admin-panel-only. + +### Block dispatcher + +Page collection uses a block dispatcher under `components/blocks/index.tsx` (or `layouts/partials/blocks/` for Hugo). It maps the GraphQL `__typename` (e.g. `PageBlocksHero`) to a block component. The 5 block templates are `hero`, `features`, `cta`, `testimonial`, `content`. Block schemas live in `tina/schemas/blocks.ts` (shared across React-ish apps); Hugo inlines the equivalent in `page.tsx` via cascade. + +### `TinaMarkdown` custom components + +Rich-text bodies render via ``. Every React-ish app (Next / Astro / React) ships the same four custom components: + +- `code_block` — lazy-loads Prism from `tinacms/dist/rich-text/prism` to keep the highlighter out of the main bundle. +- `BlockQuote` — nests another `TinaMarkdown` for the quote body, with optional author attribution. +- `DateTime` — formats the current date (iso / utc / local). +- `NewsletterSignup` — UI-only stub with a `TODO: integrate with an actual newsletter service`. + +Hugo renders body content via Goldmark instead and does not support these custom templates. + +### Image sanitisation + +CMS-controlled image and link URLs must pass through `sanitizeImageSrc()` / `sanitizeHref()` (in each app's `lib/utils.ts` or `src/lib/utils.ts`) to block `javascript:`, `data:`, `vbscript:`, and protocol-relative URLs. Hugo uses inline `hasPrefix` checks in templates for the same purpose. Don't render raw `data.*.image` / href values without running them through sanitisation. + +### Tailwind theme + +Theme colors use CSS custom properties driven by a `data-theme` attribute on the root div. Each app's `tailwind.config.js` maps `theme-*` utilities (`bg-theme-400`, `text-theme-600`) to `var(--theme-*)` tokens declared in its global CSS. The `data-theme` value (e.g. `blue`, `teal`) comes from the Global collection's theme settings. Dark mode uses Tailwind's `dark:` variant with class-based toggling. + +### TinaCMS admin UI — selectors and behaviour + +These patterns are not obvious from TinaCMS source; they were discovered through E2E debugging and apply to every app's admin panel. + +- **"Enter Edit Mode" dialog:** Appears once per browser context on first admin visit. Target with `button[data-test="enter-edit-mode"]`. State persists in localStorage — won't reappear after dismissal in the same context. Each Playwright test gets a fresh context, so the dialog appears on every test. +- **Error modals:** Render in `#modal-root` with a backdrop that blocks all pointer events. Close buttons match `#modal-root button:has-text("Close")` or `#modal-root button:has-text("OK")`. +- **Save button states:** `opacity-70 cursor-wait` while submitting, `opacity-30 cursor-not-allowed` when pristine, `pointer-events-none` when validation fails. Shows `LoadingDots` during save, "Save" text when idle. +- **Field labels:** Standard fields use `