Parent skill: adom-wiki-skillpack · Sibling: wiki-readme (markdown mode + the content rules that apply to BOTH modes)

Building a custom HTML readme (readme.html)

Verified end to end 2026-07-15 with a live probe page (publish → API variants → rendered iframe → styles applied → <script> ran).

When to use html vs markdown

Situation Mode
Prose + images + tables sells the page fine README.md (cheaper to write and maintain; see wiki-readme)
Tabs, accordions, toolbars, lightboxes, custom typography, interactive JS readme.html
A component CAD hub with an Open-in toolbar + provenance lightbox readme.html (see wiki-component)

Markdown is still the right default. An HTML readme is a designed artifact: it costs real effort to build well, and a half-styled HTML page reads worse than clean markdown. Choose it when the layout IS the value.

How to ship one

Put readme.html at the root of the publish tree and adom-wiki pkg publish. The page registers it (API: readme_variants_json.bareHtml = "readme.html") and the Overview tab switches to HTML mode.

The shadowing rule (the #1 gotcha)

readme.html fully shadows README.md on the page. The publish linter warns: README_SHADOWED: readme.html is what renders on the page, so edits to README.md will not be visible. Keep a README.md anyway, the Files tab, repo clone readers, and git tooling still use it, but every page-visible edit goes in the HTML file. If "my readme edit isn't showing up," check which file you edited.

How it renders (and what the sandbox forbids)

Only HTML-mode pages get an iframe (markdown renders inline in the page). The Overview embeds:

https://wiki.adom.inc/readme/<owner>/<type>/<slug>?variant=public

in <iframe sandbox="allow-scripts allow-downloads">. The endpoint serves your file byte-for-byte, no sanitizer. Consequences:

  • <style>, style=, and <script> all work. Scripts run inside the sandbox.
  • No allow-same-origin: the iframe runs as an opaque origin. No cookies, no wiki session, no fetch to the wiki API, no localStorage. The readme cannot know who is viewing it or call authenticated endpoints.
  • No allow-popups / allow-top-navigation: target="_blank" links and window.open are blocked by the sandbox. Plain same-frame links navigate the iframe itself, not the wiki page. Design navigation accordingly (prefer in-page JS interactions; verify any external link opens before relying on it).
  • It is a full document of its own: include your own base styles; nothing cascades in from the wiki.

Self-containment (assets)

Treat the file like an email template: everything inline.

  • Inline all CSS and JS. Base64-inline fonts and small images.
  • Repo files are reachable at absolute /blob/<type>/<slug>/... URLs for <img>/<video>, but note the known content-type bug (SVGs served as application/octet-stream, adom/wiki issue #38); PNG/JPG are safe.
  • Never reference the hero image, the page header already renders it (wiki-readme's #1 rule applies in both modes).

Theme: the wiki is dark

The wiki chrome around your iframe is dark. A bare HTML file renders on a default white body and will glare. Set your own background and text colors explicitly (match the wiki's dark look or design deliberately), and follow the Adom brand rules (tokens, brand fonts, monochrome icons, no emoji, no em-dashes).

Variants (public/private pages)

The page tracks six slots in readme_variants_json: bareHtml/bareMd, publicHtml/publicMd, privateHtml/privateMd. A plain readme.html registers as bareHtml. The html-vs-md choice composes with the private-readme system for private-source/public-releases pages (wiki-visibility), the ?variant= query selects which one renders.

Verify loop (do all three)

  1. curl the endpoint, confirm your exact bytes are live: curl -s "https://wiki.adom.inc/readme/<owner>/<type>/<slug>?variant=public"
  2. Open the page in pup (cache-bust ?v=<ver>) and screenshot, confirm the iframe shows your layout, styles applied, at full height with no clipping or double scrollbars. The iframe starts at a ~600px fallback; use the readme-height contract (next section) so it grows to fit your content.
  3. Check any JS ran, bake a visible self-test into the page while iterating (an element whose text a script rewrites), remove it when done.

Sizing: the readme-height contract (ask the parent to fit you)

Verified against the live wiki bundle 2026-07-25. The page renders your readme in the sandboxed iframe at a ~600px fallback min-height. You cannot size it from CSS, but the parent LISTENS: post it your height and it resizes the iframe to fit, and clears the fallback so short readmes fit exactly.

parent.postMessage({ type: 'readme-height', height: N }, '*');
// { type: 'resize', height: N } is also accepted

Drop-in reporter (load + layout changes + a slow safety interval, deduped):

(function(){
  var last = 0;
  function report(){
    var h = Math.ceil(document.documentElement.scrollHeight) + 2;
    if (Math.abs(h - last) > 2) { last = h; parent.postMessage({ type: 'readme-height', height: h }, '*'); }
  }
  window.addEventListener('load', report);
  if (window.ResizeObserver) new ResizeObserver(report).observe(document.documentElement);
  setInterval(report, 1200);
  report();
})();

THE TRAP: no vh units once you auto-resize. 100vh inside the iframe means "the iframe's height", so a viewport-sized section makes scrollHeight grow every time the parent grows the iframe: an unbounded feedback loop. Give full-bleed sections a FIXED pixel height in the readme build and let the document flow; report scrollHeight; done. (Learned live on adom/orbital-lab: the 100vh app stage chased its own resize until it was pinned at 780px.)

Verify it worked: from the parent page, document.querySelector('iframe[src*=readme]').getBoundingClientRect().height should read your content height, not 600.

Pitfalls recap

  • Edited README.md, page unchanged → shadowing rule; edit readme.html.
  • Iframe stuck at ~600px / inner scrollbars -> you never posted readme-height; add the reporter above.
  • Iframe grows forever -> a vh-sized section is chasing the resize; pin it to fixed pixels.
  • Renders but unstyled/white → you assumed wiki CSS; the iframe gets nothing.
  • Buttons/links dead → sandbox blocks popups/top-nav; keep interactions in-frame.
  • Fetch to the wiki API fails → no same-origin; the readme is display-only.
  • SVG <img> broken → content-type bug (issue #38); use PNG or inline the SVG markup.
  • Works locally, stale on the wiki → you pushed the repo but didn't pkg publish (or vice versa); the readme the page renders comes from the published layer.

Roll your own Adom Babylon9 3D viewer inside readme.html

The 3D tab on every Components page is the Adom 3D Viewer (Babylon 9.5). You can embed and CUSTOMIZE that same engine inside a readme.html, custom layers panels, exploded-view animation, scrub bars, your own toolbars, because readme.html runs scripts. This is proven live (the BQ25792 charger page ran a full animated exploded PCB stackup this way). The facts below are the difference between it working and hours of silent failure.

Where the viewer lives

  • Runtime bundle (load this in readme.html): https://wiki.adom.inc/static/vendor/adom-3d-viewer-babylon9/adom-3d-viewer-babylon9.esm.js It sets window.Adom3DViewerBabylon9 and window.BABYLON, so your script can use the shipped viewer (view cube, layers toolbar, ground shadows, Z-up CAD framing) or raw Babylon on top.
  • Source code: the adom-3d-viewer page (adom-owned), full src/, standalone.ts, vite configs in its Files tab. Read it there before customizing; never fork blind.
  • NEVER roll your own engine. Extend this one. A hand-rolled viewer was rejected hard in review.

⛔ NEVER pin the bundle URL with ?v=

The bundle is a tiny code-split stub (~30 bytes: import "./chunk-<hash>.js"). The chunk hash changes on every rebuild of the wiki. If you pin ?v=..., a cached stub keeps importing a chunk that no longer exists and the viewer 404s AFTER a wiki deploy, weeks later, on a page you already shipped. Load it unpinned.

The sandbox will kill the viewer unless you shim three things

readme.html runs in sandbox="allow-scripts allow-downloads", no same-origin. Consequences, each found the hard way:

  1. localStorage THROWS (SecurityError) in a sandboxed document, and the viewer touches it on init, which aborts the whole rig. Shim it in <head> BEFORE the bundle loads:
    <script>try{localStorage}catch(e){var _m={};Object.defineProperty(window,'localStorage',
      {value:{getItem:k=>_m[k]??null,setItem:(k,v)=>{_m[k]=String(v)},removeItem:k=>{delete _m[k]},clear:()=>{_m={}}}});}</script>
    
  2. blob: images are blocked (img-src has no blob:). Anything doing URL.createObjectURL for textures needs a shim that converts to data: URIs instead.
  3. connect-src 'none': NO fetch/XHR at all. You cannot download a GLB at runtime. Ship model bytes INSIDE the page instead: base64 data: URIs, or (the proven trick for multi-MB models) pack the GLB bytes into a lossless PNG's RGB channels (4-byte little-endian length header), reference the PNG as a normal repo image, and decode via canvas at runtime. Images load fine; fetch does not.

Size budget

A readme.html has roughly a 4-5 MB practical cap. Optimize models before embedding: service-step2glb / gltf-transform (join, weld, prune, Draco) turns a raw ~16 MB board GLB into ~465 KB. Budget models first, code second.

The default vs your own

If you just need "a 3D model on the page", you do NOT need any of this: give the page a component block / GLB and the wiki renders the standard viewer on the 3D tab (see wiki-component). Roll your own INSIDE readme.html only when you need custom behavior the standard viewer lacks (exploded animation, custom panels, scripted tours), and even then, build ON window.Adom3DViewerBabylon9, not beside it.