docs
/
Building on Appmint

The gotcha index

Every trap in this section listed by what you actually see, because at 2am you search for the symptom, not the cause.

Documentation is usually organised by cause. Debugging starts from a symptom. This page is the index that bridges them.

Every entry is something that has actually happened, and most of them share a property that makes them expensive: they do not produce an error. The call returns 201, the record looks right, and the page is wrong.

Quick index

What you seeCause
Page deploys, nothing changes on screendata.screens overrides data.html
Your JavaScript never runsWritten to data.style, not top-level style
<script> tag present but emptyThe parser discards script bodies
getElementById returns null at startupNo readiness gate
create => Not a new metrics…Missing isNew: true
This form is not available for public accessGuest missing from permission.read
Reservation Definition not foundWrong datatype name
Booking returns 401 from a pagecreate needs auth
Favicon does not changesite.data.favicon must be a string
Whole site renders unstyledCSS link regex matched one spelling
Nav links 404 across every pageFragment links not rewritten
One click, two recordsTwo bootstraps, or no send guard
Prices and images disappeared{"data": {...}} replaced the whole object
Products load but no category has themCategories belong in post
Product tiles blank after a loadimage not set alongside images
Upload path contains the filename twicelocation is a folder
SVG will not renderServed as application/octet-stream
403 with body error code: 1010No User-Agent
404 customer … not found on a readx-client-authorization sent with an operator token
data-bind renders blankBindings cannot call JS globals
Configured options are not chargedNot in options[] with a numeric price
[object Object] where a price should becalculatedPrice is an object
Canvas text draws in the wrong fontfonts.load() needs a size
Every URL returns 200Unknown paths serve the fallback body
Order is missing the customer's choicesCheckout drops freeform fields
A global defined in one handler is undefined in anotherAttribute handlers do not share globals
Confirmation emails never arriveOrg has no credit

Page deploys, nothing changes

You see: update-partial returns 201. The rendered page is unchanged. You redeploy three times before suspecting anything.

Cause: the page has data.screens, which the renderer uses in preference to data.html. Any page ever saved in the visual editor has it.

Fix: clear the other shapes in the same write.

{ "data.html": "…", "data.screens": [], "data.sections": [], "data.content": "" }

Your JavaScript never runs

You see: no errors, no effect. The page behaves as if the code is not there.

Two causes.

You wrote to data.style.javascript. The renderer reads a top-level style, a sibling of data. Nothing reads data.style.

print("style" in rec)                   # must be True
print("style" in rec.get("data", {}))   # must be False

Or you used a <script> tag. The HTML parser is configured with blockTextElements: { script: false }, which discards script bodies. The element survives as an empty node:

[...document.querySelectorAll('script:not([src])')].filter(s => s.textContent.length === 0)

Where your code goes

getElementById returns null

You see: your code runs — a flag at the top is set — but every element lookup is null, or window.appmint is undefined.

Cause: style.javascript is injected into the head. It executes before the body is parsed and before the runtime mounts.

Fix: a readiness gate, checking a real element and the specific namespace you use.

(function boot() {
  if (!document.getElementById('app-root') || !(window.appmint && window.appmint.cart)) {
    return void setTimeout(boot, 30);
  }
  // …
})();

Not DOMContentLoaded — it may have fired before your script was injected, and then nothing runs at all.

create => Not a new metrics…

You see:

{ "statusCode": 400,
  "error": "create => Not a new metrics, please use update or set the new property" }

Cause: the payload is missing isNew. The message says "the new property", which reads as a field named new. It is not.

Fix: "isNew": true at the top level of the body.

Form "not available for public access"

You see: an anonymous submit refused with a message about public access, while permission.create clearly includes Guest.

Cause: the message is about read. Before accepting a submission the platform loads the form definition as the caller, and an anonymous caller with no read grant cannot load it.

Fix: Guest in both permission.create and permission.read.

Collect form submissions

Reservation Definition not found

You see: the slots endpoint cannot find a definition whose id you are certain is correct.

Cause: it was created with the wrong datatype. The correct one is reservation_definition — not crm_reservation_definition, which creates a record in a collection the slot generator never looks in.

Related: workDays must be capitalised full day names. "monday" matches nothing and that day generates no slots, silently.

Booking returns 401

You see: definitions and slots work anonymously; create returns missing_authorization_header.

Cause: crm/reservations/create requires auth. The public endpoints let you show availability without credentials, but not take a booking.

Fix: call it through window.appmint.reservation.create on a hosted page, or through your own server. Never by putting an operator token in the page.

Favicon does not change

You see: the site record has a favicon, the file fetches fine, the tab shows the platform default.

Cause: site.data.favicon was stored as an object. It is passed straight into the framework's icons field with none of the unwrapping the logo gets, so an object yields no icon link at all.

Fix: store a string URL. Park the detail elsewhere.

{ "data.favicon": "https://…/favicon.png?…",
  "data.faviconAssets": { "ico": "…", "png": "…" } }

Verify on the rendered page, never the record:

curl -s "https://your-site/?cb=$RANDOM" | grep -oE '<link rel="icon" href="[^"]{0,80}'

Related: page-level <link rel="icon"> tags are deliberately stripped by the renderer. The site record is the only mechanism.

Site renders unstyled

You see: a wall of unstyled text. It looks catastrophic.

Cause: the deploy's CSS-inlining regex matched <link …> but the page uses <link … /> (or the reverse), so no substitution happened and the relative stylesheet 404s.

Fix: match both spellings, and assert the count before substituting.

n = len(re.findall(r'<link rel="stylesheet" href="styles\.css"\s*/?>', html))
assert n == 1, f"{name}: {n} styles.css links"

You see: every page's navigation is broken, on a deploy that reported success throughout.

Cause: the link-rewriting regex required the closing quote straight after .html, so anchored links — index.html#pricing, index.html#modules — were never rewritten. Those are exactly the links in a nav.

Fix: capture the fragment, and assert nothing survives.

html = re.sub(r'href="([a-z0-9\-]+)\.html([#?][^"]*)?"', rewrite, html)
assert not re.findall(r'href="(?!https?:)[^"]*\.html[^"]*"', html)

One click, two records

You see: identical submissions milliseconds apart. It looks like a double-click.

Two causes, often together.

Two bootstraps on the page. A deploy script that appends by slicing the document re-captures the bootstrap from the previous run. Strip every existing one and regenerate — never dedupe by picking a survivor.

No send guard. Disabling the button is not enough; a second submit can come from the keyboard.

if (form.dataset.sending) return;
form.dataset.sending = '1';
// … delete it in the final .then()

Prices and images disappeared

You see: an update meant to change one field, and the record has lost everything else.

Cause: {"data": {...}} replaces the entire data object.

Fix: dot-paths — "data.price": 40 — one key each. And snapshot before any bulk write; a find dump is the only thing that makes this recoverable.

Products load but categories are empty

You see: every product created successfully; no category lists any of them.

Cause: categories and tags were put in data. They are read from post.

"post": { "categories": [...], "tags": [...] }

Product tiles are blank

Cause: data.images was set — an array of objects — but data.image, a plain string thumbnail, was not. Both are needed.

Upload path doubled

You see: products/chair/chair.jpg/chair.jpg.

Cause: location is the folder. The endpoint appends the filename.

SVG will not render

Cause: uploads are served as application/octet-stream. Browsers sniff raster formats, so PNG and JPEG survive it; SVG does not.

Fix: inline the SVG markup, or use a raster format.

403 error code: 1010

Cause: no User-Agent. The CDN rejects the default one some HTTP clients send. It looks like a permissions failure and is not.

Fix: set a browser-like User-Agent on every request.

404 customer … not found

You see: a repository read failing with a message about a customer that has nothing to do with your request.

Cause: x-client-authorization was sent alongside an operator token, so the caller was resolved as a customer — and the operator is not one. The lookup fails before your request is considered.

Fix: for operator calls send Authorization: Bearer <token> and nothing else.

data-bind renders blank

Cause: binding expressions cannot call JS globals — Number, Math, parseInt, toLocaleString are shadowed to undefined, and the binding silently blanks.

Fix: use data-format for formatting, or compute the value in your own code.

Configured options are not charged

You see: the customer picks £40 of upgrades and is charged the base price.

Cause: the choices were not in options[], or their price was not numeric. calculate-cart counts a delta only from options[].

options: [{ name: 'size', label: 'Size', value: 'Large', price: 90 }]

[object Object] in a price

Cause: calculatedPrice is an object — { finalPrice, originalPrice, discount, … }. Read .finalPrice.

Canvas text, wrong font

You see: canvas text renders in a fallback face even though the webfont is loaded.

Cause: document.fonts.load() needs a size in the specifier. 'Orbitron' resolves without loading anything; '64px "Orbitron"' works.

Fix: repaint as each face lands, and again on document.fonts.ready.

Every URL returns 200

You see: your 404 check passes for paths you invented.

Cause: an unknown path serves the site's fallback body with a 200.

Fix: verify by <h1>, not status code.

Order missing choices

Cause: storefront/checkout-cart persists options[] and drops freeform attributes and design.

Fix: push every choice through options[], including ones priced at zero.

Global is undefined

You see: onclick="doThing(this)" reports doThing is not defined, though the code defining it demonstrably ran.

Cause: attribute handlers on a hydrating host are not guaranteed to see globals assigned elsewhere.

Fix: bind with addEventListener from inside your code. If you must use an inline handler, put the whole body in the attribute.

Emails never arrive

You see: a booking or submission succeeds, the record is there, nobody is notified. Sometimes a banner about credits.

Cause: outbound mail is billed and the org has no credit. The record is still written — only the notification is lost.

Fix: top up, and do not rely on the email as your only signal. Poll the collection.


Most of these are silent. If you build one habit from this page, make it verify the outcome, not the response: read the record back, fetch the rendered page, count the rows. A 201 is a receipt, not a result.