← All Pull Requests

wiki-readme-html: fetch works now, and private pages get a capability #30

Open opened by Colby Knox 2026-07-27

Three claims in this skill are no longer true, and they teach people into workarounds they no longer need (this is what adom/wiki#81 turned out to be).

  1. "connect-src 'none': NO fetch/XHR at all" -> the live policy is connect-src 'self' data:; fetch works, credential-free. The PNG-packing trick stays documented as the zero-request option, not the only option.
  2. "no fetch to the wiki API" in the sandbox summary: same correction.
  3. "Fetch to the wiki API fails, the readme is display-only" in troubleshooting: same.

Adds a Private pages section for the capability that shipped with adom/wiki#73 and #81: the wiki mints a short-lived, page-scoped, read-only token into the iframe src and injects a bootstrap that appends it to same-host fetches and media srcs, so a private page's readme can read its OWN files and the frame loads even where cookies cannot ride (cross-site embeds, Bearer clients). Existing readmes need no changes; hand-built URLs can read window.__wikiCap; parent integrations can re-mint at GET /api/pages///cap.

Cross-check against the authoritative contract doc: git-wiki docs/readme-sandbox.md.

Diff

--- a/skills/wiki-readme-html/SKILL.md+++ b/skills/wiki-readme-html/SKILL.md@@ -1,225 +1,255 @@-----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.+---+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 localStorage. `fetch` DOES work (`connect-src 'self' data:`),+  but credential-free: you see what an anonymous visitor sees. On a PRIVATE page+  the wiki injects a short-lived page-scoped capability so the readme can still+  read its OWN page's files (see "Private pages" below). 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).++## Private pages: the injected capability (adom/wiki#73, #81)++A readme on a PRIVATE page used to render blank or half-broken: its fetches are+credential-free, so its own page's assets hit the privacy wall, and in embeds+where the session cookie cannot ride (the wiki inside another app's webview, or+a Bearer-authenticated client) even the frame document 404'd.++Now, when the wiki serves the frame to a viewer who has already passed the+page's access check, it mints a short-lived CAPABILITY: the token rides in the+iframe `src` (so the document loads anywhere) and a small injected bootstrap+appends it to same-host `fetch()` calls and static media `src`s (so the+readme's own asset loads work unchanged).++- Existing readmes need NO changes.+- Code that builds URLs by hand: read `window.__wikiCap` and append+  `?cap=<token>` yourself.+- A parent-page integration that outlives the 15-minute TTL can re-mint at+  `GET /api/pages/<owner>/<slug>/cap`.+- The capability is page-scoped, READ-only, expiring, and unmintable from+  inside the frame. It is not a session: no API authority, no writes, no other+  page's files.+- Public pages carry no token at all (their frame is anonymously readable and+  stays cacheable).++## 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 returns anonymous-visitor data (opaque origin, no credentials ever). On a private page, its own page's files still work via the injected capability; anything else is anonymous-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 'self' data:`: fetch WORKS, credential-free.** (This used to be+   `connect-src 'none'`; the policy was relaxed, and on a private page the wiki now injects a+   page-scoped capability so the readme can read its own repo files. See "Private pages".)+   Fetching a GLB at runtime is therefore fine. The old workaround still works and is still the+   safest bet for multi-MB models with zero request overhead: 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.++### 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.+

Comments

No comments yet.

Log in to comment.