@typeroll/mcp-server 0.45.30 → 0.45.31

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.
@@ -11,7 +11,7 @@ export const BUNDLED_SKILLS = {
11
11
  "tr-forms": "---\nname: tr-forms\ndescription: Use when the user wants to add a contact form, newsletter signup, booking form, or other web form to a Typeroll site.\n---\n\n# Add a form to a Typeroll site\n\nTyperoll forms are server-backed. The platform renders the form shell, accepts\nsigned submissions, validates declared fields, stores submissions, and can run\nadmin-configured email or webhook actions.\n\n## Choose the placement\n\n- Block-mode page: add `{ type: 'core/form', data: { form_id } }`.\n- HTML-mode page: add `<x-form id=\"form-id\" />` to `html_content`.\n\nBoth are authoring references to the same renderer. Preview and static\ngeneration expand them to the complete form HTML, signed token, honeypot,\ninitial step state, styles, and shared runtime. Never hand-write the `<form>`\nshell or paste a token into page HTML.\n\n## Create a simple form\n\n`fields[]` is authoring sugar for one static step:\n\n```json\n{\n \"id\": \"newsletter\",\n \"name\": \"Newsletter\",\n \"fields\": [\n {\"name\":\"email\", \"type\":\"email\", \"label\":\"E-postadress\", \"required\":true}\n ],\n \"submit_text\": \"Prenumerera\",\n \"success_message\": \"Tack! Du är anmäld.\"\n}\n```\n\nField names must match `[a-z][a-z0-9_-]*`. Labels may be localized. Supported\nsimple types include `text`, `email`, `tel`, `url`, `number`, `textarea`,\n`select`, `radio`, `checkbox`, `hidden`, and `gdpr_consent`.\n\n## Place the form\n\nOn a block-mode page:\n\n```\nadd_block target={kind:'page', id:'start'}\n block={type:'core/form', data:{form_id:'newsletter'}}\n```\n\nOn an HTML-mode page:\n\n```html\n<section class=\"newsletter-signup\">\n <h2>Få våra nyheter</h2>\n <x-form id=\"newsletter\" />\n</section>\n```\n\n`<x-form>` is not a browser-side shortcode. It is replaced during server\npreview/build, so it also supports multi-step forms and never needs inline JS.\n\n## Multi-step form\n\nFor a funnel, write `steps[]` directly. Each step is a block tree containing\n`form/*` field blocks and optional content blocks:\n\n```\nupdate_form form_id=ansokan patch={steps:[\n {id:'company', title:'Företag', blocks:[\n {type:'form/text', data:{name:'company', label:'Företag', required:true}},\n {type:'form/email', data:{name:'email', label:'E-post', required:true}}\n ]},\n {id:'details', title:'Detaljer', blocks:[\n {type:'form/textarea', data:{name:'message', label:'Meddelande'}},\n {type:'form/consent', data:{name:'consent', text:'<p>Jag godkänner …</p>'}}\n ]}\n]}\n```\n\nSubmissions accumulate in one partial record and become complete on the final\nstep. Abandoned partials use `partial_ttl_days` (default 30).\n\n## Storage and integrations\n\nCompleted submissions appear in Forms → Submissions. Admins can configure\nactions in the form editor:\n\n- Email notification or autoresponder through the site's email connector.\n- Generic webhook to a public HTTPS endpoint. The admin chooses an explicit\n field allowlist and signing secret. Typeroll signs the exact request body in\n `X-Typeroll-Signature`, sends an idempotency key, retries transient failures,\n and stores delivery status.\n\nActions are deliberately excluded from MCP/API-key writes and reads because\nthey can exfiltrate submitted data. Direct the user to the portal form editor\nto configure them.\n\n## Verify\n\n1. `read_form form_id=\"newsletter\"` and confirm the steps/fields.\n2. Preview the page and confirm the authoring reference has expanded to a form\n with `data-tr-form-el`, a signed token, and the platform runtime.\n3. Submit a test entry and confirm it appears in Forms → Submissions.\n4. If a webhook is configured, confirm its delivery status and the receiving\n system's idempotency key before deploying.\n\n## Common patterns\n\nNewsletter:\n\n```json\n{\"fields\":[{\"name\":\"email\",\"type\":\"email\",\"label\":\"E-postadress\",\"required\":true}]}\n```\n\nContact:\n\n```json\n{\"fields\":[\n {\"name\":\"name\",\"type\":\"text\",\"label\":\"Namn\",\"required\":true},\n {\"name\":\"email\",\"type\":\"email\",\"label\":\"E-post\",\"required\":true},\n {\"name\":\"message\",\"type\":\"textarea\",\"label\":\"Meddelande\",\"required\":true}\n]}\n```\n\nBooking request:\n\n```json\n{\"fields\":[\n {\"name\":\"name\",\"type\":\"text\",\"label\":\"Namn\",\"required\":true},\n {\"name\":\"email\",\"type\":\"email\",\"label\":\"E-post\",\"required\":true},\n {\"name\":\"time\",\"type\":\"select\",\"label\":\"Tid\",\"options\":[\"09:00\",\"10:00\",\"14:00\"]},\n {\"name\":\"notes\",\"type\":\"textarea\",\"label\":\"Kommentar\"}\n]}\n```\n\n## Pitfalls\n\n- Do not hand-write a form, token, honeypot, or submit script.\n- Do not put a raw `<script>` in page HTML; the sanitizer removes it.\n- Do not expose action configuration through agent surfaces.\n- Do not send every submitted field to a webhook by default; choose the\n smallest allowlist the external register needs.\n- `submit_token` is stable until the platform rotates its form-signing secret;\n a rebuild refreshes it after rotation.\n",
12
12
  "tr-header-footer": "---\nname: tr-header-footer\ndescription: Build accessible native header and footer partials with the site logo, semantic navigation, responsive disclosure, and optional native cookie consent.\n---\n\n# Native header and footer compositions\n\nUse block-mode partials for new work. Core 0.1.8 / capability contract 0.43.0\nships `template/site_logo`, `core/navigation`, layout primitives, and a\nrevision-safe partial mode operation. HTML partials remain supported for\nlegacy markup, but generic navigation and responsive layout no longer require\ntenant HTML, CSS, or JavaScript.\n\n## Preconditions\n\n1. Read `get_site_capabilities`, `read_site_settings`, `list_partials`, and\n `list_block_types`.\n2. Require `supports_native_navigation`, `supports_partial_mode_switching`,\n `supports_responsive_data_fields`, and\n `supports_versioned_block_type_inheritance`.\n3. Read the existing header/footer before replacing anything. Work in a branch\n for redesigns and preview at desktop, 390 px, keyboard-only, and 200% zoom.\n\n## Header tree\n\nStage this tree with `update_partial partial_id=\"header\" patch={blocks:[...]}\nsave=true`, then call `set_partial_mode partial_id=\"header\" to=\"blocks\"` and\nread it back. Localize both accessible labels and every link label.\n\n```json\n[\n {\n \"type\": \"core/section\",\n \"data\": { \"width\": \"wide\", \"padding_y\": \"sm\" },\n \"children\": [{\n \"type\": \"core/container\",\n \"data\": {\n \"direction\": { \"mobile\": \"column\", \"tablet\": \"row\" },\n \"align_main\": \"space-between\",\n \"align_cross\": \"center\",\n \"gap\": \"md\",\n \"width\": \"wide\",\n \"padding_y\": \"none\",\n \"padding_x\": \"none\"\n },\n \"children\": [\n { \"type\": \"template/site_logo\", \"data\": { \"height\": \"md\", \"link_to_home\": true } },\n {\n \"type\": \"core/navigation\",\n \"data\": {\n \"aria_label\": \"Main navigation\",\n \"menu_label\": \"Menu\",\n \"links\": [\n { \"label\": \"Home\", \"href\": \"/\" },\n { \"label\": \"About\", \"href\": \"/about/\" },\n { \"label\": \"Contact\", \"href\": \"/contact/\" }\n ]\n }\n }\n ]\n }]\n }\n]\n```\n\n`core/navigation` emits a real `nav` and list in initial HTML, marks the\nmatching page with `aria-current=\"page\"`, and progressively enhances to a\nbutton-controlled mobile menu. Verify Tab and Shift-Tab order, Enter/Space,\nEscape returning focus to the disclosure, visible focus, long translated\nlabels, and no horizontal overflow. Without JavaScript the links remain\nvisible and usable.\n\n## Footer tree and consent\n\nUse the same tree for `footer`, normally with `padding_y: \"lg\"`, a small site\nlogo, localized footer navigation, and optional contact/prose blocks. The\nfooter navigation gets its own accessible label; it is not the main\nnavigation landmark.\n\nCookie consent is a site feature, not a footer widget. Configure it separately:\n\n```json\n{\n \"cookie_consent\": {\n \"enabled\": true,\n \"text\": \"We use optional cookies. <a href=\\\"/privacy/\\\">Privacy policy</a>\",\n \"privacy_policy_url\": \"/privacy/\",\n \"reload_after_consent\": false\n }\n}\n```\n\nUse `update_site_settings`, then read settings back. `scripts_necessary` and\n`scripts_optional` execute in visitors' browsers and must be reviewed like\nother API-key-authorized script surfaces. Verify accept/necessary/reject,\noptional-script gating, and keyboard focus in preview and the actual build.\nThe signed hosted preview runs at an opaque origin. Consent choices therefore\nuse in-memory state for the current preview document; they are intentionally\nnot persisted as portal cookies across a reload or page navigation. Accepting\nall must still activate optional scripts immediately, which is enough to test\nthe gate without weakening the preview sandbox.\n\n## Version and dependency rules\n\nChild versions inherit installed, custom, and third-party block types from\ntheir base chain. If preview reports a missing type, stop and inspect\n`list_block_types` for that exact version; do not duplicate an Extension or\ninstallation. A type missing after capability 0.43.0 is a dependency error to\nreport, not a reason to let the block silently disappear.\n\n## Final checks\n\n- Header and footer landmarks exist exactly once around their content.\n- Logo is fully visible, keeps its aspect ratio, has useful alt text, and links home.\n- Current-page state and localized navigation labels are correct.\n- Mobile disclosure is keyboard accessible and does not trap focus.\n- No tenant CSS is required for spacing, focus, wrapping, or the narrow layout.\n- Preview and a hosted static build match before promotion.\n",
13
13
  "tr-imagegen": "---\nname: tr-imagegen\ndescription: Use when the user wants to generate images for a Typeroll site with AI models (Gemini, OpenAI, Higgsfield) — hero images, illustrations, section backgrounds, og-images. Triggers on \"generera bilder\", \"generate images\", \"skapa en hero-bild\", \"AI-bilder\", \"bildgenerering\", or when a brief calls for imagery that doesn't exist in assets/. Covers the local lab loop (generate → review → pick) and uploading winners to the Typeroll media library.\n---\n\n# Generate images for a Typeroll site (local lab → media library)\n\nImage generation runs **locally** in the site workdir — provider API keys\nlive in the folder's `.env`, candidates land in `images/lab/`, and only\nthe picked winners are uploaded to the Typeroll media library via the\nregular media tools (see `tr-images` for the upload/variants half).\n\nWhy local: you can look at the candidates (Read the files), iterate on\nprompts cheaply, and never ship a key or a reject anywhere.\n\n## Folder convention\n\n```\n<site>/\n├── .env # provider keys — GITIGNORED, never committed\n├── prompts/\n│ └── image-style.md # the site's image style profile (see below)\n└── images/\n └── lab/ # generated candidates — gitignored, disposable\n```\n\n`.env` keys (only the ones the user has — check before assuming):\n\n```\nGEMINI_API_KEY=...\nOPENAI_API_KEY=...\n```\n\n(Higgsfield needs no key here — it connects as an MCP server, see below.)\n\nLoad them per-command (`source .env` doesn't persist between Bash calls):\n\n```bash\nexport $(grep -v '^#' .env | xargs) # prepend to each generation command\n```\n\n## The style profile — prompts/image-style.md\n\nEvery site gets ONE style profile that you **prepend to every image\nprompt**. This is what keeps 20 images generated across 5 sessions\nlooking like one site. Derive it from `assets/brand.md` + the brief if\nit doesn't exist yet, and confirm it with the user before generating at\nscale. Keep it short (5–10 lines): art direction, palette, mood,\nphotography vs illustration, what to avoid.\n\nExample shape:\n\n```markdown\n# Bildstil — <Sajtnamn>\nVarm, folklig illustration med mjuka rundade former. Platt 2D med\nsubtila skuggor — ingen 3D, ingen fotorealism. Palett: kobolt #1F4FB8,\nsol #FFC83D, grädde #FFF8EC; accenter sparsamt. Människor: enkla,\ninkluderande, glada — inga karikatyrer. Undvik: stockfoto-känsla, text\ni bilden, logotyper, watermarks.\n```\n\n## Generate candidates\n\nName candidates descriptively: `images/lab/<motiv>-<modell>-<n>.png`.\nGenerate 3–6 candidates per slot (mix models when several keys exist),\nthen **Read the files to actually look at them** before showing the\nuser your shortlist.\n\n### Gemini (gemini-2.5-flash-image)\n\n```bash\ncurl -s \"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent\" \\\n -H \"x-goog-api-key: $GEMINI_API_KEY\" -H 'Content-Type: application/json' \\\n -d '{\n \"contents\": [{\"parts\": [{\"text\": \"<STYLE PROFILE>\\n\\n<MOTIF PROMPT>\"}]}],\n \"generationConfig\": {\"imageConfig\": {\"aspectRatio\": \"16:9\"}}\n }' | jq -r '.candidates[0].content.parts[] | select(.inlineData) | .inlineData.data' \\\n | base64 -d > images/lab/hero-gemini-1.png\n```\n\nAspect ratios: `1:1`, `16:9`, `4:3`, `3:4`, `9:16`. Gemini also does\nimage *editing* — pass an existing image as an `inlineData` part plus an\ninstruction to restyle/extend it (useful for \"same illustration but\nwinter\").\n\n### OpenAI (gpt-image-1)\n\n```bash\ncurl -s https://api.openai.com/v1/images/generations \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" -H 'Content-Type: application/json' \\\n -d '{\n \"model\": \"gpt-image-1\",\n \"prompt\": \"<STYLE PROFILE>\\n\\n<MOTIF PROMPT>\",\n \"size\": \"1536x1024\",\n \"quality\": \"high\"\n }' | jq -r '.data[0].b64_json' | base64 -d > images/lab/hero-openai-1.png\n```\n\nSizes: `1024x1024`, `1536x1024` (landscape), `1024x1536` (portrait).\n\n### Higgsfield (MCP server — no API key)\n\nHiggsfield exposes its models through a hosted MCP server at\n`https://mcp.higgsfield.ai/mcp` (OAuth-protected — first connect opens a\nbrowser login; no secret lands in any file). Add it next to the\ntyperoll server in the site folder's `.mcp.json`:\n\n```json\n\"higgsfield\": { \"type\": \"http\", \"url\": \"https://mcp.higgsfield.ai/mcp\" }\n```\n\nThen use its tools directly — list what's available rather than\nassuming tool names. Save/download outputs into `images/lab/` with the\nsame naming convention, and prepend the style profile to prompts here\ntoo. **Headless caveat:** OAuth-protected MCP servers need an existing\nlogin session on the machine — connect once interactively before\nrelying on it in a headless run. (The same URL works as a custom\nconnector in Claude Desktop, for editors who don't use Claude Code.)\n\n## Review → pick → upload\n\n1. **Look at every candidate** (Read the image files) and write one line\n per candidate in `build-log.md` (keep/reject + why).\n2. Show the user the shortlist (file paths) and let them pick — unless\n they've delegated the pick to you.\n3. Upload winners with the regular media tools (see `tr-images`):\n - small files → `upload_media_inline` (base64);\n - larger → `create_upload_url` + HTTP PUT + `finalize_media`.\n Give real `alt` text and a descriptive filename at upload time.\n4. `generate_image_variants` for responsive sizes when the image is\n placed full-width.\n5. Place via `update_block` (`core/image`, hero fields, …) or page HTML.\n6. `images/lab/` is disposable — leave rejects there; never upload them.\n\n## Pitfalls\n\n- **Never put text in generated images** (headlines, buttons) — text is\n HTML's job; generated text renders as gibberish in non-English. And\n models sneak text onto surfaces where it \"feels natural\" — jerseys,\n signs, banners, packages — even when the prompt doesn't ask for any.\n Forbid it explicitly in the prompt (\"plain unmarked clothing, no\n text, no letters, no numbers anywhere in the image\") and make the\n same rule part of every site's style profile.\n- **Don't commit `.env` or `images/lab/`** — both are gitignored by the\n kit convention; keep it that way.\n- **Cost discipline:** generation costs real money per image. Batch\n thoughtfully (3–6 per slot, not 20), and reuse via Gemini's\n edit-an-image mode instead of regenerating from scratch.\n- **Licensing/provenance:** AI-generated imagery is fine for site\n decoration, but never generate fake \"photos\" of real people, products\n the customer doesn't sell, or anything presented as documentary fact.\n- **The style profile is the contract.** If the user rejects a batch on\n style grounds, fix `prompts/image-style.md` first, then regenerate —\n don't just tweak one prompt.\n",
14
- "tr-images": "---\nname: tr-images\ndescription: Use when the user asks for an image, hero, illustration, or logo to be created and embedded in a Typeroll page. Covers the two-step signed-URL upload flow so the agent doesn't try to POST bytes through the CMS API (it can't).\n---\n\n# Add images to a Typeroll site\n\nThe Typeroll API does NOT accept image bytes directly. Uploads go\nthrough a signed PUT URL straight to Cloudflare R2, and the API only\nsees the metadata. Two-step flow:\n\nFor public source URLs, prefer `upload_media_from_url`; for 2–50 files use\n`upload_media_batch_from_urls` (max 25 MiB per file). Both download, upload,\nand finalize each asset automatically. Batch results preserve input order and\nreport failures per item, so retry only the failed entries. Use the manual\nsigned-URL flow below for local files or when you need direct control over the\nPUT.\n\n## Recipe\n\n### 1. Get an image\n\nOptions, in order of preference:\n\na. **Reuse an existing one.** `list_media` returns CDN URLs for every\n image already on this site. Search the list before generating\n anything — saves bandwidth and keeps the visual catalog tight.\n\nb. **Generate locally.** The user's Claude Code installation has\n access to whatever image-gen tools they've configured (DALL-E,\n Midjourney, Stable Diffusion, Replicate, etc.). Generate and save\n to a tempfile.\n\nc. **Source from the web** with appropriate licensing (the user is\n responsible for clearing rights). Save locally before upload.\n\n### 2. Mint a signed upload URL\n\n```\ncreate_upload_url filename=\"hero-services.png\"\n content_type=\"image/png\"\n size=<bytes>\n alt_text=\"Office worker reviewing documents at a desk\"\n```\n\nReturns:\n\n```json\n{\n \"upload_url\": \"https://...r2.cloudflarestorage.com/.../signed-...\",\n \"cdn_url\": \"https://cdn.example.com/orgs/.../images/...png\",\n \"key\": \"orgs/.../images/...png\",\n \"media_id\": \"abc123\",\n \"expires_in\": 300\n}\n```\n\nThe signed URL is valid for 5 minutes. The media doc is already\nregistered — even before the upload completes — so it'll show in\n`list_media` immediately.\n\n### 3. PUT the bytes\n\nOutside the MCP, hit the signed URL directly:\n\n```\nPUT <upload_url>\nContent-Type: <same content_type as in step 2>\nBody: <file bytes>\n```\n\nIn a shell:\n\n```bash\ncurl -sS -X PUT --data-binary @hero-services.png \"$UPLOAD_URL\"\n```\n\nOptionally with `-H \"Content-Type: image/png\"` if you need to override what R2 will infer. **Nothing else.**\n\n> The signed URL embeds checksum-related query parameters\n> (`x-amz-checksum-crc32`, `x-amz-sdk-checksum-algorithm`) for legacy\n> SDK compatibility, but `X-Amz-SignedHeaders=host` — only the host\n> header is part of the signature. Sending the `x-amz-*` values as\n> request headers yields `403 SignatureDoesNotMatch`. Don't. Just\n> `--data-binary` the file at the URL.\n\nOr from JS (Claude Code can run a one-line script):\n\n```js\nawait fetch(uploadUrl, {\n method: 'PUT',\n headers: { 'Content-Type': contentType },\n body: await fs.readFile(path),\n});\n```\n\nParallelise N uploads via shell `&` + `wait`:\n\n```bash\nwhile IFS=$'\\t' read -r filename signed_url; do\n curl -sS -X PUT --data-binary @\"$filename\" \"$signed_url\" &\ndone < manifest.tsv\nwait\n```\n\nA 200 OK from R2 means the image is now live at `cdn_url`.\n\n### 4. Patch metadata (alt text, etc.)\n\nYou set `alt_text` at create time, but if you generate the image first\nand only THEN realize what to caption it as, patch later:\n\n```\nupdate_media media_id=<id> alt_text=\"...\" filename=\"hero-services-v2.png\"\n```\n\n### 4b. Fill missing alt-text on existing media\n\nWhen a customer has uploaded a bunch of images without alt-text (very\ncommon after a WP migration), don't make it up — use vision:\n\n```\nlist_media → find items with empty alt_text\nsuggest_alt_text_context media_id=<id> → returns { image_url, suggested_prompt,\n language, used_on_pages, current_alt_text }\n# Pass image_url + the returned suggested_prompt to YOUR OWN vision\n# capability (you can fetch the URL and pass bytes to vision).\nupdate_media media_id=<id> alt_text=\"<what vision returned>\"\n```\n\nThe prompt is tuned for SEO-grade output: short (5-15 words), no \"image\nof / picture of\" filler, written in the site's content language,\ndecorative images return empty string. Run it sequentially on a\nlist_media batch and you can fix alt-text gaps across a whole site\nwithout burning your context on prompt design. The platform does NOT\nrun vision on your behalf — your model does, your usage.\n\n### 5. Embed in a page\n\n`read_page` the target, insert `<img>` in the right spot:\n\n```html\n<img src=\"<cdn_url>\"\n alt=\"<alt_text>\"\n style=\"width: 100%; height: auto; display: block; margin: 2rem 0;\" />\n```\n\nThen `update_page` with the new HTML. Or, if you're generating a hero\nfor a brand-new page, include the `<img>` directly in `create_page`'s\n`html_content`.\n\n## Pitfalls\n\n- **Always set `alt_text`.** Empty alt is bad for SEO + accessibility.\n Default to a one-sentence description of what's in the image.\n- **`<script>` etc. in SVGs.** The page sanitizer drops `<script>`\n inside SVG, so an icon set that includes script-based animations\n won't render correctly. Use static SVG or a JPG/PNG export.\n- **CSS background-image references aren't dedup'd.** If you set the\n same image as a CSS background on multiple pages, the alt-text +\n metadata are page-irrelevant. The sanitizer allows\n `background-image: url(...)` in inline styles, but think about\n whether an `<img>` is actually better.\n- **Source URL leakage.** If you generated the image from a prompt\n that contains internal info, don't bake that prompt into the\n filename. Use a descriptive but generic filename.\n\n## Format choice\n\n- **PNG** for logos, icons with hard edges, anything with text.\n- **JPG** for photos. Smaller file, better for big hero images.\n- **WebP** if the target audience runs modern browsers (95%+ in 2026).\n- **SVG** for icons + simple illustrations. Vector scales perfectly.\n- **PDF** is supported by `create_upload_url` for document downloads;\n link with `<a href>`, not `<img>`.\n",
14
+ "tr-images": "---\nname: tr-images\ndescription: Use when the user asks for an image, hero, illustration, or logo to be created and embedded in a Typeroll page. Covers the two-step signed-URL upload flow so the agent doesn't try to POST bytes through the CMS API (it can't).\n---\n\n# Add images to a Typeroll site\n\nThe Typeroll API does NOT accept image bytes directly. Uploads go\nthrough a signed PUT URL straight to Cloudflare R2, and the API only\nsees the metadata. Two-step flow:\n\nFor public source URLs, prefer `upload_media_from_url`; for 2–50 files use\n`upload_media_batch_from_urls` (max 25 MiB per file). Both download, upload,\nand finalize each asset automatically. Batch results preserve input order and\nreport failures per item, so retry only the failed entries. Use the manual\nsigned-URL flow below for local files or when you need direct control over the\nPUT.\n\n## Recipe\n\n### 1. Get an image\n\nOptions, in order of preference:\n\na. **Reuse an existing one.** `list_media` returns CDN URLs for every\n image already on this site. Search the list before generating\n anything — saves bandwidth and keeps the visual catalog tight.\n\nb. **Generate locally.** The user's Claude Code installation has\n access to whatever image-gen tools they've configured (DALL-E,\n Midjourney, Stable Diffusion, Replicate, etc.). Generate and save\n to a tempfile.\n\nc. **Source from the web** with appropriate licensing (the user is\n responsible for clearing rights). Save locally before upload.\n\n### 2. Mint a signed upload URL\n\n```\ncreate_upload_url filename=\"hero-services.png\"\n content_type=\"image/png\"\n size=<bytes>\n alt_text=\"Office worker reviewing documents at a desk\"\n```\n\nReturns:\n\n```json\n{\n \"upload_url\": \"https://...r2.cloudflarestorage.com/.../signed-...\",\n \"cdn_url\": \"https://cdn.example.com/orgs/.../images/...png\",\n \"key\": \"orgs/.../images/...png\",\n \"media_id\": \"abc123\",\n \"expires_in\": 300\n}\n```\n\nThe signed URL is valid for 5 minutes. The media doc is already\nregistered — even before the upload completes — so it'll show in\n`list_media` immediately.\n\n### 3. PUT the bytes\n\nOutside the MCP, hit the signed URL directly:\n\n```\nPUT <upload_url>\nContent-Type: <same content_type as in step 2>\nBody: <file bytes>\n```\n\nIn a shell:\n\n```bash\ncurl -sS -X PUT --data-binary @hero-services.png \"$UPLOAD_URL\"\n```\n\nOptionally with `-H \"Content-Type: image/png\"` if you need to override what R2 will infer. **Nothing else.**\n\n> The signed URL embeds checksum-related query parameters\n> (`x-amz-checksum-crc32`, `x-amz-sdk-checksum-algorithm`) for legacy\n> SDK compatibility, but `X-Amz-SignedHeaders=host` — only the host\n> header is part of the signature. Sending the `x-amz-*` values as\n> request headers yields `403 SignatureDoesNotMatch`. Don't. Just\n> `--data-binary` the file at the URL.\n\nOr from JS (Claude Code can run a one-line script):\n\n```js\nawait fetch(uploadUrl, {\n method: 'PUT',\n headers: { 'Content-Type': contentType },\n body: await fs.readFile(path),\n});\n```\n\nParallelise N uploads via shell `&` + `wait`:\n\n```bash\nwhile IFS=$'\\t' read -r filename signed_url; do\n curl -sS -X PUT --data-binary @\"$filename\" \"$signed_url\" &\ndone < manifest.tsv\nwait\n```\n\nA 200 OK from R2 means the image is now live at `cdn_url`.\n\n### 4. Patch metadata (alt text, etc.)\n\nYou set `alt_text` at create time, but if you generate the image first\nand only THEN realize what to caption it as, patch later:\n\n```\nupdate_media media_id=<id> alt_text=\"...\" filename=\"hero-services-v2.png\"\n```\n\n### 4b. Fill missing alt-text on existing media\n\nWhen a customer has uploaded a bunch of images without alt-text (very\ncommon after a WP migration), don't make it up — use vision:\n\n```\nlist_media → find items with empty alt_text\nsuggest_alt_text_context media_id=<id> → returns { image_url, suggested_prompt,\n language, used_on_pages, current_alt_text }\n# Pass image_url + the returned suggested_prompt to YOUR OWN vision\n# capability (you can fetch the URL and pass bytes to vision).\nupdate_media media_id=<id> alt_text=\"<what vision returned>\"\n```\n\nThe prompt is tuned for SEO-grade output: short (5-15 words), no \"image\nof / picture of\" filler, written in the site's content language,\ndecorative images return empty string. Run it sequentially on a\nlist_media batch and you can fix alt-text gaps across a whole site\nwithout burning your context on prompt design. The platform does NOT\nrun vision on your behalf — your model does, your usage.\n\n### 5. Embed in a page\n\n`read_page` the target, insert `<img>` in the right spot:\n\n```html\n<img src=\"<cdn_url>\"\n alt=\"<alt_text>\"\n style=\"width: 100%; height: auto; display: block; margin: 2rem 0;\" />\n```\n\nThen `update_page` with the new HTML. Or, if you're generating a hero\nfor a brand-new page, include the `<img>` directly in `create_page`'s\n`html_content`.\n\n## Pitfalls\n\n- **Always set `alt_text`.** Empty alt is bad for SEO + accessibility.\n Default to a one-sentence description of what's in the image.\n- **`<script>` etc. in SVGs.** The page sanitizer drops `<script>`\n inside SVG, so an icon set that includes script-based animations\n won't render correctly. Use static SVG or a JPG/PNG export.\n- **CSS background-image references aren't dedup'd.** If you set the\n same image as a CSS background on multiple pages, the alt-text +\n metadata are page-irrelevant. The sanitizer allows\n `background-image: url(...)` in inline styles, but think about\n whether an `<img>` is actually better.\n- **Source URL leakage.** If you generated the image from a prompt\n that contains internal info, don't bake that prompt into the\n filename. Use a descriptive but generic filename.\n\n## Format choice\n\n- **PNG** for logos, icons with hard edges, anything with text.\n- **JPG** for photos. Smaller file, better for big hero images.\n- **WebP** if the target audience runs modern browsers (95%+ in 2026).\n- **SVG** for icons + simple illustrations. Vector scales perfectly.\n- **PDF** is supported by `create_upload_url` for document downloads;\n link with `<a href>`, not `<img>`.\n\n## Check publication resolution (Core 0.2.37+)\n\nNative AVIF/WebP generation includes each smaller preset and the full original\nwidth, without upscaling or duplicate width descriptors. Check `currentSrc` and\nthe actual encoded dimensions on the published site; the CMS preview can use the\noriginal and hide an undersized derivative. Do not compensate with custom CSS\nor `sizes` alone. Existing static sites need republication for the versioned v2\nrecipe; unchanged subsequent builds reuse verified media. Explicit API variant\nworkflows must regenerate their variant metadata before rebuilding.\n",
15
15
  "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**Before bulk conversion:** read `tr-migration-evidence` with `read_skill`.\nComplete its source baseline and one verified prototype per active template.\nRead target content to protect edits, not to justify accidental target defaults.\nIts shared evidence record is required for visual acceptance.\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 Typeroll site exists with working header/footer.\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; decide preservation or retirement from their actual purpose.\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. Establish source geometry\nand validate native prototypes 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 verified native fields from the source capability map. Preserve appearance\nby default; do not substitute target defaults or corrective tenant CSS.\n\n### 4. Create pages as drafts\n\n```\ncreate_page title=\"Om oss\" slug=\"om-oss\"\n html_content=\"<cleaned, adapted HTML>\"\n content_mode=\"html\" 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:\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>\n```\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 → convert to CSS grid with inline `<img>` tags\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- Watch for table-based layouts (pre-2010 sites) — convert to CSS grid\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- **Analytics/tracking code.** If the source has GA4 or similar, don't\n copy it into pages. Set it via `update_site_settings scripts_head=\"...\"`.\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",
16
16
  "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**Before bulk conversion:** read `tr-migration-evidence` with `read_skill`.\nComplete its source baseline and one verified prototype per active template.\nRead target content to protect edits, not to justify accidental target defaults.\nIts shared evidence record is required for visual acceptance.\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\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",
17
17
  "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**Before bulk conversion:** read `tr-migration-evidence` with `read_skill`.\nComplete its source baseline and one verified prototype per active template.\nRead target content to protect edits, not to justify accidental target defaults.\nIts shared evidence record is required for visual acceptance.\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\nRequires an **org-scoped API key** (`create_site` needs it, and one key\nreaching every site is the whole point here). With MCP you pass `site_id`\nper call; over stdio you'll be re-pointing `TYPEROLL_SITE_ID` per site.\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\nThe blockers (media storage, hosting credentials) are **platform-level, not\nper-site** — if they fail for one site they fail for all ten, and finding out\nafter building three sites means redoing three sites' worth of image work.\nGet them fixed before Phase 1, and re-run the check per site once the sites\nexist (the warnings — verification URL, form email, design reference — are\nper-site).\n\nAsk explicitly:\n- Which site 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\nMark the obvious throwaways immediately, so the work list is real:\n\n```\nadd_migration_urls urls=[{ url: \"/wp-admin\", excluded: true }, { url: \"/tag/nyheter\", excluded: true }]\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\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's gone on purpose → `update_migration_url url_id=… excluded=true notes=\"Old campaign LP, signed off by <name>\"`\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- [ ] 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",
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.30';
11
+ export const VERSION = '0.45.31';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeroll/mcp-server",
3
- "version": "0.45.30",
3
+ "version": "0.45.31",
4
4
  "description": "Typeroll CMS MCP server – connect AI agents to the REST API to manage content and publish static websites.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -173,3 +173,13 @@ for a brand-new page, include the `<img>` directly in `create_page`'s
173
173
  - **SVG** for icons + simple illustrations. Vector scales perfectly.
174
174
  - **PDF** is supported by `create_upload_url` for document downloads;
175
175
  link with `<a href>`, not `<img>`.
176
+
177
+ ## Check publication resolution (Core 0.2.37+)
178
+
179
+ Native AVIF/WebP generation includes each smaller preset and the full original
180
+ width, without upscaling or duplicate width descriptors. Check `currentSrc` and
181
+ the actual encoded dimensions on the published site; the CMS preview can use the
182
+ original and hide an undersized derivative. Do not compensate with custom CSS
183
+ or `sizes` alone. Existing static sites need republication for the versioned v2
184
+ recipe; unchanged subsequent builds reuse verified media. Explicit API variant
185
+ workflows must regenerate their variant metadata before rebuilding.