docs
/
Building on Appmint

Where your code goes

The page record's style.javascript field, the readiness gate every page app needs, and why the script tag in your HTML does nothing.

Before you can build anything in a page, two mechanical facts have to be right. Both are cheap to get wrong and expensive to debug, because neither failure produces an error.

style.javascript, on the record

A page record carries a top-level style object, a sibling of data, with four slots:

SlotInjected as
style.javascriptAn inline <script> in the document head
style.cssAn inline <style>
style.scriptLinks<script src> elements
style.styleLinks<link rel="stylesheet"> elements

Your application goes in style.javascript. It arrives as a real script with a body and executes normally.

s, rec = req("GET", f"/repository/get/page/{SK}", token=t)

req("POST", f"/repository/update-partial/page/{SK}", {
    "sk": SK,
    "version": rec["version"],
    "style": {"javascript": open("app.js").read()},   # beside "data", not inside it
}, token=t)
`style` is top-level. `data.style` is read by nothing.

This is the mistake that sends people looking for workarounds they do not need. A payload written to data.style.javascript is stored happily, returns 201, and never runs. There is no warning and no error.

Verify where it landed:

s, rec = req("GET", f"/repository/get/page/{SK}", token=t)
print("style" in rec)                   # True  — correct
print("style" in rec.get("data", {}))   # False — the wrong place

There is no practical size limit. Print Oxygen's design studio is 113,115 characters in this field.

{"style": {}} clears it.

Keep the code in a real .js file in your source tree and have your deploy script read it in. Editing a large payload as a quoted string inside a JSON record is not something you want to do twice. The same goes for style.css.

The readiness gate

Here is the part that is not obvious and bites everybody once.

style.javascript is injected into the head. It therefore runs:

  • before the body DOM exists — every getElementById returns null
  • before window.appmint is mounted — the runtime is not there yet

So the first thing your application does is wait for both. The pattern is a self-rescheduling boot function:

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

  // From here: the DOM you need exists, and the runtime is up.
  var $ = function (id) { return document.getElementById(id); };
  // … your application
})();

Two details make this work rather than half-work:

Gate on what you actually use. Checking window.appmint alone is not enough — namespaces mount progressively, so test the specific one you need (appmint.cart, appmint.reservation). And name a real element from your own markup, not document.body, which exists long before your page content does.

30ms, not 0. A tight reschedule burns CPU during hydration on slow devices for no gain. 30ms is imperceptible and cheap.

Do not replace the gate with DOMContentLoaded. On a hydrating host the event may have fired before your script is injected, in which case your listener never runs at all — and the failure is silent and intermittent, which is the worst combination to debug.

If it seems not to run

Put a flag at the very top, above the gate, while developing:

window.__APP_LOADED = true;
(function boot() { /* … */ })();

Then on the rendered page:

window.__APP_LOADED   // true  → the field is wired; your gate never opened
                      // false → the field is in the wrong place, or empty

That single check separates "my code is not being delivered" from "my code is delivered and waiting for something that never arrives", which are completely different problems.

Why the <script> in your HTML does nothing

Page HTML is not handed to the browser as a document. It is parsed into a virtual DOM and re-rendered, and the parser is configured like this:

const root = parse(html, {
  lowerCaseTagName: false,
  comment: false,
  voidTag: { tags: ['area', 'base', 'br', /* … */] },
  blockTextElements: { script: false, noscript: false, style: false, pre: true },
});

blockTextElements names the elements whose raw text is retained. script: false means script bodies are discarded during parsing. The element survives; its contents do not.

From the console on any rendered page:

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

Your script tags are in that list, as empty elements.

The <head> of your page HTML fares no better, for a different reason: it is injected with dangerouslySetInnerHTML, and HTML inserted that way never executes its scripts. That is a browser rule, not a platform choice. Stylesheets and <style> work fine, because browsers process those wherever they appear.

What does survive

In page HTMLResult
<script> with a bodyBody discarded — never runs
<script src="…">Element survives and loads
<style>, <link rel="stylesheet">Work normally
onclick, onload and other handler attributesWork — attributes survive intact
<link rel="icon">Deliberately stripped; favicons come from the site record

The fallback: an attribute bootstrap

There is one situation where style.javascript is not available to you: your pipeline produces HTML and nothing else. A static deploy that writes data.html and never touches the record's other fields can still carry code, by putting the whole payload on a 1×1 image's onload:

<img alt="" aria-hidden="true" width="1" height="1"
     style="position:absolute;left:-9999px;top:0"
     src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
     onload='(function(){ /* your code */ })()'>

The image decodes immediately from its data URI and the handler fires synchronously, with the surrounding DOM already present — so this route does not need the readiness gate for the DOM, though it still does for window.appmint.

Prefer style.javascript. The bootstrap carries real constraints: no single quotes anywhere in the payload, <, > and & must be HTML-escaped, and everything must be one expression in one attribute. It is a workaround, not a design.

The full rules, the generator, and the duplicate-bootstrap failure it can cause are in Run JavaScript on a hosted page.

Do not split handlers from your code

This applies to both routes and costs an afternoon the first time.

<!-- your code defines it… -->
<script>window.doThing = function (el) { /* … */ };</script>

<!-- …and markup calls it -->
<button onclick="doThing(this)">Go</button>

On the deployed page doThing is undefined when the click fires, even when your code demonstrably ran. Attribute handlers on a hydrating host are not guaranteed to see globals assigned elsewhere.

Bind with addEventListener from inside your application. If you must use an inline handler, put the entire body in the attribute.

document.querySelectorAll('[data-tab]').forEach(function (el) {
  el.addEventListener('click', function () { switchTo(el); });
});

Make it idempotent

A page can render more than once on a hydrating host. Mark what you bind to:

var root = $('app-root');
if (root.dataset.bound) return;
root.dataset.bound = '1';

And guard any request separately, so a double event cannot produce two records:

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

Checklist

  • Code in top-level style — confirmed by reading the record back
  • A readiness gate that names a real element and the specific namespace you use
  • No reliance on DOMContentLoaded
  • Listeners bound with addEventListener, not globals called from markup
  • Bind guard on the root, send guard on each request

Next: The read/write asymmetry — what your application is allowed to persist.