# Alus Builder — AI Generation Reference (non-MCP / copy-paste) You are generating a page for **Alus Builder** (the Alusio visual website builder). The user will **copy your JSON and Import it** into the builder. Follow this exactly. ## OUTPUT CONTRACT — read first Output **one JSON code block** — the minimal page schema: ```json { "elements": [ /* AlusElement[] — the page tree */ ] } ``` - `elements` is the page tree (array of elements). This shape alone is enough — the Import dialog accepts it directly. - You do **NOT** need any envelope. The `{ "source": "alusioCopiedElements", "version", "exportedAt", … }` wrapper from the builder's Export is **optional** metadata, never required — do not hand-author it. - A single bare element node `{ "id", "type", "props", … }` and a bare array `[ {…} ]` are also accepted, but prefer `{ "elements": [...] }`. - Do **NOT** build a global header/footer — the site adds them automatically around every page. - Every page MUST be **mobile-responsive**: add `tablet`/`mobile` overrides — stack multi-column grids to 1 column and reduce tall section padding on phones. Standardize spacing/radius/shadow/color/font-size through `var(--theme-*)` tokens, never hardcoded values. See the Mobile & responsive + theme-token sections below. - Output ONLY the JSON block (no commentary) unless the user asks you to explain. > There are no tools to call here — **everything you need is in this document**: the rules, the full > element type list, the template-type gating, and worked examples. --- # Alus Builder — AI Generation Guide (Entry Point) > **Read this first.** This is the thin orientation layer for generating valid `BuilderSchema` > JSON. It does NOT repeat the full block spec — it points you at the right resource, lists the > mistakes that actually break output, and gives you a decision tree. The detail lives in the > resources indexed at the bottom; pull them on demand instead of guessing. --- ## 0. The 30-second contract 1. Output shape is always `{ "elements": AlusElement[] }`. Nothing else at the root. 2. Every element needs a unique `id` (short random string, e.g. `"el-a1b2"`) and a `type`. 3. Only `section` / `container` / `block` carry arbitrary `children`. `iconList` carries `iconListItem` children only; `accordion` carries `accordionItem` children only. Nothing else may have `children`. 4. `props` is required (can be `{}`). `style` and `selectorConfig` are optional — omit when unused. 5. **The save tool is the validator.** `pages_set_builder_schema` / `kits_set_builder_schema` / `themes_set_template` validate server-side AND run the contrast lint in the same call, returning a fix-list on error. Submit the schema **as an object** directly — do NOT run a separate `validate_schema` pass first (it just doubles the payload). If the save returns errors, fix and resubmit. Use `validate_schema` for a schema you are NOT yet saving — e.g. one you will paste into the builder's Import Schema box; it runs the identical gate (accepts the object directly — no need to stringify). 6. Colors/fonts/radius come from the site **Theme** via CSS vars. Use `var(--theme-*, #fallback)` — never invent token names, always include a hex fallback. --- ## 1. Decision tree — what element do I reach for? **Need a layout box?** - Full-bleed band (one per logical page section) → `section` (`htmlTag: "section"`). - Centered content wrapper inside a section → `container`. **It is ALREADY boxed + centered** at the theme's container width (`--theme-container-width`, ~1100px) via CSS — do NOT set `maxWidth: "1100px"` or `marginLeft/Right: "auto"` for the default. Only set `maxWidth` to make it **narrower** (e.g. `"720px"` for a reading column) or wider; margins stay auto automatically. - A flex/grid child cell (column, card, row item) → `block`. - Columns side-by-side → `container`/`block` with `style.layout.display: "grid"` + `gridTemplateColumns: "1fr 1fr"`. **Never** reach for legacy `twoColLayout`/`tabs`/`col1` — removed. **Need text?** - Big title → `heading` (`level` h1–h6). Inline emphasis only → `mode: "rich"` + `richText`. - Paragraph with inline formatting / multiple blocks → `richText` (`body` = HTML). - Plain paragraph, no toolbar needed → `basicText`. **Need it to pull live content (product/post/page/course)?** - → Use a **Dynamic element** (`productTitle`, `postContent`, …) OR a primitive + entity tag (`heading` + `{product_title}`). See §3. These only resolve inside the matching **template type** or a `queryLoop` on that data source — elsewhere they render placeholders. **Need a repeating grid of items (recent products, blog list)?** - → ONE template `block`/card with `props.queryLoop` set, inside a grid `container`. The block IS the repeated template (repeat-self model). Descendants read the current row via entity tags. Don't hand-emit N copies. See §4. **Need a carousel?** → `slider` with each child a slide (or one `queryLoop` child for dynamic). **Need a checklist / feature list?** → `iconList` > `iconListItem[]` (NOT generic blocks + icons). **Need an FAQ?** → `accordion` > `accordionItem[]`. When unsure which element exists, call `list_blocks` / `get_block_schema` — don't invent a type. --- ## 2. Anti-patterns — the mistakes that actually fail generation | ❌ Don't | ✅ Do | Why | |---|---|---| | `children` on `heading`/`image`/`button`/etc. | Keep leaf elements childless | Validator rejects it | | `iconListItem`/`accordionItem` at root or under `section` | Only inside `iconList`/`accordion` | Child-only; validator rejects | | Invent a type like `"hero"`, `"card"`, `"column"` | Compose from `section`/`container`/`block` | Only registry types render | | Literal `gap: "24px"` | `gap: "var(--theme-space-lg)"` | Literals freeze layout off the theme scale | | `maxWidth: "1100px"` + `margin auto` on a `container` | Nothing — leave it; only set `maxWidth` to go narrower/wider | `container` is already centered at the theme width; the redundant value overrides the theme token and sticks as a default in the inspector | | Light text with no background color set | Set `style.background.color` on the band, or dark text | The classic invisible-text bug (contrast lint) | | Invent `--theme-brand-primary` etc. | Use names from `get_theme_tokens`, always with hex fallback | Unknown vars resolve to nothing | | `display: "grid"` expecting 3 columns by default | Set `gridTemplateColumns` explicitly | Empty grid = single `auto` column | | `layoutMode: "simple"` on a horizontal row / grow-flex item | `layoutMode: "advanced"` | Wrong mode hides Inspector controls | | Dynamic element (`addToCartButton`) on a `home`/`single_page` template | Only on its compatible surface (see `get_template_guide`) | Renders placeholder otherwise | | `pageTitle` / `pageContent` inside a **Page** | `heading` / `richText` with the copy written in | Rejected on save. They bind to the page entity and exist only on the `single_page` TEMPLATE; the Page builder hides them from the palette, so the merchant could never edit what you emitted | | Recreating the global header/footer inside a page/template schema | Leave them out — the site renders the theme header/footer automatically | Duplicated, misaligned chrome | | Raw HTML in a page's text `content` to fake a layout | Use `pages_set_builder_schema` (builder mode) — never the text field | The text field is not a design surface; it won't render as a designed page | | Empty `children: []` / placeholder-less output | Fill with real placeholder content | Empty sections look broken | | Per-page colors/fonts in `style` everywhere | Lean on theme tokens; only override intentionally | Keeps the theme cascade working | **`layoutMode` rule of thumb:** emit `"advanced"` for any horizontal row, any grow/shrink flex item, or any grid beyond plain columns. Emit `"simple"` only for a plain column stack or a plain columns-grid. When in doubt → `"advanced"`. --- ## 3. Dynamic content — tags vs dedicated elements Both are valid and coexist: - **Dedicated Dynamic elements** (`productTitle`, `productPrice`, `postFeaturedImage`, …) — richer Inspector controls, friendlier. Prefer these when a dedicated element exists for the field. - **Primitive + entity tag** — `heading` with `text: "{product_title}"`, `richText` with `{post_content}`. Handy for inline text and inside query loops. Tags resolve against: (a) the ambient entity on a matching **template type**, or (b) the current row inside a `queryLoop` on that data source. Outside both → placeholder text. - Iteration metadata inside a loop: `{{loop.index}}`, `{{loop.first}}`, `{{loop.count}}`. - The legacy data-source-agnostic `{{item.*}}` set is **removed** — do not emit it. - Price tags (`{product_price}`, `{product_sale_price}`, `{product_original_price}`) are **pre-formatted** in the site currency — do NOT add a `|currency` filter. Call `get_dynamic_tags` (optionally scoped by context: product/post/course/page) for the live list, and `list_dynamic_filters` for filters (`truncate`, `date`, `default`, `upper`, `image_url`, …). --- ## 4. Query Loop (repeat-self model) The element holding `props.queryLoop` is the **template that gets repeated N times**. Typical: ``` container[display:grid, gridTemplateColumns:"repeat(3,1fr)"] └─ block[queryLoop: { source:"products", limit:6 }] ← repeated once per product ├─ productImage / image+{product_image} ├─ heading + {product_title} └─ productPrice / {product_price} ``` Do not emit one block per item by hand. One template block + `queryLoop` → renderer expands it. For a dynamic carousel: a `slider` with ONE `queryLoop` child. For masonry: a grid container with `style.layout.masonry: true` wrapping the loop block. Call `get_data_contexts` for available sources. --- ## 5. Template type gating — what belongs where Every page is rendered through a **template type**. Dynamic elements are gated to compatible types; some elements are forbidden in some types. Before generating any template, call **`get_template_kit(type)`** — ONE call returns the guide (purpose, required/recommended/forbidden, layout, notes) + every relevant block schema + suggested patterns + critical rules. Don't fan out into many `get_block_schema` calls. Quick map (full detail via `get_template_guide`): | Template type | Renders | Key dynamic elements allowed | |---|---|---| | `home` | Landing / homepage | Recent-items loop subset (title/image/price/excerpt) | | `tenantPage` | **A tenant Page** (`pages_set_builder_schema`) — the ordinary case | none: write the copy straight into `heading` / `richText` | | `single_page` | The theme TEMPLATE wrapped around every page | page* (title/content/featured image) | | `single_post` | One blog post | post* (title/content/featured/excerpt/meta) | | `post_archive` / `post_category` / `search` | Post lists | post* in a loop | | `single_product` | One product | full product* set incl. addToCart, variants, quantity | | `shop_catalog` / `product_category` | Product lists | product* loop subset | | `single_bundle` | A product bundle | product* + checkout helpers | | `single_course` | One course | course* + curriculum | | `course_archive` | Course list | course* loop subset | | `checkout` | Checkout flow | checkout* family (items/form/shipping/payment/summary) | | `header` / `footer` | Site chrome fragments | logo, menu, searchBar, socialIcons; site_* shortcodes | Interactive single-entity elements (`addToCartButton`, `productVariants`, `productQuantity`, `productDescription`, `postContent`) need a single ambient entity → only on `single_*`, never in a recent-items grid on `home`. --- ## 6. Mobile, responsive & theme tokens (REQUIRED) ### Mobile & responsive — do not skip Every page is viewed on phones first. `style` is keyed by breakpoint and cascades: **`desktop` is the base; `tablet` and `mobile` are partial overrides.** Breakpoints: **tablet ≤1024px**, **mobile ≤767px**. You only repeat the keys that change. Checklist for every page you generate: - **Stack multi-column grids.** Any `gridTemplateColumns` with >1 column MUST add a `mobile` override to `"repeat(1, 1fr)"`, and usually a `tablet` step too (e.g. 4→2→1, 3→2→1, 2→1). - **Reduce big section padding on mobile.** Section `paddingTop`/`paddingBottom` of `var(--theme-space-section)` (96) is too tall on phones — drop to `var(--theme-space-xl)` (32) or `-lg` (24) at `style.mobile.layout`. - **Let type auto-scale.** Set `style.desktop.typography.fontSize` and leave `tablet`/`mobile` **empty** — it auto-scales 0.9× (tablet) / 0.85× (tablet→mobile) / 0.75× (desktop→mobile). Only set a `mobile` fontSize for a non-proportional size. `rem`/`em` desktop values cascade as-is (follow browser zoom, no auto-scale). - Switch a flex `row` to `direction: "column"` on mobile when items get cramped. ```json { "type": "container", "style": { "desktop": { "layout": { "display": "grid", "gridTemplateColumns": "repeat(3, 1fr)", "gap": "var(--theme-space-xl)", "paddingTop": "var(--theme-space-section)" } }, "tablet": { "layout": { "gridTemplateColumns": "repeat(2, 1fr)" } }, "mobile": { "layout": { "gridTemplateColumns": "repeat(1, 1fr)", "paddingTop": "var(--theme-space-xl)" } } } } ``` ### Standardize with theme tokens — don't hardcode Pull every standardizable value from a theme token — `var(--token, #fallback)`. Always include a fallback; never invent token names (unknown names silently fall back). Call `get_theme_tokens` for the live values. | Dimension (CSS) | Token family | |---|---| | `gap` / `rowGap` / `columnGap`, `padding*` | `--theme-space-xxs…-section` | | `borderRadius` | `--theme-radius-button` / `-card` / `-pill` / `-sm` / `-md` | | `boxShadow` / elevation | `--theme-shadow-xs` / `-sm` / `-md` / `-lg` | | `fontSize` | `--theme-text-display-xl/lg/md` · `-title-lg/md` · `-body-md/sm` · `-caption` | | `lineHeight` | `--theme-leading-tight` / `-normal` / `-relaxed` | | `color` / `backgroundColor` / `borderColor` | `--theme-ink` / `-body` / `-muted` / `-primary` / `-on-primary` / `-canvas` / `-surface-card` / `-surface-dark` / `-on-dark` / `-hairline` | | `fontFamily` | `--theme-heading-font` / `--theme-body-font` | | container `maxWidth` | `--theme-container-width` | - **Spacing scale:** `--theme-space-xxs`(4) `-xs`(8) `-sm`(12) `-md`(16) `-lg`(24) `-xl`(32) `-xxl`(48) `-section`(96). Default body gaps → `var(--theme-space-lg)`. Literal px only for intentionally out-of-scale values (`0px`, `2px` hairline). - A card should pull `borderRadius: var(--theme-radius-card)` + `boxShadow: var(--theme-shadow-sm)` + `backgroundColor: var(--theme-surface-card, #fff)` rather than ad-hoc values. --- ## 7. Generation workflow (recommended order) 1. Identify the **surface** from the request. Building one concrete page a merchant asked for → `tenantPage`. Building a reusable theme slot → its template type (`home` for the site landing, `single_page` for the wrapper rendered around every page, etc.). 2. `get_template_kit(type)` — one call for guide + schemas + patterns + rules. 3. Look at a **full-page example** for the closest topic: `list_examples` → `get_example(name)`. 4. Assemble: `section` → `container` → content, top to bottom. Fill placeholders with real copy. 5. Use tokens for spacing/color; set background on dark bands. 6. **Submit directly** via the save tool (`pages_set_builder_schema` / `kits_set_builder_schema` / `themes_set_template`) — pass the schema as an object. It validates + lints in one call. 7. If it returns errors or contrast warnings, fix and resubmit. Done when it saves clean. (Only use `validate_schema` to preview something you are not yet saving — it runs the same gate.) **Field caps that bite** (the Zod gate, not style advice): `style.*.background.color` and `border.color` are capped at **50 characters** — `var(--theme-x, #hex)` fits, a long `color-mix(in srgb, var(--theme-x, #hex) 15%, transparent)` does NOT (use `rgba()` or drop the fallback). `boxShadow` 200, `gradientCss` 2000, `border.radius` 100, typography fields 100. `layout.flexGrow` / `flexShrink` / `order` are **numbers**, not strings. `heading` requires `text` even when `mode: "rich"` (keep it as the plain-text fallback next to `richText`). ### Editing an existing page: `set` vs `patch` - **Creating a page, or replacing its whole layout** → `pages_set_builder_schema` (full schema). - **Tweaking a page that is already builder-type** (recolor a section, add one FAQ item, remove a block, reorder) → `pages_patch_builder_schema`. Send only the delta by element `id` instead of regenerating the entire JSON — far less to write, much faster. 1. `pages_get` to read current element `id`s and the layout `version`. 2. Send `ops` (applied in order): `update {id, set}` (deep-merge; `null` deletes a key), `replace {id, element}`, `insert {element, parentId?, index?|before?|after?}`, `remove {id}`, `move {id, parentId?, …}`. A later op may reference an id an earlier op created. 3. Pass `baseVersion` = the `version` from step 1 so a concurrent edit can't be clobbered. The patch validates the FULL resulting tree (same gates as `set`) and saves nothing if anything fails. --- ## 8. Resource index (call these, don't memorize) | Need | Resource | |---|---| | Full narrative spec | `builder-spec.md` (knowledge file) | | Machine spec (props/defaults/categories) | `builder-spec.json` + `get_block_schema(type)` | | All types | `list_blocks` (optional `category`) | | One element's controls | `get_element_controls(type)` / `get_control_options` | | Per-template rules | `get_template_guide(type)` | | One-shot template bundle | `get_template_kit(type)` | | Section UI patterns | `list_patterns` → `get_pattern(name)` | | **Full-page & per-template examples** | `list_examples` → `get_example(name)` | | Theme tokens | `get_theme_tokens` | | Dynamic tags / filters | `get_dynamic_tags`, `list_dynamic_filters` | | Loop data sources | `get_data_contexts` | | Nesting rules | `get_nesting_rules` | | Typography slots / responsive | `get_typography_slots`, `get_responsive_typography_rules` | | Header config | `get_header_settings` | | Save (validates + lints) | `pages_set_builder_schema` / `kits_set_builder_schema` / `themes_set_template` | | Incremental edit by id | `pages_patch_builder_schema` (update/replace/insert/remove/move; pass `baseVersion`) | | Preview-only validate | `validate_schema` (same gate as save; accepts object or string) | > Maintenance note: this guide is intentionally thin so it doesn't drift. Detailed truth lives in > `template-rules.ts`, per-element `meta.ts`, and `builder-spec.json`. If a rule here contradicts > those, those win — fix this file. ## All element types ### layout - `section` (container) — Full-width flex container — root-level page divider. Use as the outermost wrapper for each page section. - `container` (container) — 1100px centered flex container. Place inside section for centered, max-width content. - `block` (container) — Full-width flex-grow block. Use as flex item that fills available space or as a column inside a grid container. ### content - `heading` — Text heading H1-H6 with alignment and optional link. Supports plain mode (default) and rich mode for inline per-word styling (bold/italic/underline/color/link). - `richText` — HTML rich text content block. Supports full HTML including paragraphs, lists, bold, italic. - `basicText` — Plain paragraph text without rich-text toolbar. Use for short paragraphs with optional link, words limit, or read-more. Pick this over Rich Text when no headings/inline formatting is needed. - `image` — Single image with optional link and alt text. - `video` — Embedded YouTube video with configurable aspect ratio. - `button` — Clickable button. linkType: 'url' (default), 'whatsapp' (click-to-chat), or 'checkout' (buy 1 product → /checkout, like the deprecated Checkout Button). Supports Meta and TikTok pixel tracking. - `textLink` — Inline text hyperlink. - `counter` — Animated number/statistic counter with label. - `divider` — Horizontal or vertical divider line. - `spacer` — Empty vertical space. - `accordion` (container) — Expandable FAQ / content accordion. Panels are REAL child elements of type accordionItem; each accordionItem is itself a NESTABLE drop zone holding any elements (heading, richText, image, …). Add accordionItem nodes to children — do NOT use a props.items array. - `accordionItem` (container) — A single panel inside an Accordion. CHILD-ONLY: may exist ONLY as a child of accordion (never at root, never inside section/container/block/slider). NESTABLE: its children are the panel body (any non-child-only elements). Toggle icon/titleTag come from the parent accordion. - `iconList` (container) — List of features/benefits. Items are REAL child elements of type iconListItem (each fully stylable). Add iconListItem nodes to children — do NOT use a props.items array. - `alertBanner` — Notification/announcement banner. - `iconListItem` — A single item inside an Icon List. CHILD-ONLY: may exist ONLY as a child of iconList (never at root, never inside section/container/block/slider). Has its own full Style tab (border, padding, background) + typography slots. ### marketing - `countdownTimer` — Countdown timer — either to a fixed date/time, or an evergreen per-visitor countdown. ### ecommerce - `cart` — Cart icon trigger + badge count + optional subtotal + slide-in panel. Best in header. - `productImageGallery` — Product image gallery (featured + gallery images). Used in single_product templates with shortcodes. - `checkoutItems` — Cart items list for checkout page. - `cartItems` — Editable cart line-items list for the CART PAGE (not checkout): image, name, variant, quantity steppers, line total, remove button. Reads the cart store directly. Use ONLY on the `cart` template. - `cartSummary` — Cart-page order summary: subtotal + item count + 'Lanjut ke Checkout' CTA + optional continue-shopping link. Reads the cart store directly. Use ONLY on the `cart` template (pairs with cartItems). - `checkoutShipping` — Shipping method selector for checkout. - `checkoutPayment` — Payment method selector for checkout. - `miniCheckout` — Self-contained inline checkout for 1 product or 1 bundle. Customer enters data, picks payment, and submits without leaving the page. Use on landing pages for 'Beli Sekarang' conversion flow. ### navigation - `breadcrumb` — Manual breadcrumb trail (Home > Section > Page). Auto-emits BreadcrumbList JSON-LD for SEO rich result eligibility. - `menu` (container) — Self-contained site navigation menu. EMIT THE FLAT FORM: put `brand`, `menuItem`, and `menuAction` directly in the menu's `children`. The builder reorganises them on load into three fixed bar zones (Kiri/Tengah/Kanan) wrapped in `menuZone` elements — do NOT emit `menuZone`, `menuItemsGroup`, or `menuActionsGroup` yourself. Per-item dropdown can be 'none' (link only), 'simple' (text sub-menu, max depth 2), or 'mega' (a `customMega` child holding a designable block tree). - `searchBar` — Multi-mode site search. Submits to actionUrl as GET with `?q=...` plus any additionalParams as hidden inputs. Defaults to `/search` route. Modes: `input` (form inline, server-renderable, no-JS friendly — best for hero/inline placement), `overlay` (icon button opens fullscreen modal with scale animation, ESC + close button + click-outside dismiss — best for prominent header search), `inlineReveal` (icon expands to inline input on click — best for compact navbar). Use in header templates, single_post templates, or anywhere a search affordance is needed. ### form - `nameField` — Full name input field. Only inside form builder. Always full-width — wrap in container/block for layout. - `emailField` — Email input field. Always full-width. - `phoneField` — Phone/WhatsApp input field. Always full-width. - `textareaField` — Long text input. Always full-width. - `selectField` — Dropdown select field. Always full-width. - `checkboxField` — Checkbox for consent/agreement. Always full-width. - `submitButton` — Form submit button. ### embed - `customHtml` — Raw HTML block. - `htmlEmbed` — Embed external HTML code or widgets. Always rendered inside an isolated iframe (srcdoc) and auto-resized to content height. Viewport units (vh/svh/dvh/lvh/vw) inside the embed are automatically rewritten to absolute pixels based on the parent page viewport so '100svh' fills the device viewport instead of the iframe viewport. - `mapEmbed` — Google Maps embed. embedUrl accepts ANY of: a keyword/address ('Monas, Jakarta'), lat,lng, a full Google Maps URL, an embed (/maps/embed?pb=) URL, a whole