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:
| Slot | Injected as |
|---|---|
style.javascript | An inline <script> in the document head |
style.css | An 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)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 placeThere 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
getElementByIdreturnsnull - before
window.appmintis 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 emptyThat 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 HTML | Result |
|---|---|
<script> with a body | Body discarded — never runs |
<script src="…"> | Element survives and loads |
<style>, <link rel="stylesheet"> | Work normally |
onclick, onload and other handler attributes | Work — 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.