---
name: wiki-readme-html
description: >-
  How to build a custom HTML readme (readme.html) for an Adom Wiki page, the
  fully designed, unsanitized alternative to README.md. Covers when to choose
  html over markdown, the shadowing rule (readme.html makes README.md invisible
  on the page), the sandboxed-iframe render pipeline and its exact limits
  (scripts run, but no same-origin, no cookies, no wiki API, external links and
  popups need care), self-containment (inline CSS/JS, base64 fonts/images,
  /blob/ asset URLs), the six readme variant slots (bare/public/private ×
  html/md), dark-theme styling so the page doesn't flash white, and the
  curl + pup verification loop. Read this BEFORE writing a readme.html and when
  one renders blank, unstyled, clipped, or won't update. Trigger words: html
  readme, readme.html, custom readme, custom html readme, designed readme,
  readme with tabs, readme with accordion, readme iframe, readme sandbox,
  readme shadowed, README_SHADOWED, readme.html not rendering, readme not
  updating, fully custom page layout, custom page body, unsanitized readme.
user-invocable: true
---

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.

```js
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):

```js
(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`](https://wiki.adom.inc/adom/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:
   ```html
   <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.
