@typeroll/mcp-server 0.45.86 → 0.45.88

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,7 +17,7 @@ export const BUNDLED_SKILLS = {
17
17
  "tr-import-url": "---\nname: tr-import-url\ndescription: Use when the user wants to import or migrate content from a non-WordPress website — a Squarespace site, a Wix site, a static HTML site, a Webflow export, or any URL the user points at. Also triggers on \"copy content from\", \"rebuild this site\", \"import from Squarespace/Wix/Webflow\", or \"make it look like this site\". For WordPress sources use tr-migrate-wp instead.\n---\n\n# Import content from a non-WordPress site\n\n**Who runs this:** this recipe is for the user's own AI agent (Claude Code, Codex or\nanother MCP client) working through MCP and the public API. The portal's built-in\nAI assistant has no migration tools: it does not import, convert or verify sites.\n\n**Before import or reconstruction:** read `tr-migration-evidence` with\n`read_skill`. Choose preserve or renew independently for text and design/images,\nrecord text_intent and design_intent in the brief and verify one prototype per active template before bulk writes.\nUse standard blocks first, reviewed custom types next, rare documented HTML last.\nAll four combinations must preserve or meaningfully redirect every old public URL. That\nshared contract defines acceptance; this recipe only adds source-specific steps.\n\n\n> **The buffer model (draft writes).** Every content write in this recipe\n> (pages, blocks and partials) lands in an unsaved per-doc\n> DRAFT — deploys and plain previews only see SAVED content. For recipe-style\n> build work, pass `save: true` on write calls (the work is pre-approved by\n> the task itself), or run `commit_working_copy` per doc before any\n> `trigger_deploy`. Preview your drafts with `include_working_copy: true`.\n\n\n## When to use this vs tr-migrate-wp\n\n| Source | Use |\n|---|---|\n| WordPress with `/wp-json` accessible | `tr-migrate-wp` |\n| WordPress with REST disabled | This skill (scrape HTML) |\n| Squarespace, Wix, Webflow, static HTML | This skill |\n| CSV / spreadsheet data | This skill (skip scraping, just parse) |\n| Any URL the user points at | This skill |\n\n## Preconditions\n\n- Target Site and Version are selected through OAuth or a scoped API key.\n- Run `get_import_readiness` before importing content or media; verify the\n Organization storage first. Build shared header/footer against the chosen brief.\n- Source URL(s) accessible (check with a quick `fetch`; if blocked, mention\n it and ask the user for an HTML export or screenshot).\n\n## Recipe\n\n### 1. Inventory the source site\n\nFetch the homepage and build a URL list:\n\n```\nfetch <source-url> # root HTML\nfetch <source-url>/sitemap.xml # XML sitemap if it exists\n```\n\nParse `<a href>` links to discover internal pages. Build a list:\n- Homepage\n- Top-level pages (About, Services, Contact, etc.)\n- Any sub-pages that look important\n\nExclude private/session/admin URLs deliberately. Inventory public pagination and\nquery routes and keep them working or map relevant redirects. Do not silently retire public routes.\n\n**Map source styling to named styles.** A class the source uses for a\nrecurring treatment (`.eyebrow`, `.lead`, `.btn-outline`, `.section-dark`)\nbecomes a site style with `create_style` and is applied with `style_id`. A\nsmall label directly above a heading is an eyebrow style, not a heading of its\nown: put it in the heading block's `eyebrow` field (and a line under the\nheading in `subtitle`). Never carry classed raw HTML into text blocks to keep a\nlook: the editor cannot edit it, and the next render version may not keep it.\nBuild such sections with heading, text, image and button blocks; Typeroll does\nnot convert HTML into blocks.\n\n### 2. Measure the source and protect existing target work\n\n```\nread_site_settings\nread_partial partial_id=\"header\"\nlist_pages limit=5\n```\n\nRead existing target data to avoid overwriting work. Compare prototypes to the\nsource when design_intent is preserve, or to the agreed new design when it is renew. Review text separately. Validate\nstandard/custom blocks using `tr-migration-evidence` before mass writes.\n\n### 3. Fetch and clean each source page\n\nFor each URL:\n\n**a. Fetch the HTML.**\n```\nfetch <page-url>\n```\n\nIf the site returns a bot-block (Cloudflare, 403, or clearly JS-only\nSPA output), note it. Tell the user: \"This page blocked direct fetching.\nCan you provide the page source or an HTML export?\"\n\n**b. Extract the main content.**\n\nInventory shared navigation/header/footer, consent and runtime features first;\nmap them to reusable partials or supported runtimes before excluding them from\nindividual Page bodies.\nKeep: `<main>`, `<article>`, the largest content region.\n\nClean the HTML:\n- Strip platform-specific classes: `sqsrte-*`, `wf-*`, `et_*`,\n `elementor-*`, `fl-*`, `divi-*`, `vc_*`\n- Remove empty `<div>`, `<span>`, `<section>` wrappers (no class, no content)\n- Unwrap redundant nesting: `<div><p>text</p></div>` → `<p>text</p>`\n- Keep: `<h1>`–`<h6>`, `<p>`, `<ul>`, `<ol>`, `<img>`, `<a>`, `<table>`,\n `<blockquote>`, `<figure>`, `<figcaption>`, `<strong>`, `<em>`\n- Fix headings: ensure exactly one `<h1>` per page (the page title)\n\n**c. Transfer images.** For each `<img src>`:\n```\nupload_media_from_url url=\"<source-img-url>\" alt=\"...\"\n```\nReplace the src with the returned CDN URL. Classify tracking pixels separately. Preserve meaningful decorative images and\nicons. A required file that fails transfer blocks acceptance; do not silently\nomit it. Inventory PDFs, download hrefs, srcsets and backgrounds as well.\n\n**d. Adapt to the target's design.**\nUse standard blocks first, then reusable custom types with editable fields.\nReview appearance and text independently according to design_intent and text_intent;\ncompare each preserved dimension to the source and each renewed dimension to its approved brief. Raw HTML is the documented last resort.\n\n### 4. Create pages as drafts\n\n```\ncreate_page title=\"Om oss\" slug=\"om-oss\"\n path=\"/om-oss/\" blocks=[<reviewed block tree>]\n content_mode=\"blocks\" status=\"draft\"\n seo_title=\"Om oss — Acme\"\n seo_description=\"...\"\n```\n\nAlways draft first. The user signs off before publishing.\n\n### 5. Handle redirects\n\nIf the source URLs differ from the target slugs, create redirects:\n\n```\ncreate_redirect from_path=\"/about\" to_path=\"/om-oss\"\ncreate_redirect from_path=\"/services.html\" to_path=\"/tjanster\"\n```\n\n### 6. Preview with the user\n\n```\nget_preview_link\n```\n\nWalk through every imported page with the user. Common issues:\n- Heading hierarchy wrong (two H1s, or H3 used where H2 belongs)\n- Images missing alt text\n- Squarespace column layouts that don't work without their grid system\n- Embedded forms or maps that need re-setup\n\n### 7. Publish + deploy\n\nAfter approval, save working copies and confirm publishing readiness:\n```\nbatch_update_pages updates=[\n {page_id: \"om-oss\", patch: {status: \"published\"}},\n {page_id: \"tjanster\", patch: {status: \"published\"}}\n]\ntrigger_deploy\nget_deploy_status job_id=<id>\nverify_migration_urls\nget_migration_launch_report\n```\n\nThis is candidate verification, not permission to change traffic DNS. Every old\npublic URL must serve its content or a relevant redirect before cutover.\n\n## Platform-specific notes\n\n### Squarespace\n- Main content is inside `.content-wrapper` or `[data-section-theme]` blocks\n- Portfolio images are usually high-resolution originals in `/universal/images/`\n- JSON-LD is Squarespace's own schema — strip it\n- Gallery blocks → standard image/gallery layouts, or a reusable custom type\n\n### Wix\n- Wix sites are React SPAs — `fetch` returns an empty shell\n- Ask the user for the Wix site's \"Export to HTML\" (available in some plans)\n or take screenshots for reference\n- Best path: get content from the user (text + image files), rebuild clean\n\n### Webflow\n- Usually fetchable; clean output\n- Classes like `w-container`, `w-row`, `w-col-*` can be stripped\n- Webflow CMS items are server-rendered — they appear in the HTML\n\n### Static HTML / old sites\n- Often the cleanest import. Fetch, strip nav/footer, keep body.\n- Rebuild table-based layouts with layout blocks; keep actual tabular data as tables\n\n## Pitfalls\n\n- **Don't import `<style>` blocks from the source site.** They reference\n external fonts, resets, and classes that don't exist in the target.\n Strip all `<style>` tags from source HTML and rewrite styles in the\n target's conventions.\n- **Don't break the single-H1 rule.** Many source sites have no H1 or\n several. Fix it.\n- **Squarespace/Wix forms.** They won't work after import — the backend\n is vendor-locked. Create a Typeroll form instead: `create_form`.\n- **Scripts and tracking.** Never copy the source's JavaScript. Carry\n analytics and advertising tags (GA4, GTM, Meta Pixel and others) over by\n ID in the Integrations app, with the cookie banner enabled so they wait for\n consent; never paste the vendor snippet into `scripts_head`. See\n tr-migration-evidence, \"Scripts, tracking and consent\".\n- **Videos.** YouTube/Vimeo embeds are fine (`<iframe>` is allowed).\n Hosted MP4s need re-uploading if the source URL won't persist.\n\n## Storage prerequisite\n\nBefore reading or importing source content, call `get_import_readiness` (Core\n0.1.95 / MCP 0.44.68 or later). If `ready` is false, stop the import and show the\nreturned message and Publishing settings link. The organization must connect\nand verify its own storage first. Do not fall back to draft storage, hotlink\nsource images, or use ordinary page-write tools to bypass the import gate.\nUse `upload_media_from_url` for referenced source images; the customer’s\nCloudflare transfer Worker copies and verifies them directly in R2, independently\nof the GitHub/Cloudflare build-provider choice.\n",
18
18
  "tr-migrate-astro": "---\nname: tr-migrate-astro\ndescription: Migrate an existing Astro site, including Astro Content Collections, to native Typeroll Pages and Content types while preserving routes, design and media.\n---\n\n# Migrate an Astro site\n\n**Who runs this:** this recipe is for the user's own AI agent (Claude Code, Codex or\nanother MCP client) working through MCP and the public API. The portal's built-in\nAI assistant has no migration tools: it does not import, convert or verify sites.\n\n**Before import or reconstruction:** read `tr-migration-evidence` with\n`read_skill`. Choose preserve or renew independently for text and design/images,\nrecord text_intent and design_intent in the brief and verify one prototype per active template before bulk writes.\nUse standard blocks first, reviewed custom types next, rare documented HTML last.\nAll four combinations must preserve or meaningfully redirect every old public URL. That\nshared contract defines acceptance; this recipe only adds source-specific steps.\n\n\nAstro Content Collections are source data. In Typeroll, each source record\nbecomes a Page, and each source schema becomes a Content type. Static Astro\npages use that same Page model. Shared Astro layouts become Page templates,\npartials or reusable block types; no separate item storage is created.\n\n## Inventory before writing\n\nRead the source repository's routing, content loaders, schema, frontmatter,\nlayouts, components, styling and public assets. New Astro projects can define\nloaders in `src/content.config.ts`; older projects may use\n`src/content/config.ts` and files under `src/content/`. Follow what the actual\nproject uses, including generated route logic and trailing-slash policy.\n\nDiscover the target with `get_site`, `get_site_capabilities`, `list_pages`,\n`list_content_types`, `list_page_templates` and `list_block_types`. Existing target\ncontent remains out of scope unless the user authorized replacing it. Create a\nsite version for the migration and pass it consistently on all versioned calls.\nCall `get_migration_readiness` before import; connect verified customer storage\nbefore transferring any source content or media.\n\nRecord a URL inventory from the existing site and source routing. Use explicit\nPage paths to preserve nested routes or frontmatter-based permalinks. Do not\nassume that a filename alone determines the production URL.\n\n## Map the source\n\n- Common frontmatter such as title, slug, author, publication date and SEO maps\n to top-level Page metadata.\n- Other schema fields become the Content type's custom fields and Page `fields`.\n Preserve typed objects and arrays; use `page_ref`/`page_ref_list` for relations.\n- Markdown becomes native Page body blocks. First render Markdown to HTML using\n the source project's parser, then review block conversion. MDX components\n require explicit equivalents; never drop them as if they were plain Markdown.\n- A shared layout becomes a reusable Page template with `template_content_slot`.\n Assign it as the type's default. Header/footer and reusable fragments can be\n partials. Use native blocks before choosing a rare reviewed HTML/embed exception.\n- Browser scripts, integrations and server-rendered endpoints need separate\n review. A static Page cannot inherit a private server runtime by copying its\n source text. Configure the supported Forms/Extension runtime explicitly.\n Do not copy client scripts; analytics and advertising tags move to the\n Integrations app behind the cookie banner (tr-migration-evidence, \"Scripts,\n tracking and consent\").\n\nCreate templates with `create_page_template`, types with `create_content_type`,\nthen one `create_page` per source record. Use returned Page IDs, not source IDs\nor slugs, for later updates and previews. For linked content, create the Pages\nfirst and resolve their reference fields in a second pass.\n\nUpload assets to connected storage and rewrite source-relative image references\nto returned media URLs. Use direct upload URLs for local bytes and the customer\ntransfer flow for remote media; do not relay large base64 payloads through the\nmodel. Preserve alt text, captions, dimensions, links and image variants.\n\n## Listing and verification\n\nAn archive is a Page with `core/page_list` filtered by `content_type`. Configure\nsorting and pagination from the block schema. It reads saved Pages at build\ntime; do not generate a second listing representation or call a regeneration tool.\n\nCheck representative long/short bodies, code, tables, images, captions, embedded\ncomponents and related links at desktop and mobile widths. Compare the source\nand Typeroll preview, then a fresh authorized static build. Verify every inventory\nURL and redirect, canonical/hreflang, sitemap inclusion, noindex state and assets.\nSave working copies before publication. Keep the source host and DNS in place\nuntil the candidate passes checks and the user authorizes its cutover.\n",
19
19
  "tr-migrate-multisite": "---\nname: tr-migrate-multisite\ndescription: Use when migrating SEVERAL sites at once — a WordPress multisite network, a group of country/language sites on different top-level domains, or any batch of related sites moving to Typeroll together. Triggers on \"multisite\", \"network of sites\", \"10 sites\", \"our .se/.de/.co.uk sites\", \"language versions\", \"migrate all our sites\". For a single WordPress site use tr-migrate-wp; for a single non-WP source use tr-import-url.\n---\n\n# Migrate a multisite / multi-domain family to Typeroll\n\n**Who runs this:** this recipe is for the user's own AI agent (Claude Code, Codex or\nanother MCP client) working through MCP and the public API. The portal's built-in\nAI assistant has no migration tools: it does not import, convert or verify sites.\n\n**Before import or reconstruction:** read `tr-migration-evidence` with\n`read_skill`. Choose preserve or renew independently for text and design/images,\nrecord text_intent and design_intent in the brief and verify one prototype per active template before bulk writes.\nUse standard blocks first, reviewed custom types next, rare documented HTML last.\nAll four combinations must preserve or meaningfully redirect every old public URL. That\nshared contract defines acceptance; this recipe only adds source-specific steps.\n\n\n> **The buffer model (draft writes).** Every content write in this recipe\n> (pages, blocks and partials) lands in an unsaved per-doc\n> DRAFT — deploys and plain previews only see SAVED content. For recipe-style\n> build work, pass `save: true` on write calls (the work is pre-approved by\n> the task itself), or run `commit_working_copy` per doc before any\n> `trigger_deploy`. Preview your drafts with `include_working_copy: true`.\n\n## The first decision: one site or many?\n\n**One Typeroll site owns exactly one domain** (plus its apex/www sibling).\nSo:\n\n| The old family looks like | Build it as |\n|---|---|\n| `example.se`, `example.de`, `example.co.uk` — separate domains | **N Typeroll sites**, one per domain |\n| `example.com/se/`, `example.com/de/` — one domain, language folders | **One site**, using `path` on each page (`/se/om-oss`) |\n| A WP multisite on subdomains that the customer wants to consolidate onto one domain | **One site** + redirects from every old subdomain |\n\nConfirm this with the user before creating anything — it's the one decision\nthat is expensive to reverse (domains, deploys, and analytics all hang off\nit). The rest of this recipe assumes the common case: **N sites, one per\ndomain**, sharing a design.\n\nUse personal OAuth with the required Organization permissions or an org-scoped\nAPI key. Select each Site explicitly and use its returned context; never reuse\nanother Site's OAuth context. Keep Site and Version explicit on batch calls.\n\n## Phase 0 — Plan the batch (do this once, in writing)\n\nProduce a table and get the user to confirm it before touching the platform:\n\n| Old URL | Language | New Typeroll site | Domain | Notes |\n|---|---|---|---|---|\n\n**Check platform readiness before the plan is even agreed:**\n\n```\nget_migration_readiness site_id=<any existing site>\n```\n\nThen once per market, with that market's own source:\n\n```\nget_migration_readiness site_id=<de-site> source_url=\"https://example.de\"\n```\n\nCheck each target Site: storage is shared per Organization, but Hosting Groups,\npermissions and verification origins can differ. Verified customer storage is\nrequired before import; GitHub/build/domain readiness is checked separately for\npublication. Do not infer that every Site shares the same blockers.\n\nAsk explicitly:\n- Which text_intent and design_intent apply per Site, each preserve or renew? Which source or approved\n prototype is the **design reference**? Build that one properly first.\n- Are the sites **translations of each other** (same page structure) or\n independent? This decides whether hreflang clusters are mechanical or\n hand-mapped.\n- Any domains being **retired or merged**? Those need HTTP redirects served by the old hostname; DNS itself is not\n a redirect. Plan that hosting separately.\n\n## Phase 1 — Inventory EVERY old site, before building anything\n\nDo this for all sites up front. It's cheap, it's the only artefact that tells\nyou when you're done, and it stops you discovering an untouched 400-URL blog\nin week three.\n\nFor each source site, create the target site first (the inventory lives on\nit):\n\n```\ncreate_site name=\"Example DE\"\n```\n\nThen walk the source and post what you find:\n\n```\nfetch https://example.de/sitemap.xml # follow sitemap-index children\nfetch https://example.de/wp-json/wp/v2/pages?per_page=100 # walk X-WP-TotalPages\n```\n\n```\nadd_migration_urls site_id=<de-site> source_origin=\"https://example.de\" source=\"sitemap\" urls=[\n { url: \"https://example.de/ueber-uns\" },\n { url: \"https://example.de/kontakt\" },\n …\n]\n```\n\n**`source_origin` is not optional in a multisite job.** It rejects URLs from\nanother origin, which is the guard that stops domain B's `/kontakt` landing\nin domain A's inventory — where it would silently read as \"covered\" because\ndomain A happens to have a `/kontakt` too.\n\nAdd every source you have, each with its own label — they merge per URL:\n- `source=\"sitemap\"` — the sitemap(s)\n- `source=\"rest\"` — WP REST pages/posts/custom types\n- `source=\"gsc\"` — a Search Console export, **with `gsc_clicks`**. This is\n what makes the coverage report prioritise itself: the 12 URLs carrying all\n the traffic sort to the top.\n- `source=\"crawl\"` — anything you found by following internal links\n\nExclude only private/admin/session noise, with reasons. Public tags and archives\nneed working routes or relevant redirects, even when omitted from new navigation:\n\n```\nadd_migration_urls urls=[{ url: \"/wp-admin\", excluded: true, notes: \"Private source administration\" }, { url: \"/wp-login.php\", excluded: true, notes: \"Private source login; not public content\" }]\n```\n\nRead it back and note the starting number:\n\n```\nlist_migration_urls site_id=<de-site> status=\"unhandled\" limit=50\n```\n\n## Phase 2 — Build the reference site\n\nFollow `tr-migrate-wp` (or `tr-import-url`) for the ONE reference site:\ndesign, header/footer partials, page templates, block types. Get the user to\napprove it before replicating — every fix you make after this point costs\nN times as much.\n\n## Phase 3 — Replicate the design to the other sites\n\nReplicate only source-validated native recipes and brand settings. Confirm each\nmarket's template overrides and business workflows independently. Export/import\ncustom block types only for specifically reviewed source-specific exceptions,\nnot generic corrective blocks or copied tenant CSS.\n\nThen per site:\n- `read_site_settings` on the reference → `update_site_settings` on the\n target with the same colors/fonts (translate `site_name`, `tagline`,\n contact details — those are per-market, not shared).\n- Recreate header/footer partials with translated nav labels.\n- Page templates: rebuild with `add_block target={kind:'template', id:…}`.\n\nSet the language per site — it drives `<html lang>`, `og:locale` and\nalt-text generation:\n\n```\nupdate_site site_id=<de-site> language=\"de\"\n```\n\n## Phase 4 — Preserve each market's business functions\n\nUse the business-function inventory from `tr-migration-evidence`. Some markets\nuse native Forms, others an owned lead flow, affiliate destination or partner\niframe. Do not replace them all with a new contact form. Configure a Form only\nwhere it is the approved equivalent. Verify each market's recipient, destination,\nlanguage and attribution. Use authorized synthetic tests with a safe recipient;\nrecord delivery UNVERIFIED when it cannot be tested safely. Never create real\nleads or send mail to third parties as a routine migration test.\n\nScripts follow tr-migration-evidence, \"Scripts, tracking and consent\": copy no\nsource JavaScript, and carry each market's own analytics and advertising IDs\nover in that site's Integrations app with its cookie banner enabled. Never\nreuse one market's tracking IDs on another.\n\n## Phase 5 — Redirects for everything you didn't preserve\n\nWork the coverage report, not your memory:\n\n```\nlist_migration_urls site_id=<de-site> status=\"unhandled\"\n```\n\nFor each entry, one of three outcomes — no fourth option:\n\n- It moved → `create_redirect from_path=\"/alte-seite\" to_path=\"/neue-seite\"`\n- It was merged → redirect to the relevant replacement, not the homepage\n- It should exist and doesn't → go back and migrate it\n\nRe-read the list. `unhandled` reaching zero is the exit condition for this\nphase.\n\nPattern redirects require verified source-to-destination content mappings.\nA category/tag/year shape alone is not proof of equivalent destinations. Check\nexpanded artifact size and provider limits, actual slash/query variants and\nthat no rule hides a preserved Page. A 200 at the wrong content fails.\n\n## Phase 6 — Wire the hreflang cluster\n\nThis is the step that only exists because the family is multi-domain, and the\none most likely to be skipped. Each page declares its siblings on the other\ndomains; the renderer adds the page's own self-reference.\n\n```\nupdate_page site_id=<se-site> page_id=\"om-oss\" patch={ alternates: [\n { hreflang: \"de\", href: \"https://example.de/ueber-uns\" },\n { hreflang: \"en-GB\", href: \"https://example.co.uk/about-us\" },\n { hreflang: \"x-default\", href: \"https://example.com/about-us\" }\n]}\n```\n\nRules the search engines actually enforce:\n- **Reciprocal.** Every page in a cluster must list every other one. Write\n all N sides or the cluster is ignored. `batch_update_pages` is the sane way\n to do this once you have the mapping table.\n- **One `x-default`** per cluster, pointing at the language selector or the\n fallback market. Optional, but useful when the family doesn't cover a\n visitor's language.\n- **Absolute URLs on the final domain** — not the `*.typeroll` fallback\n subdomain. The cluster is what you want live after cutover, and a fallback\n URL in there is a leak you'll be cleaning up for months.\n- Invalid entries are **rejected at write time** with the reason. If a write\n fails, fix the tag/href — don't strip the field to make it pass.\n\nPages that have no equivalent on the other domains get no alternates at all.\nA cluster of one is meaningless markup.\n\n## Phase 7 — Verify BEFORE touching DNS\n\nDeploy each site (`trigger_deploy`), then check what it actually serves:\n\n```\nverify_migration_urls site_id=<de-site> source_origin=\"https://example.de\" check_source=true\n```\n\nThis requests every inventory URL against the site's fallback subdomain —\nthe real domain still points at the old host, which is exactly why the check\nis possible at all. Verdicts:\n\n| Verdict | Meaning | Action |\n|---|---|---|\n| `ok` | 200 at the same path | none |\n| `ok_redirect` | redirects to a 200 | none; flatten if `hops` > 1 |\n| `missing` | 404/410 | **the gap** — redirect it or migrate it |\n| `broken_redirect` | loop, or chain ending on an error | fix the rule |\n| `error` | 5xx / timeout | inconclusive, re-run |\n\n`check_source=true` also requests the OLD site, so a URL that already 404s\nupstream needs a reviewed retention/retirement decision, including traffic and\nbacklinks. Never automatically mark source 404s `excluded`.\n\nSlash-equivalent source URLs are one inventory decision but remain separate\nverification requests through `observed_paths`. Therefore `checked` can exceed\nthe normalized inventory size, and a missing slash variant remains a launch\nblocker. Redirect builds cover both source spellings and send internal targets\nto the site's configured trailing-slash style.\n\nNote what the check does NOT catch: it verifies that a URL *resolves*, not\nthat the page at the other end is the right content. Review content and SEO\nparity, then record the exact dataset and result with\n`record_migration_seo_acceptance`.\n\nIterate until `missing` and `broken_redirect` are both zero **on every site**.\nOnly after explicit cutover authorization, per site (`add_domain` prepares a\nnew canonical build and is not an inventory-only operation):\n\n1. `add_domain` with the intended apex/www canonical; leave `auto_deploy` on\n2. Wait for that canonical/sitemap deploy to succeed, then point DNS\n3. `poll_domain` until verified — there is no separate activation step\n4. Re-run `verify_migration_urls target_origin=\"https://example.de\"` against\n the real domain, to confirm the cutover kept what the pre-check proved\n5. Submit the new sitemap in Search Console; keep the old property open for\n a few weeks and watch the 404 report\n6. Rerun the complete, unfiltered URL check and SEO review, then require\n `get_migration_launch_report` to return `launch_ready: true`\n\n## Definition of done (per site)\n\n- [ ] `get_migration_readiness source_url=<this market's old site>` → `ready: true`, warnings reviewed\n- [ ] `list_migration_urls status=\"unhandled\"` → 0\n- [ ] Complete `verify_migration_urls` → 0 `missing`, 0 `broken_redirect`, 0 `error`, 0 untested variants\n- [ ] `record_migration_seo_acceptance` is accepted for the latest deploy\n- [ ] `get_migration_launch_report` → `launch_ready: true`\n- [ ] No source JavaScript copied; analytics/advertising via the Integrations app with the cookie banner on (`scripts.status: \"clean\"` in the launch report)\n- [ ] hreflang cluster written on both/all sides, absolute, final domains\n- [ ] `language` set on the site; `<html lang>` correct in the deployed HTML\n- [ ] Internal links rewritten (no lingering absolute links to the old domain)\n- [ ] Market-specific business functions mapped; authorized synthetic interaction/delivery verified or explicitly UNVERIFIED\n- [ ] Media uploaded to this site's own library (no cross-site `cdn_url`), alt text in this market's language\n- [ ] If cutover is authorized: domain declared, canonical deploy succeeded, DNS verified; sitemap submitted. Otherwise report pre-cutover complete separately.\n\n## Pitfalls specific to this job\n\n- **Don't share one inventory across domains.** Inventory entries key on\n path; two markets both have `/kontakt`. One site, one inventory.\n- **Don't build all N sites in parallel from scratch.** Build one, approve,\n replicate. Parallel building multiplies every design mistake by N.\n- **Don't skip the parity check because coverage says 100%.** Coverage is a\n claim about the data; parity is a measurement of the server. They disagree\n exactly when it matters — an unpublished target page, a typo'd path, a\n redirect chain.\n- **Don't point DNS site-by-site on a whim.** Cutting over one market at a\n time is fine and usually wise, but the hreflang cluster spans markets: a\n page pointing at a domain that still serves the old site is pointing at\n content that doesn't match. Either cut over close together, or write the\n cluster after the last market lands.\n\n## Storage prerequisite\n\nBefore reading or importing source content, call `get_import_readiness` (Core\n0.1.95 / MCP 0.44.68 or later). If `ready` is false, stop the import and show the\nreturned message and Publishing settings link. The organization must connect\nand verify its own storage first. Do not fall back to draft storage, hotlink\nsource images, or use ordinary page-write tools to bypass the import gate.\nUse `upload_media_from_url` for referenced source images; the customer’s\nCloudflare transfer Worker copies and verifies them directly in R2, independently\nof the GitHub/Cloudflare build-provider choice.\n",
20
- "tr-migrate-wp": "---\nname: tr-migrate-wp\ndescription: Use when the user asks to migrate a WordPress site to Typeroll, mentions wp-json, or names a WP source URL. Walks the WP REST API, preserves cleaned HTML and shared references, transfers media, sets redirects, and leaves everything as drafts for human review. Typeroll does not convert HTML into blocks.\n---\n\n# Migrate from WordPress to Typeroll\n\n**Who runs this:** this recipe is for the user's own AI agent (Claude Code, Codex or\nanother MCP client) working through MCP and the public API. The portal's built-in\nAI assistant has no migration tools: it does not import, convert or verify sites.\n\n**Before import or reconstruction:** read `tr-migration-evidence` with\n`read_skill`. Choose preserve or renew independently for text and design/images,\nrecord text_intent and design_intent in the brief and verify one prototype per active template before bulk writes.\nUse standard blocks first, reviewed custom types next, rare documented HTML last.\nAll four combinations must preserve or meaningfully redirect every old public URL. That\nshared contract defines acceptance; this recipe only adds source-specific steps.\n\n\n> **The buffer model (draft writes).** Every content write in this recipe\n> (pages, blocks and partials) lands in an unsaved per-doc\n> DRAFT — deploys and plain previews only see SAVED content. For recipe-style\n> build work, pass `save: true` on write calls (the work is pre-approved by\n> the task itself), or run `commit_working_copy` per doc before any\n> `trigger_deploy`. Preview your drafts with `include_working_copy: true`.\n\n\nThe platform's migration workflow is the \"managed\" path for customers who\nwant one-click. It runs on the server and is also available to you:\n`start_workflow type=\"migration\" config={ wp_url }` on an existing site, or\n`create_site_and_migrate` (organization key) for a new one; poll `get_workflow`\nand approve its review gate only with the user's consent. This skill is the\n\"power-user\" path: you do it locally, mix data sources freely, and the user\nreviews each step in their terminal.\n\n## Site tokens before block overrides\n\nCore 0.2.106 / template capabilities 0.54.0 adds palette names in all colour\nfields and named typography/spacing steps. Check the connected capability\nversion; on older renderers use `var(--color-surface)` rather than a bare name.\n\nSet colours and font families in site settings, then use named styles and\npalette names in blocks. Do not write hex, rgb or font-family literals in block\ncontent. Prefer `primary`, `secondary`, `accent`, `background`, `surface`, `text`,\n`text_light`, and `on_primary`/`on_secondary`/`on_accent` for text on brand colours.\nUse `surface_hover` for a darker interactive surface (derived automatically\nunless explicitly configured). Existing semantic roles should be reused; a\nnew visual shade is not automatically a new semantic role. For a derived shade\nuse `color-mix(in srgb, var(--color-surface) 92%, var(--color-primary))`.\nFor an additional role not in the native palette, define a named site style\nor a site CSS variable derived from existing palette variables, and document it.\n\nChoose `heading`, `body` or `mono` for fonts. Font families belong only in the\nsite's `fonts` settings. Use type-size steps `xs`, `sm`, `md`, `lg`, `xl`, `2xl`,\n`3xl`; spacing `none`, `xs`, `sm`, `md`, `lg`, `xl`; line height `tight`, `snug`,\n`normal`, `relaxed`, `loose`. Presentation fields accept responsive maps of these\nnames, for example `font_size_px: {mobile: \"md\", desktop: \"lg\"}`. Read the field\nschema: counts, widths and breakpoints are not spacing steps. Pixel values are\nexplicit exceptions, not the default authoring method. Configure scales in\n`design_scales`, then reuse them in role styles and blocks.\n\nRecord deliberate exceptions in `decisions.md`, with source/block, reason and\nreview criteria (for example white text on a photograph). Run `lint_design_tokens`\nbefore launch and after major design changes. It warns, never blocks launch.\nUse the starter's `scripts/typeroll/color_tokens.py` for an exact-match dry run;\nreview the plan before applying it in a dedicated Version. Equal hex values can\nmean different roles: never automatically choose between background and text.\nVerify a primary/surface change in a test Version across header, footer, cards,\nmenus and buttons, then restore the test settings. Do not republish automatically.\n\n\n\n## Preconditions\n\n**Run the readiness check FIRST — before touching any content:**\n\n```\nget_migration_readiness source_url=\"https://oldsite.com\"\n```\n\nWhen the migration includes reusable page/item layouts, include each proposed\nblock composition and its content type fields in this same call. A\n`waiting_for_native_support` result means inspect the actual blocker. Review\nintentional custom types and rare HTML exceptions using `tr-migration-evidence`;\nfix missing types/fields and report renderer defects rather than masking them. Independent content and SEO work may continue. Rerun the review after the\nrequired Core version is deployed, then verify preview and a fresh hosted\nbuild.\n\nPass `source_url` — that adds the checks on the site you're migrating FROM.\nAn old host that answers 403/429 to server-side requests is a **blocker**: the\nimport would produce empty pages, or pages containing the host's block page,\nwhich reads as real content and is worse. Whether `/wp-json` answers is a\nwarning, since scraping is a real fallback (it just loses ACF/custom fields).\n\nIf `ready` is false, STOP and report the blockers to the user. Do not start\nthe import \"and fix it after\": every blocker is one whose failure is invisible\nonce the work is done, so discovering it late means redoing the expensive part.\n\n- **Media storage** — without it, every `<img>` keeps its WordPress URL. The\n new site looks perfect and is still served images by the old host. It breaks\n the day the customer cancels that hosting, months later, all at once.\n- **Publishing** — with verified Organization storage, customer publishing can\n import and preview before GitHub, builds or website domains are connected.\n Publishing readiness is a separate gate before a hosted build/cutover. Legacy\n self-hosted configurations may report a missing hosting adapter; follow the\n returned blocker rather than assuming all installations have the same setup.\n\nWarnings are worth relaying but don't stop you: no verification origin (the\npre-cutover parity check can't run), forms without a\nnotification address, or a target site with no design to rebuild INTO.\n\nThen the ordinary preconditions:\n\n- MCP connected through personal OAuth or an appropriately scoped API key.\n Select and confirm the Site/Version before writes; do not request pasted secrets.\n- The source WP site has `/wp-json` reachable (Google for \"wordpress\n REST API disabled\" if not — common for hardened hosts).\n- The target exists. Establish the source design baseline first, then prove\n representative target templates using `tr-migration-evidence`. Preserve\n appearance when design_intent is preserve; use the approved design brief when it is renew. Review text against its separate choice.\n- If the target already has content, you must NOT clobber it — always\n `list_pages` first and only write to slugs that don't already exist.\n Pass the slug explicitly: `create_page` then refuses an address that is\n already in use (409 `slug_in_use` naming the existing page) instead of\n numbering it.\n\n## Recipe\n\n### 1. Probe and inventory\n\n```\nfetch <wp-url>/wp-json # confirm REST is on\nfetch <wp-url>/wp-sitemap.xml or /sitemap.xml # URL inventory\n```\n\nBuild a list of every URL you intend to migrate. WP custom post types\nneed their REST endpoint (e.g. `/wp-json/wp/v2/news?per_page=100`),\nwalking `X-WP-TotalPages` to paginate.\n\n### 2. Measure the source and protect existing target work\n\n```\nget_site\nread_site_settings # colors, fonts, voice cues\nlist_partials # header / footer / shared\nread_partial partial_id=\"header\" # nav structure\nlist_pages limit=5\nbatch_read_pages page_ids=[<2-3 representative ids>] # see actual conventions\n```\n\nDon't skip this. Imposing a stranger's design on a customer's site is\nthe biggest avoidable mistake.\n\n### 3. Normalize shared data before importing page bodies\n\nRead taxonomy definitions and paginated terms from the helper's `/taxonomies`\nand `/terms/{taxonomy}` (Helper 0.3.2), or WordPress REST. Create a Content type\nfor each taxonomy and one Page per term. Preserve term IDs in a stable source\nmapping, archive URLs, parents and custom fields such as icons/emoji.\n\nAdd `page_ref_list` fields to article Content types. Store term Page IDs, never\ncopied category names, emojis, slugs or sort values on every article. Category\narchives can list articles using `core/repeater` with `source_type: backlinks`.\nUse an explicit primary category for breadcrumbs when available; do not guess\namong multiple categories. A table of contents is a derived template block,\nnot a `toc_html` field.\n\nCategory and tag Pages have editable block bodies. Scaffold the source term\ndescription plus a reverse-reference listing, leaving room for unique editorial\ncontent. Multiple tag/category references do not give a Page multiple parents:\npreserve its source page parent, or use an unambiguous primary category. Tags do\nnot set its parent. `parent` controls breadcrumb ancestry, not URL generation;\npreserve the source `path` even when it differs from the chosen hierarchy.\n\nThe managed importer preserves existing imported Pages on retry and refuses\nURL/identity conflicts or dangling term references. An older flattened import\nrequires a separately reviewed mapping repair; do not re-import over edits.\n\n**Styles.** Recurring source treatments (theme classes for eyebrows, leads,\nbutton variants, highlighted sections) become named site styles\n(`create_style`) applied with `style_id`, so editors can change them in one\nplace. Do not preserve them as classed raw HTML in text blocks.\n\n### 4. Import one page at a time, draft status\n\nFor each source URL:\n\na. Choose access based on missing evidence, not a mandatory plugin install:\n\n | Source requirement | Access path |\n | --- | --- |\n | Public Pages, posts, media and rendered builder content exposed in REST | Public paginated `/wp-json/wp/v2/pages`, `/posts`, `/media` and rendered HTML can suffice |\n | SEO exposed in `yoast_head_json`, visible menus/backgrounds | Public REST + rendered HTML/CSS; verify actual coverage |\n | Private/draft content, REST-hidden types, ACF/custom fields, exact menus | Authenticated helper/export when available and authorized |\n | REST blocked by security policy | Owner-approved helper/export; do not bypass access controls |\n | Authoritative old slugs or plugin redirects | Owner export/helper **if supported**; a public crawl only approximates them |\n\n Do not ask the owner to install the helper when public sources cover the scope.\n Inventory both Pages and posts (including sitemap-excluded public records),\n crawl internal links and inspect redirects, menus and CSS background images.\n Do not claim the helper exports a field without reading its current contract.\n The helper is distributed through Typeroll's documented package; do not claim\n WordPress.org availability. Listing it there is separate publication work.\n\nb. Clean the HTML. Parse HTML and discard comments before extracting text or\n constructing blocks (comments may contain old phone-plugin metadata). Do not\n use regex tag removal as a text parser. Resolve relative `../` links against\n the source URL; preserve query strings and fragments. Report missing targets\n instead of pointing everything at the home page. Strip Elementor / Gutenberg / Breakdance class\n soup. Drop empty `<div>` and `<span>` wrappers. Keep semantic tags,\n tables, iframes from known hosts (YouTube / Vimeo / Calendly).\n Remove every `<script>`: plugin and theme JavaScript is never copied.\n Analytics and advertising tags move to the Integrations app behind the\n cookie banner (tr-migration-evidence, \"Scripts, tracking and consent\").\n\nc. Migrate referenced images with `upload_media_from_url`. The customer's\n transfer Worker copies and verifies them directly in R2. Use the returned\n media reference only after verification. Stop and report a failed required\n transfer; do not silently hotlink the WordPress source.\n\nThe managed importer stores cleaned HTML. Typeroll does not convert HTML into blocks, and `set_page_mode` never rewrites HTML. You, the external agent, rebuild the imported content with ordinary block tools. The owner does not need to convert every Page manually. There is no editor conversion preview; it was removed in Core 0.2.58. Preserve the complete source before switching modes and never publish the empty intermediate Page.\n\nd. Rebuild into editable blocks according to the selected intent. Preserve exact\n wording when text_intent is preserve; track approved changes when it is renew, independently of design. Map headings,\n prose, images, buttons and layout to standard fields first, then create\n reviewed custom types where needed. Use raw HTML only for a documented,\n exceptional fragment. Preserve anchors and map every old URL in either mode.\n\ne. Write the page as a draft:\n\n ```\n create_page title=\"...\" slug=\"<last-path-segment>\"\n path=\"/<preserved-wordpress/path/>\"\n blocks=[<native block tree>]\n status=\"draft\" kind=\"article\" author=\"...\"\n seo_title=\"...\" seo_description=\"...\"\n ```\n\n **Preserve the source URL.** WP post URLs like\n `/2024/01/foo-bar/` uses `slug: \"foo-bar\"` and\n `path: \"/2024/01/foo-bar/\"`. Slug is one segment; `path` preserves the\n complete nested URL.\n\n### 5. Redirects\n\nAfter migration, every URL the agent didn't preserve verbatim needs a\nredirect:\n\n```\ncreate_redirect from_path=\"/old-services\" to_path=\"/services\"\n```\n\nWalk the inventory; for each URL: did it become a page with the same\npath? If yes, no redirect. If renamed, `create_redirect`. If\ncombined with another page, redirect to the relevant combined content. Do not\nmark a public page excluded just because the new design omits it. Public URL\nretirement requires a separately agreed exception; admin/session noise can be\nexcluded with a recorded reason.\n\nUse a pattern only after verifying every affected source-to-destination mapping.\nFor an explicitly approved prefix change, `/old-blog/:slug` → `/articles/:slug`\ncan preserve intent if each destination contains the corresponding article.\nDo not blanket-redirect category/tag/author/feed/year families to a home or blog\npage. Date paths need actual route mappings; stripping a year is not enough.\nReview source 404s rather than automatically excluding them.\n\nRules:\n\n- **Trailing `*` only.** A mid-path splat (`/blog/*/comments`) is dropped\n silently by Cloudflare — the platform refuses it at write time.\n- **`:splat`** carries the captured remainder; **`:name`** matches exactly one\n segment and can be replayed by name.\n- **A pattern that would hide a live page is refused**, naming the pages. That\n is the platform protecting you: redirects are applied before static files, so\n `/blogg/*` would make every real article under `/blogg/` unreachable. Narrow\n the prefix instead.\n- **Query-string URLs can't be matched.** WP's `/?p=123` has no path to key on;\n these require an explicit query-routing solution on the future host or a\n reviewed retirement decision. The old host cannot handle requests after its\n hostname moves.\n- Rules are emitted most-specific-first, so a narrow rule always beats a broad\n one — you can safely have `/blogg/recept/*` alongside `/blogg/*`.\n\nThen verify against reality before anything is cut over:\n\n```\nimport_sitemap url=\"https://old.example.com/sitemap.xml\"\n# Optional: direct Search Console query (platform service account must have property access)\nimport_gsc_performance property=\"https://old.example.com/\" months=6\n# Or paste a Search Console Pages CSV via csv=\"...\" and source_origin.\ncheck_internal_links # database preflight before deploy\nverify_migration_urls # after trigger_deploy; compact exceptions by default\nrecord_migration_seo_acceptance # after reviewing this exact deployment\nget_migration_launch_report # final fail-closed launch gate\n```\n\nThe inventory merges slash-equivalent URLs into one work item but preserves\ntheir source spellings in `observed_paths`. Verification requests every one of\nthose spellings; treat a failing variant as a real gap even when coverage is\notherwise complete. Fresh builds expand redirect sources to both slash forms.\n\nFor a site imported before Typeroll normalized WordPress plain-text fields,\naudit and repair the legacy records before visual review:\n\n```\nrepair_migration_plain_text # dry_run=true by default; returns exact field diffs\n```\n\nThis operation is deliberately limited to `title`, `seo_title`,\n`seo_description`, and schema-defined plain-text `excerpt` fields. It cannot\ntouch bodies, HTML, slugs, paths, or URLs. Show all returned diffs and\nconflicts to the user. Only after explicit approval, repeat the same scope and\nselectors with `dry_run=false save=true`. If `truncated=true`, narrow the\nselection or raise `diff_limit` and review the omitted diffs first. Never work\naround a `working_copy` conflict: that resource contains another edit which\nmust be resolved separately.\n\n### 6. Preview + review with the user\n\n```\nget_preview_link page_id=<id> # one URL the user can click\n```\n\nCompare the source and target at desktop and mobile widths. Review representative\narticles, an archive and the home page. Measure heading sizes, readable width,\nspacing, TOC placement and image/card proportions. Scroll below the fold: verify\nheadings after media, the TOC below a sticky header, anchor landing positions\nand related articles inside the intended content column. In Core 0.2.7+, use\n`rhythm: \"article\"` on the Page content slot and `appearance: \"card\"` on Post\nCards for these native presentation choices; no per-page corrective CSS. Check breadcrumbs and shared\ncategory references; test forms/Extensions. HTTP 200 and text presence do not\nprove visual fidelity. Document intentional improvements such as responsive video.\n\nRecord `fidelity: { desktop, mobile, shared_data, integrations, evidence }` with\n`record_migration_seo_acceptance`, based on real checks of the exact deployment.\nThe launch report requires this evidence in Core 0.2.5.\nNever mark an unavailable integration or uninspected screenshot as accepted.\n\n### 7. Ship\n\nWhen the user signs off:\n\n```\n# Bulk-publish drafts that look right\nbatch_update_pages updates=[{page_id, patch:{status:\"published\"}}, ...]\n\n# Deploy\ntrigger_deploy\nget_deploy_status job_id=<id> # poll\n```\n\n## Pitfalls\n\n- **Don't publish during migration.** Always import as `draft`. Even\n if the agent is confident, the customer needs the chance to spot-check.\n- **WP slugs sometimes drift.** A post saved with slug `foo-bar` may\n have been served at `/2024/01/foo-bar/` due to the permalink\n structure. The full URL is what users see in Google; preserve that,\n not the bare slug.\n- **Image bandwidth.** R2 upload is metered. Use `find_pages_matching`\n contains=\"<old-domain>\" on already-imported content to spot images\n that weren't transferred.\n- **Plugin JavaScript.** Never copy scripts from plugins, the theme or\n WordPress core into settings, pages, partials or block types, and never\n copy the cookie plugin. Carry analytics and advertising over by ID in the\n Integrations app with `cookie_consent.enabled: true`. The launch report\n fails on copied source scripts and on tracking outside consent.\n- **WP-specific JSON-LD** (Yoast, Rank Math) is usually wrong after a\n redesign because it references old URLs. Strip it; let Typeroll\n emit fresh Article/Page schemas via `kind: 'article'` + `author`.\n\n## When the source isn't WordPress\n\nThe same shape applies for any source — Squarespace export, custom\nCMS, scraped HTML, CSV. Replace step 1's \"WP REST\" probe with whatever\ndiscovery the source supports, and the rest of the recipe is unchanged.\n\n## Storage prerequisite\n\nBefore reading or importing source content, call `get_import_readiness` (Core\n0.1.95 / MCP 0.44.68 or later). If `ready` is false, stop the import and show the\nreturned message and Publishing settings link. The organization must connect\nand verify its own storage first. Do not fall back to draft storage, hotlink\nsource images, or use ordinary page-write tools to bypass the import gate.\nUse `upload_media_from_url` for referenced source images; the customer’s\nCloudflare transfer Worker copies and verifies them directly in R2, independently\nof the GitHub/Cloudflare build-provider choice.\n\n### Raw-source media order (Core 0.2.35+)\n\nPass raw body HTML into conversion before any parse/serialize round trip with\nanother parser. Core applies browser tree construction before lazy-media cleanup.\nAn image-only H2 becomes an image at that position, not a semantic heading.\nCompare ordered headings/text/media/CTAs, including malformed wrappers, lazy and\nnoscript images and related-content boundaries. Existing Pages are not rewritten;\nrepair confirmed placement errors against raw source without reimporting edits.\n\n\n## Navigation and empty-media variants\n\nInventory first-step input banners independently of final submission providers.\nPreserve their fields, required/minimum validation, service tabs and direct links,\nmobile collapsed/open states and navigation even when the final integration is\ndeferred. Core 0.2.40 provides native `core/navigation_form` and mixed `core/tabs`.\nRead their schema instead of rebuilding them in HTML or reintroducing a provider\nblock. Defaults are same-origin and tab-scoped; never place addresses in URLs or\nclaim cross-origin prefill. The receiving form must explicitly accept the fields.\n\nCompare listing rows with both real and missing images. Use shared post-card\nplaceholder settings for theme decoration, preserving real featured images and\nexplicit `show_image: false`. Compare actual footer link height plus gap, wrapping,\nfocus and column breakpoints; compact navigation density is an intentional option.\nVerify native preview and static output on the agreed viewport boundaries.\n\n\n## Before retiring the source\n\nFollow the starter's workflows/source-media.md. Archive unused and print originals\nprivately with a verified SHA-256 manifest and a restore test; do not import the\nwhole archive into CMS or assume ETag means MD5. ICC-convert CMYK/TIFF derivatives,\nkeep originals, and review colour/framing before uploading. Archive completion\nand URL continuity are separate acceptance checks. Do not remove old hosting\nuntil the owner has approved retirement.\n\nSaving future hosts must not block normal publication on the current/temporary\naddress. Publish and review intended changes there first. Shortly before changing\nDNS, explicitly prepare the last published snapshot for the future hosts with\n`prepare_publishing_domain_change`. Review frozen indexing settings and approve\ntraffic separately. If newer content is published while the candidate waits,\nprepare again before cutover; do not overwrite it with an outdated snapshot. Respect preserve/renew choices for owner-supplied facts;\n\"timeless\" wording must not invent recurring events or erase preserved copy.\n\n## Fluid typography and component rhythm\n\nCore 0.2.114 / MCP 0.45.85 / capabilities 0.55.0 adds responsive scales.\nCheck the connected versions before using this contract. New sites use render\nversion 10. For an existing site, test a Version first; upgrading rendering\nand replacing existing roles are separate, deliberate choices.\n\nUse `fonts.size_base` (default 16), `typography.ratio_mobile` (1.2),\n`typography.ratio_desktop` (1.25), `typography.rhythm` (1), and\n`typography.line_length` (65 ch). Set the controls once, then use named type,\nspacing and line-height steps. `apply_standard_styles` uses those steps;\n`overwrite: true` resets existing roles, so review their current values first.\nExplicit sizes and breakpoint overrides still win and are not silently migrated.\n\nHeading role margins belong to editorial flow. Component titles use their own\npadding/gap, and opening headings in containers/sections add no top margin.\nUse role-linked `core/heading` size `auto` or `theme`; explicit hero sizes remain\ndeliberate exceptions. Do not compensate with per-card negative margins.\nCompare 375, 768 and 1280px in Site settings → Design and in a full site preview.\nAfter a rendering upgrade/rebuild, recheck cards, boxed headings, leading text\nand webfonts. Boxes should get their outer air from padding. Keep exceptions\nin decisions.md, and run lint_design_tokens before launch.\n",
20
+ "tr-migrate-wp": "---\nname: tr-migrate-wp\ndescription: Use when the user asks to migrate a WordPress site to Typeroll, mentions wp-json, or names a WP source URL. Walks the WP REST API, preserves cleaned HTML and shared references, transfers media, sets redirects, and leaves everything as drafts for human review. Typeroll does not convert HTML into blocks.\n---\n\n# Migrate from WordPress to Typeroll\n\n**Who runs this:** this recipe is for the user's own AI agent (Claude Code, Codex or\nanother MCP client) working through MCP and the public API. The portal's built-in\nAI assistant has no migration tools: it does not import, convert or verify sites.\n\n**Before import or reconstruction:** read `tr-migration-evidence` with\n`read_skill`. Choose preserve or renew independently for text and design/images,\nrecord text_intent and design_intent in the brief and verify one prototype per active template before bulk writes.\nUse standard blocks first, reviewed custom types next, rare documented HTML last.\nAll four combinations must preserve or meaningfully redirect every old public URL. That\nshared contract defines acceptance; this recipe only adds source-specific steps.\n\n\n> **The buffer model (draft writes).** Every content write in this recipe\n> (pages, blocks and partials) lands in an unsaved per-doc\n> DRAFT — deploys and plain previews only see SAVED content. For recipe-style\n> build work, pass `save: true` on write calls (the work is pre-approved by\n> the task itself), or run `commit_working_copy` per doc before any\n> `trigger_deploy`. Preview your drafts with `include_working_copy: true`.\n\n\nThe platform's migration workflow is the \"managed\" path for customers who\nwant one-click. It runs on the server and is also available to you:\n`start_workflow type=\"migration\" config={ wp_url }` on an existing site, or\n`create_site_and_migrate` (organization key) for a new one; poll `get_workflow`\nand approve its review gate only with the user's consent. This skill is the\n\"power-user\" path: you do it locally, mix data sources freely, and the user\nreviews each step in their terminal.\n\n## Site tokens before block overrides\n\nCore 0.2.106 / template capabilities 0.54.0 adds palette names in all colour\nfields and named typography/spacing steps. Check the connected capability\nversion; on older renderers use `var(--color-surface)` rather than a bare name.\n\nSet colours and font families in site settings, then use named styles and\npalette names in blocks. Do not write hex, rgb or font-family literals in block\ncontent. Prefer `primary`, `secondary`, `accent`, `background`, `surface`, `text`,\n`text_light`, and `on_primary`/`on_secondary`/`on_accent` for text on brand colours.\nUse `surface_hover` for a darker interactive surface (derived automatically\nunless explicitly configured). Existing semantic roles should be reused; a\nnew visual shade is not automatically a new semantic role. For a derived shade\nuse `color-mix(in srgb, var(--color-surface) 92%, var(--color-primary))`.\nFor an additional role not in the native palette, define a named site style\nor a site CSS variable derived from existing palette variables, and document it.\n\nChoose `heading`, `body` or `mono` for fonts. Font families belong only in the\nsite's `fonts` settings. Use type-size steps `xs`, `sm`, `md`, `lg`, `xl`, `2xl`,\n`3xl`; spacing `none`, `xs`, `sm`, `md`, `lg`, `xl`; line height `tight`, `snug`,\n`normal`, `relaxed`, `loose`. Presentation fields accept responsive maps of these\nnames, for example `font_size_px: {mobile: \"md\", desktop: \"lg\"}`. Read the field\nschema: counts, widths and breakpoints are not spacing steps. Pixel values are\nexplicit exceptions, not the default authoring method. Configure scales in\n`design_scales`, then reuse them in role styles and blocks.\n\nRecord deliberate exceptions in `decisions.md`, with source/block, reason and\nreview criteria (for example white text on a photograph). Run `lint_design_tokens`\nbefore launch and after major design changes. It warns, never blocks launch.\nUse the starter's `scripts/typeroll/color_tokens.py` for an exact-match dry run;\nreview the plan before applying it in a dedicated Version. Equal hex values can\nmean different roles: never automatically choose between background and text.\nVerify a primary/surface change in a test Version across header, footer, cards,\nmenus and buttons, then restore the test settings. Do not republish automatically.\n\n\n\n## Preconditions\n\n**Run the readiness check FIRST — before touching any content:**\n\n```\nget_migration_readiness source_url=\"https://oldsite.com\"\n```\n\nWhen the migration includes reusable page/item layouts, include each proposed\nblock composition and its content type fields in this same call. A\n`waiting_for_native_support` result means inspect the actual blocker. Review\nintentional custom types and rare HTML exceptions using `tr-migration-evidence`;\nfix missing types/fields and report renderer defects rather than masking them. Independent content and SEO work may continue. Rerun the review after the\nrequired Core version is deployed, then verify preview and a fresh hosted\nbuild.\n\nPass `source_url` — that adds the checks on the site you're migrating FROM.\nAn old host that answers 403/429 to server-side requests is a **blocker**: the\nimport would produce empty pages, or pages containing the host's block page,\nwhich reads as real content and is worse. Whether `/wp-json` answers is a\nwarning, since scraping is a real fallback (it just loses ACF/custom fields).\n\nIf `ready` is false, STOP and report the blockers to the user. Do not start\nthe import \"and fix it after\": every blocker is one whose failure is invisible\nonce the work is done, so discovering it late means redoing the expensive part.\n\n- **Media storage** — without it, every `<img>` keeps its WordPress URL. The\n new site looks perfect and is still served images by the old host. It breaks\n the day the customer cancels that hosting, months later, all at once.\n- **Publishing** — with verified Organization storage, customer publishing can\n import and preview before GitHub, builds or website domains are connected.\n Publishing readiness is a separate gate before a hosted build/cutover. Legacy\n self-hosted configurations may report a missing hosting adapter; follow the\n returned blocker rather than assuming all installations have the same setup.\n\nWarnings are worth relaying but don't stop you: no verification origin (the\npre-cutover parity check can't run), forms without a\nnotification address, or a target site with no design to rebuild INTO.\n\nThen the ordinary preconditions:\n\n- MCP connected through personal OAuth or an appropriately scoped API key.\n Select and confirm the Site/Version before writes; do not request pasted secrets.\n- The source WP site has `/wp-json` reachable (Google for \"wordpress\n REST API disabled\" if not — common for hardened hosts).\n- The target exists. Establish the source design baseline first, then prove\n representative target templates using `tr-migration-evidence`. Preserve\n appearance when design_intent is preserve; use the approved design brief when it is renew. Review text against its separate choice.\n- If the target already has content, you must NOT clobber it — always\n `list_pages` first and only write to slugs that don't already exist.\n Pass the slug explicitly: `create_page` then refuses an address that is\n already in use (409 `slug_in_use` naming the existing page) instead of\n numbering it.\n\n## Recipe\n\n### 1. Probe and inventory\n\n```\nfetch <wp-url>/wp-json # confirm REST is on\nfetch <wp-url>/wp-sitemap.xml or /sitemap.xml # URL inventory\n```\n\nBuild a list of every URL you intend to migrate. WP custom post types\nneed their REST endpoint (e.g. `/wp-json/wp/v2/news?per_page=100`),\nwalking `X-WP-TotalPages` to paginate.\n\n### 2. Measure the source and protect existing target work\n\n```\nget_site\nread_site_settings # colors, fonts, voice cues\nlist_partials # header / footer / shared\nread_partial partial_id=\"header\" # nav structure\nlist_pages limit=5\nbatch_read_pages page_ids=[<2-3 representative ids>] # see actual conventions\n```\n\nDon't skip this. Imposing a stranger's design on a customer's site is\nthe biggest avoidable mistake.\n\n### 3. Normalize shared data before importing page bodies\n\nRead taxonomy definitions and paginated terms from the helper's `/taxonomies`\nand `/terms/{taxonomy}` (Helper 0.3.2), or WordPress REST. Create a Content type\nfor each taxonomy and one Page per term. Preserve term IDs in a stable source\nmapping, archive URLs, parents and custom fields such as icons/emoji.\n\nAdd `page_ref_list` fields to article Content types. Store term Page IDs, never\ncopied category names, emojis, slugs or sort values on every article. Category\narchives can list articles using `core/repeater` with `source_type: backlinks`.\nUse an explicit primary category for breadcrumbs when available; do not guess\namong multiple categories. A table of contents is a derived template block,\nnot a `toc_html` field.\n\nCategory and tag Pages have editable block bodies. Scaffold the source term\ndescription plus a reverse-reference listing, leaving room for unique editorial\ncontent. Multiple tag/category references do not give a Page multiple parents:\npreserve its source page parent, or use an unambiguous primary category. Tags do\nnot set its parent. `parent` controls breadcrumb ancestry, not URL generation;\npreserve the source `path` even when it differs from the chosen hierarchy.\n\nThe managed importer preserves existing imported Pages on retry and refuses\nURL/identity conflicts or dangling term references. An older flattened import\nrequires a separately reviewed mapping repair; do not re-import over edits.\n\n**Styles.** Recurring source treatments (theme classes for eyebrows, leads,\nbutton variants, highlighted sections) become named site styles\n(`create_style`) applied with `style_id`, so editors can change them in one\nplace. Do not preserve them as classed raw HTML in text blocks.\n\n### 4. Import one page at a time, draft status\n\nFor each source URL:\n\na. Choose access based on missing evidence, not a mandatory plugin install:\n\n | Source requirement | Access path |\n | --- | --- |\n | Public Pages, posts, media and rendered builder content exposed in REST | Public paginated `/wp-json/wp/v2/pages`, `/posts`, `/media` and rendered HTML can suffice |\n | SEO exposed in `yoast_head_json`, visible menus/backgrounds | Public REST + rendered HTML/CSS; verify actual coverage |\n | Private/draft content, REST-hidden types, ACF/custom fields, exact menus | Authenticated helper/export when available and authorized |\n | REST blocked by security policy | Owner-approved helper/export; do not bypass access controls |\n | Authoritative old slugs or plugin redirects | Owner export/helper **if supported**; a public crawl only approximates them |\n\n Do not ask the owner to install the helper when public sources cover the scope.\n Inventory both Pages and posts (including sitemap-excluded public records),\n crawl internal links and inspect redirects, menus and CSS background images.\n Do not claim the helper exports a field without reading its current contract.\n The helper is distributed through Typeroll's documented package; do not claim\n WordPress.org availability. Listing it there is separate publication work.\n\nb. Clean the HTML. Parse HTML and discard comments before extracting text or\n constructing blocks (comments may contain old phone-plugin metadata). Do not\n use regex tag removal as a text parser. Resolve relative `../` links against\n the source URL; preserve query strings and fragments. Report missing targets\n instead of pointing everything at the home page. Strip Elementor / Gutenberg / Breakdance class\n soup. Drop empty `<div>` and `<span>` wrappers. Keep semantic tags,\n tables, iframes from known hosts (YouTube / Vimeo / Calendly).\n Remove every `<script>`: plugin and theme JavaScript is never copied.\n Analytics and advertising tags move to the Integrations app behind the\n cookie banner (tr-migration-evidence, \"Scripts, tracking and consent\").\n\nc. Migrate referenced images with `upload_media_from_url`. The customer's\n transfer Worker copies and verifies them directly in R2. Use the returned\n media reference only after verification. Stop and report a failed required\n transfer; do not silently hotlink the WordPress source.\n\nThe managed importer stores cleaned HTML. Typeroll does not convert HTML into blocks, and `set_page_mode` never rewrites HTML. You, the external agent, rebuild the imported content with ordinary block tools. The owner does not need to convert every Page manually. There is no editor conversion preview; it was removed in Core 0.2.58. Preserve the complete source before switching modes and never publish the empty intermediate Page.\n\nd. Rebuild into editable blocks according to the selected intent. Preserve exact\n wording when text_intent is preserve; track approved changes when it is renew, independently of design. Map headings,\n prose, images, buttons and layout to standard fields first, then create\n reviewed custom types where needed. Use raw HTML only for a documented,\n exceptional fragment. Preserve anchors and map every old URL in either mode.\n\ne. Write the page as a draft:\n\n ```\n create_page title=\"...\" slug=\"<last-path-segment>\"\n path=\"/<preserved-wordpress/path/>\"\n blocks=[<native block tree>]\n status=\"draft\" kind=\"article\" author=\"...\"\n seo_title=\"...\" seo_description=\"...\"\n ```\n\n **Preserve the source URL.** WP post URLs like\n `/2024/01/foo-bar/` uses `slug: \"foo-bar\"` and\n `path: \"/2024/01/foo-bar/\"`. Slug is one segment; `path` preserves the\n complete nested URL.\n\n### 5. Redirects\n\nAfter migration, every URL the agent didn't preserve verbatim needs a\nredirect:\n\n```\ncreate_redirect from_path=\"/old-services\" to_path=\"/services\"\n```\n\nWalk the inventory; for each URL: did it become a page with the same\npath? If yes, no redirect. If renamed, `create_redirect`. If\ncombined with another page, redirect to the relevant combined content. Do not\nmark a public page excluded just because the new design omits it. Public URL\nretirement requires a separately agreed exception; admin/session noise can be\nexcluded with a recorded reason.\n\nUse a pattern only after verifying every affected source-to-destination mapping.\nFor an explicitly approved prefix change, `/old-blog/:slug` → `/articles/:slug`\ncan preserve intent if each destination contains the corresponding article.\nDo not blanket-redirect category/tag/author/feed/year families to a home or blog\npage. Date paths need actual route mappings; stripping a year is not enough.\nReview source 404s rather than automatically excluding them.\n\nRules:\n\n- **Trailing `*` only.** A mid-path splat (`/blog/*/comments`) is dropped\n silently by Cloudflare — the platform refuses it at write time.\n- **`:splat`** carries the captured remainder; **`:name`** matches exactly one\n segment and can be replayed by name.\n- **A pattern that would hide a live page is refused**, naming the pages. That\n is the platform protecting you: redirects are applied before static files, so\n `/blogg/*` would make every real article under `/blogg/` unreachable. Narrow\n the prefix instead.\n- **Query-string URLs can't be matched.** WP's `/?p=123` has no path to key on;\n these require an explicit query-routing solution on the future host or a\n reviewed retirement decision. The old host cannot handle requests after its\n hostname moves.\n- Rules are emitted most-specific-first, so a narrow rule always beats a broad\n one — you can safely have `/blogg/recept/*` alongside `/blogg/*`.\n\nThen verify against reality before anything is cut over:\n\n```\nimport_sitemap url=\"https://old.example.com/sitemap.xml\"\n# Optional: direct Search Console query (platform service account must have property access)\nimport_gsc_performance property=\"https://old.example.com/\" months=6\n# Or paste a Search Console Pages CSV via csv=\"...\" and source_origin.\ncheck_internal_links # database preflight before deploy\nverify_migration_urls # after trigger_deploy; compact exceptions by default\nrecord_migration_seo_acceptance # after reviewing this exact deployment\nget_migration_launch_report # final fail-closed launch gate\n```\n\nThe inventory merges slash-equivalent URLs into one work item but preserves\ntheir source spellings in `observed_paths`. Verification requests every one of\nthose spellings; treat a failing variant as a real gap even when coverage is\notherwise complete. Fresh builds expand redirect sources to both slash forms.\n\nFor a site imported before Typeroll normalized WordPress plain-text fields,\naudit and repair the legacy records before visual review:\n\n```\nrepair_migration_plain_text # dry_run=true by default; returns exact field diffs\n```\n\nThis operation is deliberately limited to `title`, `seo_title`,\n`seo_description`, and schema-defined plain-text `excerpt` fields. It cannot\ntouch bodies, HTML, slugs, paths, or URLs. Show all returned diffs and\nconflicts to the user. Only after explicit approval, repeat the same scope and\nselectors with `dry_run=false save=true`. If `truncated=true`, narrow the\nselection or raise `diff_limit` and review the omitted diffs first. Never work\naround a `working_copy` conflict: that resource contains another edit which\nmust be resolved separately.\n\n### 6. Preview + review with the user\n\n```\nget_preview_link page_id=<id> # one URL the user can click\n```\n\nCompare the source and target at desktop and mobile widths. Review representative\narticles, an archive and the home page. Measure heading sizes, readable width,\nspacing, TOC placement and image/card proportions. Scroll below the fold: verify\nheadings after media, the TOC below a sticky header, anchor landing positions\nand related articles inside the intended content column. In Core 0.2.7+, use\n`rhythm: \"article\"` on the Page content slot and `appearance: \"card\"` on Post\nCards for these native presentation choices; no per-page corrective CSS. Check breadcrumbs and shared\ncategory references; test forms/Extensions. HTTP 200 and text presence do not\nprove visual fidelity. Document intentional improvements such as responsive video.\n\nRecord `fidelity: { desktop, mobile, shared_data, integrations, evidence }` with\n`record_migration_seo_acceptance`, based on real checks of the exact deployment.\nThe launch report requires this evidence in Core 0.2.5.\nNever mark an unavailable integration or uninspected screenshot as accepted.\n\n### 7. Ship\n\nWhen the user signs off:\n\n```\n# Bulk-publish drafts that look right\nbatch_update_pages updates=[{page_id, patch:{status:\"published\"}}, ...]\n\n# Deploy\ntrigger_deploy\nget_deploy_status job_id=<id> # poll\n```\n\n## Pitfalls\n\n- **Don't publish during migration.** Always import as `draft`. Even\n if the agent is confident, the customer needs the chance to spot-check.\n- **WP slugs sometimes drift.** A post saved with slug `foo-bar` may\n have been served at `/2024/01/foo-bar/` due to the permalink\n structure. The full URL is what users see in Google; preserve that,\n not the bare slug.\n- **Image bandwidth.** R2 upload is metered. Use `find_pages_matching`\n contains=\"<old-domain>\" on already-imported content to spot images\n that weren't transferred.\n- **Plugin JavaScript.** Never copy scripts from plugins, the theme or\n WordPress core into settings, pages, partials or block types, and never\n copy the cookie plugin. Carry analytics and advertising over by ID in the\n Integrations app with `cookie_consent.enabled: true`. The launch report\n fails on copied source scripts and on tracking outside consent.\n- **WP-specific JSON-LD** (Yoast, Rank Math) is usually wrong after a\n redesign because it references old URLs. Strip it; let Typeroll\n emit fresh Article/Page schemas via `kind: 'article'` + `author`.\n\n## When the source isn't WordPress\n\nThe same shape applies for any source — Squarespace export, custom\nCMS, scraped HTML, CSV. Replace step 1's \"WP REST\" probe with whatever\ndiscovery the source supports, and the rest of the recipe is unchanged.\n\n## Storage prerequisite\n\nBefore reading or importing source content, call `get_import_readiness` (Core\n0.1.95 / MCP 0.44.68 or later). If `ready` is false, stop the import and show the\nreturned message and Publishing settings link. The organization must connect\nand verify its own storage first. Do not fall back to draft storage, hotlink\nsource images, or use ordinary page-write tools to bypass the import gate.\nUse `upload_media_from_url` for referenced source images; the customer’s\nCloudflare transfer Worker copies and verifies them directly in R2, independently\nof the GitHub/Cloudflare build-provider choice.\n\n### Raw-source media order (Core 0.2.35+)\n\nPass raw body HTML into conversion before any parse/serialize round trip with\nanother parser. Core applies browser tree construction before lazy-media cleanup.\nAn image-only H2 becomes an image at that position, not a semantic heading.\nCompare ordered headings/text/media/CTAs, including malformed wrappers, lazy and\nnoscript images and related-content boundaries. Existing Pages are not rewritten;\nrepair confirmed placement errors against raw source without reimporting edits.\n\n\n## Navigation and empty-media variants\n\nInventory first-step input banners independently of final submission providers.\nPreserve their fields, required/minimum validation, service tabs and direct links,\nmobile collapsed/open states and navigation even when the final integration is\ndeferred. Core 0.2.40 provides native `core/navigation_form` and mixed `core/tabs`.\nRead their schema instead of rebuilding them in HTML or reintroducing a provider\nblock. Defaults are same-origin and tab-scoped; never place addresses in URLs or\nclaim cross-origin prefill. The receiving form must explicitly accept the fields.\n\nCompare listing rows with both real and missing images. Use shared post-card\nplaceholder settings for theme decoration, preserving real featured images and\nexplicit `show_image: false`. Compare actual footer link height plus gap, wrapping,\nfocus and column breakpoints; compact navigation density is an intentional option.\nVerify native preview and static output on the agreed viewport boundaries.\n\n\n## Before retiring the source\n\nFollow the starter's workflows/source-media.md. Archive unused and print originals\nprivately with a verified SHA-256 manifest and a restore test; do not import the\nwhole archive into CMS or assume ETag means MD5. ICC-convert CMYK/TIFF derivatives,\nkeep originals, and review colour/framing before uploading. Archive completion\nand URL continuity are separate acceptance checks. Do not remove old hosting\nuntil the owner has approved retirement.\n\nSaving future hosts must not block normal publication on the current/temporary\naddress. Publish and review intended changes there first. Shortly before changing\nDNS, explicitly publish the last published snapshot for the future hosts with\n`prepare_publishing_domain_change` (Core 0.2.116 / MCP 0.45.87). This replaces\nmain at the current address before DNS changes; new-host links may wait for DNS.\nReview frozen indexing settings and approve DNS separately, without another\nbuild or upload. Later normal publications retain the new origins and refresh\nthe receipt; read the latest candidate_id before approval. No separate old site\nis kept at the temporary hostname. Older Core versions use the older branch flow. Respect preserve/renew choices for owner-supplied facts;\n\"timeless\" wording must not invent recurring events or erase preserved copy.\n\n## Fluid typography and component rhythm\n\nCore 0.2.114 / MCP 0.45.85 / capabilities 0.55.0 adds responsive scales.\nCheck the connected versions before using this contract. New sites use render\nversion 10. For an existing site, test a Version first; upgrading rendering\nand replacing existing roles are separate, deliberate choices.\n\nUse `fonts.size_base` (default 16), `typography.ratio_mobile` (1.2),\n`typography.ratio_desktop` (1.25), `typography.rhythm` (1), and\n`typography.line_length` (65 ch). Set the controls once, then use named type,\nspacing and line-height steps. `apply_standard_styles` uses those steps;\n`overwrite: true` resets existing roles, so review their current values first.\nExplicit sizes and breakpoint overrides still win and are not silently migrated.\n\nHeading role margins belong to editorial flow. Component titles use their own\npadding/gap, and opening headings in containers/sections add no top margin.\nUse role-linked `core/heading` size `auto` or `theme`; explicit hero sizes remain\ndeliberate exceptions. Do not compensate with per-card negative margins.\nCompare 375, 768 and 1280px in Site settings → Design and in a full site preview.\nAfter a rendering upgrade/rebuild, recheck cards, boxed headings, leading text\nand webfonts. Boxes should get their outer air from padding. Keep exceptions\nin decisions.md, and run lint_design_tokens before launch.\n",
21
21
  "tr-migration-evidence": "---\nname: tr-migration-evidence\ndescription: Choose text and design/image preservation independently, map standard and custom blocks, preserve old URLs, and verify content, design and behavior before migration acceptance.\n---\n\n# Migration evidence contract\n\n**Who runs this:** this recipe is for the user's own AI agent (Claude Code, Codex or\nanother MCP client) working through MCP and the public API. The portal's built-in\nAI assistant has no migration tools: it does not import, convert or verify sites.\n\nUse this shared contract with WordPress, Astro, URL and multisite recipes.\n## Choose the migration intent before writes\n\nRecord two independent choices in `brief/migration.md` (or the project's\ncorresponding brief): `text_intent` and `design_intent`, each `preserve` or `renew`.\nDesign includes images. These are planning labels, not migration API parameters.\nUse the starter's `workflows/interview.md` when guidance or decisions are missing;\nresume existing answers. Do not infer either choice from a vague \"migrate\" or\n\"renew\", or silently apply an old single-choice brief to both dimensions.\n\n| Text | Design/images | Acceptance |\n| --- | --- | --- |\n| Preserve | Preserve | Compare wording and appearance separately with the source. |\n| Preserve | Renew | Keep wording; compare appearance with approved new prototypes. |\n| Renew | Preserve | Review copy against the editorial brief; keep source appearance, allowing documented layout adjustments for new text length. |\n| Renew | Renew | Review copy and appearance against their separately approved briefs. |\n\nPreserved text includes headings, body copy, CTA labels and captions. Record\nmetadata, alt text, legal text and translation rules explicitly; SEO is not an\nexception permitting silent rewrites. With renewed design, list required assets\nand which images may be replaced or generated, including cropping, credits and\nrights. Format/size optimization must not change a preserved image's motif.\n\nRecord source URLs, dates, approved scope, per-Page exceptions and acceptance\nreferences separately. Both choices preserve facts, ownership, required behavior\nand historical URL continuity. Renewing design does not authorize text rewrites;\nrenewing text does not authorize new imagery. Neither authorizes invented claims,\nunreviewed content removal or domain cutover. Protect existing target edits.\n\n## Use editable blocks for all four combinations\n\n1. Inspect standard block schemas, composition starters, named styles and Page\n templates. Prefer these when they fit.\n2. When they do not fit, read `tr-custom-blocks` and create a reusable Site block\n type with meaningful editable fields. Prefer a composition of standard blocks;\n a field-driven template type is appropriate when composition cannot express it.\n Validate the type, preview it, save/read it back and test a representative use\n in static output. Do not store a whole page as an opaque HTML field.\n3. Raw HTML is a last resort for a small, exceptional fragment. Record the exact\n block/Page, why neither standard nor custom types fit, editing limitations and\n verification. Keep the surrounding page block-based whenever possible.\n\nA justified HTML exception is a unique art-directed hero: layered imagery,\nanimation and text placed precisely over a background at different breakpoints.\nPrefer a field-driven custom type when editors need to change its text, images\nor links repeatedly; a one-off hero may remain an isolated HTML block. Verify\nactual breakpoints, cropping, keyboard access, contrast and reduced motion. Keep\nessential copy and links usable without animation. Do not copy source-platform\nscripts; any custom script needs explicit scope and the supported runtime.\n\nOn Core 0.2.89 / MCP 0.45.69+, composition preflight accepts reviewed custom types in\n`reviewed_custom_block_types: [{type, reason}]` and rare HTML blocks in\n`reviewed_html_blocks: [{block_id, reason}]`. Inspect the deployed schema first on older installations. These are reviewer\nattestations, not proof of quality. They only acknowledge those specific choices; missing\nblock types/fields and unreviewed workarounds still block readiness. Do not call\nsomething business-specific just to bypass a check. A confirmed renderer bug\nstill needs a platform fix, not a disguised HTML/CSS workaround.\n\n## Import is not block reconstruction\n\nThe managed WordPress importer deterministically stores cleaned HTML and shared\nreferences, without AI rewriting. That is a source-preserving intermediate step.\nThe external agent rebuilds it into reviewed blocks/templates for all four combinations;\n`set_page_mode` does not perform conversion. Keep the complete source, map every\nin-scope content element, and do not silently truncate long pages. When text_intent is preserve retain wording; when it is renew track approved\nrewrites and source facts, independently of design_intent.\nAn import finishing does not mean design, block reconstruction or launch is done.\n\n## Old URLs must continue to work\n\nInventory public URLs from sitemaps, CMS records, internal links, pagination and\navailable traffic/backlink exports, including important assets and anchors.\nRecord each old URL as preserved or redirected to a relevant working destination.\nRenewal may merge pages, but every old address still needs a meaningful mapping.\nNever blanket-redirect to the homepage or mark public content `excluded` merely\nto make coverage green. Private/admin/session noise can be excluded with a reason;\nretiring a public URL requires a separately agreed exception to this contract.\n\nUse `add_migration_urls` with `source_origin`, keep a per-URL mapping, and run\n`verify_migration_urls` against the authorized deployed candidate, then\n`get_migration_launch_report`. Check both slash forms, redirect chains/loops,\nquery routes and fragments with their actual routing mechanism. DNS is not an\nHTTP redirect: changed hostnames need a host that continues serving redirects.\nA missing route or a 200 at unrelated content fails continuity in either path.\nDo not switch traffic until these checks pass and cutover is authorized.\n\n## Gate 1: source baseline, before conversion\n\nRecord both chosen intents, authoritative source URL/date, scope, relevant Core version, active\nPage templates and overrides. Inventory home, category/archive, long/short\narticles, downloads, empty related lists and each distinct business surface.\nFor design_intent=preserve, capture source and target geometry; for design_intent=renew, compare the target\nwith the approved design while retaining source coverage evidence. Capture\nscreenshots and computed geometry at matching actual `innerWidth`\nvalues (320/390/768/1024/1280/1536 where relevant, plus both sides of actual\nbreakpoints). Record loaded font families/weights, color panels, logo/header,\nhero, gutters, article/sidebar widths, card anatomy/grid and footer. Inspect\nloaded images, menus, scrolled TOC, anchor landings and long links.\nInventory semantic CTA anchors/buttons, downloads and affiliate links before\ncopying prose: retain decoded labels, href paths, query parameters, fragments,\n`target` and meaningful `rel` tokens. A source CSS class may identify a CTA\nwhose appearance would disappear when copied into prose.\n\nBefore removing builder wrappers/classes, preserve the information they carry:\nanchors, lazy/srcset/background images, captions, tables, ordering, downloads,\nembeds and interactions. Content modernization is a separate decision.\n\n## Gate 2: one verified prototype per active recipe, before bulk writes\n\nCall `list_block_types` and `read_block_type` on the deployed runtime. Map each\nsource component to exact fields/bindings, defaults, minimum version and expected\ngeometry. A registry entry does not prove the capability works. Save and read\nback each prototype, then inspect its preview and an authorized static build.\n\nExample: a 48px heading uses `core/heading.font_size_px: 48` on Core 0.2.25+;\nread the saved block back and measure computed `fontSize` at 1280px. Writing\n`font_size_px` to `core/prose`, which does not declare it, is an unsupported\nmapping even if an API preserves that unknown key. A new presentation may use\na reviewed custom type as described above. Missing or ignored declared behavior\nis a platform defect: stop that recipe with `waiting_for_native_support` and do\nnot disguise the failure with tenant CSS.\n\nValidate shared Page references, item order, missing images and empty/unpublished\nreferences. Keep semantics separate from presentation: category emoji belongs\nto its shared Page field, not copied into every article or card excerpt.\nValidate actual field types, not just key names. A presentation field whose schema declares\n`design_token` also accepts the named steps for that scale, including responsive maps\non responsive fields (Core 0.2.106 / capabilities 0.54.0). Otherwise number fields use JSON numbers\n(`16`, `1.6`, `1`), not strings (`\"16\"`, `\"1.6\"`, `\"1\"`); booleans use `true`/\n`false`. Check responsive map values too. Read-back retention is not enough:\nmeasure the resulting font size, line height and paragraph spacing. If an\nincorrect type is accepted but ignored, fix the prototype before bulk writes.\n\nMap standalone CTAs to `core/button` when its declared fields preserve the\nsource behavior, rather than leaving class-dependent anchors in prose.\nCore 0.2.25+ `new_tab: true` emits `target=\"_blank\"` and\n`rel=\"noopener noreferrer\"` in static HTML; false/unset keeps the same tab.\nVerify the rendered target/rel, preserved affiliate queries/fragments, keyboard\nactivation, actual tab opening and narrow-screen wrapping without requiring\nJavaScript. Do not silently discard source `sponsored`/`nofollow`, download\nbehavior or other semantics unsupported by the chosen block: record the gap\nand use a supported composition or stop for native support.\nDo not apply a template to hundreds of Pages until its prototype passes.\n\n## Distinguish custom presentation from platform defects\n\nA new layout or reusable visual component is a legitimate custom block, not\nnecessarily a Core defect. Use the block hierarchy above. When a declared Core\nfeature fails, do not cover it with site/page CSS, raw HTML or a replacement\nblock. Record and report it. Check these five causes first:\n\n1. **A platform default no setting reaches.** Compare computed text rendering\n and base styles on `body`, links and headings (`-webkit-font-smoothing`,\n `line-height`, `text-decoration`) with the source. If the difference comes\n from Typeroll's base CSS, check Site settings and the render versions first\n (`read_site_settings` → `render.upgrades`).\n2. **A replaced control ignores the block's settings.** A capability can swap a\n field for a provider element (address suggestions). Compare that control\n with a plain field in the same block: border, radius, height, background,\n text colour, font, placeholder, icon and colour scheme in light and dark mode.\n3. **A missing part.** The source has a part (icon, badge, second label) the\n block has no field for. Check `read_block_type` for a field before\n concluding it is missing.\n4. **A value that is stored and ignored.** Read every write response's\n `warnings` (`field_type_mismatch`, `unknown_style_override`): a wrong type or\n key is saved and never rendered. Measure the computed result, not the\n read-back.\n5. **Preview differs from the published page.** A setting that shows in preview\n but not in a fresh hosted build is a publication gap; report both URLs.\n\nReport each gap with: site ID, Organization, Core version and render version;\nsource and target URL; viewport (actual `innerWidth`), browser and DPR; the\nblock ID and type or the setting; source and target computed values; what is\nmissing, ignored or wrong; the proposed setting or fix; and acceptance\ncriteria a reviewer can measure. State that no workaround CSS is in place.\nMark the affected recipe `waiting_for_native_support`, continue independent\nwork, and verify against the chosen acceptance reference after the release.\n\n## Separate files, addresses and business behavior\n\nInventory both file bytes and every discovered historical asset URL, including\nPDF/download hrefs, image click targets, srcsets and backgrounds. Verify transfer\nand rendering separately from old-address preservation. Deduplicate by content\nidentity, not similar filenames. Preflight expanded redirect artifact bytes and\nprovider rule limits. A 200 at an unrelated destination is a failure. A source\n404 is not permission to discard a URL with traffic/backlinks: record a decision.\nQuery URLs need explicit routing support at the future host; keeping the old\nserver does not preserve them when the same hostname moves.\n\nClassify each CTA: contact form, owned lead workflow, affiliate link, partner\niframe/script or other runtime. Preserve market, language, destinations and\nattribution. Never clone recipients or partner IDs across markets. Test loading,\ninteraction and conversion separately; only use an authorized synthetic mode and\nsafe recipient. Unavailable delivery remains UNVERIFIED. No real leads or mail\nto third parties merely to fill a checklist.\n\n## Scripts, tracking and consent\n\nDo not copy JavaScript from the source site. WordPress plugins and themes\ninject their own runtime: jQuery, sliders, lightboxes, form and cookie\nplugins, page builders (Elementor, Breakdance, Divi), emoji and AJAX calls to\n`admin-ajax.php` or `/wp-json/`. It loads from, or posts to, a site that is\nabout to be switched off. Inventory every `<script>` on representative source\npages (head and body, inline and external, and which plugin or theme it\nbelongs to), then decide each one:\n\n| Source script | Decision |\n| --- | --- |\n| WordPress core, theme or plugin runtime | Never copy. Rebuild what it did with native blocks, Forms or settings, or report a platform gap (see \"Report platform gaps\"). |\n| Cookie plugin (CookieYes, Complianz, Cookiebot, Borlabs, …) | Never copy. Enable the site's cookie banner (`cookie_consent.enabled: true`) with Swedish or local text and the privacy policy URL. |\n| Analytics or advertising tag in the Integrations catalog (GA4, GTM, Meta Pixel, LinkedIn, TikTok, Hotjar, Clarity, Pinterest, Snap, Reddit, X, Microsoft Ads, HubSpot, Plausible, Fathom, Matomo) | Carry over the ID only: enable the Integrations app with the source's ID (`list_apps`, `update_app`). Enable the cookie banner; analytics and advertising integrations wait for the visitor's consent. Never paste the vendor snippet. |\n| Tracking tag the catalog does not cover | Only with the owner's approval, in `cookie_consent.scripts_optional` with the banner enabled. Never in `scripts_head`, `scripts_body_end`, `scripts_necessary`, pages or partials. |\n| Partner widget or embed the page needs to work | Prefer a native block or an iframe from an allowlisted host. Keep custom code minimal, record why, and do not load it from the source site. |\n\nAsk the owner to confirm the analytics IDs; never invent or reuse one from\nanother market. Before launch, `get_migration_launch_report` must show\n`scripts.status: \"clean\"`: it fails on copied source-platform scripts\n(`source_runtime_script`), tracking outside consent\n(`tracking_outside_consent`) and analytics or advertising integrations without\nthe banner (`tracking_without_consent_banner`). In the hosted preview, check\nthat no request goes to the source host or `/wp-content/`, no tracking request\nfires before consent, and the tags fire after \"accept all\".\n\n## Evidence record (repeat per recipe, viewport and state)\n\n- Source URL/date and target URL; Page/template IDs; exact publication ID.\n- Runtime/Core version and saved content/config revision.\n- Actual viewport `innerWidth` and height; scroll/menu/anchor state.\n- Source and target screenshot paths; measured expected and actual geometry.\n- Capability mapping: block type, field, read-back value and computed result.\n- Separate PASS / FAIL / UNVERIFIED for routing, content, visual, assets,\n keyboard/interaction and delivery; reviewer and timestamp.\n- Visible deviation, specific user benefit, accepted scope and rationale.\n\nFilled examples:\n\n| Observation | Technical result | Visual decision |\n| --- | --- | --- |\n| Source video overflows a 390px viewport; native video fits and shows the entire frame | Width 390px, original aspect preserved; captures reviewed | PASS: deliberate improvement in mobile usability |\n| Source has four cards and turquoise hero at 1280px; target has three plain cards and no color panel | HTTP 200, no overflow, all content is blocks | FAIL: brand/layout lost; those technical facts cannot attest visual fidelity |\n| Heading round-trip retains 48 but computed size stays 32 | Save succeeds | FAIL: find ignored field/cascade before bulk conversion |\n\nKeep imported, saved, locally checked, hosted-preview checked, published and\ntraffic-cut-over distinct. Template/theme changes invalidate affected acceptance;\nnever attach old evidence to a new publication. `record_migration_seo_acceptance`\nbooleans are reviewer attestations, not automated visual proof. Do not set them\nfrom HTTP status, native-block count or overflow alone. Report a completed\npre-cutover scope honestly when DNS or integrations remain out of scope.\n\n## Complete census and visual signoff\n\nThe initial route census must combine source sitemaps, internal links, archive\npagination, CMS inventory and known utility routes. Include homepage, archives,\n404/search behavior and routes outside the sitemap where present. Do not invent features absent from the source; include additions only in the agreed brief. Assign every route a recipe, template override,\ncomposition or documented exception. Inventory the whole shell: content outside\n`main`, header, footer, service navigation and archive heroes are not optional.\n\nDerive required evidence from that census. For design_intent=preserve, pair every\nin-scope source/target route at mobile and desktop; for design_intent=renew, compare\nevery target route to its approved prototype. Independently compare preserved text\nto the source and renewed text to its editorial brief; account for the source\ncontent/URL mapping in every combination; add each unique component, breakpoint and interaction\nstate. Inspect page-specific media, lists, tables, embeds, CTAs, anchors and related\ncontent. One representative article cannot attest every article's content.\n\nA coverage row records route, recipe, component, state, actual viewport dimensions,\nsource revision, target content/config/Core/publication revision, paired screenshot\nreferences, reviewer, review time and result. All rows start `UNVERIFIED`.\nKeep independent results for content, links, list cardinality, full membership,\nsequence, appearance, keyboard behavior and delivery. Screenshots must be inspected;\nimage diffs and contact sheets assist review and do not prove aesthetics.\n\nFor menus capture source and target closed, open and scrolled states, nested groups\nif present, short landscape, focus order, Escape, link activation and breakpoint\ntransitions. For TOCs capture top/middle/bottom and anchor landings. Include tabs,\naccordions and empty/loading/error/success states where applicable. Separate mobile\nand desktop menu compositions require separate evidence; shared links may be reused\nbut one variant's screenshots do not attest the other.\n\nReport denominators separately: routes/content, paired visual pages, recipes,\ncomponents and states. Missing an archive, open menu, footer or a unique embed leaves\ncoverage incomplete even if all captured screenshots pass. Shared block, theme,\npartial or template changes invalidate every dependent row; content changes\ninvalidate that route and dependent listings. An old revision cannot approve a new\none. Source availability failures remain `UNVERIFIED`, not silently excluded.\n\nVisual signoff is against the chosen intent: source fidelity for preserve,\napproved design/content for renew. It requires zero uncovered required surfaces\nand zero unexplained regressions against that reference. `FAIL` or `UNVERIFIED` blocks that signoff, even when a separately scoped\ntechnical publication has approval. An intentional deviation needs exact before/\nafter evidence, specific user benefit and accepted scope; “cleaner” is not a reason.\n\nPreserve-mode examples from a moving-services migration:\n\n| Surface | Passing evidence | Separate visual result |\n| --- | --- | --- |\n| Mobile menu | Keyboard focus and Escape work | FAIL: header moves, source service group is missing, open-menu composition differs |\n| Category archive | Every destination URL exists | FAIL: source sequence, card anatomy and columns differ |\n| Article body | All text imported | UNVERIFIED: unique embed and footer were never compared |\n\nDo not report “all design checked” from a viewport or recipe sample. Keep source\nlink-order comparisons and paired contact sheets with the report. The same contract\napplies to WordPress, URL, Astro and multisite migration; do not create separate,\nweaker checklists for individual import mechanisms.\n\n\n## Navigation and empty-media variants\n\nStructured research imports must use explicit import authority, not ordinary\neditor API writes. Core 0.2.95/MCP 0.45.74 support `create_page(write_mode:\n\"import\")`, `read_page_authority` and atomic `import_page_fields`. Preserve\napproved answers and owner-cleared nulls. Add historical anchors to existing\nkeyed items as individual leaves; never send replacement arrays to add one\nfield. Use the same selected Site/version and the current authority revision.\nStop on 409 rather than overriding or resetting provenance. See the Pages tools\nguide for schema opt-in, version availability and path escaping.\n\nInventory first-step input banners independently of final submission providers.\nPreserve their fields, required/minimum validation, service tabs and direct links,\nmobile collapsed/open states and navigation even when the final integration is\ndeferred. Core 0.2.40 provides native `core/navigation_form` and mixed `core/tabs`.\nRead their schema instead of rebuilding them in HTML or reintroducing a provider\nblock. Defaults are same-origin and tab-scoped; never place addresses in URLs or\nclaim cross-origin prefill. The receiving form must explicitly accept the fields.\n\nCompare listing rows with both real and missing images. Use shared post-card\nplaceholder settings for theme decoration, preserving real featured images and\nexplicit `show_image: false`. Compare actual footer link height plus gap, wrapping,\nfocus and column breakpoints; compact navigation density is an intentional option.\nVerify native preview and static output on the agreed viewport boundaries.\n\n## Domain migration and redirect capacity\n\nUse `guide_publishing_domain_change` when advertised by this installation. Read\nits checklist, preview settings impact before replacing a candidate, preserve\nverified content when only redirect hosts change, and obtain explicit approval\nfor the traffic summary and any interruption. A saved future hostname is not live.\nOrganization-admin `onboard_publishing_domain_zone` can create a pending zone and\nreview a DNS scan; copy records DNS only and verify the full inventory (especially\nmail and DKIM) before the human registrar step. Never infer completeness from a scan.\n\nCore 0.2.107 checks Pages capacity during dry-run and final upload:\n2,000 static, 100 dynamic, 1,000 characters per line, with generated slash variants\nand hostname aliases included. `_redirects` has a bounded 10 MiB transport envelope;\n`_headers` keeps 128 KiB. Source preflight counts configured rules conservatively;\nfinal output excludes redirects shadowing built pages. Remove obsolete shadowed\nrules at source when close to capacity. Never split `_redirects` into multiple files.\nAfter publication, run `verify_migration_urls` against the agreed full inventory.\nDo not delete old provider redirect lists without separate explicit approval.\n\nRecord render/media reuse and actual preparation duration separately. A cache\nformat, renderer, indexing or content change may require one full baseline build;\na synthetic cache test does not prove a customer site's timing target. Media\ntransfer timeouts retry the same frozen job with checkpoints, not a new publication.\n",
22
22
  "tr-new-site": "---\nname: tr-new-site\ndescription: Use when the user wants to create a new Typeroll site from scratch, set up the initial design, or bootstrap a blank site with working header/footer, brand colors, and a homepage. Also triggers on \"start a new site\", \"set up a site for\", or \"build a website for [company]\".\n---\n\n# Bootstrap a new Typeroll site\n\n## Site tokens before block overrides\n\nCore 0.2.106 / template capabilities 0.54.0 adds palette names in all colour\nfields and named typography/spacing steps. Check the connected capability\nversion; on older renderers use `var(--color-surface)` rather than a bare name.\n\nSet colours and font families in site settings, then use named styles and\npalette names in blocks. Do not write hex, rgb or font-family literals in block\ncontent. Prefer `primary`, `secondary`, `accent`, `background`, `surface`, `text`,\n`text_light`, and `on_primary`/`on_secondary`/`on_accent` for text on brand colours.\nUse `surface_hover` for a darker interactive surface (derived automatically\nunless explicitly configured). Existing semantic roles should be reused; a\nnew visual shade is not automatically a new semantic role. For a derived shade\nuse `color-mix(in srgb, var(--color-surface) 92%, var(--color-primary))`.\nFor an additional role not in the native palette, define a named site style\nor a site CSS variable derived from existing palette variables, and document it.\n\nChoose `heading`, `body` or `mono` for fonts. Font families belong only in the\nsite's `fonts` settings. Use type-size steps `xs`, `sm`, `md`, `lg`, `xl`, `2xl`,\n`3xl`; spacing `none`, `xs`, `sm`, `md`, `lg`, `xl`; line height `tight`, `snug`,\n`normal`, `relaxed`, `loose`. Presentation fields accept responsive maps of these\nnames, for example `font_size_px: {mobile: \"md\", desktop: \"lg\"}`. Read the field\nschema: counts, widths and breakpoints are not spacing steps. Pixel values are\nexplicit exceptions, not the default authoring method. Configure scales in\n`design_scales`, then reuse them in role styles and blocks.\n\nRecord deliberate exceptions in `decisions.md`, with source/block, reason and\nreview criteria (for example white text on a photograph). Run `lint_design_tokens`\nbefore launch and after major design changes. It warns, never blocks launch.\nUse the starter's `scripts/typeroll/color_tokens.py` for an exact-match dry run;\nreview the plan before applying it in a dedicated Version. Equal hex values can\nmean different roles: never automatically choose between background and text.\nVerify a primary/surface change in a test Version across header, footer, cards,\nmenus and buttons, then restore the test settings. Do not republish automatically.\n\n\n> **The buffer model (draft writes).** Every content write in this recipe\n> (pages, blocks and partials) lands in an unsaved per-doc\n> DRAFT — deploys and plain previews only see SAVED content. For recipe-style\n> build work, pass `save: true` on write calls (the work is pre-approved by\n> the task itself), or run `commit_working_copy` per doc before any\n> `trigger_deploy`. Preview your drafts with `include_working_copy: true`.\n\n\nStart here when the site already exists as a database record (created via\nthe portal UI or API) but has no design, no header/footer, and no pages.\nThe goal is to go from blank to a working 4-page site with correct brand\nidentity in a single session.\n\n**Pages are built in block mode** — the platform default. Blocks give\nstructured, per-field editing, native full-bleed sections, responsive\nbreakpoints, and templates. HTML mode is the secondary path for\nhand-crafted one-offs and migrated content (section at the end).\n\n## Workspace and context\n\nAn optional agent-neutral workspace stores the brief, decisions and QA. It does\nnot create a Site or replace CMS content. See the public Agent workspace guide.\nFor compact MCP: search_tools, describe_tool, then the appropriate read/write/admin\nwrapper. Load only the relevant guide section and recipe. Discover site apps with\nread_app_documentation when needed; do not bundle private app guides locally.\n\nNo Site yet? Use hosted MCP with an Organization key and create_site, or create it\nin the portal. To start from an AI site plan instead of a blank Site, use\ncreate_site_and_plan, poll get_workflow and approve its structure review only\nwith the user's consent. That AI plan needs the Organization's own portal AI\nkey for new Sites; without it the call answers `403 ai_not_enabled` and the\n`site_planning` workflow fails with `failure_code: \"ai_not_enabled\"`. Then plan\nit yourself: `create_site`, propose the site name, tagline and page list (title,\nslug, purpose, target keywords) from the business description, get the\nuser's approval, and build with this recipe. Tell the user you planned it\nyourself; don't retry the workflow. Read back its ID before editing. Use one explicit Site/Version for\nrelated operations. Larger changes to an existing site should use a branch.\n\nBefore importing content or a media library, require get_import_readiness to pass.\nPublishing accounts may remain unconfigured while authoring new drafts and previews.\n\n## Preconditions\n\n- `@typeroll/mcp-server` configured with a valid `TYPEROLL_API_KEY`.\n- The site exists (confirm with `get_site`).\n- You have the customer brief: company name, industry, 2–3 key brand colors,\n tone of voice, and a list of initial pages.\n\n## Recipe\n\n### 1. Audit current state\n\n```\nget_site\nget_site_capabilities # template_capabilities_version — what this deployment supports\nread_site_settings # see what (if anything) is already configured\nlist_pages # don't overwrite pages that already exist\nlist_partials # check if header/footer already have content\nlist_block_types # the per-site block palette — NEVER assume, always list\n```\n\n**Do not choose a content mode or file a platform gap before these calls\nreturn.** For every required visual/behavioral element, map it to an existing\nblock or composition first. If the summary suggests a match, use\n`read_block_type` to inspect the exact schema. Only call something “missing”\nafter checking capabilities, the complete palette, and custom block types.\n\nCommon requirements that are easy to misclassify:\n\n| Requirement | Existing Typeroll primitive |\n|---|---|\n| Full-bleed hero flush below the header | `core/section` + `core/hero`; block-mode sections already own the full width and have no page-shell padding |\n| Responsive icon/card grid | `core/grid` + `core/icon_box`, or `core/feature_grid`; set responsive fields with `set_block_responsive` |\n| Custom cards backed by Pages | `core/repeater` / `core/page_list` with a site-authored `item_compatible` block type as `item_block` |\n| Grouped Page listing | `core/repeater` with `group_by`; array-valued fields place an item in every matching group |\n| Breadcrumbs in a page template | `template/page_breadcrumbs`; page and item routes supply a server-rendered trail |\n| Shared article typography | Set `font_size`, `line_height` and `paragraph_spacing` on `template_content_slot`; blank values retain block defaults (Core 0.2.2+) |\n| Generated heading index | `core/table_of_contents`; choose heading levels; links always come from the current Page headings |\n| Previous/next Page links | `template/page_navigation`; defaults to content type sort order and can bind explicit neighbor fields |\n| Download CTA that disappears without a file | `template/show_if` around a context-bound `core/button`; a dedicated download block is only editor convenience |\n| Sticky/custom header and multi-column footer | Block-mode header/footer partials plus layout blocks, or one reusable custom block type |\n| Cookie notice | `settings.cookie_consent`, not a page block |\n| Consent copy owned by an installed Extension | `list_extension_installations` → `read_extension_installation` → `update_extension_installation_config`, using exact manifest schema keys; the update queues a production deploy by default |\n| One-off HTML + JavaScript embed | `core/embed`; reusable widgets use a custom block type with `script` |\n| CTA/button variants | `core/cta` and `core/button`, styled from site tokens or a narrowly scoped class |\n| Two-column image/text | `core/media_card`, `core/feature_row`, or `core/columns` |\n| Figures, captions and tables | `core/prose` / richtext; the sanitizer preserves these semantic tags |\n| Full-width block page without a content shell | Native block mode; top-level `core/section` is unconstrained |\n| Brand colors and typography | Site `colors`/`fonts` tokens consumed by core blocks |\n\nReal gaps should say what was checked and why the nearest primitive is not\nenough. “I did not see a dedicated block name” is not evidence by itself.\n\n### 2. Brand + settings\n\nOne `update_site_settings` call with every field you know:\n\n```json\n{\n \"site_name\": \"Acme Studio\",\n \"tagline\": \"Short, punchy tagline\",\n \"language\": \"sv\",\n \"trailing_slash\": \"always\",\n \"colors\": {\n \"primary\": \"#1a1a2e\",\n \"secondary\": \"#16213e\",\n \"accent\": \"#e94560\",\n \"background\": \"#f5f5f5\",\n \"surface\": \"#ffffff\",\n \"text\": \"#1a1a2e\",\n \"text_light\": \"#6b7280\"\n },\n \"fonts\": { \"heading\": \"Playfair Display\", \"body\": \"Inter\", \"size_base\": 16 },\n \"contact\": { \"email\": \"hej@acme.se\", \"phone\": \"+46 8 123 456\" },\n \"social\": { \"instagram\": \"https://instagram.com/acme\" }\n}\n```\n\nRead it back with `read_site_settings`. Google Fonts names are\ncase-sensitive display names (\"Plus Jakarta Sans\", not \"plus jakarta\").\n\n**Site icons are part of brand setup** — upload favicon (32–64px), a\n180×180 apple touch icon, and a 192×192 app icon; set `favicon`,\n`apple_touch_icon`, and `icon_192`. No icon assets? Derive a proposal (see\n`tr-brand`). Also set `settings.logo` to the uploaded brand mark — it feeds\nOG/schema even if the header uses a different lockup.\n\nSet `iframe_allowed_hosts` when customer content embeds a provider outside\nthe built-in YouTube/Vimeo/Google Maps/Calendly set. Values are exact domain\nhostnames, never URLs or wildcards. Read the setting back before deciding that\nan iframe cannot be represented.\n\n### 2b. Styles before pages\n\nSet the site's look as named styles before building any page:\n\n1. `list_styles`. New sites already have the standard set (body, H1–H6, link,\n lead, eyebrow, small, quote, buttons, section). If roles are missing,\n `apply_standard_styles`.\n2. Adjust them to the brand with `update_style`: the font, the type scale and\n spacing for `base` (phones) and `at.tablet`/`at.desktop`, and colours from\n palette tokens. Keep text readable: contrast is checked with APCA and\n WCAG 2; text that fails both is refused, and text below its APCA level\n comes back as a warning.\n3. Add a named style for every look the design repeats: a card title, a price\n note, a highlighted section, a CTA button variant. Use the site's language\n for names.\n\nPages then choose styles with each block's `style_id` field. A per-block\nfont size, colour or class is only for something clearly unique.\n\nTo change a sub-element a block styles itself (the underline on formatted\nheading links, a hero's heading or buttons), use its parts:\n`read_block_type` lists them. A style with `parts: [\"core/rich_heading.link\"]`\napplies to that part on every block; `data.part_styles` on one block replaces\nit there.\n\n### 3. Header + footer partials\n\n**Start from the native preset — don't hand-roll navigation.** Read\n`tr-header-footer` and use its `template/site_logo` + `core/navigation_menu` with `core/navigation_links` slots\ncomposition. The server-rendered landmark, current-page state, no-JS links,\nmobile disclosure, focus treatment, and responsive behavior are Core\ncontracts rather than tenant CSS/JavaScript.\n\nStage the complete tree with `update_partial partial_id=\"header\"\npatch={blocks:[...]} save=true`, then call `set_partial_mode partial_id=\"header\"\nto=\"blocks\"`. Repeat for the footer and read both partials back. The inactive\nHTML representation is retained for rollback; changing `content_mode` through\nordinary PATCH/PUT is rejected intentionally.\n\nUse HTML mode only when preserving legacy authored markup that cannot yet be\nrepresented natively. Partials receive the same `{{site.*}}` render context as\npage blocks. Keep the first section and header backgrounds intentional; do not\nadd a decorative border merely to compensate for mismatched spacing.\n\n### 4. Homepage — block tree\n\n`create_page` with the whole tree in one call. Omit block `id`s — the\nplatform assigns them (`blk_…`); you read them back for later\n`update_block` calls.\n\n```\ncreate_page title=\"Start\" slug=\"\" status=\"draft\" content_mode=\"blocks\" blocks=[...]\n```\n\nA proven landing-page skeleton (every top-level block is a `core/section`\n— sections are **natively full-bleed**: the background runs edge-to-edge,\ncontent is constrained by the section's own inner container via the\n`width` field. NEVER use 100vw negative-margin hacks. Anchor ids and\ncustom classes via `style_overrides` are safe on sections from\ntemplate_capabilities_version ≥ 0.15.3; on 0.14.x–0.15.2 they wrap the\nsection in a div and silently break full-bleed — there, put the anchor\non a block *inside* the section instead):\n\n```json\n[\n { \"type\": \"core/section\", \"data\": { \"background\": \"surface\", \"padding_y\": \"lg\" }, \"children\": [\n { \"type\": \"core/media_card\", \"data\": {\n \"image\": \"MEDIA_URL\", \"image_alt\": \"…\", \"image_side\": \"right\",\n \"heading\": \"Huvudrubriken\", \"heading_level\": \"h2\",\n \"text\": \"<p>Ingress …</p>\",\n \"button_label\": \"Kontakta oss\", \"button_url\": \"/#kontakt\"\n } }\n ] },\n { \"type\": \"core/section\", \"data\": { \"padding_y\": \"lg\" }, \"children\": [\n { \"type\": \"core/heading\", \"data\": { \"text\": \"Så funkar det\", \"level\": \"h2\", \"align\": \"center\" } },\n { \"type\": \"core/grid\", \"data\": { \"cols\": { \"mobile\": 1, \"tablet\": 2, \"desktop\": 3 }, \"gap\": \"lg\" }, \"children\": [\n { \"type\": \"core/step_card\", \"data\": { \"number\": \"1\", \"title\": \"…\", \"text\": \"<p>…</p>\" } },\n { \"type\": \"core/step_card\", \"data\": { \"number\": \"2\", \"title\": \"…\", \"text\": \"<p>…</p>\" } },\n { \"type\": \"core/step_card\", \"data\": { \"number\": \"3\", \"title\": \"…\", \"text\": \"<p>…</p>\" } }\n ] }\n ] },\n { \"type\": \"core/cta\", \"data\": {\n \"heading\": \"Redo att börja?\",\n \"primary_label\": \"Kontakta oss\", \"primary_url\": \"/kontakt\"\n } }\n]\n```\n\nBlock-palette guidance (verify against `list_block_types` — the source of\ntruth):\n\n- **Hero:** `core/media_card` inside a white section (image beside copy),\n or `core/hero` (eyebrow/heading/subheading + `primary_*`/`secondary_*`\n buttons — rendered server-side; `layout: split-right` puts the image\n beside the text). For a plain text hero: section + heading + prose +\n button.\n- **Alternating image+text rows (\"zig-zag\" feature/step lists):**\n `core/feature_row` (template_capabilities_version ≥ 0.29.0) — balanced\n halves that hug the center gutter so wide screens never open dead air,\n natural-aspect image, `image_side: left|right` per row, mobile stack\n order as a field. Prefer it over hand-building `core/columns` with an\n unbalanced ratio + width-capped text (the classic dead-air trap). Use\n `core/media_card` instead when you want a *boxed card* with a\n filled/cropped image.\n- **`core/heading`** decouples `level` (h1–h6, semantics) from `size`\n (visual) — exactly one `level: h1` per page.\n- **Images:** `core/image` with an uploaded media URL. The build pipeline\n emits responsive output from prepared media. Upload through a signed direct\n grant, finalize it, and use returned IDs/URLs. Follow media preparation status;\n do not routinely regenerate unchanged images or use legacy maintenance tools. Use the\n `radius` field for rounded corners.\n- **Repeaters/listings:** `core/page_list`, `gallery`,\n `feature_grid` etc. — alias blocks over `core/repeater`. Use these for\n Pages of a content type instead of hand-writing listing markup. The base\n repeater also supports `group_by`, group ordering/headings, multi-valued\n membership, filters, custom `item_block`, and `item_overrides`.\n- **Long-form navigation:** `core/table_of_contents` builds an anchor list\n from page headings. Page templates use\n `template/page_navigation` for deterministic previous/next links.\n- **Forms:** `create_form`, then place `core/form` with its `form_id`.\n HTML-mode pages use `<x-form id=\"…\" />`; both paths render the same signed\n shell and runtime. See `tr-forms`.\n- **`core/html`** is the escape hatch for the genuinely unique thing —\n not a default. If you reach for it more than once or twice per page,\n note why (that's block-library feedback).\n\nSlot containers (`core/columns`, `core/tabs`): populate them either by\npassing the whole tree inline (`block={ type: 'core/columns', slots:\n[[…],[…]] }`) or incrementally with `add_block parent_id=<columns-id>\nslot_index=0|1`. Requires template_capabilities_version ≥ 0.15.2 — on\nolder sites use `core/grid` (children flow into columns) instead.\n\nPartial last rows: keep `last_row: 'start'` on `core/grid`. Incomplete rows\nstay left-aligned, preserving the reading edge. Center only after an explicit\nowner design request, never as an automatic fix. Do not widen one peer card\nor invent six-column CSS to fill a gap.\n\nIcons (template_capabilities_version ≥ 0.16.0): `type: 'icon'` fields on\n`core/icon`, `core/icon_box`, and `core/step_card` render inline SVG when\nthe value is a name from `get_site_capabilities → core_icon_names` (a\ncurated Lucide subset — `check`, `star`, `shield-check`, `mail`,\n`arrow-right`, `truck`, `chart-line`, …). Any other value (emoji, plain\ntext) renders as text, so emoji stand-ins keep working. Icons inherit\nsize from font-size and color from `currentColor`/the block's color\nfield. On older sites icons don't render — use emoji or CSS markers.\n\nEditor schemas (template_capabilities_version ≥ 0.39.0): labelled enum\noptions, newline-edited string lists, nested object/repeating-array fields,\nand internal-page pickers for URL fields are first-class. Do not flatten an\nExtension's `props_schema` merely to make it editable.\n\nFor an Extension placed in an HTML header or footer, require\n`supports_extension_html_partial_directive: true`. The broader\n`supports_extension_html_directive` flag covers HTML page bodies and is not\nproof that an older static build expands partial directives.\n\nFor a cross-page Extension flow, require both\n`supports_extension_site_navigation` and `supports_extension_storage`. Use\n`context.site.navigate(\"/path/\")` and installation-scoped\n`context.storage.session` rather than direct root-path navigation, Web Storage,\nor personal data in query parameters. This keeps navigation inside the current\npreview and preserves state without exposing it through URLs or referrers.\n\nTo prefill an Extension component from a native `core/navigation_form` banner\non the previous page, require `supports_extension_page_handoff`, check that the\ninstallation grants `page_handoff:read`, and set the component block's\n`page_handoff_key` to the banner's `handoff_key`; the banner's `destination`\nmust be the component's page. The component reads the values with\n`context.handoff.read()`. Do not add query parameters for this.\n\nKnown limitations (honest list — don't fight them):\n\n- **`core/tabs` label icons don't render** (the tab strip is built\n client-side without the icon pipeline). Text labels only.\n\nTheming: block primitives render neutral. Brand color/typography comes\nfrom settings (step 2) and named styles (step 2b). For page-specific polish\n(e.g. a colored card treatment) that no style covers, give the block a\n`custom_class` and write the rule in the page's `custom_css`. Never put a\n`<style>` in a `core/html` block or target `[data-bid]`/`[data-block]`.\n\n### 5. Inner pages\n\nSame pattern: `create_page` with `content_mode: \"blocks\"` and a section\ntree. Standard set: Om oss, Tjänster, Kontakt — or what the brief says.\nDefault new pages to `status: \"draft\"`; publish after review.\n\nSections that repeat across pages: a call to action or contact strip that\nmust stay identical becomes a global block (`make_block_global`, then\n`core/global_block` on the other pages). A section shape you will reuse with\ndifferent content (a pricing row, a feature trio) becomes a block template\n(`save_block_template`, then `insert_block_template`).\n\n### 5b. If the legacy site is still live, scrape canonical content\n\nFor pages with canonical text (privacy policy, terms, about), `WebFetch`\nthe live page and carry the text verbatim — don't rewrite legal copy\nfrom memory. Convert to prose blocks (or one `core/html` for complex\nlegacy markup).\n\n### 6. Preview + iterate\n\n```\nget_preview_link # signed URL for browser review\nget_page_preview page_id=\"home\" # rendered HTML for structural checks\n```\n\n**Self-review the visuals before you call it done — appearance AND\nreadability, not just structure.** Screenshot the deployed/preview site at\ndesktop (~1440px) and mobile (~390px) and look: logo FULLY VISIBLE (not clipped by\na header's overflow:hidden) + legible + brand-compliant against its actual\nbackground — screenshot the header IN CONTEXT, not the logo element in isolation\n(an element shot hides layout clipping); a light wordmark must not sit bare on a\nlight surface. Text contrast everywhere, no horizontal scroll or mid-word\nbreaks, every image rendered, mobile layout actually collapsed. \"No overflow +\ncopy present\" is not a design review — never report a build as done/perfect off\nstructural metrics alone.\n\nShare the preview link. Iterate on feedback with `update_block` /\n`add_block` / `move_block` — that's the point of block mode: surgical\nedits, not full-page rewrites.\n\n### 7. Deploy\n\nWhen the user approves, verify read_publishing_readiness and save the intended\nVersion before deploying. A published CMS status is not a live deployment.\n```\ntrigger_deploy\nget_deploy_status job_id=<id>\n```\n\n## Secondary path: HTML mode\n\nFor migrated legacy pages or a hand-crafted one-off, `content_mode:\n\"html\"` still works. Rules that apply there (and only there):\n\n- Wrap the body in a page-scope `<article class=\"my-page\">` and prefix\n selectors with it — the `.page-content` shell sets width/padding at\n normal specificity.\n- HTML-mode bodies are container-constrained; full-bleed requires the\n negative-margin escape (`margin-left: calc(50% - 50vw); width: 100vw`)\n — never combine with `overflow-x: clip` on a wrapper.\n- `set_page_mode` flips a page between modes and does not convert HTML.\n Typeroll has no HTML-to-blocks conversion: to move a page to blocks,\n build its content with blocks.\n\n## Pitfalls\n\n- **Don't create pages that already exist** — `list_pages` first; use\n `update_page` if the slug is taken.\n- **`update_page` takes a `patch` object**, not flat fields.\n- **Don't hardcode block field names from memory** — schemas are\n per-site (`list_block_types`/`read_block_type`).\n- **One `level: h1` per page** (core/heading) — SEO + screen readers.\n- **Don't simplify data during import** — preserve Swedish characters in\n labels (`affärsutveckling`, not the ASCII-folded slug), keep titles\n verbatim; slugs are derived for URLs only.\n- **Minimal JS.** Inline `<script>` in page content is stripped by the\n sanitizer. One-off interactivity uses `core/embed`; reusable interactivity\n uses a block-type `script`; site-wide code belongs in `scripts_body_end`.\n\n## Organization access\n\nWhen a Cloud write reports `organization_trial_ended` or `organization_access_ended`, use `read_organization_access` on the selected Site. Explain the deadline and recovery URL; keep reads and export available when permitted. Do not repeatedly retry or create another organization to bypass the lock. Self-hosted installations are unrestricted by default. A trial does not limit AI credits: the organization supplies its own provider key.\n\n## Fluid typography and component rhythm\n\nCore 0.2.114 / MCP 0.45.85 / capabilities 0.55.0 adds responsive scales.\nCheck the connected versions before using this contract. New sites use render\nversion 10. For an existing site, test a Version first; upgrading rendering\nand replacing existing roles are separate, deliberate choices.\n\nUse `fonts.size_base` (default 16), `typography.ratio_mobile` (1.2),\n`typography.ratio_desktop` (1.25), `typography.rhythm` (1), and\n`typography.line_length` (65 ch). Set the controls once, then use named type,\nspacing and line-height steps. `apply_standard_styles` uses those steps;\n`overwrite: true` resets existing roles, so review their current values first.\nExplicit sizes and breakpoint overrides still win and are not silently migrated.\n\nHeading role margins belong to editorial flow. Component titles use their own\npadding/gap, and opening headings in containers/sections add no top margin.\nUse role-linked `core/heading` size `auto` or `theme`; explicit hero sizes remain\ndeliberate exceptions. Do not compensate with per-card negative margins.\nCompare 375, 768 and 1280px in Site settings → Design and in a full site preview.\nAfter a rendering upgrade/rebuild, recheck cards, boxed headings, leading text\nand webfonts. Boxes should get their outer air from padding. Keep exceptions\nin decisions.md, and run lint_design_tokens before launch.\n",
23
23
  "tr-page-template": "---\nname: tr-page-template\ndescription: Create or edit a reusable Page template for articles, checklists, products, directory entries or other Pages that share a layout.\n---\n\n# Share a layout across Pages\n\nConfirm the organization, site and version first. Personal OAuth calls require\nthe `site_context` returned by `select_site`. Read `read_site_instructions`\nand the selected site's enabled App guidance before changing content.\n\n\nRead capabilities, available block types, content types and Page templates first.\nUse a site version for substantial design changes. The Page owns its body blocks;\nthe template owns the shared layout around that body. The content type selects a\ndefault template, and an individual Page can override it.\n\n1. `create_page_template name=\"article-layout\" label=\"Article\" starter=\"article\" status=\"published\"`.\n Alternatively supply `blocks` instead of `starter`. Presets include article,\n blog, checklist, team, events, products, profile, landing and custom.\n2. `update_content_type name=\"articles\" patch={template:\"article-layout\"}`.\n For a single-page override, `update_page page_id=... patch={template:\"article-layout\"} save=true`.\n3. Read and edit the template through `read_page_template` or block tools with\n `target: { kind: \"template\", id: \"article-layout\" }`. Template edits save\n immediately; content-type and template changes are isolated by `version`.\n4. Keep a `template_content_slot` where the Page's body belongs. Its `max_width`\n can be full, narrow, normal or wide. Use native columns/containers for layout.\n5. Metadata blocks include `template/page_title`, `template/page_date`,\n `template/page_featured_image`, `template/page_breadcrumbs` and\n `template/page_navigation`. The outline block reads the rendered body headings.\n6. Use `{{page.title}}` or `{{page.custom_field}}` in template bindings.\n A repeater's children use `{{item.title}}` for the current listed Page.\n Custom fields can be typed objects, arrays or Page references; preserve those\n structures instead of storing hand-generated list HTML.\n7. Preview several Pages using the template: long title, short body, missing\n optional image, populated reference list. Inspect desktop and mobile output.\n Verify the authorized static build as well as the live database preview.\n\nPrefer native image, table, list, heading and content blocks. A rare specialized\nwidget can use a reviewed HTML/embed block. Reusable custom behavior belongs in\none custom block type rather than repeated HTML bodies. Existing HTML-mode pages\nmay share fragments through partials and `<x-include>`, but use block-based page templates\nfor new content families.\n\nHeader/footer partials are global and remain separate from content types. Keep\nsite-level design tokens in settings and shared layout changes in the template.\nPage body edits stay specific to that Page. Read the template before mutation;\nnever replace it with a starter merely because an import is being retried.\n\n\nContent types also own `sort_field`/`sort_dir` and optional `allowed_templates`.\nTypes define content; templates define presentation. Multiple compatible Page\ntemplates can be allowed, with `template` selecting the default. Null/absent\n`allowed_templates` is unrestricted, `[]` allows none, and a configured default\nmust be in an explicit allowed list. Page overrides must be allowed; null/empty\n`Page.template` restores inheritance. Page template changes use Save/Discard.\n\nListings inherit type sorting unless explicitly overridden. `Page.sort_order`\nis the manual numeric order; null clears it. Missing sort values come last and\nIDs break ties. Explicit ID lists keep their order. Typed `list_pages` queries\ninherit type sorting and accept `sort_by`/`sort_order`; unfiltered API lists\ndefault to stable IDs. See the public Content types guide for editor steps.\n\n## Structured fields without empty labels\n\nCheck `supports_page_field_list` before adding `core/field_list`. Configure\n`data.fields` as `{ field, label? }` rows, optional `title` and `layout`\n(`stack` or `two-column`). It omits empty rows and an entirely empty section,\nrespects `rendered: false`, uses select display labels and links Page references.\nZero is displayed. From Core 0.2.14, boolean rows hide false/unset by default; set `boolean_display: \"yes-no\"` to show both set values with `true_label` / `false_label` (defaults Yes/No). Stored false remains real data. Optional row `html` has only `{{label}}` and\n`{{value}}` slots; row `css` contains declarations, and `css_class` supports\nshared selectors. Values stay in Page.fields; never duplicate them into rich text.\nRead the current block schema and content type, and verify preview before publishing.\n\nFor checkbox lists, `item_html` decorates each selected option with the same\nlabel/value slots (for example a check icon). `item_links: [{ value, url }]`\nlinks only chosen stored options to listing URLs, retaining their schema labels.\nStyle list items via the row css_class and normal block Custom CSS.\n\n## Heading and field-list defaults (Core 0.2.13)\n\nA template H1 owns the Page title. Do not send body `core/heading` blocks with\n`level: \"h1\"` when the selected/default template supplies one. Use H2. Creation,\nPage writes and Save reject duplicates with a corrective error. Existing duplicate\nheadings render as H2 without mutating storage. A Page without a template H1 can\nkeep one body H1. The layout itself does not inject an additional title.\n`core/field_list` defaults to a 0.7rem row gap, weight-600 labels and values that\nwrap long URLs. Empty rows and entirely empty groups stay hidden.\n\n### Native precision (Core 0.2.25 candidate)\n\nRead current block schemas before relying on new controls. Heading/Page title\nsupport responsive `font_size_px`, unitless `line_height` and `color`. Sections\nseparate full-width background from `max_width_px`, `padding_x` and\n`content_gap_px`; Containers add `max_width_px`, `gap_px`, `padding_x_px` and\n`padding_y_px`. Two columns add `gap_px` and `right_width_px`. Use numeric values\nor `{mobile, tablet, laptop, desktop, wide}` overrides, not CSS strings.\nBreadcrumbs have `padding_before_px`, `padding_after_px`, `divider`; TOC has\n`padding_px`, `radius_px`, `border`, `shadow`, `link_color: neutral` and\n`list_style: chevron`. Keep TOC generated from headings. Icon box adds native\ncard dimensions and `whole_card_link`; map real Page URLs rather than freezing\ncustomer routes in HTML. Site logo accepts `height_px`. The `system` font family\nuses the device stack without a webfont request. Preserve complete images and\nvalidate actual mobile/desktop output, including scrolled outlines.\n\nColumns `stack_below` accepts the select strings `\"721\"` (default), `\"768\"`,\n`\"1024\"`, `\"1280\"`. A direct-slot TOC follows the selected stacked boundary: its\n`mobile_display: \"hidden\"` releases the otherwise empty slot; `\"visible\"` keeps\nit in flow. Use `\"1024\"` for full article width below laptop when justified by\nthe source comparison. Do not merely hide the TOC with `hidden_on` while leaving\na reserved two-column track, or duplicate body content for tablet/desktop.\n\nFor a sticky site header, use an outer semantic Container (`tag: \"header\"`,\n`sticky: true`, optional responsive `sticky_top_px`, 0–240) outside main. Keep\nits background opaque and its containing block tall enough; do not create a\nshort header wrapper or clip it with ancestor overflow. Sticky is opt-in. The\npage reserves the header's height as `scroll-padding-top`, so anchors,\n`scrollIntoView()` and focused fields land below it without site CSS.\nContainer `padding_top_px`/`padding_bottom_px` override `padding_y_px` per side\nand accept responsive maps. Grid/Repeater and repeater aliases expose responsive\n`gap_px` (0–240). Use these fields for exact source spacing rather than empty\nspacers or corrective CSS. TOC links use the same landing line; remove tenant\n`scroll-margin-top`/`scroll-padding-top` header offsets that duplicate it.\n\n\n## Qualified composition starters (Core 0.2.32 / MCP 0.45.26 and later)\n\nCheck `supports_composition_starters` and `supports_default_presentation_contract`.\nUse `get_composition_starter` to read one editable `header`, `footer`, `archive`\nor Page-template starter without changing CMS data. An archive also requires\n`content_type`; optional `title` names its heading. Replace sample navigation,\nmap the Profile's empty field_list to real public fields, and save through the\nnormal version-aware tools. `create_page_template` accepts the same Page starter\nnames as Templates → New template. Starters are copies, never live links that\nreplace an existing design after an upgrade.\n\nSections own background/gutters, Containers default to no padding, and article\nflow owns heading rhythm. Inspect Appearance before Advanced settings. Reset\nrestores block defaults; don't mirror styles into canonical fields. Formatted\nheadings have typed responsive typography. Container `overflow: \"clip\"` and\nmenu `panel_padding: \"none\"` are deliberate opt-ins. Default PDF text links are\nunderlined theme links. Missing template image/date/excerpt/author emits no node.\n\nFor an existing Container whose padding was omitted, review the new zero-padding\noutput; explicitly set the former `md` values if they should remain (horizontal\n2rem, vertical 4rem). Never bulk-replace existing templates. Read\nhttps://typeroll.com/docs/tools/blocks/ for the full default/migration contract.\n\n\n## Native surface controls (Core 0.2.33 / MCP 0.45.27)\n\nCheck `supports_surface_feedback_and_gradients`. Section/Container\n`background_gradient: { from: \"#f8fbff\", to: \"#e8f4fc\", angle: 135 }` adds\nan opt-in two-stop gradient; remove the object to return to the solid/image\nbackground. Use real colors or simple theme variables, never CSS declarations.\nPost Card `hover_background`, `hover_border_color`, `hover_color` and outlined\nPDF `download_hover_background`/`download_hover_color` control pointer, pressed\nand keyboard feedback. Repeater/page_list item_overrides use the same schema.\nKeep focus visible and check contrasting color pairs; don't move the card.\nNavigation Links `density: \"compact-desktop\"` gives footer links a 24px minimum\non desktop with fine pointers; mobile/touch retains 44px. For dense taxonomy\nfooters use `direction: \"column\"`, `font_size_px: 14.4`, `line_height: 1.1`,\n`gap_px: 4` and `padding_y_px: 0`. Primary menus keep comfortable defaults. Verify both narrow and\nwide views and publish the intended version after saving; no automatic migration.\n\n\n## Publication fidelity (Core 0.2.34)\n\nStatic Repeater rows and aliases export the selected item block’s declared public\nfields, explicit card mappings and grouping labels. Use `title`/`href` for static\nPost Cards and `item_overrides` for common appearance; never place private data\nin public fields. `rendered: false` still excludes the field. `breadcrumb_label`\nis preserved separately from the Page title. Older public artifacts need a new\ndeployment; do not rewrite the saved rows to compensate. Native Heading and Page\nTitle wrap long words without shrinking authored type. Firestore history adds no\nextra Page nesting; an actual storage depth limit returns structured HTTP 422\n`storage_document_too_deep` with the offending field path.\n",
@@ -54,7 +54,7 @@ export const BUNDLED_WORKSPACE_TEMPLATES = {
54
54
  "scripts/typeroll/trlib.py": "\"\"\"Small, version-bound MCP helper. Credentials are injected, never persisted.\nNo automatic retries for mutations. Run from the workspace root.\n\"\"\"\nimport hashlib\nimport json\nimport os\nfrom pathlib import Path\nimport urllib.error\nimport urllib.parse\nimport urllib.request\n\n\nclass NoRedirect(urllib.request.HTTPRedirectHandler):\n def redirect_request(self, *args, **kwargs):\n return None # Never forward Authorization or signed PUTs to another URL.\n\n\ndef workspace():\n for directory in (Path.cwd(), *Path.cwd().parents):\n file = directory / 'typeroll.json'\n if file.is_file():\n value = json.loads(file.read_text())\n if not all(value.get(k) for k in ('portal', 'organization_id', 'site_id', 'version')):\n raise ValueError('Complete portal, organization_id, site_id and version in typeroll.json first')\n origin = urllib.parse.urlsplit(value['portal'])\n if origin.username or origin.password or origin.query or origin.fragment or origin.path not in ('', '/'):\n raise ValueError('portal must be a plain origin')\n if origin.scheme != 'https' and not (origin.scheme == 'http' and origin.hostname in ('localhost', '127.0.0.1', '::1')):\n raise ValueError('portal requires HTTPS (HTTP is allowed only on loopback)')\n return value\n raise ValueError('Run from a workspace containing typeroll.json')\n\n\nOFFICIAL_PORTAL = 'https://app.typeroll.com'\n\n\ndef trusted_portal(config):\n \"\"\"Return the portal origin that may receive the API key.\n\n typeroll.json is committed, so its portal alone is trusted only when it is\n the hosted portal. Another origin must be confirmed by TYPEROLL_API_URL.\n \"\"\"\n portal = config['portal'].rstrip('/')\n confirmed = (os.environ.get('TYPEROLL_API_URL') or '').strip().rstrip('/')\n if confirmed:\n if confirmed != portal:\n raise ValueError('TYPEROLL_API_URL conflicts with typeroll.json')\n return portal\n if portal != OFFICIAL_PORTAL:\n raise ValueError(f'typeroll.json names the portal {portal}. The API key is sent only to '\n f'{OFFICIAL_PORTAL} unless you confirm another portal: set '\n f'TYPEROLL_API_URL={portal} in your private environment')\n return portal\n\n\nclass Client:\n def __init__(self, config=None):\n self.config = config or workspace()\n self.url = self.config['portal'].rstrip('/') + '/api/mcp?tools=full'\n self.session = None\n self.sequence = 0\n self.tools = None\n self.verified = False\n self.opener = urllib.request.build_opener(NoRedirect)\n\n def rpc(self, method, params=None, notify=False):\n self.sequence += 1\n body = {'jsonrpc': '2.0', 'method': method}\n if params is not None:\n body['params'] = params\n if not notify:\n body['id'] = self.sequence\n key = os.environ.get('TYPEROLL_API_KEY')\n if not key:\n raise ValueError('Inject TYPEROLL_API_KEY privately before running')\n if trusted_portal(self.config) + '/api/mcp?tools=full' != self.url:\n raise ValueError('Workspace portal changed after the client was created')\n headers = {'Authorization': 'Bearer ' + key, 'Content-Type': 'application/json',\n 'Accept': 'application/json, text/event-stream'}\n if self.session:\n headers['Mcp-Session-Id'] = self.session\n request = urllib.request.Request(self.url, data=json.dumps(body).encode(), headers=headers)\n try:\n with self.opener.open(request, timeout=180) as response:\n self.session = response.headers.get('Mcp-Session-Id') or self.session\n raw = response.read().decode()\n except urllib.error.HTTPError as error:\n raise RuntimeError(f'MCP HTTP {error.code}; no mutation was retried') from None\n if notify:\n return None\n result = decode_rpc(raw, self.sequence)\n if 'error' in result:\n raise RuntimeError(f\"MCP RPC error {result['error'].get('code')}\")\n return result['result']\n\n def initialize(self):\n self.rpc('initialize', {'protocolVersion': '2025-03-26', 'capabilities': {},\n 'clientInfo': {'name': 'typeroll-workspace', 'version': '1'}})\n self.rpc('notifications/initialized', notify=True)\n self.tools = {}\n cursor = None\n seen = set()\n while True:\n result = self.rpc('tools/list', {'cursor': cursor} if cursor else {})\n self.tools.update({t['name']: t for t in result['tools']})\n cursor = result.get('nextCursor')\n if not cursor:\n break\n if cursor in seen:\n raise RuntimeError('tools/list repeated its cursor')\n seen.add(cursor)\n\n def call(self, tool, args=None):\n if self.tools is None:\n self.initialize()\n if tool not in self.tools:\n raise ValueError(f'Tool unavailable: {tool}')\n if not self.verified:\n info = self.invoke('get_site', {})\n if info.get('organization_id') != self.config['organization_id'] or info.get('id') != self.config['site_id']:\n raise ValueError('Connected Organization/Site does not match typeroll.json; nothing was written')\n self.verified = True\n return self.invoke(tool, args or {})\n\n def invoke(self, tool, args):\n props = self.tools[tool]['inputSchema'].get('properties', {})\n data = dict(args)\n for key, wanted in [('site_id', self.config['site_id']), ('version', self.config['version'])]:\n if key in data and data[key] != wanted:\n raise ValueError(f'{key} differs from typeroll.json; change the binding explicitly')\n if key in props:\n data[key] = wanted\n if 'version' not in props and self.config['version'] != 'main' and tool != 'get_site':\n raise ValueError(f'{tool} has no version parameter; refusing an implicit main-version operation')\n result = self.rpc('tools/call', {'name': tool, 'arguments': data})\n if result.get('isError'):\n raise RuntimeError(f'{tool} failed; inspect through your authenticated MCP client (no retry)')\n if 'structuredContent' in result:\n return result['structuredContent']\n text = ''.join(c.get('text', '') for c in result.get('content', []) if c.get('type') == 'text')\n return json.loads(text)\n\n\ndef decode_rpc(raw, request_id):\n if raw.lstrip().startswith('{'):\n value = json.loads(raw)\n if value.get('id') != request_id:\n raise ValueError('MCP response did not contain the matching request ID')\n return value\n # Streamable HTTP may include notifications, multiline data, and keepalives.\n for event in raw.replace('\\r\\n', '\\n').split('\\n\\n'):\n data = '\\n'.join(line[5:].lstrip() for line in event.splitlines() if line.startswith('data:'))\n if data:\n value = json.loads(data)\n if value.get('id') == request_id:\n return value\n raise ValueError('MCP response did not contain the matching request ID')\n\n\n_client = None\n\n\ndef call(tool, args=None):\n global _client\n if _client is None:\n _client = Client()\n return _client.call(tool, args)\n\n\ndef site_id():\n return workspace()['site_id']\n\n\ndef walk(blocks):\n for block in blocks or []:\n yield block\n yield from walk(block.get('children'))\n for slot in block.get('slots') or []:\n yield from walk(slot)\n\n\ndef all_pages(full=False):\n cursor, seen = None, set()\n while True:\n args = {'limit': 200, **({'full': True} if full else {}), **({'cursor': cursor} if cursor else {})}\n result = call('list_pages', args)\n yield from result['pages']\n cursor = result.get('next_cursor')\n if not cursor:\n return\n if cursor in seen:\n raise RuntimeError('list_pages repeated its cursor')\n seen.add(cursor)\n\n\ndef all_migration_urls():\n offset = 0\n while True:\n result = call('list_migration_urls', {'limit': 1000, 'offset': offset})\n rows = result['urls']\n yield from rows\n offset += len(rows)\n if not rows or offset >= result.get('total_matching', float('inf')):\n return\n\n\ndef containers():\n for page in all_pages():\n yield {'kind': 'page', 'id': page['id']}\n for kind, tool, key in [('template', 'list_page_templates', 'templates'), ('partial', 'list_partials', 'partials')]:\n result = call(tool)\n rows = result if isinstance(result, list) else result.get(key, result.get('page_templates'))\n if not isinstance(rows, list):\n raise ValueError(f'Unexpected response from {tool}')\n for row in rows:\n yield {'kind': kind, 'id': row['id']}\n\n\ndef upload(file, content_type, alt=''):\n content = Path(file).read_bytes()\n sha = hashlib.sha256(content).hexdigest()\n mint = call('create_upload_url', {'filename': Path(file).name, 'content_type': content_type,\n 'size': len(content), 'alt_text': alt})\n if urllib.parse.urlsplit(mint['upload_url']).scheme != 'https':\n raise ValueError('Signed upload must use HTTPS')\n request = urllib.request.Request(mint['upload_url'], data=content, method='PUT', headers={'Content-Type': content_type})\n try:\n with urllib.request.build_opener(NoRedirect).open(request, timeout=120) as response:\n response.read()\n except Exception:\n raise RuntimeError('Signed upload failed; URL withheld; no automatic retry') from None\n result = call('finalize_media', {'media_id': mint['media_id'], 'expected_sha256': sha})\n evidence = result.get('result', result)\n if evidence.get('sha256') != sha:\n raise ValueError('Finalize did not confirm the source SHA-256; do not reference this asset')\n return {'media_id': mint['media_id'], 'cdn_url': mint['cdn_url'], 'sha256': sha}\n",
55
55
  "workflows/design.md": "# Layout defaults and owner decisions\n\nUse current native composition starters and block schemas. These defaults are\nstarting points, not permission to overwrite an approved design.\n\n- Keep incomplete grid rows left-aligned (`last_row: start`). Never enlarge one\n peer card just to fill the gap. Center only on an explicit owner request.\n- Prefer `core/navigation_menu` with `core/navigation_links` desktop/mobile slots\n for a header drawer (below 1024 px). Read tr-header-footer; test 1023/1024 px.\n- Separate dense footer about text from the link grid. Give it a readable width\n and a larger gap; inspect 390/1024/1280/1536 px. Equal columns need equal density.\n- Sections own gutters. Do not erase side padding with `padding: 3rem 0` when\n only vertical spacing is intended. Wrap column-slot contents in a flow\n container with article rhythm when they need vertical block spacing.\n- Give cards one clear destination. Use whole_card_link without action_label\n or download actions. Check excerpt, image, whitespace and keyboard activation.\n- Do not fade body text with opacity. Verify actual text/background contrast,\n current links on light/dark navigation, and all states.\n- Preserve image subjects across widths. Choose contain/original sizing when\n cropping or upscaling is not intended. Hero crop, text placement, plates,\n corner treatment and category-page composition are project decisions in\n brief/brand.md, not a universal design.\n- After migration, check heading hierarchy, unchanged preserved text, HTML\n comments, links with ../, and the home SEO title/suffix. Shared headings and\n titles must not be duplicated in body content.\n\nRecord deliberate departures in decisions.md with scope and owner approval.\nUse a QA round per review: method, pages/viewports, expected differences, results,\nunverified items and remaining defects. Record platform findings in\nqa/typeroll-platform-feedback.md; do not bury them in workaround CSS.\n\n## Site tokens before block overrides\n\nCore 0.2.106 / template capabilities 0.54.0 adds palette names in all colour\nfields and named typography/spacing steps. Check the connected capability\nversion; on older renderers use `var(--color-surface)` rather than a bare name.\n\nSet colours and font families in site settings, then use named styles and\npalette names in blocks. Do not write hex, rgb or font-family literals in block\ncontent. Prefer `primary`, `secondary`, `accent`, `background`, `surface`, `text`,\n`text_light`, and `on_primary`/`on_secondary`/`on_accent` for text on brand colours.\nUse `surface_hover` for a darker interactive surface (derived automatically\nunless explicitly configured). Existing semantic roles should be reused; a\nnew visual shade is not automatically a new semantic role. For a derived shade\nuse `color-mix(in srgb, var(--color-surface) 92%, var(--color-primary))`.\nFor an additional role not in the native palette, define a named site style\nor a site CSS variable derived from existing palette variables, and document it.\n\nChoose `heading`, `body` or `mono` for fonts. Font families belong only in the\nsite's `fonts` settings. Use type-size steps `xs`, `sm`, `md`, `lg`, `xl`, `2xl`,\n`3xl`; spacing `none`, `xs`, `sm`, `md`, `lg`, `xl`; line height `tight`, `snug`,\n`normal`, `relaxed`, `loose`. Presentation fields accept responsive maps of these\nnames, for example `font_size_px: {mobile: \"md\", desktop: \"lg\"}`. Read the field\nschema: counts, widths and breakpoints are not spacing steps. Pixel values are\nexplicit exceptions, not the default authoring method. Configure scales in\n`design_scales`, then reuse them in role styles and blocks.\n\nRecord deliberate exceptions in `decisions.md`, with source/block, reason and\nreview criteria (for example white text on a photograph). Run `lint_design_tokens`\nbefore launch and after major design changes. It warns, never blocks launch.\nUse the starter's `scripts/typeroll/color_tokens.py` for an exact-match dry run;\nreview the plan before applying it in a dedicated Version. Equal hex values can\nmean different roles: never automatically choose between background and text.\nVerify a primary/surface change in a test Version across header, footer, cards,\nmenus and buttons, then restore the test settings. Do not republish automatically.\n\n## Fluid typography and component rhythm\n\nCore 0.2.114 / MCP 0.45.85 / capabilities 0.55.0 adds responsive scales.\nCheck the connected versions before using this contract. New sites use render\nversion 10. For an existing site, test a Version first; upgrading rendering\nand replacing existing roles are separate, deliberate choices.\n\nUse `fonts.size_base` (default 16), `typography.ratio_mobile` (1.2),\n`typography.ratio_desktop` (1.25), `typography.rhythm` (1), and\n`typography.line_length` (65 ch). Set the controls once, then use named type,\nspacing and line-height steps. `apply_standard_styles` uses those steps;\n`overwrite: true` resets existing roles, so review their current values first.\nExplicit sizes and breakpoint overrides still win and are not silently migrated.\n\nHeading role margins belong to editorial flow. Component titles use their own\npadding/gap, and opening headings in containers/sections add no top margin.\nUse role-linked `core/heading` size `auto` or `theme`; explicit hero sizes remain\ndeliberate exceptions. Do not compensate with per-card negative margins.\nCompare 375, 768 and 1280px in Site settings → Design and in a full site preview.\nAfter a rendering upgrade/rebuild, recheck cards, boxed headings, leading text\nand webfonts. Boxes should get their outer air from padding. Keep exceptions\nin decisions.md, and run lint_design_tokens before launch.\n",
56
56
  "workflows/interview.md": "# Guided website interview\n\n## When to start or resume\n\nUse this routine when the owner asks for guidance or a material decision is missing\nfrom the brief. Read the existing briefs, decisions and brief/interview.md first.\nDo not restart an interview or ask questions already answered. If the brief is\ncomplete, summarize it and continue with the relevant routine. This is an agent\nconversation, not a portal wizard or a new API workflow.\n\n## How to conduct it\n\n- Use the owner's language. Ask one short question at a time; offer concrete\n choices and allow a free answer or \"not sure\". Explain technical choices plainly.\n- Reuse explicit instructions already given. Distinguish confirmed answers,\n proposals and unknowns. Never treat silence as approval.\n- If an answer is unknown, suggest an option with its consequences. Research and\n reversible prototypes may continue; do not perform dependent CMS writes until\n the necessary decision is confirmed.\n- Save each answer and its source/date in brief/interview.md, and the approved\n decision in the relevant brief. Keep the next question and unresolved items so\n another session can resume. Do not store credentials, personal contact lists\n or raw private transcripts here. No MCP connection is needed to plan locally.\n- Reopen only affected questions when the owner changes direction. Record the\n change in decisions.md; do not silently replace confirmed rules.\n\n## Interview stages\n\n1. **Goal and scope** — New website or migration? Purpose, audience, language,\n desired visitor actions, source URL and what is in/out of scope.\n Save to brief/site.md and brief/migration.md when relevant.\n2. **Target and access** — Existing or new Typeroll Site? Discover accessible\n Organizations/Sites through MCP and confirm the exact target and Version before\n writes. Explain any missing storage or access when it becomes relevant; never\n ask for a secret in chat. Save non-secret IDs to typeroll.json only when known.\n3. **Text decision (migration)** — Preserve existing wording or renew it?\n Preserve covers headings, body copy, CTA labels, captions and other authored\n text. Record SEO titles/descriptions, alt text, legal text, translation and any\n page-specific exceptions explicitly. Renewal needs an agreed editorial brief;\n it is not permission to invent facts. Save text_intent in brief/migration.md.\n4. **Design and image decision (migration)** — Independently preserve or renew\n layout, typography, colors and imagery? Offer all four text/design combinations.\n For renewal, ask which existing assets must remain and which may be replaced\n or generated. Clarify cropping, logos, credits and image rights. Optimization\n alone must not change the motif. Save design_intent and asset exceptions in\n brief/migration.md, and the direction in brief/brand.md. A fresh design never\n grants permission to rewrite preserved text.\n5. **Content and behavior** — Page types, shared templates, navigation, forms,\n downloads, integrations and who will edit them. Identify missing capabilities\n before scaling up. Save to brief/content-model.md. New websites skip migration\n preservation choices and establish their brand/content brief here.\n6. **SEO and continuity** — Markets, search intent, useful topics and existing\n valuable URLs. Record evidence separately from hypotheses in brief/seo.md.\n Confirm metadata policy; text preservation cannot be bypassed as \"SEO cleanup\".\n Every old public URL must remain or redirect meaningfully regardless of the\n two migration choices. Retiring a URL requires a separate agreed exception.\n7. **Owner rules and review** — Non-negotiable copy/design constraints, editing\n roles and who approves prototypes, publication, DNS or messages. Save confirmed\n rules to brief/rules.md. Keep technical access separate from user authorization.\n8. **Recap and next action** — Show the scope, both migration decisions (if any),\n image exceptions, acceptance references, open questions and the next concrete\n step. Ask the owner to correct/confirm the brief before dependent writes.\n Reuse existing approval if it already covers that exact step. Brief approval\n alone does not authorize publication, domain cutover or mail.\n\n## Completion\n\nMark the interview ready for the agreed next step only when its blocking questions\nare answered; list deferred decisions explicitly. Continue with workflows/new-site.md\nor workflows/migration.md and load only the relevant MCP recipe. Confirm one\nprototype per distinct layout before bulk work. Review preserved dimensions\nagainst the source and renewed dimensions against the approved brief separately.\n",
57
- "workflows/migration.md": "# Migration kickoff\n\nIf guidance is needed, start or resume workflows/interview.md. Otherwise fill in\nthis prompt from the confirmed brief. The two preservation choices are independent.\n\n```text\nMigrate [source URL] to Typeroll, Organization [ID/name], Site [ID/name], Version [ID].\nText: [preserve existing wording / renew against an agreed editorial brief].\nDesign and images: [preserve / renew against an agreed design brief].\nRead AGENTS.md and use workflows/interview.md for missing decisions.\nRead tr-migration-evidence, then tr-migrate-wp (or the appropriate source recipe).\nConfirm the target and import readiness before writes. Inventory URLs, content,\nmedia, templates and integrations; record both choices and exceptions in brief/migration.md.\nBuild and verify one prototype per distinct layout before bulk reconstruction.\nUse standard blocks first, editable custom types next, isolated HTML only for\njustified exceptions. Rebuild imported HTML with block tools; a mode switch does not convert it.\nKeep preserved text unchanged even when the design is new. Record which images\nmay change. Preserve every old public URL or redirect to a relevant destination.\nProtect existing CMS edits. Review text and design/images against their own\nacceptance references, plus mobile/desktop behavior, downloads, forms and URL coverage.\nReport unverified items separately. Do not publish, switch DNS or send messages\nwithout explicit approval.\n```\n\nThe managed importer stages cleaned HTML, media and shared references in Review.\nThe external agent can then reconstruct those Pages; no manual editor conversion\nis required. An agent can also build block Pages directly from source material.\nNeither path includes an automatic HTML-to-blocks converter. Read and preserve the\nfull source before switching modes, then save/read back and preview the rebuilt\nPage on the selected Version. Never publish an empty intermediate block Page.\n\nTrack import, block reconstruction, separate text/design review, candidate\npublication and domain cutover as separate milestones. Import completion is not\nlaunch approval. All four text/design combinations use the same block hierarchy\nand URL continuity contract. Planning choices are not migration API fields.\n\n## Repeatable reconstruction and acceptance\n\nRead workflows/design.md and scripts/README.md. First check whether public\nWordPress REST + HTML covers the source; use tr-migrate-wp's access decision\ntable before asking for a helper plugin. Inventory both Pages and posts plus\ninternal links and known old slugs. A sitemap alone is not complete.\n\nDiscard HTML comments before extracting text, resolve relative links against\nthe source, and keep an explicit old URL map. Do not redirect deleted content\nto the home page merely to get a 200. Preserve/renew text and design separately.\nUse capture_text.cjs + compare_text.py for preserved text with explicit selectors.\n\nBefore every authorized test deployment run check_unsaved.py (Pages and partials).\nDeploy saved content to the agreed protected test host, then run check_url_parity.py,\ncheck_assets.py and shoot_hosted.cjs. Check whole-card targets with\ncard-stacking-check.cjs and inspect actual desktop/mobile screenshots. Record a\nQA round and platform findings. Review on the owner's phone when possible.\nDomain preparation/cutover and production indexing follow their own explicit\napproval scope and the platform's current guided flow; do not bypass readiness.\nDo not request a second approval for a step the owner has already authorized.\n\n\n## Native controls and publication checks\n\nOn Core 0.2.103 / MCP 0.45.79 or later, discover the actual block schemas first.\nUse column `content_gap_px`, responsive section `padding_y`, heading-role\n`space_before`/`space_after`, hero focal/fit/text-position fields and image\n`never_upscale` instead of one-off CSS. Responsive block keys start at `mobile`,\nnot `base`. For vertical-only spacing, do not reset horizontal padding with CSS\nshorthand. Optional `click_to_load` videos need an explicit poster if desired;\nchoose a first-party poster to avoid early provider requests.\n\n`import_sitemap` can include public WordPress REST (`include_wordpress: true`)\nand a bounded crawl (`crawl_pages: 10`). Record truncation and access warnings.\nA `matched_draft` URL is accounted for in the CMS but not ready for launch.\nResolve `unpublished_targets`, redirect loops/chains and actual deployed parity.\n\nA deploy with working copies returns `unsaved_content`; review the targets.\nSave explicitly or obtain approval to publish saved content only using\n`allow_unsaved: true`. Never silently save somebody else's work.\n\nKeep the bound Organization/Site/Version explicit. A site API key cannot switch\nto another Organization: use an authorized OAuth connection or a separate key.\nRead the schema for target envelopes and pagination; do not guess aliases.\nPage/partial block writes update working copies; check `saved` and use\n`commit_working_copy` with `target: {kind: \"page\"|\"partial\", id: \"...\"}`.\nPage templates save immediately and do not have working copies. REST read envelopes and\nMCP tool text are different layers; `call.py` returns the decoded tool result.\n\n## Design token check\n\nFollow workflows/design.md for palette/font roles and named scales. Run\n`lint_design_tokens` before launch and after major design changes. Review exact\ncolour replacements with scripts/typeroll/color_tokens.py; record deliberate\nexceptions in decisions.md. Preserve text independently from design changes.\n\n## Source retirement, analytics and domain preparation\n\nBefore old hosting is retired, follow workflows/source-media.md: preserve unused\nand print originals in a private archive with independently verified checksums.\nOnly approved web assets enter the CMS. Use ICC-aware conversion for CMYK/TIFF.\n\nEnable Analytics through the authorized Typeroll API/MCP (`update_app` or the\nOrganization analytics tools), then read back settings and verify the beacon and\ncookies on the actual artifact. Resolve missing provider scopes through the\nnormal approval flow. A separately authorized provider-side setup with a supplied\nbeacon token is an alternative, not a way to bypass Organization permissions.\nNever expose a provider API token as a public beacon token.\n\nFrom Core 0.2.106, saving future hosts does not redirect normal publication.\nKeep using `trigger_deploy` to update and review the current/temporary address.\nRead `next_publication` in `read_publishing_domains` for the destination.\nShortly before DNS changes, deliberately call `prepare_publishing_domain_change`\nwith the current revision. It prepares the last successful public snapshot,\nnot later unpublished CMS edits. Review its frozen indexing settings and DNS\nrequirements, then approve traffic separately. If another normal publication\nsucceeds while the candidate waits, prepare again: an outdated snapshot cannot\nreplace newer published content. There is no need to clear saved future hosts.\nTo correct candidate indexing, save the intended setting, publish normally, and\nprepare again. Temporary-host protection keeps the review address noindex even\nwhen the published authored setting permits indexing on the future host.\nKeep MX/SPF and unrelated DNS unchanged; review any validation window and obtain\nthe already-defined traffic-switch authorization.\n\n## Owner-supplied facts and photos\n\nRecord owner statements, source and confirmation date. Preserve approved copy\nunder `text: preserve`; do not silently delete dates/prices or rewrite it into\n\"timeless\" language. Under `text: renew`, propose removing stale details or using\nrecurring-event wording only when a source supports recurrence. Flag uncertain\nfacts for review or keep new Pages in draft; missing evidence does not prove that\nan event stopped. Link to the actual organiser when relevant. Upload approved\nphotos with SHA-256 verification and review focal points at mobile/desktop widths.\n\n## Internal Page notes\n\nFollow workflows/page-notes.md and pages/README.md. Create notes during migration,\nread before edits, update afterward, check external links at each editorial review,\nand archive notes for confirmed removed Pages under pages/archived/. Refreshing\ngenerated facts never counts as a completed review. Keep notes out of public output.\n\n## Fluid typography and component rhythm\n\nCore 0.2.114 / MCP 0.45.85 / capabilities 0.55.0 adds responsive scales.\nCheck the connected versions before using this contract. New sites use render\nversion 10. For an existing site, test a Version first; upgrading rendering\nand replacing existing roles are separate, deliberate choices.\n\nUse `fonts.size_base` (default 16), `typography.ratio_mobile` (1.2),\n`typography.ratio_desktop` (1.25), `typography.rhythm` (1), and\n`typography.line_length` (65 ch). Set the controls once, then use named type,\nspacing and line-height steps. `apply_standard_styles` uses those steps;\n`overwrite: true` resets existing roles, so review their current values first.\nExplicit sizes and breakpoint overrides still win and are not silently migrated.\n\nHeading role margins belong to editorial flow. Component titles use their own\npadding/gap, and opening headings in containers/sections add no top margin.\nUse role-linked `core/heading` size `auto` or `theme`; explicit hero sizes remain\ndeliberate exceptions. Do not compensate with per-card negative margins.\nCompare 375, 768 and 1280px in Site settings → Design and in a full site preview.\nAfter a rendering upgrade/rebuild, recheck cards, boxed headings, leading text\nand webfonts. Boxes should get their outer air from padding. Keep exceptions\nin decisions.md, and run lint_design_tokens before launch.\n",
57
+ "workflows/migration.md": "# Migration kickoff\n\nIf guidance is needed, start or resume workflows/interview.md. Otherwise fill in\nthis prompt from the confirmed brief. The two preservation choices are independent.\n\n```text\nMigrate [source URL] to Typeroll, Organization [ID/name], Site [ID/name], Version [ID].\nText: [preserve existing wording / renew against an agreed editorial brief].\nDesign and images: [preserve / renew against an agreed design brief].\nRead AGENTS.md and use workflows/interview.md for missing decisions.\nRead tr-migration-evidence, then tr-migrate-wp (or the appropriate source recipe).\nConfirm the target and import readiness before writes. Inventory URLs, content,\nmedia, templates and integrations; record both choices and exceptions in brief/migration.md.\nBuild and verify one prototype per distinct layout before bulk reconstruction.\nUse standard blocks first, editable custom types next, isolated HTML only for\njustified exceptions. Rebuild imported HTML with block tools; a mode switch does not convert it.\nKeep preserved text unchanged even when the design is new. Record which images\nmay change. Preserve every old public URL or redirect to a relevant destination.\nProtect existing CMS edits. Review text and design/images against their own\nacceptance references, plus mobile/desktop behavior, downloads, forms and URL coverage.\nReport unverified items separately. Do not publish, switch DNS or send messages\nwithout explicit approval.\n```\n\nThe managed importer stages cleaned HTML, media and shared references in Review.\nThe external agent can then reconstruct those Pages; no manual editor conversion\nis required. An agent can also build block Pages directly from source material.\nNeither path includes an automatic HTML-to-blocks converter. Read and preserve the\nfull source before switching modes, then save/read back and preview the rebuilt\nPage on the selected Version. Never publish an empty intermediate block Page.\n\nTrack import, block reconstruction, separate text/design review, candidate\npublication and domain cutover as separate milestones. Import completion is not\nlaunch approval. All four text/design combinations use the same block hierarchy\nand URL continuity contract. Planning choices are not migration API fields.\n\n## Repeatable reconstruction and acceptance\n\nRead workflows/design.md and scripts/README.md. First check whether public\nWordPress REST + HTML covers the source; use tr-migrate-wp's access decision\ntable before asking for a helper plugin. Inventory both Pages and posts plus\ninternal links and known old slugs. A sitemap alone is not complete.\n\nDiscard HTML comments before extracting text, resolve relative links against\nthe source, and keep an explicit old URL map. Do not redirect deleted content\nto the home page merely to get a 200. Preserve/renew text and design separately.\nUse capture_text.cjs + compare_text.py for preserved text with explicit selectors.\n\nBefore every authorized test deployment run check_unsaved.py (Pages and partials).\nDeploy saved content to the agreed protected test host, then run check_url_parity.py,\ncheck_assets.py and shoot_hosted.cjs. Check whole-card targets with\ncard-stacking-check.cjs and inspect actual desktop/mobile screenshots. Record a\nQA round and platform findings. Review on the owner's phone when possible.\nDomain preparation/cutover and production indexing follow their own explicit\napproval scope and the platform's current guided flow; do not bypass readiness.\nDo not request a second approval for a step the owner has already authorized.\n\n\n## Native controls and publication checks\n\nOn Core 0.2.103 / MCP 0.45.79 or later, discover the actual block schemas first.\nUse column `content_gap_px`, responsive section `padding_y`, heading-role\n`space_before`/`space_after`, hero focal/fit/text-position fields and image\n`never_upscale` instead of one-off CSS. Responsive block keys start at `mobile`,\nnot `base`. For vertical-only spacing, do not reset horizontal padding with CSS\nshorthand. Optional `click_to_load` videos need an explicit poster if desired;\nchoose a first-party poster to avoid early provider requests.\n\n`import_sitemap` can include public WordPress REST (`include_wordpress: true`)\nand a bounded crawl (`crawl_pages: 10`). Record truncation and access warnings.\nA `matched_draft` URL is accounted for in the CMS but not ready for launch.\nResolve `unpublished_targets`, redirect loops/chains and actual deployed parity.\n\nA deploy with working copies returns `unsaved_content`; review the targets.\nSave explicitly or obtain approval to publish saved content only using\n`allow_unsaved: true`. Never silently save somebody else's work.\n\nKeep the bound Organization/Site/Version explicit. A site API key cannot switch\nto another Organization: use an authorized OAuth connection or a separate key.\nRead the schema for target envelopes and pagination; do not guess aliases.\nPage/partial block writes update working copies; check `saved` and use\n`commit_working_copy` with `target: {kind: \"page\"|\"partial\", id: \"...\"}`.\nPage templates save immediately and do not have working copies. REST read envelopes and\nMCP tool text are different layers; `call.py` returns the decoded tool result.\n\n## Design token check\n\nFollow workflows/design.md for palette/font roles and named scales. Run\n`lint_design_tokens` before launch and after major design changes. Review exact\ncolour replacements with scripts/typeroll/color_tokens.py; record deliberate\nexceptions in decisions.md. Preserve text independently from design changes.\n\n## Source retirement, analytics and domain preparation\n\nBefore old hosting is retired, follow workflows/source-media.md: preserve unused\nand print originals in a private archive with independently verified checksums.\nOnly approved web assets enter the CMS. Use ICC-aware conversion for CMYK/TIFF.\n\nEnable Analytics through the authorized Typeroll API/MCP (`update_app` or the\nOrganization analytics tools), then read back settings and verify the beacon and\ncookies on the actual artifact. Resolve missing provider scopes through the\nnormal approval flow. A separately authorized provider-side setup with a supplied\nbeacon token is an alternative, not a way to bypass Organization permissions.\nNever expose a provider API token as a public beacon token.\n\nFrom Core 0.2.116 / MCP 0.45.87, saving future hosts remains inert.\nPublish and review intended changes at the current/temporary address first.\nShortly before DNS changes, explicitly call `prepare_publishing_domain_change`\nwith the current revision: it republishes the last successful public snapshot\non main with the new origins, replacing the site at its current address.\nLater unpublished CMS changes are excluded. New-host absolute links and media\nmay not work until DNS/certificates are ready. Do not promise a parallel old copy.\nReview frozen indexing and DNS requirements, then approve DNS separately; that\nstep does not push, build or upload again. Normal publications before DNS retain\nthe newly embedded origins and refresh the current receipt. Read the latest\ncandidate_id before approval; no new preparation is needed for a content edit.\nTo correct indexing, save the intended setting and publish normally.\n`published.hosts` describes main output; `active` describes verified traffic.\nDNS rollback restores DNS, not an older artifact. For older servers that still\nprepare a preview branch, update Core before using this workflow.\nKeep MX/SPF and unrelated DNS unchanged; review any validation window and obtain\nthe already-defined traffic-switch authorization.\n\n## Owner-supplied facts and photos\n\nRecord owner statements, source and confirmation date. Preserve approved copy\nunder `text: preserve`; do not silently delete dates/prices or rewrite it into\n\"timeless\" language. Under `text: renew`, propose removing stale details or using\nrecurring-event wording only when a source supports recurrence. Flag uncertain\nfacts for review or keep new Pages in draft; missing evidence does not prove that\nan event stopped. Link to the actual organiser when relevant. Upload approved\nphotos with SHA-256 verification and review focal points at mobile/desktop widths.\n\n## Internal Page notes\n\nFollow workflows/page-notes.md and pages/README.md. Create notes during migration,\nread before edits, update afterward, check external links at each editorial review,\nand archive notes for confirmed removed Pages under pages/archived/. Refreshing\ngenerated facts never counts as a completed review. Keep notes out of public output.\n\n## Fluid typography and component rhythm\n\nCore 0.2.114 / MCP 0.45.85 / capabilities 0.55.0 adds responsive scales.\nCheck the connected versions before using this contract. New sites use render\nversion 10. For an existing site, test a Version first; upgrading rendering\nand replacing existing roles are separate, deliberate choices.\n\nUse `fonts.size_base` (default 16), `typography.ratio_mobile` (1.2),\n`typography.ratio_desktop` (1.25), `typography.rhythm` (1), and\n`typography.line_length` (65 ch). Set the controls once, then use named type,\nspacing and line-height steps. `apply_standard_styles` uses those steps;\n`overwrite: true` resets existing roles, so review their current values first.\nExplicit sizes and breakpoint overrides still win and are not silently migrated.\n\nHeading role margins belong to editorial flow. Component titles use their own\npadding/gap, and opening headings in containers/sections add no top margin.\nUse role-linked `core/heading` size `auto` or `theme`; explicit hero sizes remain\ndeliberate exceptions. Do not compensate with per-card negative margins.\nCompare 375, 768 and 1280px in Site settings → Design and in a full site preview.\nAfter a rendering upgrade/rebuild, recheck cards, boxed headings, leading text\nand webfonts. Boxes should get their outer air from padding. Keep exceptions\nin decisions.md, and run lint_design_tokens before launch.\n",
58
58
  "workflows/new-site.md": "# New website kickoff\n\nUse this prompt only when the brief is already clear. Otherwise start or resume\nworkflows/interview.md; the owner does not need to fill in every document alone.\n\n```text\nCreate [website] for [audience and purpose] in Typeroll Organization [ID/name].\nRead AGENTS.md, confirm the Site and Version, and use workflows/interview.md for\nmissing decisions. Record confirmed goals, brand, content model, owner rules and\nSEO strategy in brief/. Read tr-new-site and relevant tool schemas on demand.\nBuild representative desktop/mobile layouts with editable blocks and shared templates.\nShare previews, record accepted decisions and keep unresolved items explicit.\nDo not publish, change DNS or send messages without explicit approval.\n```\n\nThe CMS owns current content and settings. The repository owns the brief, rules,\ndecisions and evidence. Load recipes when needed, not the whole manual.\n\n## Design token check\n\nFollow workflows/design.md for palette/font roles and named scales. Run\n`lint_design_tokens` before launch and after major design changes. Review exact\ncolour replacements with scripts/typeroll/color_tokens.py; record deliberate\nexceptions in decisions.md. Preserve text independently from design changes.\n",
59
59
  "workflows/page-notes.md": "# Page notes and editorial review\n\n1. During migration or creation, create pages/<page-id>.md using pages/README.md.\n Record sources, preservation choices, owner decisions and open questions.\n2. Before editing a Page, read its note and relevant decisions.md entries. Missing\n notes mean missing context, not permission to invent facts. Read current CMS\n content too; do not overwrite newer edits from repository snapshots.\n3. After an authorized edit, append a dated change/decision/comment entry, update\n sources and open questions, and refresh generated facts with page_notes.py.\n Saving or refreshing is not editorial review and must not advance last_checked.\n4. At each review, inspect the current rendered Page and follow every external\n link, including template links. Verify destination and relevance, not just HTTP\n status. Record redirects, failures and uncertain checks; propose corrections\n instead of inventing replacements. Record actual review date and next review.\n5. For a confirmed removed Page, log deletion, replacement/redirect and archive\n location, then move its note into pages/archived/. Keep its history in Git.\n\nUse index.md as a review queue; no scheduler or mail is enabled by this routine.\nPublication and DNS changes still require their normal authorization. These notes\nare repository-only; no new MCP Page-notes fields or portal storage are implied.\n",
60
60
  "workflows/source-media.md": "# Preserve source media before retirement\n\nThis is a separate owner-approved archive, not an import of every old file into\nthe CMS. Do not delete source files or cancel old hosting as part of this routine.\n\n1. Inventory originals and downloads from the source API/export, including files\n absent from published pages. Include drafts, private records and historical\n attachments when authorized. A public sitemap or REST response cannot prove\n the library is complete. Keep the export under ignored `sources/private/`.\n2. Compare with the new site's references, including templates, partials and\n retained URLs. Record used, unused and uncertain files. \"Unused\" is not an\n instruction to delete. Preserve print originals even when a web derivative exists.\n3. Agree a private archive destination, its owning account, retention and access.\n Verify account/bucket identity and that public bucket/custom-domain access is\n disabled. Use the owner's credentials through their secret manager; never put\n credentials in scripts or manifests. Do not provision or expose a bucket silently.\n4. Copy with bounded concurrency and bounded reads, stream large files and use\n resumable transfers when supported. Preserve a collision-safe key derived from\n the full source host/path, not just the filename. Never overwrite a different\n object merely because the basename matches.\n5. Record source URL, destination key, source ID, byte count, SHA-256, media type,\n title/alt/caption, copy time and verification state in a private manifest.\n ETag is not universally MD5 (multipart objects and providers differ). Use a\n documented checksum API or stream the destination back and compare SHA-256\n and size. Metadata claiming a hash is not proof of the uploaded bytes.\n6. Resume only verified matches; report every unavailable source and hash conflict.\n Verify a restore from the archive, including a large file. Keep an independent\n private copy of the manifest. The owner reviews completeness and grants a\n separate old-host retirement decision. This archive does not preserve old\n public URLs by itself: keep URL parity/redirect acceptance separate.\n\n## Prepare print images for the web\n\nUse `scripts/typeroll/prepare_image.py` for a reviewed local TIFF or other original.\nInstall Pillow with ImageCms in an isolated environment. It applies EXIF\norientation, converts through the embedded (or explicitly supplied) ICC profile\nto sRGB, embeds the sRGB profile, strips source EXIF metadata and writes a new\nJPEG. It never changes the original, uploads files or edits a Page.\n\n```sh\npython3 scripts/typeroll/prepare_image.py sources/private/original.tif sources/private/web.jpg\n```\n\nCMYK without the correct profile fails; obtain the actual source profile instead\nof guessing. `--source-profile sources/private/original.icc` supplies it.\nUnprofiled RGB/gray requires an explicit `--assume-srgb` decision. Multi-page,\nanimated and transparent images require a separate reviewed workflow. Compare\ncolour, subject framing and sharpness visually before upload. Upload through the\nnormal integrity-checked media flow and choose alt/caption text independently.\nStore the tool's hash report privately; never commit customer images or archives.\n"
@@ -27,9 +27,9 @@ export const deployTools = [
27
27
  },
28
28
  {
29
29
  name: 'retry_deploy_verification',
30
- description: 'Retry only public verification of an already-served customer publication that failed with publication_observation_timeout. Reuses the exact job, Git commit and build artifact; never builds, uploads or changes DNS. Requires completed build/candidate receipts, unchanged hosting and no newer deployment for this version. Does not publish later CMS edits. Poll get_deploy_status for the same job_id. Admin permission required.',
31
- inputSchema: { job_id: z.string(), version: versionParam },
32
- handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.post(siteId, 'deploy', { retry_verification_job_id: args.job_id }, v(args.version)))),
30
+ description: 'Retry only public verification of an already-served customer publication that failed with publication_observation_timeout or publication_cache_refresh_required/external/failed. Reuses the exact job, Git commit and build artifact; never builds, uploads or changes DNS. Requires completed build/candidate receipts, unchanged hosting and no newer deployment for this version. Does not publish later CMS edits. A cache refresh is NEVER implicit: only after explicit owner approval, provide approve_cache_zone_id from job.cache_recovery to refresh that ENTIRE Cloudflare zone (including other websites/subdomains). Requires owning-organization administrator access, the exact publication and unchanged DNS account/zone; cached bytes must still pass normal URL verification. Otherwise omit it for read-only verification. Poll get_deploy_status for the same job_id. Admin permission required.',
31
+ inputSchema: { job_id: z.string(), approve_cache_zone_id: z.string().min(1).optional().describe('Only with explicit owner approval for a zone-wide cache refresh. Copy the exact zone_id from this failed job.cache_recovery; it affects all subdomains in that zone. Requires owning-organization admin access.'), version: versionParam },
32
+ handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.post(siteId, 'deploy', { retry_verification_job_id: args.job_id, ...(args.approve_cache_zone_id !== undefined ? { approve_cache_zone_id: args.approve_cache_zone_id } : {}) }, v(args.version)))),
33
33
  },
34
34
  {
35
35
  name: 'list_deploys',
@@ -24,7 +24,7 @@ export const domainTools = [
24
24
  },
25
25
  {
26
26
  name: 'guide_publishing_domain_change',
27
- description: 'Guide a site domain move. Start with check: returns hosting, zone, nameserver, certificate, permission, publishing, redirect capacity and candidate checks, current phase, next action and an explicit switch summary. Preview proposed hosts before save; confirm_rebuild is required if an existing candidate would be replaced. Prepare freezes the last published content, not current CMS edits. Adding redirect hosts preserves the content candidate. Approve requires explicit user approval of the switch summary and any validation interruption. Automatic DNS is journaled and rolled back after a failed check or the agreed deadline; later external edits require manual attention. Rollback requests restoration through the serialized publication chain. External DNS remains the caller’s responsibility.',
27
+ description: 'Guide a site domain move. Start with check: returns hosting, zone, nameserver, certificate, permission, publishing, redirect capacity and candidate checks, current phase, next action and an explicit switch summary. Saving future hosts is inert. Prepare republishes the last public content on main with the new origins, replacing the current site before DNS changes; newer unpublished CMS edits are excluded. Later normal publications retain the new origins. Approve changes DNS for the same main artifact without another build. Adding redirect hosts preserves the main artifact. Approve requires explicit user approval of the switch summary and any validation interruption. Automatic DNS is journaled and rolled back after a failed check or the agreed deadline; later external edits require manual attention. Rollback requests restoration through the serialized publication chain. External DNS remains the caller’s responsibility.',
28
28
  inputSchema: {
29
29
  action: z.enum(['check', 'preview', 'save', 'prepare', 'approve', 'rollback']),
30
30
  revision: z.string().optional(), candidate_id: z.string().optional(),
@@ -183,7 +183,7 @@ export const domainTools = [
183
183
  },
184
184
  {
185
185
  name: 'prepare_publishing_domain_change',
186
- description: 'Explicitly prepare for a domain switch, shortly before changing DNS, using only the last successfully published snapshot. Publish intended edits normally at the current or temporary address first. Newer unpublished CMS changes are excluded. Normal publication remains available; a newer successful publication makes this candidate outdated and requires preparation again. Requires the current domain revision. Poll read_publishing_domains for certificate and DNS requirements, then explicitly approve the verified cutover.',
186
+ description: 'Publish for the new domain shortly before changing DNS, using only the last successfully published snapshot. This replaces main at the current or temporary address; it does not create a separate preview branch. Publish intended edits normally at the current or temporary address first. Newer unpublished CMS changes are excluded. Normal publication remains available and keeps these embedded domain addresses while DNS is pending. It refreshes the receipt for the same main site; no second preparation is needed. Requires the current domain revision. Poll read_publishing_domains for certificate and DNS requirements, then explicitly approve the verified cutover.',
187
187
  inputSchema: { revision: z.string() },
188
188
  handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.post(siteId, 'publishing/prepare', args))),
189
189
  },
@@ -209,7 +209,7 @@ export const domainTools = [
209
209
  },
210
210
  {
211
211
  name: 'approve_publishing_domain_cutover',
212
- description: 'Approve switching website traffic to the verified frozen deployment. Read publishing domains first and review DNS requirements, certificate readiness and redirect_preparations. Existing traffic is preserved until validation succeeds. With automatic DNS, redirect hosts switch after the website host: Typeroll ensures their zone redirect rule, then changes their reviewed record to a proxied placeholder. The publication goes live only after every redirect host answers 301 to the website host with the same path and query. When preparation.validation_blocker.resolution is accept_validation_window, Cloudflare can validate the website host only after its one existing record points at Pages: approving with accept_validation_window: true lets Typeroll change that record, and the host does not answer until Cloudflare validates it (Cloudflare does not document how long; previous_records holds the rollback). Get the user\'s explicit go-ahead for that window and choose a quiet time. External DNS management remains the caller’s responsibility.',
212
+ description: 'Approve switching DNS to the current verified main publication, without another Git push, build or upload. Read the latest candidate_id; older receipts are rejected. Read publishing domains first and review DNS requirements, certificate readiness and redirect_preparations. Existing traffic is preserved until validation succeeds. With automatic DNS, redirect hosts switch after the website host: Typeroll ensures their zone redirect rule, then changes their reviewed record to a proxied placeholder. The publication goes live only after every redirect host answers 301 to the website host with the same path and query. When preparation.validation_blocker.resolution is accept_validation_window, Cloudflare can validate the website host only after its one existing record points at Pages: approving with accept_validation_window: true lets Typeroll change that record, and the host does not answer until Cloudflare validates it (Cloudflare does not document how long; previous_records holds the rollback). Get the user\'s explicit go-ahead for that window and choose a quiet time. External DNS management remains the caller’s responsibility.',
213
213
  inputSchema: {
214
214
  revision: z.string(), candidate_id: z.string(),
215
215
  validation_timeout_minutes: z.number().int().min(5).max(45).optional(),
@@ -219,16 +219,16 @@ export const domainTools = [
219
219
  },
220
220
  {
221
221
  name: 'read_publishing_domains',
222
- description: 'Read the desired and active website, redirect and media hosts, retained media aliases, DNS management mode and domain configuration revision. host_checks lists each future host with its Cloudflare zone and structured blockers and warnings (code, host, message), for example apex_zone_required (an apex such as example.com needs an active Cloudflare zone in the hosting account), zone_not_active (the zone is pending until its nameservers change) or redirect_zone_required. Resolve blockers before preparing. preparation and redirect_preparations hold DNS records, the zone redirect rule and certificate state after preparation. Saving intent does not switch traffic. next_publication explains that normal publication keeps updating the active or temporary host. Only prepare_publishing_domain_change builds for future hosts; a newer normal publication makes an older candidate outdated. candidate.sitewide_noindex describes the frozen candidate, not current settings; do not approve an unintentionally noindex launch.',
222
+ description: 'Read the desired and active website, redirect and media hosts, retained media aliases, DNS management mode and domain configuration revision. host_checks lists each future host with its Cloudflare zone and structured blockers and warnings (code, host, message), for example apex_zone_required (an apex such as example.com needs an active Cloudflare zone in the hosting account), zone_not_active (the zone is pending until its nameservers change) or redirect_zone_required. Resolve blockers before preparing. preparation and redirect_preparations hold DNS records, the zone redirect rule and certificate state after preparation. Saving intent does not switch traffic. published identifies the addresses embedded in main, separately from active (verified traffic). Saving desired hosts is inert. prepare_publishing_domain_change explicitly replaces main with the new origins; later normal publication retains them and refreshes the DNS approval receipt. candidate.sitewide_noindex describes the frozen candidate, not current settings; do not approve an unintentionally noindex launch.',
223
223
  inputSchema: {},
224
224
  handler: withErrorBoundary(async (_args, { client, siteId }) => ok(await client.get(siteId, 'publishing/domains'))),
225
225
  },
226
226
  {
227
227
  name: 'set_publishing_domains',
228
- description: 'Save future website, redirect and media hosts before preparing a deployment. Normal trigger_deploy continues updating the current or temporary address, even while future hosts await validation. Shortly before changing DNS, explicitly call prepare_publishing_domain_change to prepare the last published snapshot for these hosts. A newer normal publication makes the older candidate outdated; prepare again before cutover. Requires the current revision from read_publishing_domains. This does not publish content or change traffic DNS. Choose external DNS when your agent manages Cloudflare independently. redirect_hosts (for example www.example.com when example.com is the website host) answer 301 to the website host, keeping path and query, through a Single Redirect rule in the host\'s Cloudflare zone; they are switched and verified with the website. The response includes host_checks: resolve its blockers before preparing.',
228
+ description: 'Save future website, redirect and media hosts before preparing a deployment. Normal trigger_deploy continues updating the current or temporary address, even while future hosts await validation. Shortly before changing DNS, explicitly call prepare_publishing_domain_change to publish the last published snapshot again with these hosts. That explicit publication replaces main before DNS changes. Later normal publications retain its new origins and refresh the DNS approval receipt; no second build is needed to switch DNS. Requires the current revision from read_publishing_domains. This does not publish content or change traffic DNS. Choose external DNS when your agent manages Cloudflare independently. redirect_hosts (for example www.example.com when example.com is the website host) answer 301 to the website host, keeping path and query, through a Single Redirect rule in the host\'s Cloudflare zone; they are switched and verified with the website. The response includes host_checks: resolve its blockers before preparing.',
229
229
  inputSchema: {
230
230
  revision: z.string(), website_host: z.string().nullable(), media_host: z.string().nullable(),
231
- confirm_rebuild: z.boolean().optional().describe('Confirm replacing a prepared content candidate after reviewing domain_rebuild_confirmation_required or the guide preview.'),
231
+ confirm_rebuild: z.boolean().optional().describe('Legacy input; Core 0.2.116+ does not require confirmation to save inert future addresses.'),
232
232
  media_path_prefix: z.string().optional().describe('Empty on a separate media host; /media when using the website host.'),
233
233
  redirect_hosts: z.array(z.string()).max(4).optional().describe('Hostnames that redirect permanently to website_host, such as www.example.com. Omit or send [] for none.'),
234
234
  dns_mode: z.enum(['automatic', 'external']),
package/dist/version.js CHANGED
@@ -8,4 +8,4 @@
8
8
  //
9
9
  // Keep it in lockstep with package.json: tests/version.test.ts asserts
10
10
  // VERSION === package.json.version, so a bump that forgets this line fails CI.
11
- export const VERSION = '0.45.86';
11
+ export const VERSION = '0.45.88';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeroll/mcp-server",
3
- "version": "0.45.86",
3
+ "version": "0.45.88",
4
4
  "description": "Typeroll CMS MCP server \u2013 connect AI agents to the REST API to manage content and publish static websites.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -424,10 +424,13 @@ until the owner has approved retirement.
424
424
 
425
425
  Saving future hosts must not block normal publication on the current/temporary
426
426
  address. Publish and review intended changes there first. Shortly before changing
427
- DNS, explicitly prepare the last published snapshot for the future hosts with
428
- `prepare_publishing_domain_change`. Review frozen indexing settings and approve
429
- traffic separately. If newer content is published while the candidate waits,
430
- prepare again before cutover; do not overwrite it with an outdated snapshot. Respect preserve/renew choices for owner-supplied facts;
427
+ DNS, explicitly publish the last published snapshot for the future hosts with
428
+ `prepare_publishing_domain_change` (Core 0.2.116 / MCP 0.45.87). This replaces
429
+ main at the current address before DNS changes; new-host links may wait for DNS.
430
+ Review frozen indexing settings and approve DNS separately, without another
431
+ build or upload. Later normal publications retain the new origins and refresh
432
+ the receipt; read the latest candidate_id before approval. No separate old site
433
+ is kept at the temporary hostname. Older Core versions use the older branch flow. Respect preserve/renew choices for owner-supplied facts;
431
434
  "timeless" wording must not invent recurring events or erase preserved copy.
432
435
 
433
436
  ## Fluid typography and component rhythm
@@ -103,18 +103,21 @@ normal approval flow. A separately authorized provider-side setup with a supplie
103
103
  beacon token is an alternative, not a way to bypass Organization permissions.
104
104
  Never expose a provider API token as a public beacon token.
105
105
 
106
- From Core 0.2.106, saving future hosts does not redirect normal publication.
107
- Keep using `trigger_deploy` to update and review the current/temporary address.
108
- Read `next_publication` in `read_publishing_domains` for the destination.
109
- Shortly before DNS changes, deliberately call `prepare_publishing_domain_change`
110
- with the current revision. It prepares the last successful public snapshot,
111
- not later unpublished CMS edits. Review its frozen indexing settings and DNS
112
- requirements, then approve traffic separately. If another normal publication
113
- succeeds while the candidate waits, prepare again: an outdated snapshot cannot
114
- replace newer published content. There is no need to clear saved future hosts.
115
- To correct candidate indexing, save the intended setting, publish normally, and
116
- prepare again. Temporary-host protection keeps the review address noindex even
117
- when the published authored setting permits indexing on the future host.
106
+ From Core 0.2.116 / MCP 0.45.87, saving future hosts remains inert.
107
+ Publish and review intended changes at the current/temporary address first.
108
+ Shortly before DNS changes, explicitly call `prepare_publishing_domain_change`
109
+ with the current revision: it republishes the last successful public snapshot
110
+ on main with the new origins, replacing the site at its current address.
111
+ Later unpublished CMS changes are excluded. New-host absolute links and media
112
+ may not work until DNS/certificates are ready. Do not promise a parallel old copy.
113
+ Review frozen indexing and DNS requirements, then approve DNS separately; that
114
+ step does not push, build or upload again. Normal publications before DNS retain
115
+ the newly embedded origins and refresh the current receipt. Read the latest
116
+ candidate_id before approval; no new preparation is needed for a content edit.
117
+ To correct indexing, save the intended setting and publish normally.
118
+ `published.hosts` describes main output; `active` describes verified traffic.
119
+ DNS rollback restores DNS, not an older artifact. For older servers that still
120
+ prepare a preview branch, update Core before using this workflow.
118
121
  Keep MX/SPF and unrelated DNS unchanged; review any validation window and obtain
119
122
  the already-defined traffic-switch authorization.
120
123