# Instapage page-building guide for AI

Version 2026-11-06

## About this guide

This guide is for the AI assistant. It lists how Instapage page JSON behaves in practice, based on pages built through the MCP and checked live and in the editor.

- **What comes first:** `page_json_skill_get` is Instapage's official guide inside the MCP, and `page_json_schema_get` is the schema. Read the skill at the start of every task.
- **What this guide adds:** details the skill doesn't cover yet: wrappers, spacing, forms, styling pitfalls, images, SEO and accessibility fields, Collections and how to check your work.
- **When sources disagree:** the MCP's own guide and the schema win over this guide. A test you run wins over both. Tell the user what you found and suggest an update.
- **Instapage Help Center:** its article pages can block automated visits, so read it through its JSON API. Search with `https://help.instapage.com/api/v2/help_center/articles/search.json?query=<words>&locale=en-us`, then open an article with `https://help.instapage.com/api/v2/help_center/en-us/articles/<id>.json`. The article text is HTML in the `body` field.

## Workflow for both task types

Both task types follow the same steps. Steps 3 and 4 differ between a new page and an import.

1. **Read the task.** Read the brief and the user's documents that matter. For a client's page, use that client's brand documents, never the user's own; for an import with none, use only the source. If an essential is missing (the source or brief, link targets, conversions, the images, the mobile rule), ask once, in one message.
2. **Check the MCP.** Call `page_json_skill_get`, and note any tool or element that is new to you.
3. **Plan the page.**
   - **New page:** turn the goal and the user's documents into sections, for example hero, proof, benefits, offer, FAQ and final call to action. Take facts only from the documents. Mark anything missing as a placeholder.
   - **Import:** analyze the source. A live URL or HTML gives exact text, colors and sizes, and so does a Figma design read through a Figma connector. PNG exports or screenshots give colors, but sizes are estimates, so say so. A PDF gives exact text but unreliable colors and fonts, so ask for PNG exports too, or instead if you can't read PDFs. Map each section to a pattern.
4. **Get the plan approved** when the user's settings call for it (by default for new pages only): all copy, each image with its alt text and source, the mobile changes, the SEO texts and any open questions, plus a quick HTML mockup if the app can show one. Mock up only layouts native elements can build, then build from the approved mockup as you would import a design.
5. **Decide the layout.** Use Fluid Grid by default, and a canvas block only for overlapping layers. For every section, decide what stacks, reorders or hides on mobile.
6. **Find the workspace, folder and images.** Use `workspaces_list` and `groups_list`, and create the folder with `group_create` if it's missing. For images, see Images below.
7. **Build the JSON.** Write a desktop variation and a full mobile copy, using native elements, tracked conversion links, alt texts and aria labels, and the fonts in use. With code, generate the JSON with a script. Saving validates it (see Saving below).
8. **Save.** Call `page_snapshot_save`, then `page_update` to move the page into the folder.
9. **Check.** Check the real render (see the last section): on a new pagedemo.co address if test publishing is allowed, otherwise on the builder's preview link. Fix problems by saving a corrected page as a new version.
10. **Report and clean up.** List the links, which earlier version the page replaces, any differences from the source, brief or plan, the placeholders, the SEO and social settings to paste, what you couldn't check, and what the user must finish in the app. End with one page under the agreed name: unpublish your superseded test versions and ask once whether to delete your drafts.

## Fluid Grid blocks: the JSON shape

A page is an array of two variations: desktop (`id 1`) and a full mobile copy (`id 2`, `responsive_mode: "mobile"`) with the same blocks, elements and ids. Per-device values, such as sizes, spacing, alignment and visibility, live in each entry. Text is the exception: only the desktop entry's `contents.html` renders, on every device, so keep words together with `&nbsp;` rather than adding line breaks for one device.

- **Block:** `blockOrigin: "Responsive"`, a `minHeight`, and a `responsive` object that is identical in both entries. Block background color, image and overlay work as on canvas blocks, and the color may be a gradient object.
- **`responsive.layout`:** `{id: "1", columns, rows, flexbox, cells, viewports: {desktop: {gaps: {row, column}, padding}, mobile: {...}}}`.
    - Columns and rows go up to 24.
    - `rows` must cover the most rows used on either device.
    - The mobile viewport adds `"__edited-gaps": true` and `"__edited-padding": true`.
- **Layout cell:** `{id: <uuid>, createdOn: "desktop", viewports: {desktop: {...}, mobile: {...}}}`. Each viewport has:
    - `columnStart`, `columnEnd`, `rowStart` and `rowEnd`: grid lines, 1-based, end exclusive;
    - `visible`, `flexbox` (direction, justify, align) and `padding`;
    - `background {image, position, color, repeat, size, overlayColor, overlayColorTransparency, overlayBorderRadius}`: `overlayColor` is a string (`""` for none), `overlayColorTransparency` a number from 0 to 100, and `overlayBorderRadius` a string such as `"24px"`;
    - `border {color, style, width, radius}`, where `width` and `radius` are numbers, so one radius covers all four corners; and `elementSettings.spaceBetween`.
  The mobile viewport adds `"__edited-<key>": true` for each key it sets. A card is a cell with a background, border and radius; a gradient object works as the cell color.
- **`responsive.cells[]`:** `{id: <same uuid>, viewports: {desktop: {order: [widget ids]}, mobile: {order: [...]}}}`. List only widget ids, never wrapper ids.
- **Widget:**
    - Layout keys: `is_responsive: true`, `cell: "cell-<uuid>"`, `top` and `left: "auto"`.
    - Size: `width` is `"auto"`, `"100%"`, a percentage or px; `height` is `"auto"` or px.
    - `margin {top, right, bottom, left}`, and `parentId`: the wrapper id or null, mirrored in `contents.parentId`.
    - Ids: `id` equals `wid`, taken from the variation's `max_widget_id` counter, which wrappers share.
- **Text:** set `font_weight` explicitly on every headline: the source's weight for an import, and 700–800 for a new page unless the brand says otherwise. The default 400 makes `h3` headlines look thin. The headline level comes from `headline_type` (`h1`–`h6`); use one `h1` per page.
- **Images:** set `image_width` and `image_height` in `contents` and the widget size in px. Use Instapage storage URLs (`//storage.googleapis.com/instapage-user-media/...`).

## Spacing and the mobile layout

Every cell and every wrapper inherits the block's grid gap (`gap: inherit`), so the block gaps also space the elements inside them. Vertical stacks use the row gap; horizontal rows use the column gap. A cell's `spaceBetween` adds margins on top of that gap.

- **Set spacing with the block gaps.** Leave cell `spaceBetween` at `"initial"` and set the gaps to the spacing you want between elements; 14–16px works well. The alternative is a gap of 0 with element margins. Never combine the two, or the spacing doubles.
- **Separate rows with padding.** For more room between a heading cell and the cards below it, give the heading cell bottom padding, for example 24px, or give one element a margin.
- **Watch zero gaps.** A mobile column gap of 0 makes elements in a horizontal wrapper touch, so give that wrapper a mobile `spaceBetween`.
- **Empty rows:** `rows` is shared by both devices. When mobile stacks into more rows than desktop, desktop gets empty rows at the bottom of the block, and each still adds a row gap, so trim that device's bottom padding.

Rendering facts that matter for the mobile layout:

- **Breakpoint:** the CSS is mobile-first, and desktop rules apply from 768px.
- **Widths:** the inner width is capped at 1200px (75rem). From 400 to 767px the mobile layout stretches, so test 360, 400 and 767px. Sizes render in rem, and below 400px the root font size is 4vw, so the whole mobile layout shrinks with the screen (to 90% at 360px). Plan mobile at 400px.
- **Rearranging cells:** give each cell its own mobile grid lines. A two-column split becomes two rows, a four-column footer becomes a 2×2 grid, and a pricing row can put the highlighted plan first.
- **Hiding:** `visible: false` hides a cell on one device, and `is_hidden_in_desktop` / `is_hidden_in_mobile` hide one element.

## Wrappers

Wrappers group elements inside a cell with their own direction, alignment, padding, background and border. They're the native way to build pills, button rows, nav bars, number badges, author rows and stat tiles.

- **Where they live:** the variation's `groups` array.
- **Shape:**
    - `{id, parentId, zIndex, locked: false, isHiddenOnMobile, isHiddenOnDesktop, prevState: {}, groupType: "responsive"}`;
    - `viewports.{desktop, mobile}: {width, alignSelf, padding, flexbox {flex-direction, justify-content, align-items, flex-wrap}, elementSettings.spaceBetween, background {...}, border {color, style, width, radius}}`, with border `width` and `radius` as numbers.
  The `id` comes from the widget counter. `parentId` nests one wrapper in another.
- **Children:** each element in a wrapper carries the wrapper's id as `parentId`. The cell's `order` still lists the elements, not the wrapper.
- **Wrapping:** a wrapper with `flex-wrap: wrap` and width `auto` collapses into a single column. Use `nowrap`, or set the width to `100%`.
- **Fixed sizes:** a fixed-size item in a row, such as an icon or a round box, shrinks unless it sits in its own fixed-width wrapper and the text beside it has a width, for example 80%.
- **Tile grids:** use an outer wrapper with width 100%, row direction, `space-between`, `wrap` and `spaceBetween` 0. Make the tiles 22% wide on desktop and 46% on mobile. This gives four across and 2×2, and still fits at 768px.
- **Badges:** for a round number badge, use a wrapper 44px wide with 10px vertical padding and radius 999, holding one headline with a line height of 1.333. Give the text beside it a width, for example 80%: without one, a 44px badge shrank to 26px at 360px.

## Styling: page CSS, fonts and buttons

Don't put `customStyles` in element JSON. It renders on the live page, but it causes three problems:

1. The editor shows buttons and text that carry customStyles without their own design: transparent background, default gray text, wrong size.
2. Saving the page in the editor resets images that carry customStyles to their natural pixel size, which can make the page scroll sideways.
3. Mobile customStyles are ignored when the desktop entry has none.

For the few effects Instapage has no setting for, add one `<style>` block to the page's head custom code (`custom_code.html.head`, base64) and target `#element-<id>`. Button links render as `.btn` and images as `img` inside that element. Keep a list of element ids in your build so the selectors stay right.

- **Full-width buttons:** there's no native setting, because the button link renders as a table cell. Give the widget width `"100%"` and add `#element-<id> .btn { display: block; width: 100%; }`. For mobile only, wrap the rule in `@media (max-width: 767px)`.
- **Button borders:** use hex colors. A semi-transparent (rgba) border renders live but not in the editor.
- **Rounded corners, shadows and rotation on images:** use page CSS, for example `#element-<id> img { border-radius: 24px; }`.
- **Fonts:** fonts set on elements load from Google Fonts on the live page. Also list each one in the variation's `external_fonts.Google`, copying an entry from an export (`family`, `style`, `source`, `weights` such as `{"Bold": 700}`, `name`, `label`, `service`, `disabled`, `slugified_family`); without it, the editor shows a fallback font. Fonts used only inside text spans don't load, so add a Google Fonts `<link>` to the head custom code.
- **Brand fonts that aren't Google fonts:** the MCP can't set custom fonts (added under Assets → Fonts, on the Convert plan) or Adobe Fonts, because `external_fonts.Hosted` must stay empty. Use the closest Google font, name it in the plan, and tell the user they can switch to their own font in the editor. Use `@font-face` in the head code only if the user hosts web font files licensed for it, and warn that the editor then shows a fallback font.
- **Inline text styles:** spans with color, letter-spacing or font-style work. Write `font-family` without quotes or with `&quot;`; single quotes are dropped.

## Forms and multistep forms

Use only the field types the editor creates. Any other type passes schema validation but breaks the editor, and the published page returns 404.

| Field type | Use it for | Notes |
| --- | --- | --- |
| `text` | Name, email, website, company and other single lines | Set `subtype` (`firstName`, `lastName`, `email`, `website`, ...). Email adds `has_email_validation: true` |
| `textarea` | Longer messages |  |
| `phone` | Phone numbers | `default_country_code`, `excluded_countries`, `excluded_numbers` |
| `hidden` | Tracking values | A value such as `[utm_source]` is filled from the page URL |
| `select`, `radio`, `checkbox` | Choices | Options are `elements: [{name, id}]` |
| `terms_checkbox` | Consent | `display_text`; never pre-ticked |
| `date` | Dates | `format`, `days_of_week`, `min`, `max` |

- **Form button:** it lives in `contents.button` with its own `wid` and `id` from the counter, plus `cell`, `is_responsive: true` and `width: "100%"`.
- **Submission:** use `submission_type: "url"` with `submission_url` to redirect, or `"page"` with a base64 `thank_you_message`. Field names must be unique within the form.
- **Multistep form** (type `"multistep-form"`):
    - The parent holds the submission settings, the thank-you message, and `ga_event: {enabled: false, event_name: null, parameters: []}`; a null `ga_event` fails the schema.
    - It also holds `multistep_navigation: {arrows: null | {position, color, disabled_color}, dots: null | {position, selected_color, unselected_color, disabled_color}}` and `steps_data`, a list of form widgets.
    - Each step is a full `form` widget with its own `wid`/`id`, `name: "Step 1"`, `width: "100%"`, `multistep_form_id` set to the parent's id, `submission_type: null`, and its button text in `contents.text` (for example "Next").
    - Only the parent goes in the cell's order.
    - A lead is saved only after the last step is completed.

## Popups, sticky bars and other elements

- **Click-to-open popup:** a button link of type `"popup"` that carries its own popup block (`block: {width, height}`) and canvas widgets, with separate desktop and mobile sizes.
- **Page popup:** `page_popup: {enabled: true, settings: {trigger: "exit" | "time" | "scroll", delay, scrollDepth, visibleOn: "everywhere"}, block: {width, height}, widgets: [...]}`. Its widgets are canvas elements with `block: "popup-page-popup"`, and it shows once per visit.
- **Sticky header:** `sticky_bar.header.enabled` makes the first block sticky.
- **Sticky mobile button:** `sticky_bar.footer` with `visibleOn: "onlyOnMobile"` turns the last block into a bottom bar. Give that block desktop height 0 and hide its button on desktop.
- **Accordion:** `contents` holds `items`, `order`, `contentFormat: "html"`, `settings` and `customStyles: null`. In `settings`, `borderRadius` is an object of numbers (`topLeft`, `topRight`, `bottomLeft`, `bottomRight`) plus `consistent: true`, `spacing.vertical` is a number, and `background.color` is a color string. `contents.parentId` is optional. Copy the shape from an editor export.
- **Countdown timer:** include `width 600, height 150, min_height 60, min_width 240, min_height_for_labels 90, min_width_for_labels 320, labels_size 16, timer_size 48`. The schema requires these even in fluid cells.
- **Video:** give the YouTube link in `contents.link` with `host: "youtube"`. With width `auto`, the video fills its cell.
- **HTML element:** base64 code in `contents.contents` with `base64: true`. Use it only for embeds Instapage has no element for, such as a data table.
- **Conversion tracking:** set `track_conversion: true` on the link and list every tracked href in the variation's `conversions.links`. Otherwise the editor turns tracking off. Include links inside popups. Links that only scroll to a form (`#…`) aren't conversions, so don't track them: the form submission counts.

## Canvas blocks

Use a canvas block only when elements must overlap: layered artwork, text on a shape, circles, or the carousel. A page can mix canvas and Fluid Grid blocks. Don't fake an overlap in Fluid Grid with negative margins or `zIndex`: the editor may stack it differently from the live page.

- **Coordinates:** every element has absolute `left`, `top`, `width` and `height` in px, separately in the desktop and mobile entries. The block has a fixed height per device.
- **Desktop bounds:** the canvas is 960px with guides up to 1200px, so keep `left` at -120 or more and `left + width` at 1080 or less.
- **Mobile bounds:** the mobile canvas is 400px, so keep every element between 0 and 400. Check hidden elements and the full width of text boxes too; anything outside makes the page scroll sideways.
- **Mapping a design:** for a 1440px desktop design, the canvas x is the design x minus 240. For a 390px mobile design, multiply x by 400/390.
- **Editor quirks:**
    - The editor copies the desktop `border_radius` of buttons and boxes to mobile.
    - It ignores padding on canvas buttons and turns a 1px button border into 2px.
    - Transparent button text renders black.
- **Text:** `<font color>` accepts hex only, so use a span for rgba colors. The variation's font styles need `font-weight` as a string, such as `"400"`.

## Collections

A Collection is one template page plus rows of placeholder values; each row becomes its own page with its own URL. The user creates the collection in the app. You can prepare the source page and fill the rows.

1. **Prepare the source page.** Put text placeholders in double brackets in headlines, paragraphs and buttons, for example `[[City]]`. To make an image replaceable, set `contents.isPlaceholderOn: true` and `placeholderName: "Hero image"` on it.
2. **The user creates the collection** in the app (**Collections → Create collection**) from that page. The source page must be live on a custom domain or pagedemo.co, with no running experiment.
3. **Read the placeholders** with `collection_get`. Their names come back in capitals: `CITY`, `HERO IMAGE`.
4. **Add pages** with `collection_page_add`. Text values are `{name: "CITY", type: "text", valueObject: {textValue}}`; images are `{name: "HERO IMAGE", type: "image", valueObject: {mediaIndexId}}`, with the id from `asset_images_list`. An optional `urlSuffix` sets the page's address.
5. **Publish** with `collection_page_publish`, after asking. The page can return 404 for two to three minutes after the tool reports it published.

Limits:

- The Default Page's values can be set only in the app.
- Stored values can't be read back through the MCP.
- A replacement image is stretched to the original image's box, so use the same proportions.

## Images

Every image on a page must be in the workspace's Image Manager. The MCP adds an image only from a public HTTPS link; it can't take file bytes.

- **Folders:** `asset_folders_list` shows them. `upload` (Uploaded) and custom folders accept uploads. `free` is read-only and holds 100 Instapage background images named `background-1` to `background-100`.
- **Finding images:** `asset_images_list` returns up to 100 images per call, one folder at a time. Put the protocol-relative `uri` in `contents.image` and the image `id` in `contents.mediaindex`.
- **Uploading from a link:** `asset_image_upload` needs a direct HTTPS image URL with no redirect and no login, up to 10 MiB. The file name becomes the URL's last path segment. The result has no image URL, so look the new image up in that folder with `asset_images_list`.
- **No link:** with browser control, upload the file through the Image Manager (Assets → Images in the dashboard) or the editor's image dialog, using its file input. If the upload fails, the file may be in a folder your browser tool can't read, so move it to your working folder. Without browser control, give the user the files with their names and the folder, and wait.
- **Limits:** WEBP, PNG, JPEG, GIF or SVG, at most 4000 × 4000 px, and up to 10 MiB through `asset_image_upload` or 30 MB through the Image Manager. The Image Manager rejects an SVG whose first line isn't an XML declaration (`<?xml version="1.0" encoding="UTF-8"?>`).
- **Making artwork:** draw icons, patterns and simple illustrations as SVG, or render them to PNG at 2× with code. Keep text out of images: text stays in native elements.
- **Cropping:** when the design crops a photo to a frame, such as a card or a circle, make a cropped copy at 2× the frame size and upload that.
- **In the JSON:** set `contents.image`, `image_width` and `image_height` (px strings), the widget size in px, and `contents.alt`. Images that carry meaning belong in image elements, because cell and block backgrounds have no alt text.

## SEO, schema and accessibility

The SEO settings aren't in the page JSON or in any MCP tool, so a page built through the MCP publishes with its internal name as the browser title and no meta description.

- **Where they live:** in the dashboard, click the page. **SEO** holds Show in search results, the page title, canonical link URL, keywords, meta description (its counter allows 320 characters), favicon and **Schema markup** (Add schema or Generate schema). **Social Media** holds the title, description and site name for Facebook and LinkedIn, the title, description (up to 280 characters) and card type for X, and the sharing image.
- **What to prepare:** a title of up to 60 characters; a meta description of up to 160, because search results cut longer ones; keywords only from the user's research; a favicon only if the brand has a square icon (PNG or JPG in multiples of 48 px); the social texts, including the X title and description; and a 1200 × 627 px image, which is uploaded in the Social Media panel itself, not the Image Manager. Leave the canonical URL empty unless the page duplicates another page.
- **Schema:** write JSON-LD for the Schema markup box as one plain JSON object, without `<script>` tags, as the box asks; it adds the schema to the page head. The box rejects an array, so put several types in one object's `@graph`: `{"@context": "https://schema.org", "@graph": [...]}`. Pick the type that fits, such as Organization, Product with an Offer, Service, Event, Course, LocalBusiness, SoftwareApplication, or FAQPage when the FAQ is on the page. Use only facts shown on the page, no ratings without real reviews on it, and absolute https URLs. For a Collection template, add only schema that is true for every row.
- **Entering them:** if you can control the user's browser with the Instapage app open, ask whether to enter them yourself or paste them in the chat; otherwise paste them. In the chat, group them by panel, one field per line. In the app, work only on pages you created in this task, and save each panel. If the app is in another workspace, `app.instapage.com/dashboard2/dashboard/<workspace id>` switches it; switch it back when you're done. Saved changes reach the live page when it is next published or updated (UPDATE in the editor).
- **Search visibility:** keep `hide_search_engines: true` in both variation entries, as in every export. The published page then carries `noindex,nofollow`. The user switches on Show in search results when the page goes live.

Accessibility fields (alt text, button labels and the video title were checked on a live page):

| Element | Field |
| --- | --- |
| Image | `contents.alt`, rendered as the image's `alt` |
| Button | `contents.aria_label`, rendered as the link's `aria-label`; also set `contents.link.aria_label` |
| Form button | `contents.button.aria_label` |
| Multistep step button | `aria_label` in each step's `contents.button` |
| Text link | `aria_label` in its `contents.links` entry |
| Slider | `arrows.previousAriaLabel` and `arrows.nextAriaLabel` (default "Previous slide" and "Next slide"); translate them on pages that aren't in English |
| Video | `contents.title`, rendered as the iframe `title` |
| Form field | its `label`, kept even with `hide_label: true` |

## Saving, publishing and checking the result

**Saving**

- **Small pages:** send the JSON inline to `page_snapshot_save` (up to 5,000,000 characters).
- **Large or script-built pages:** call `page_json_upload_link_create`, PUT the raw JSON file to the link (`curl -X PUT --data-binary @page.json <url>`; from a browser, send an ArrayBuffer body), then pass the returned `snapshotId` to `page_snapshot_save`.
- **Validation:** `page_snapshot_save` checks the JSON against the schema and creates no page when it fails. Its error details can be cut short, so fix what it names and save again. Call `page_json_schema_get` only to debug an unclear error, and only if you can save its result to a file.
- **Retries:** saving isn't idempotent, so check `pages_list` before retrying after a timeout. After an occasional 500 error, retry once. If `group_create` returns 409, the folder exists: find it with `groups_list`.
- **Folders:** new pages land in no folder, so move each one with `page_update`.
- **Revisions:** the MCP can't change a saved page in place. Before a revision, export the current page and keep any edits the user made in the editor. Save each revision as a new page and tell the user which one replaces which. End with one page under the agreed name: unpublish your superseded test versions and ask once, at the end, whether to delete your drafts. Never delete anything without asking.

**Publishing, only as the user's settings allow**

- **Test version:** `page_publish` with `freeDomain` and a new `name.pagedemo.co` address. This needs a Manager key.
- **Custom domain:** only when asked, use `page_publish` with `customDomain` and `host/path`. First check with `domains_list` that the domain shows `ssl: "applied"`.
- **Pages that are already live:** never publish, unpublish or change their URL unless the user asks for that exact action.

**Checking the render**

- **Where:** on the test page if test publishing is allowed. Otherwise use the builder's preview: open the page in the builder and click the eye icon. The preview embeds `https://page-preview.instapage.com/api/v1/page/html?roomHash=<hash>&…`, which serves the same HTML as the live page, so open that address and run the same checks there. Say what you couldn't check.
- **Narrow widths:** browser windows usually can't go below about 500px, so load the page into 360px and 400px wide iframes from a tab on the same address, and measure inside them.
- **Sideways scrolling:** at 360, 400, 767, 1024 and 1440px wide, compare the document's `scrollWidth` with its `clientWidth`. Carousel slides outside the view are clipped and don't count.
- **Layout:** check the key elements: rows stack as planned, buttons fill their cells where intended, nothing overlaps.
- **Spacing:** at 1440 and 400px, compare the visible space between sections and under the last element of each block, and fix outliers, such as an empty band above a hero or under a form button.
- **Fonts:** list `document.fonts` and check that each font in use shows `status: "loaded"`. A screenshot can mislead, because a font installed on your computer hides a missing web font.
- **Images:** they load lazily and can look gray until scrolled into view, so check again before calling one broken.
- **Accessibility:** every `img` has a non-empty `alt`, every `.btn` link has an `aria-label`, and the page has one `h1`.
- **SEO:** until the SEO settings are saved and the page is updated, the title is the page's internal name and there is no description or schema. After you enter them, check the title, the description and one `script[type="application/ld+json"]` that parses. `robots` stays `noindex,nofollow` until the user switches on Show in search results.
- **Lead flow:** don't submit test leads unless the user asks.

**Checking in the editor (when you can open web pages)**

- **Rendering:** open the page in the builder and confirm that buttons, text colors and borders look as they do live.
- **Edited pages:** a page saved in the editor exports JSON that may fail the schema. Nested widgets lose `customStyles`, `links` and `marker_color`, and multistep forms get `ga_event: null`. Restore those keys before you save a modified copy.

**Keeping token use low**

- **Exports and schema:** never read a full page export or the schema into the conversation. Save results to files and parse them with code; use `page_get` for status.
- **Image lists:** `asset_images_list` returns up to 100 images per call, so ask for one folder at a time.
