HOC SiteDocumentation

Design agent playbook

Token-efficient loop for editing pages, chrome, and components via MCP/API.

Verify loop

  1. get_* with mode=summary to see block ids/types/propsKeys. propsKeys are the saved top-level keys, including group names and shell.
  2. Patch with save_* ops (set/insert/remove/move by blockId + prop path). Prefer ops over full content replace. Use the nested paths from the design schema menu, such as typography.fontSize, action.url, shell.visibility.showOnBreakpoints, shell.anchor.anchorId, and shell.box.sizeByBreakpoint.
  3. Slim update responses return changedBlockIds, contentLocale, and hasWarnings—not the full document.
  4. If hasWarnings is true, call get_design_warnings for details (schema, layout props, and routePageId checks; no DOM overflow — screen_capture_page reports that).
  5. get_* with blockId for one subtree, or full get only when you need the whole document.
  6. Insert only types from the get_docs surface catalogs. Seeing an unknown type in existing JSON is not a recipe to copy it. Flat keys still load; the next save writes the nested shape.

Redesign requests

When the user asks to update, improve, or redesign something, treat it as a structural job: read the page, then name two to four concrete improvements and apply them in the same turn. Change the section order, add or remove sections and blocks, and fix headings and copy before reaching for colours and spacing. Do not ask for permission per step; pick the sensible default, apply the whole change, and report what changed afterwards with up to three next steps. Ask at most one blocking question, and group any options at the end of the message.

Block-id ops

Ops shape: set {blockId, path, value}, insert {parentBlockId, slot?, index, block}, remove {blockId}, move {blockId, parentBlockId, slot?, index}. An entry may omit op: block + parentBlockId is read as insert, path as set. Use blockId "root" (or path starting with root.) for root props. Pass either content or ops—not both. Errors include design.block_not_found, design.block_id_ambiguous, design.op_bad_path, design.ops_or_content, design.json_parse. set value and insert block are JSON-parsed when the string starts with {, [, or a quote. A {{bind:…}}, {{var:…}}, or {{icon:…}} token is stored as that text. A quoted JSON string unwraps to the token. Any other string starting with { or [ that is not JSON fails with design.json_parse at ops[n].value.

Writes: merge or replace

content is a sparse merge, never a replacement. Omitted keys stay, null deletes a key, and block lists match by id: a listed block merges in place, an unlisted block stays, a new id is appended, and { id, props: null } removes one.

A nested block list merges the same way, so { content: [{ id: 'section', content: [newBlock] }] } appends newBlock to that section's children — it does not replace them. To replace a slot's children, send an ops set for that path (set with an array replaces the array) or remove each existing child with { id, props: null } first.

ops set with an object value merges into the stored object; arrays and scalars replace it. Prefer a dotted path such as typography.size over sending a whole props object when you mean one value.

Block ids must be unique in a document. An op on an id used by two blocks fails with design.block_id_ambiguous and names each path, and get_design_warnings reports design.warning.duplicate_block_id. The id-1/id-2 suffixes in a diff or in search output only disambiguate repeated ids for reading; they are not stored ids, so never send them to an op.

Values, units, and decoration

Send JSON types, not strings: numbers for typography.size, shell.spacing.*, and every …ByBreakpoint map; booleans for …enabled flags. A field's code sample shows a value, not a string to copy — fontSize: 48 means the number 48.

Breakpoint maps are keyed by the website's own breakpoint ids (get_theme with section=layout). The id must match exactly: desktop does not alias breakpoint-1024. A numeric string such as "2" in columnsByBreakpoint still renders, but write numbers.

shell.box (including sizeByBreakpoint), spacing, background, border, shadow, effects and motion render only while shell.customizeEnabled is true. Without it the values are stored and ignored, and get_design_warnings reports design.warning.box_size_ignored_without_customize.

Forms & search (dataRequests)

  1. Read design/shared-concepts/data-requests.
  2. Declare editor variables on root.props.variables.
  3. Declare named dataRequests on root.props.dataRequests (op list|get|create|update; args may use {{var:id}}; blank variable filters use whenEmpty no-filter or no-results; select lists each field to return (empty returns none) and may be dotted, such as category.name; include is { as, from, via, filters, sortBy, sortDirection, limit, include, rerun } up to depth 3; a variable used only inside an include re-fetches that include unless rerun is request; resultWrites copy results, including images[].size, and may set distinct, sortBy, sortDirection, orderFrom, and defaultVariableId).
  4. Place VariableInputBlock bound to variable ids; Button/InteractiveWrapper only set action runRequest|runMutation + requestName, with optional include alias on runRequest.
  5. For list UIs: after resultWrites into an entry[]/array variable, place VariableArrayTemplateBlock with arrayBinding {{var:hits}} (or {{var:items}}) and slot tokens {{var:item.<fieldKey>}}. A size selector repeats set-variable buttons over a text array (or distinct query values) and filters the gallery with itemFilter on {{var:size}}. Map include rows from requests.<name>.results.<as>.
  6. get_* mode=detailed should list requests and per-include dependencies; client-only filters show up as clientDependencies. get_design_warnings must flag invented keys like kind/operation/mappings.
  7. Prefer save_component_preset for list|detail|form starters. A product detail page is one get plus forward category paths and reverse section and image includes. Embed with ComponentBlock. List select may be { field, format: html|excerpt, maxChars }, { field, format: date, style, pattern }, or { field, format: number, minimumFractionDigits, maximumFractionDigits, currency }. currency is an ISO 4217 code such as TRY and is rendered in the content locale. Dotted fields such as order.price use the same object. {{bind:…}} prints that formatted string. A literal currency symbol next to currency repeats the symbol. args.page / args.offset page a list (offset wins). A direct load honors ?page=. Bind {{bind:requests.<name>.total}}, .hasMore, and .page, or copy them with resultWrites source total, hasMore, or page. URL page stays on {{bind:query.page}}. Button requestNextPage and requestPrevPage write that query. VariableInputBlock labelGap sets the label gap in px. Related lists use filter overlaps, valueFromRoute, excludeRouteEntry, and fallback. SlotSlider arrayBinding repeats the first slide per row.

Breakpoints and layout

get_theme and save_theme with section=layout manage maxContentWidthPx and breakpoints. Block props often key styles by breakpoint id; after changing breakpoints, re-check those maps. Prefer ModalBlock + buttons for mobile nav patterns.

Pick the layout block by shape, not by habit:

  • Stack or row — FlexListBlock. Direction defaults to row at every breakpoint, so set layout.flexDirectionByBreakpoint explicitly: { mobile: "column", desktop: "column" } for a stack, { mobile: "column", desktop: "row" } for a responsive split. A FlexListBlock with several children and no direction renders as one row and clips its text.
  • Grid or card row — GridListBlock with columnsByBreakpoint, columnGapByBreakpoint, rowGapByBreakpoint, and optional columnSpanByChildId. A card is one GridListBlock cell wrapping a column FlexListBlock.
  • Repeating data — VariableArrayTemplateBlock with arrayBinding and columnsByBreakpoint; do not wrap it in a one-cell grid.

Grid keys (columnsByBreakpoint, the gap/align maps, *SpanByChildId) are not FlexListBlock props. They are stored but ignored, which looks exactly like a renderer bug; get_design_warnings reports design.warning.prop_not_supported_by_block.

Warning codes

get_design_warnings and the warningCodes on a save are static checks over the saved document.

  • filter-variable-unset — a request filter reads {{var:q}} and nothing can set q. Give the variable a default, fill it with resultWrites, or bind a VariableInputBlock or button variableUpdates to it.
  • design.warning.box_size_ignored_without_customize — a size is stored but shell.customizeEnabled is not true.
  • design.warning.duplicate_block_id — two blocks share an id; rename or remove one before editing by that id.
  • design.warning.result_write_bad_source — a resultWrites source such as results.name reads nothing; use results.0.name for one row or results[].name for a list.
  • unknown-variable, unused-variable, unknown-query — a token points at a variable or request that does not exist, or a variable is never used.
  • design.warning.prop_not_supported_by_block — the prop belongs to another block type and is stored but ignored here, for example columnsByBreakpoint on a FlexListBlock. Use the block the message names, or the prop that block supports.
  • design.warning.flex_list_implicit_row — a FlexListBlock with several children has no flexDirectionByBreakpoint, so it renders as a row at every breakpoint. Set { mobile: "column", desktop: "column" } for a stack, or { mobile: "row", desktop: "row" } to keep the row.

Visual checks

screen_capture_page returns the image plus servedRevision, viewport and layout.issues. The layout check measures the rendered DOM: a block outside the viewport, content clipped by its own overflow, or a direct child escaping its parent. Fix every issue it lists before you report back.

A clean list does not prove the design is good: still look at the image for spacing, colour, missing images and hierarchy. get_design_warnings is a static check over the saved document and cannot see DOM overflow; the screenshot layout check is the DOM one.

Cache timing

Saving chrome, pages, or components revalidates the public route in the web process that handled the save, so the next request renders the new revision.

Public HTML is not a cache you have to wait out. The proxy stamps every response with X-Hoc-Version (the page head revision) and X-Hoc-Chrome-* (the header and footer revisions). screen_capture_page returns those as servedRevision and chrome: compare servedRevision with the revisionId a save returned. When they differ, the capture was taken before the save landed; capture again instead of assuming a stale cache.

On a multi-instance deployment or behind a CDN, another process can still serve the previous render until its own cache expires. Trust the revision header rather than the wall clock.

Anchors / outline

Anchor id and key are stored at shell.anchor.anchorId and shell.anchor.anchorKey. Summary propsKeys lists shell when that object is present. Map the anchor to blockId for targeted patches. DOM id conventions follow those anchor fields.