> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuturuuu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# CMS

> Dedicated Tuturuuu CMS app for site editing, preview, publishing, media, and team access.

## Overview

`apps/cms` is the dedicated home for Tuturuuu CMS. It should feel like a
consumer-grade site editing product for nontechnical editors, even though it
continues to use the existing external-project implementation contracts behind
the scenes.

* It owns the CMS UI, routing, workspace picker, and root-level `/{wsId}/projects` linking console.
* It remains a satellite frontend. Protected APIs still live in `apps/web`.
* All CMS `/api/*` traffic is forwarded to `apps/web`.

## Product Vocabulary

Normal CMS product UI should use editor-friendly terms: site, site template,
connection, content, section, URL path, custom details, media, publishing,
preview, and team access. Avoid exposing implementation terms such as external
project, canonical, adapter, binding, slug, schema, metadata, profile data,
payload, or JSON in the primary UI.

When staff genuinely need exact implementation values, place them behind a
collapsed "Developer details" or internal root-console advanced panel. Code,
database names, API paths, tests, and operational docs can keep the existing
`external-project*` names so route and data contracts remain stable.

## Routing

* `/{wsId}`: overview
* `/{wsId}/games`: game-focused content library, visible only when CMS Games is enabled
* `/{wsId}/landing`: landing-page builder for hero/profile/page-section edits
* `/{wsId}/library`: content operations library for non-landing, non-game content
* `/{wsId}/library/entries/{entryId}`: compatibility redirect that opens the library fullscreen editor dialog
* `/{wsId}/library/collections/{collectionId}`: collection detail
* `/{wsId}/members`: workspace member and role management
* `/{wsId}/preview`: delivered-content preview
* `/{wsId}/settings`: legacy compatibility redirect to `/{wsId}/members`
* `/internal/projects`: internal site-template registry and workspace linking console
* `/play/{wsId}/webgl/{assetId}`: public WebGL package player. This route does not require a CMS login, but the proxied WebGL asset API still only serves the package when the owning CMS entry is published. WebGL player iframes must stay sandboxed without `allow-same-origin`.

When `wsId` resolves to the internal/root workspace, `/{wsId}` and other workspace-local routes should bounce to `/internal/projects` instead of trying to render the bound-workspace CMS surfaces.

The previous `/{wsId}/content`, `/{wsId}/collections/...`, and `/{wsId}/admin` CMS routes are removed. New entry points should link directly to the CMS-native route map above.

## Access Model

* Only workspaces with CMS access enabled appear in the CMS workspace picker.
* A workspace must still satisfy the existing external-project permissions to open CMS workspace routes.
* CMS Games is disabled by default for every workspace. Enable it from the CMS settings dialog, which writes `ENABLE_CMS_GAMES=true` to workspace configs and reveals the `/{wsId}/games` sidebar route.
* Only platform admins can open `/internal/projects`.
* Workspace member and role management lives on `/{wsId}/members`. Read paths resolve through CMS workspace access so editors with CMS access can load people, invites, roles, member defaults, and guest defaults without requiring direct workspace settings-route access. Write paths still respect member and role management permissions.

## API Ownership

* `apps/cms` rewrites `/api/:path*` to `apps/web`.
* The rewrite target must resolve to the central web app, not the CMS app origin. Prefer `INTERNAL_WEB_API_ORIGIN`, `NEXT_PUBLIC_WEB_APP_URL`, or `WEB_APP_URL` for the central origin. If a deployment-level `NEXT_PUBLIC_APP_URL` points at `cms.tuturuuu.com` or the current CMS Vercel preview URL, the CMS config ignores it and falls back to `https://tuturuuu.com` to avoid self-rewriting `/api/*` requests.
* New CMS backend behavior should be added in `apps/web`, then exposed through `packages/internal-api`.
* The root linking console reads the workspace-indexed binding summary from `GET /api/v1/admin/external-project-bindings`.
* The root linking console may load its initial server-render payload directly with the request-scoped CMS Supabase admin path after `getCmsWorkspaceAccess` proves root admin access. Avoid server-to-server Internal API fetches during that initial render, because production auth/origin forwarding failures turn `/internal/projects` into a hard RSC error page. Keep workspace-secret lookups batched; one `.in('ws_id', allWorkspaceIds)` query can exceed PostgREST/proxy URL limits and return `400 Bad Request` in production.
* Backend naming stays on `external-project*` contracts even though the product surface is branded as Tuturuuu CMS.
* Add or update the CMS product-copy hygiene test whenever new visible CMS
  strings are introduced. The test should protect the consumer vocabulary while
  allowing collapsed developer-detail and internal-only exceptions.

## Content Model

* Collections remain the content-type container. Field definitions live in `workspace_external_project_field_definitions`, while entry values continue to be stored in the existing `profile_data` and `metadata` JSON payloads so delivery and sync payloads stay backward-compatible.
* Field definitions support the manifest field contract: `string`, `markdown`, `number`, `boolean`, `date`, `datetime`, `json`, and `string-array`. Scope controls whether the value is saved under `profile_data` or `metadata`.
* The CMS Library has a `Content model` section with reusable templates for Profile, Blog Posts, Gallery, Shop Products, Writing Worlds, and Social Links. Applying a template creates the collection if needed, stores the collection schema, and creates any missing field definitions without deleting CMS-only fields.
* Landing and Library must stay visibly distinct. Landing is a page-builder surface for landing-only sections, preview-first actions, and page readiness. Library is a content operations surface for search, status, bulk publishing, collection health, and non-landing/non-game content. Keep `landing`, `library`, and `games` capability scopes mutually distinct in tests.
* The Library command center is the consolidated authoring entry point for content, content models, workflow queues, and settings. Keep new CMS operations reachable from that surface before adding another isolated toolbar or settings panel.
* Entry detail forms render schema-aware controls for the active collection field definitions. Authors should edit those typed controls instead of hand-editing raw `profile_data` or `metadata` JSON for modeled fields.
* Body and asset affordances should be driven by the collection schema when available. Naming heuristics for legacy Yoola collections remain as compatibility fallback only.
* External-project setup and sync import manifest `profileFields` and `metadataFields` into DB field definitions. Sync snapshots are built from DB-managed field definitions, and field removals should only be applied during an explicit destructive sync.

## Yashie Bridge

* Yashie is the proof case for the generic content model. Its manifest seeds profile, blog post, gallery, shop product, writing world, and social-link collections and field definitions.
* This phase does not change `../yashie` runtime pages. Yashie can be seeded and managed from CMS, but the public Yashie app continues to read its local static data until a later integration pass consumes CMS delivery payloads.
* Shop product entries are catalog-only in this phase. No checkout, payment, inventory, or commerce fulfillment behavior is implied by the template.

## Client Architecture

* Keep route pages server-side only for auth and access gates, then hand off interactive CMS surfaces to client modules backed by TanStack Query.
* Keep `cms-studio-client.tsx` focused on query state, mutations, and routing. Library rendering should stay split across dedicated gallery, workflow, and settings modules rather than growing back into one large TSX file.
* Keep CMS team access clients on `@tuturuuu/internal-api` helpers that target external-project-aware `apps/web` routes. Do not call the standard workspace members or roles settings endpoints directly from `apps/cms`.
* Entry detail now opens as a fullscreen dialog from the library route so the CMS keeps the library cache warm instead of forcing a separate navigation for each entry.
* The Games route reuses the library client with a game-like collection scope and only exposes WebGL ZIP upload controls when `ENABLE_CMS_GAMES` is explicitly enabled.
* When a user creates the first Games entry, CMS should automatically create the workspace `Games` collection and then open the new game entry instead of requiring a manual collection setup step.
* Published game entries expose a public `/play/{wsId}/webgl/{assetId}` link from the WebGL package card. Keep this route login-free in the CMS proxy, and keep publication checks in the central `apps/web` WebGL asset route so drafts and archived entries do not leak assets. The player iframe and the served WebGL document must both use sandboxing that allows scripts but does not allow same-origin access.
* CMS media uploads show per-file progress for cover images, gallery media, and WebGL ZIP packages. Cover and gallery media must use the external-project asset app-server upload route so Tuturuuu measures the actual bytes before writing storage objects, then store the returned Drive path on the CMS asset row. WebGL ZIP package uploads still upload directly to the signed storage URL returned by the self-hosted web API, then call the WebGL package finalize route so the ZIP can be extracted and mapped to a playable artifact. The WebGL asset-serving route must infer browser content types from the requested WebGL filename for known outputs such as `index.html`, `.js`, `.jpg`, `.wasm`, `.data`, and compressed `.gz`/`.br` files, because extracted storage metadata can be too generic or incorrectly recorded as `text/plain`. Served WebGL files must include a CSP `sandbox` header that omits `allow-same-origin`; served HTML is also adjusted for the CMS player so Unity canvases fill the iframe viewport instead of keeping the default fixed-size template layout, hide the stock Unity footer chrome, resize the canvas backing buffer to the iframe viewport, inject a route-local `<base>` and artifact-map URL resolver so `Build/*` and `TemplateData/*` load through the same WebGL package API route even when the Unity template creates scripts dynamically, and show in-frame resource download progress when the runtime fetches package assets.
* CMS asset delivery must respect the workspace Drive provider. Supabase assets may use signed image transforms, while R2-backed assets should resolve through the workspace storage provider signed-read path and ignore Supabase-only transform parameters.
* CMS asset rows may only store workspace storage paths under `external-projects/`; asset delivery and cleanup must not operate on paths from other workspace modules such as Drive or finance.
* CMS does not register the shared Serwist service worker or emit the shared web app manifest. It is a Vercel-hosted satellite that proxies protected APIs to `apps/web`, so PWA routes owned by `apps/web` must not be advertised from `cms.tuturuuu.com`.
* Library taxonomy edits should stay lightweight: category/tag assignment, creation, and removal can happen directly from the library dialog, while deeper editorial changes still live in the entry sidebar taxonomy card.

## Local Development

* Use `bun dev:cms` to run the CMS app with the shared packages and `apps/web`.
* Use `bun devx:cms` or `bun devrs:cms` when you also need a reset local Supabase environment.

## CI/CD

* Preview deployments run through `.github/workflows/vercel-preview-cms.yaml`.
* Production deployments run through `.github/workflows/vercel-production-cms.yaml`.
* Both workflows are gated through `ci-check.yml` and the `tuturuuu.ts` workflow allowlist.
* The Vercel project must be configured through the `VERCEL_CMS_PROJECT_ID` repository secret.
