docs
/
AI skills

Build a website page with components

A page saved the way Build Studio stores it — one full HTML document per screen, page CSS in the head, page JavaScript in Page settings, and Appmint's own building blocks for anything live — at a URL nothing else owns.

Use this for any new page on an Appmint-hosted site. The page is stored exactly as Build Studio saves one, so the owner can open it on the canvas and keep editing. Live parts (product lists, cart buttons, dialogs, forms) are built from Appmint's own blocks, not hand-written fetch code.

For the header and footer around every page, see Shared header and footer. For a shop page or product page, see Catalog pages from your products.

Ask first

  • Which site, and what URL? The slug (e.g. about for /about). Check it is free in step 1 before promising it.
  • What goes on it? The sections, the words, the images. Use the person's own text and photos; don't invent claims, prices or contact details.
  • What should be live on it? A product list, an add-to-cart button, a dialog, a sign-up form. Each one maps to a block in step 3.
  • Is it new, or does it replace a page? Replacing means editing the page that already renders at that URL, not making a second one.
  • Should it be visible as soon as it is saved? Most sites serve drafts (step 1). If not, it needs a publish.

Steps

1. Site settings: check the URL is free

Read the site (repo_find on site) and its pages (repo_find on page with { "data.site": "<site name>" }). Note data.homePage, data.features and data.publishedPagesOnly.

A page answers at /<slug> and at /<name>: the site looks the first URL segment up against the page's slug, its name, or its id, ignoring case. Some URLs never reach a page at all:

URLWhat owns it
/login, /register, /forgot-password, /reset-password, /auth/…, /demoBuilt-in sign-in screens
/account, /account/…The customer portal
/checkout, /checkout/success, /order/track, /order/…Checkout and order tracking
/api/…The site's API proxy
Anything whose first segment starts with _next, assets, favicon.ico, robots.txt, sitemap, site.webmanifest or ttStatic files. The page renders blank — so /sitemap-page, /assets-guide and /ttravel are unusable.
/<page>.htmlRedirected (301) to /<page>

Site features claim their URL when they are switched on (in Site Features; a feature can be pointed at a different page there):

FeatureURL it claims by default
Storefront (e-commerce)/store, /store/<product>, /store/checkout
Payment/pay
Ticket support/support
Service menu/services
Tables/table
Forms/form
Blog/blog
Reservations/book
Rental/rental
Content Player/content-player
Community/community
Gift card/gift-card
Unsubscribe/unsubscribe (claimed even when off)
Promotions/join?promotion=…, /confirm-subscription?token=…

A page whose slug a switched-on feature claims does not render as a page. The feature renders instead, and the page's first screen is drawn above it and its second below it, like a wrapper. A page you make at /store or /blog on a site with that feature on never shows its own content. Pick another slug, or ask whether they meant to change the feature's page.

A page with the same data.name already on the site is refused with 409 { "reason": "duplicate", "field": "name" }.

Hands on: the site name, a free slug and name, and whether the site serves drafts (publishedPagesOnly false or missing means it does). Check: no page on the site has that name or slug, and the URL is not in either table above.

2. File manager: the images

Upload each image the person gave you in the file manager (DAM), or with POST /repository/file/upload (multipart: location = the folder only, e.g. <org>/site, and file). Use the URL it returns in the page. Never inline images as base64: they count against the page size limit (step 3).

SVG files uploaded this way are served as application/octet-stream and don't show in an <img>. Put an SVG inline in the HTML instead.

Hands on: the image URLs. Check: open each URL; the image loads.

3. Build Studio: the page record

Create the page with repo_create on page, in the shape Build Studio saves:

FieldValue
name / slugfrom step 1 (lowercase letters, numbers, hyphens)
sitethe site name
appTypeweb
title / descriptionthe browser title and the search description. The site takes these from the record and strips any <title>, <meta name="description">, Open Graph tags and favicon links you put in the head.
screens[{ "id": "default", "name": "Default", "slug": "default", "order": 0, "html": "<full document>", "attributes": {}, "config": {} }]
activeScreenIddefault
htmlthe same full document as screens[0].html (a mirror; Build Studio writes it on every save)
parentoptional: the name of a page whose two screens wrap this one (see Shared header and footer)

screens[0].html is one complete document:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap">
<style>
  .about-hero { padding: 96px 24px; }
</style>
</head>
<body class="about">
<main id="about-app">
  …the page…
</main>
</body>
</html>
  • CSS and font links go in <head>. Studio keeps the head as written, and the site copies it into the page.
  • The <body> class and style are kept: the site puts them on the real <body>.
  • Links between pages are /slug, the home page is /. Never about.html.
  • No <script> in the body or the head. Page JavaScript goes in step 4.

When screens exist, only the active screen renders (activeScreenId, or screens[0]); data.html is ignored. ?screen=<index|id|name> or ?screen=all shows another screen for checking.

The building blocks

Make the live parts from these. They work on the published page with no JavaScript of your own.

NeedUse
A list from data (products, records)a container with data-source="product.list" (or another ns.method), arguments in data-source-payload='{"limit":12}', and one child marked data-bind-each that repeats per row
A list already in statedata-loop="cart.items" with a data-bind-each child
One recorddata-source="product.get" with data-source-payload='{"slug":"{{$query.slug}}"}', no data-bind-each
Show a valuedata-bind="data.name", data-bind-attr-src="data.image", data-bind-html, data-bind-class, data-bind-style-<prop>
Format itdata-format="currency" (with data-format-opt-currency="EUR"), date, datetime, number, percent, parentheses, uppercase, lowercase, capitalize
Show or hidedata-show="cart.count", data-hide="customer.current.data.email", data-show="query.utm_source === 'spring'"
Do something on click / submit / changedata-action="ns.method", its argument in data-payload, then data-on-success / data-on-error effects
Ask firstdata-confirm="Remove {{data.name}}?"
Run on load or when scrolled todata-trigger="load" or data-trigger="inview"
Cart button and count<button data-action="cart.toggle">Cart <span data-bind="cart.count" data-format="parentheses"></span></button>
Add to cart<button data-action="cart.add" data-payload="this"> inside a product row
Dialog / drawer<wm-dialog id="signup"><span slot="trigger" hidden></span><div slot="content">…</div></wm-dialog> opened by data-action="ui.dialog" data-payload='{"id":"signup","action":"open"}' (ui.drawer for wm-drawer). Both slots are required, or it never builds.
Tabs, accordion, slider, grid, mega menuwm-tabs, wm-accordion + wm-accordion-item, wm-slider, wm-grid, wm-mega-menu: every attribute, slot and event is in Appmint components on a page
Product options + quantitywm-storefront-product-control (see What it can't do yet)

data-payload takes: nothing (a form's values, an input's value, or the current row), form (the containing form), this (the current row), a JSON object with {{path}} inside ('{"id":"{{sk}}"}'), or a bare path. this.value reads a property of the element, not the row. Inside {{…}}, $query.<name>, $path.<n> and $json:<path> are also available.

Effects for data-on-success / data-on-error, joined with |: navigate:/path, refresh:#id (re-run that data-source), reload, set-state:key=value, reset-form, focus:#id. notify:Text and notify-error:Text exist but show nothing on a site page today, and a bare refresh (no #id) re-runs nothing; show the message with set-state and a data-show. Tested; see The AppMint JS engine on a page.

State you can bind to: cart.count, cart.total, cart.items, cart.isOpen, customer.current.data.email / .firstName (empty for a visitor who isn't signed in), site.title, page.title, query.<name>, path.0…, path.last, and anything set with set-state. Don't use customer.signedIn: it is true for every visitor, signed in or not.

The full lists are in Runtime attributes, Actions reference, Effects and formats and Custom elements.

Expressions can't call JavaScript globals. Number(x), Math.round(x), parseInt(x) and JSON are undefined inside data-bind/data-show, and the element renders empty with no error. Operators, ternaries and methods on a bound value (data.price.toFixed(2)) are fine; format with data-format.

Forms. There is no form-submit action in the page runtime (form.* only serializes, validates and resets). For a contact or sign-up form that saves answers, use a CRM form: see Shared form from a collection and, for the page side, Registration that creates the customer step 4. Built-in actions that take a form do exist, e.g. <form data-action="ticket.create" data-payload="form" data-on-success="set-state:sent=true | reset-form"> with <p data-show="sent">Sent.</p>.

Footguns that break the page with no error:
  • id="root" anywhere in the page. Build Studio treats the element with that id as the whole page body. The next visual save keeps only what is inside it and drops the rest. Use any other id.
  • The literal text </body> (or </head>) anywhere except the real closing tag — in an attribute, a code sample, a JS string. Studio splits the document at the first one and the page is cut short. Write &lt;/body&gt; in text.
  • data-react="…" placeholders. The React presets they name no longer exist; they render nothing, and one on the page switches the body to a slower path that drops its <script>, <link>, <meta> and <title> tags.
  • wm-navigation. Studio still offers it, but it isn't in the bundle the published site loads, so it stays inert. Build the nav as plain links (plus wm-mega-menu or wm-drawer for a menu).
  • Page size. A save sends the whole record and the API refuses bodies over 4 MB — with a 500 Internal server error, not a clear message. The HTML is stored twice (screens[0].html and html), so keep a page under about 1.9 MB of HTML. Big scripts go to a file (step 4).

Hands on: the page's id (sk). Check: read it back with repo_find_one (find returns partial records): data.screens[0].html and data.html are the same full document, activeScreenId is default. Open it in Build Studio at /app/build-studio/<page name>; the canvas shows the page.

4. Page settings → Style: the page's JavaScript

Only if the page needs code beyond the blocks above. Put it in the page's Page settings → Style → JavaScript — the record's top-level style.javascript (a sibling of data, never data.style). Write it with page_write: { "id": "<sk>", "javascript": "…" }. CSS that isn't in the document head can go in css the same way, and font/CSS files in styleLinks.

The site emits it as one <script> after the page content, on a full page load only: when the site's own screens move the visitor without a reload (for example the redirect back from /login), it doesn't run, while data-* attributes still do. Plain <a href> links in your HTML do a full load. Still gate on your own element and the runtime part you call, and skip the Studio canvas. Timing, events and the whole window.appmint API are in The AppMint JS engine on a page.

(function boot() {
  // Never run inside Build Studio's canvas: DOM built here would be saved into the page.
  if (document.body && document.body.getAttribute('data-design-mode') === 'true') return;
  var app = document.getElementById('about-app');
  if (!app || !(window.appmint && window.appmint.cart)) return void setTimeout(boot, 30);
  if (app.dataset.bound) return;
  app.dataset.bound = '1';
  // …the page's code; use addEventListener, and textContent for record values…
})();
  • Never put <script> in the page HTML. It may run on a plain first load, but not on a page that uses a data-react block, not inside the shared header/footer, and Studio and the docs treat it as unsupported.
  • page_write appends javascript and css to what is there. Running the same step twice adds the code twice. To change code, pass replaceStyle: true with the whole new script.
  • One syntax error, or one uncaught error at the top level, stops all of it (the page's code, the shared header's code and the site's tracking are joined into one script, tracking last; tested: a thrown TypeError left the page with no analytics hit, no Pixel and no cookies). Long or shared code goes in a file: save_page_asset (or the file manager) and the URL in scriptLinks.
  • To reach an API endpoint from code, fetch('/api/<path>') on the same site. The site adds the org and its app token; custom request headers are not passed on, so put tokens and codes in the query string or body. Error bodies come back wrapped as { "error": { … }, "status": 409 }.

More: Run JavaScript on a hosted page and Page JavaScript.

Hands on: nothing new; the page is complete. Check: repo_find_one shows style.javascript at the top level, and it parses (paste it into a syntax check before writing).

Stop: on a site that serves drafts, the page is live at its URL from the first save. Before saving a new page at a public URL, or saving over a page that already renders, show the person the URL and what will be on it, and wait for yes. On a site with publishedPagesOnly: true, publishing is its own step (Publish, unpublish and restore a page); ask before publishing.

5. Check it on the site

Open https://<site host>/<slug> (preview of any page: /__preview/page/<site name>/<page name>).

  • Look at the rendered page in a browser, not the page source. The source carries the HTML inside a script payload, so text you grep for is found even when the body is blank.
  • Every data-source shows rows; every bound value shows text; one click on an action does one thing (one cart line, one record).
  • The shared header and footer sit around it.
  • The cart button opens a drawer only if the site has Storefront (e-commerce) or Rental switched on; without them the cart is hidden and cart.toggle does nothing.

When it's done

Tell the person: the URL, the page name, which live blocks are on it and what each one reads or does, whether the page has its own JavaScript, and whether it is already live (drafts served) or waits for a publish. Open it in Build Studio and on the site.

What it can't do yet

  • React presets are gone. data-react blocks (forms, blog, calendar, chat, comments) render nothing. Build Studio's own AI is still told to put <script> in the head; ignore that. Blog, booking, store and forms come from Site Features at their own URLs instead.
  • wm-storefront-product-control reads its product only from a data-product JSON attribute on a published page (product-id loads only inside Studio), and its Add to cart button only fires an add-to-cart event. Listen for it in Page settings JavaScript and call appmint.cart.add, or use a plain cart.add button.
  • wm-dialog / wm-drawer read open only when they start. The open-dialog and close-dialog effects only set or remove that attribute, so they don't open or close them. Use a ui.dialog / ui.drawer button ("action":"open" / "close").
  • data-bind-html is not sanitized. Bind it only to content the business wrote, never to what a visitor typed.
  • api.* is half-usable from data-action. It is not on window.appmint, and the dispatcher passes one value, so only api.get / api.del with a path string work (data-action="api.get" data-payload='"storefront/config"'). For a POST, use fetch('/api/…') in page JavaScript.
  • Over 4 MB is a 500, not a "too large" message.

Don'ts

  • Don't write data.html alone on a page that has screens; it never renders. Write the active screen (page_write does) and keep html as its mirror.
  • Don't empty screens to make data.html win. Those screens are the page.
  • Don't edit from a find result; it is partial. Read the record with repo_find_one.
  • Don't put a page at a reserved or feature URL, or at a slug starting with sitemap, assets or tt.
  • Don't use id="root", a literal </body>, data-react or wm-navigation.
  • Don't put <script> in the page HTML, and don't put page code in data.style.
  • Don't put an operator token in a page. Calls go through the site's /api.