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.
aboutfor/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:
| URL | What owns it |
|---|---|
/login, /register, /forgot-password, /reset-password, /auth/…, /demo | Built-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 tt | Static files. The page renders blank — so /sitemap-page, /assets-guide and /ttravel are unusable. |
/<page>.html | Redirected (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):
| Feature | URL 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:
| Field | Value |
|---|---|
name / slug | from step 1 (lowercase letters, numbers, hyphens) |
site | the site name |
appType | web |
title / description | the 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": {} }] |
activeScreenId | default |
html | the same full document as screens[0].html (a mirror; Build Studio writes it on every save) |
parent | optional: 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/. Neverabout.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.
| Need | Use |
|---|---|
| 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 state | data-loop="cart.items" with a data-bind-each child |
| One record | data-source="product.get" with data-source-payload='{"slug":"{{$query.slug}}"}', no data-bind-each |
| Show a value | data-bind="data.name", data-bind-attr-src="data.image", data-bind-html, data-bind-class, data-bind-style-<prop> |
| Format it | data-format="currency" (with data-format-opt-currency="EUR"), date, datetime, number, percent, parentheses, uppercase, lowercase, capitalize |
| Show or hide | data-show="cart.count", data-hide="customer.current.data.email", data-show="query.utm_source === 'spring'" |
| Do something on click / submit / change | data-action="ns.method", its argument in data-payload, then data-on-success / data-on-error effects |
| Ask first | data-confirm="Remove {{data.name}}?" |
| Run on load or when scrolled to | data-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 menu | wm-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 + quantity | wm-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>.
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</body>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 (pluswm-mega-menuorwm-drawerfor 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].htmlandhtml), 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 adata-reactblock, not inside the shared header/footer, and Studio and the docs treat it as unsupported. page_writeappendsjavascriptandcssto what is there. Running the same step twice adds the code twice. To change code, passreplaceStyle: truewith 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
TypeErrorleft 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 inscriptLinks. - 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-sourceshows 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 andcart.toggledoes 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-reactblocks (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-controlreads its product only from adata-productJSON attribute on a published page (product-idloads only inside Studio), and its Add to cart button only fires anadd-to-cartevent. Listen for it in Page settings JavaScript and callappmint.cart.add, or use a plaincart.addbutton.wm-dialog/wm-drawerreadopenonly when they start. Theopen-dialogandclose-dialogeffects only set or remove that attribute, so they don't open or close them. Use aui.dialog/ui.drawerbutton ("action":"open"/"close").data-bind-htmlis not sanitized. Bind it only to content the business wrote, never to what a visitor typed.api.*is half-usable fromdata-action. It is not onwindow.appmint, and the dispatcher passes one value, so onlyapi.get/api.delwith a path string work (data-action="api.get" data-payload='"storefront/config"'). For a POST, usefetch('/api/…')in page JavaScript.- Over 4 MB is a 500, not a "too large" message.
Don'ts
- Don't write
data.htmlalone on a page that has screens; it never renders. Write the active screen (page_writedoes) and keephtmlas its mirror. - Don't empty
screensto makedata.htmlwin. Those screens are the page. - Don't edit from a
findresult; it is partial. Read the record withrepo_find_one. - Don't put a page at a reserved or feature URL, or at a slug starting with
sitemap,assetsortt. - Don't use
id="root", a literal</body>,data-reactorwm-navigation. - Don't put
<script>in the page HTML, and don't put page code indata.style. - Don't put an operator token in a page. Calls go through the site's
/api.