Skip to content
BetaDocs

Bootstrap a new Typeroll site

This is the tr-new-site recipe exactly as the Typeroll MCP server gives it to agents through list_skills and read_skill. See Skills and recipes for how agents load recipes.

Core 0.2.106 / template capabilities 0.54.0 adds palette names in all colour fields and named typography/spacing steps. Check the connected capability version; on older renderers use var(--color-surface) rather than a bare name.

Set colours and font families in site settings, then use named styles and palette names in blocks. Do not write hex, rgb or font-family literals in block content. Prefer primary, secondary, accent, background, surface, text, text_light, and on_primary/on_secondary/on_accent for text on brand colours. Use surface_hover for a darker interactive surface (derived automatically unless explicitly configured). Existing semantic roles should be reused; a new visual shade is not automatically a new semantic role. For a derived shade use color-mix(in srgb, var(--color-surface) 92%, var(--color-primary)). For an additional role not in the native palette, define a named site style or a site CSS variable derived from existing palette variables, and document it.

Choose heading, body or mono for fonts. Font families belong only in the site’s fonts settings. Use type-size steps xs, sm, md, lg, xl, 2xl, 3xl; spacing none, xs, sm, md, lg, xl; line height tight, snug, normal, relaxed, loose. Presentation fields accept responsive maps of these names, for example font_size_px: {mobile: "md", desktop: "lg"}. Read the field schema: counts, widths and breakpoints are not spacing steps. Pixel values are explicit exceptions, not the default authoring method. Configure scales in design_scales, then reuse them in role styles and blocks.

Record deliberate exceptions in decisions.md, with source/block, reason and review criteria (for example white text on a photograph). Run lint_design_tokens before launch and after major design changes. It warns, never blocks launch. Use the starter’s scripts/typeroll/color_tokens.py for an exact-match dry run; review the plan before applying it in a dedicated Version. Equal hex values can mean different roles: never automatically choose between background and text. Verify a primary/surface change in a test Version across header, footer, cards, menus and buttons, then restore the test settings. Do not republish automatically.

The buffer model (draft writes). Every content write in this recipe (pages, blocks and partials) lands in an unsaved per-doc DRAFT — deploys and plain previews only see SAVED content. For recipe-style build work, pass save: true on write calls (the work is pre-approved by the task itself), or run commit_working_copy per doc before any trigger_deploy. Preview your drafts with include_working_copy: true.

Start here when the site already exists as a database record (created via the portal UI or API) but has no design, no header/footer, and no pages. The goal is to go from blank to a working 4-page site with correct brand identity in a single session.

Pages are built in block mode — the platform default. Blocks give structured, per-field editing, native full-bleed sections, responsive breakpoints, and templates. HTML mode is the secondary path for hand-crafted one-offs and migrated content (section at the end).

An optional agent-neutral workspace stores the brief, decisions and QA. It does not create a Site or replace CMS content. See the public Agent workspace guide. For compact MCP: search_tools, describe_tool, then the appropriate read/write/admin wrapper. Load only the relevant guide section and recipe. Discover site apps with read_app_documentation when needed; do not bundle private app guides locally.

No Site yet? Use hosted MCP with an Organization key and create_site, or create it in the portal. To start from an AI site plan instead of a blank Site, use create_site_and_plan, poll get_workflow and approve its structure review only with the user’s consent. That AI plan needs the Organization’s own portal AI key for new Sites; without it the call answers 403 ai_not_enabled and the site_planning workflow fails with failure_code: "ai_not_enabled". Then plan it yourself: create_site, propose the site name, tagline and page list (title, slug, purpose, target keywords) from the business description, get the user’s approval, and build with this recipe. Tell the user you planned it yourself; don’t retry the workflow. Read back its ID before editing. Use one explicit Site/Version for related operations. Larger changes to an existing site should use a branch.

Before importing content or a media library, require get_import_readiness to pass. Publishing accounts may remain unconfigured while authoring new drafts and previews.

  • @typeroll/mcp-server configured with a valid TYPEROLL_API_KEY.
  • The site exists (confirm with get_site).
  • You have the customer brief: company name, industry, 2–3 key brand colors, tone of voice, and a list of initial pages.
get_site
get_site_capabilities # template_capabilities_version — what this deployment supports
read_site_settings # see what (if anything) is already configured
list_pages # don't overwrite pages that already exist
list_partials # check if header/footer already have content
list_block_types # the per-site block palette — NEVER assume, always list

Do not choose a content mode or file a platform gap before these calls return. For every required visual/behavioral element, map it to an existing block or composition first. If the summary suggests a match, use read_block_type to inspect the exact schema. Only call something “missing” after checking capabilities, the complete palette, and custom block types.

Common requirements that are easy to misclassify:

Requirement Existing Typeroll primitive
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
Responsive icon/card grid core/grid + core/icon_box, or core/feature_grid; set responsive fields with set_block_responsive
Custom cards backed by Pages core/repeater / core/page_list with a site-authored item_compatible block type as item_block
Grouped Page listing core/repeater with group_by; array-valued fields place an item in every matching group
Breadcrumbs in a page template template/page_breadcrumbs; page and item routes supply a server-rendered trail
Shared article typography Set font_size, line_height and paragraph_spacing on template_content_slot; blank values retain block defaults (Core 0.2.2+)
Generated heading index core/table_of_contents; choose heading levels; links always come from the current Page headings
Previous/next Page links template/page_navigation; defaults to content type sort order and can bind explicit neighbor fields
Download CTA that disappears without a file template/show_if around a context-bound core/button; a dedicated download block is only editor convenience
Sticky/custom header and multi-column footer Block-mode header/footer partials plus layout blocks, or one reusable custom block type
Cookie notice settings.cookie_consent, not a page block
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
One-off HTML + JavaScript embed core/embed; reusable widgets use a custom block type with script
CTA/button variants core/cta and core/button, styled from site tokens or a narrowly scoped class
Two-column image/text core/media_card, core/feature_row, or core/columns
Figures, captions and tables core/prose / richtext; the sanitizer preserves these semantic tags
Full-width block page without a content shell Native block mode; top-level core/section is unconstrained
Brand colors and typography Site colors/fonts tokens consumed by core blocks

Real gaps should say what was checked and why the nearest primitive is not enough. “I did not see a dedicated block name” is not evidence by itself.

One update_site_settings call with every field you know:

{
"site_name": "Acme Studio",
"tagline": "Short, punchy tagline",
"language": "sv",
"trailing_slash": "always",
"colors": {
"primary": "#1a1a2e",
"secondary": "#16213e",
"accent": "#e94560",
"background": "#f5f5f5",
"surface": "#ffffff",
"text": "#1a1a2e",
"text_light": "#6b7280"
},
"fonts": { "heading": "Playfair Display", "body": "Inter", "size_base": 16 },
"contact": { "email": "hej@acme.se", "phone": "+46 8 123 456" },
"social": { "instagram": "https://instagram.com/acme" }
}

Read it back with read_site_settings. Google Fonts names are case-sensitive display names (“Plus Jakarta Sans”, not “plus jakarta”). Fonts are self-hosted on the site by default; a name Google does not serve falls back silently, so verify fonts on the published address (tr-brand Step 7).

Site icons are part of brand setup — upload favicon (32–64px), a 180×180 apple touch icon, and a 192×192 app icon; set favicon, apple_touch_icon, and icon_192. No icon assets? Derive a proposal (see tr-brand). Also set settings.logo to the uploaded brand mark — it feeds OG/schema even if the header uses a different lockup.

Ask the user for these once, early in the project. Read read_site_settings first and ask only for what is missing. Each item is optional. Record a skipped item in decisions.md, so the launch check in the last step holds no surprises.

  • Default share image. Ask the user to choose or create the image that social networks and chat apps show when a page has no image of its own: upload one (create_upload_url or upload_media_from_url), pick an existing one (list_media), or create one with tr-imagegen. Use a 1200×630 px PNG or JPEG and set it as default_og_image with update_site_settings. Never generate a title card from the page title instead. The user may skip this.
  • Organization. The business name, logo and official profile URLs (LinkedIn, Instagram, GitHub and similar). Set organization with same_as; it becomes the home page’s Organization schema.

Set iframe_allowed_hosts when customer content embeds a provider outside the built-in YouTube/Vimeo/Google Maps/Calendly set. Values are exact domain hostnames, never URLs or wildcards. Read the setting back before deciding that an iframe cannot be represented.

Set the site’s look as named styles before building any page:

  1. list_styles. New sites already have the standard set (body, H1–H6, link, lead, eyebrow, small, quote, buttons, section). If roles are missing, apply_standard_styles.
  2. Adjust them to the brand with update_style: the font, the type scale and spacing for base (phones) and at.tablet/at.desktop, and colours from palette tokens. Keep text readable: contrast is checked with APCA and WCAG 2; text that fails both is refused, and text below its APCA level comes back as a warning.
  3. Add a named style for every look the design repeats: a card title, a price note, a highlighted section, a CTA button variant. Use the site’s language for names.

Pages then choose styles with each block’s style_id field. A per-block font size, colour or class is only for something clearly unique.

To change a sub-element a block styles itself (the underline on formatted heading links, a hero’s heading or buttons), use its parts: read_block_type lists them. A style with parts: ["core/rich_heading.link"] applies to that part on every block; data.part_styles on one block replaces it there.

Start from the native preset — don’t hand-roll navigation. Read tr-header-footer and use its template/site_logo + core/navigation_menu with core/navigation_links slots composition. The server-rendered landmark, current-page state, no-JS links, mobile disclosure, focus treatment, and responsive behavior are Core contracts rather than tenant CSS/JavaScript.

Stage the complete tree with update_partial partial_id="header" patch={blocks:[...]} save=true, then call set_partial_mode partial_id="header" to="blocks". Repeat for the footer and read both partials back. The inactive HTML representation is retained for rollback; changing content_mode through ordinary PATCH/PUT is rejected intentionally.

Use HTML mode only when preserving legacy authored markup that cannot yet be represented natively. Partials receive the same {{site.*}} render context as page blocks. Keep the first section and header backgrounds intentional; do not add a decorative border merely to compensate for mismatched spacing.

create_page with the whole tree in one call. Omit block ids — the platform assigns them (blk_…); you read them back for later update_block calls.

create_page title="Start" slug="" status="draft" content_mode="blocks" blocks=[...]

A proven landing-page skeleton (every top-level block is a core/section — sections are natively full-bleed: the background runs edge-to-edge, content is constrained by the section’s own inner container via the width field. NEVER use 100vw negative-margin hacks. Anchor ids and custom classes via style_overrides are safe on sections from template_capabilities_version ≥ 0.15.3; on 0.14.x–0.15.2 they wrap the section in a div and silently break full-bleed — there, put the anchor on a block inside the section instead):

[
{ "type": "core/section", "data": { "background": "surface", "padding_y": "lg" }, "children": [
{ "type": "core/media_card", "data": {
"image": "MEDIA_URL", "image_alt": "…", "image_side": "right",
"heading": "Huvudrubriken", "heading_level": "h2",
"text": "<p>Ingress …</p>",
"button_label": "Kontakta oss", "button_url": "/#kontakt"
} }
] },
{ "type": "core/section", "data": { "padding_y": "lg" }, "children": [
{ "type": "core/heading", "data": { "text": "Så funkar det", "level": "h2", "align": "center" } },
{ "type": "core/grid", "data": { "cols": { "mobile": 1, "tablet": 2, "desktop": 3 }, "gap": "lg" }, "children": [
{ "type": "core/step_card", "data": { "number": "1", "title": "…", "text": "<p>…</p>" } },
{ "type": "core/step_card", "data": { "number": "2", "title": "…", "text": "<p>…</p>" } },
{ "type": "core/step_card", "data": { "number": "3", "title": "…", "text": "<p>…</p>" } }
] }
] },
{ "type": "core/cta", "data": {
"heading": "Redo att börja?",
"primary_label": "Kontakta oss", "primary_url": "/kontakt"
} }
]

Block-palette guidance (verify against list_block_types — the source of truth):

  • Hero: core/media_card inside a white section (image beside copy), or core/hero (eyebrow/heading/subheading + primary_*/secondary_* buttons — rendered server-side; layout: split-right puts the image beside the text). For a plain text hero: section + heading + prose + button.
  • Alternating image+text rows (“zig-zag” feature/step lists): core/feature_row (template_capabilities_version ≥ 0.29.0) — balanced halves that hug the center gutter so wide screens never open dead air, natural-aspect image, image_side: left|right per row, mobile stack order as a field. Prefer it over hand-building core/columns with an unbalanced ratio + width-capped text (the classic dead-air trap). Use core/media_card instead when you want a boxed card with a filled/cropped image.
  • core/heading decouples level (h1–h6, semantics) from size (visual) — exactly one level: h1 per page.
  • Images: core/image with an uploaded media URL. The build pipeline emits responsive output from prepared media. Upload through a signed direct grant, finalize it, and use returned IDs/URLs. Follow media preparation status; do not routinely regenerate unchanged images or use legacy maintenance tools. Use the radius field for rounded corners.
  • Repeaters/listings: core/page_list, gallery, feature_grid etc. — alias blocks over core/repeater. Use these for Pages of a content type instead of hand-writing listing markup. The base repeater also supports group_by, group ordering/headings, multi-valued membership, filters, custom item_block, and item_overrides.
  • Long-form navigation: core/table_of_contents builds an anchor list from page headings. Page templates use template/page_navigation for deterministic previous/next links.
  • Forms: create_form, then place core/form with its form_id. HTML-mode pages use <x-form id="…" />; both paths render the same signed shell and runtime. See tr-forms.
  • core/html is the escape hatch for the genuinely unique thing — not a default. If you reach for it more than once or twice per page, note why (that’s block-library feedback).

Slot containers (core/columns, core/tabs): populate them either by passing the whole tree inline (block={ type: 'core/columns', slots: [[…],[…]] }) or incrementally with add_block parent_id=&lt;columns-id> slot_index=0|1. Requires template_capabilities_version ≥ 0.15.2 — on older sites use core/grid (children flow into columns) instead.

Partial last rows: keep last_row: 'start' on core/grid. Incomplete rows stay left-aligned, preserving the reading edge. Center only after an explicit owner design request, never as an automatic fix. Do not widen one peer card or invent six-column CSS to fill a gap.

Icons (template_capabilities_version ≥ 0.16.0): type: 'icon' fields on core/icon, core/icon_box, and core/step_card render inline SVG when the value is a name from get_site_capabilities → core_icon_names (a curated Lucide subset — check, star, shield-check, mail, arrow-right, truck, chart-line, …). Any other value (emoji, plain text) renders as text, so emoji stand-ins keep working. Icons inherit size from font-size and color from currentColor/the block’s color field. On older sites icons don’t render — use emoji or CSS markers.

Editor schemas (template_capabilities_version ≥ 0.39.0): labelled enum options, newline-edited string lists, nested object/repeating-array fields, and internal-page pickers for URL fields are first-class. Do not flatten an Extension’s props_schema merely to make it editable.

For an Extension placed in an HTML header or footer, require supports_extension_html_partial_directive: true. The broader supports_extension_html_directive flag covers HTML page bodies and is not proof that an older static build expands partial directives.

For a cross-page Extension flow, require both supports_extension_site_navigation and supports_extension_storage. Use context.site.navigate("/path/") and installation-scoped context.storage.session rather than direct root-path navigation, Web Storage, or personal data in query parameters. This keeps navigation inside the current preview and preserves state without exposing it through URLs or referrers.

To prefill an Extension component from a native core/navigation_form banner on the previous page, require supports_extension_page_handoff, check that the installation grants page_handoff:read, and set the component block’s page_handoff_key to the banner’s handoff_key; the banner’s destination must be the component’s page. The component reads the values with context.handoff.read(). Do not add query parameters for this.

Known limitations (honest list — don’t fight them):

  • core/tabs label icons don’t render (the tab strip is built client-side without the icon pipeline). Text labels only.

Theming: block primitives render neutral. Brand color/typography comes from settings (step 2) and named styles (step 2b). For page-specific polish (e.g. a colored card treatment) that no style covers, give the block a custom_class and write the rule in the page’s custom_css. Never put a <style> in a core/html block or target [data-bid]/[data-block].

Same pattern: create_page with content_mode: "blocks" and a section tree. Standard set: Om oss, Tjänster, Kontakt — or what the brief says. Default new pages to status: "draft"; publish after review.

Sections that repeat across pages: a call to action or contact strip that must stay identical becomes a global block (make_block_global, then core/global_block on the other pages). A section shape you will reuse with different content (a pricing row, a feature trio) becomes a block template (save_block_template, then insert_block_template).

5b. If the legacy site is still live, scrape canonical content

Section titled “5b. If the legacy site is still live, scrape canonical content”

For pages with canonical text (privacy policy, terms, about), WebFetch the live page and carry the text verbatim — don’t rewrite legal copy from memory. Convert to prose blocks (or one core/html for complex legacy markup).

get_preview_link # signed URL for browser review
get_page_preview page_id="home" # rendered HTML for structural checks

Self-review the visuals before you call it done — appearance AND readability, not just structure. Screenshot the deployed/preview site at desktop (~1440px) and mobile (~390px) and look: logo FULLY VISIBLE (not clipped by a header’s overflow:hidden) + legible + brand-compliant against its actual background — screenshot the header IN CONTEXT, not the logo element in isolation (an element shot hides layout clipping); a light wordmark must not sit bare on a light surface. Text contrast everywhere, no horizontal scroll or mid-word breaks, every image rendered, mobile layout actually collapsed. “No overflow + copy present” is not a design review — never report a build as done/perfect off structural metrics alone.

Share the preview link. Iterate on feedback with update_block / add_block / move_block — that’s the point of block mode: surgical edits, not full-page rewrites.

A site is not finished until this step has run. Call read_site_readiness before the production publication and again after it. For every item with the status warning or suggestion, either fix it with the tool its fix names, or record in decisions.md (or the starter’s qa/ folder) why it stays open. Tell the user which items remain. A missing default share image is only a suggestion; leave it if the user chose to skip it.

The report warns and never blocks a publication. A clean report is not approval to publish: publish only when the user has approved it.

When the user approves, verify read_publishing_readiness and save the intended Version before deploying. A published CMS status is not a live deployment.

read_site_readiness
trigger_deploy
get_deploy_status job_id=<id>
read_site_readiness

Run read_site_readiness again after the publication. It reads saved content and settings, so confirm the rest on the live site: the AI crawler zone check needs the site’s website host, and a Search Console property verified by DNS is not visible to Typeroll. The launch checklist explains every item.

For migrated legacy pages or a hand-crafted one-off, content_mode: "html" still works. Rules that apply there (and only there):

  • Wrap the body in a page-scope <article class="my-page"> and prefix selectors with it — the .page-content shell sets width/padding at normal specificity.
  • HTML-mode bodies are container-constrained; full-bleed requires the negative-margin escape (margin-left: calc(50% - 50vw); width: 100vw) — never combine with overflow-x: clip on a wrapper.
  • set_page_mode flips a page between modes and does not convert HTML. Typeroll has no HTML-to-blocks conversion: to move a page to blocks, build its content with blocks.
  • Don’t create pages that already exist — list_pages first; use update_page if the slug is taken.
  • update_page takes a patch object, not flat fields.
  • Don’t hardcode block field names from memory — schemas are per-site (list_block_types/read_block_type).
  • One level: h1 per page (core/heading) — SEO + screen readers.
  • Don’t simplify data during import — preserve Swedish characters in labels (affärsutveckling, not the ASCII-folded slug), keep titles verbatim; slugs are derived for URLs only.
  • Minimal JS. Inline <script> in page content is stripped by the sanitizer. One-off interactivity uses core/embed; reusable interactivity uses a block-type script; site-wide code belongs in scripts_body_end.

When 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.

Core 0.2.114 / MCP 0.45.85 / capabilities 0.55.0 adds responsive scales. Check the connected versions before using this contract. New sites use render version 10. For an existing site, test a Version first; upgrading rendering and replacing existing roles are separate, deliberate choices.

Use fonts.size_base (default 16), typography.ratio_mobile (1.2), typography.ratio_desktop (1.25), typography.rhythm (1), and typography.line_length (65 ch). Set the controls once, then use named type, spacing and line-height steps. apply_standard_styles uses those steps; overwrite: true resets existing roles, so review their current values first. Explicit sizes and breakpoint overrides still win and are not silently migrated.

Heading role margins belong to editorial flow. Component titles use their own padding/gap, and opening headings in containers/sections add no top margin. Use role-linked core/heading size auto or theme; explicit hero sizes remain deliberate exceptions. Do not compensate with per-card negative margins. Compare 375, 768 and 1280px in Site settings → Design and in a full site preview. After a rendering upgrade/rebuild, recheck cards, boxed headings, leading text and webfonts. Boxes should get their outer air from padding. Keep exceptions in decisions.md, and run lint_design_tokens before launch.