fix: inline the knowledge base CSS and layer sub-app CSS so a fragment head swap cannot mangle it - #99
Conversation
…t head swap cannot mangle it Embedded as a web fragment, Library → app → Library left the catalog rendered under the app's stylesheet, and the masthead on the next app wore two themes. reframed owns the fragment's <head> and has lost, relocated and duplicated head <link>/<style> nodes across ClientRouter swaps (web-fragments #297); the knowledge base stylesheet was one of those links, and a sub-app's link outliving its page restyled the knowledge base's own markup. Two defences, neither depending on what the runtime does to <head>: - The knowledge base stylesheet is inlined into every <body> (a `?inline` import in Base.astro) and never linked from <head>; the body is replaced whole on every navigation, so the CSS is present exactly when its page is. `build.inlineStylesheets: 'always'` folds Astro's own route-announcer rule in too, so no page links a stylesheet of its own. dist/style.css is still published, now from the inline block. - Every sub-app stylesheet — files in copyAssets(), inline <style> blocks in transform.js — is wrapped in the `kb-app` cascade layer, below the knowledge base's rules and utilities, and the masthead and catalog (`.kb-shell`) sit behind an `all: revert` fence in the `kb-reset` layer (SVG excluded: revert discards presentation attributes and every icon went solid black). Layer order `theme, base, kb-app, kb-reset, components, utilities` is emitted by the layout head, atop every rewritten sub-app stylesheet and in knowledge-base.css, so it holds whichever the browser parses first, and keeps Tailwind's Preflight below the app's own CSS. tests/css-isolation.spec.js reproduces the round trip in both embeddings and injects a leak on purpose; transform.spec.js covers the wrapper; build-integrity and standalone assertions follow the new shape. Closes #98 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Tailwind drops a bare `@layer a, b;` statement from its output, so the order written at the top of knowledge-base.css never reached the compiled sheet: the inline block opened with Tailwind's own `theme`, `base`, … layers and named `kb-app` nowhere. Any runtime that parses the body block before the layout's <head> — a head-relocating reframed, for one — would then declare `kb-app` last, above the `kb-reset` fence. Base.astro now prepends LAYER_ORDER to the inline block, the stylesheet carries a note instead of the dead statement, and build-integrity asserts every page's inline block opens with the order. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Follow-up: report of the mkdocs page losing its CSS on client navigationReported after this PR: embedded, browsing the example docs' showcase page and switching to the mkdocs part leaves the docs page without its CSS; a hard reload of the same URL renders fine. Not reproduced locally. Checked with the test host (Express gateway,
One real hole found and closed in 6e67c45: Tailwind drops a bare To pin the report down I need, from the environment where it happens: browser + version; the host's [...document.querySelectorAll('link[rel~=stylesheet], style')].map(n => [n.localName, n.parentNode.localName, n.getAttribute('href') ?? n.textContent.slice(0, 40), n.sheet ? n.sheet.cssRules.length : 'NO SHEET', n.sheet?.disabled ? 'disabled' : ''])plus |
catalog → showcase → docs → Customising → Adding Pages by ClientRouter, in both embeddings; each page's sub-app content styling and stylesheet inventory must equal a hard load of the same URL taken afterwards in the same browser. Pins the reported symptom — the mkdocs part losing its CSS after client navigation — without pinning literal values. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Arms the fragment probe before the first click and, after every hop, requires the host document alive, the reframed iframe unchanged, and exactly N ClientRouter swaps and N host view transitions for N hops. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reproduced, real host — the reported symptom is a same-route re-navigation, not covered by 977362b/c6b1f35Ran the new However, that test only visits distinct new routes each hop ( Reproawait page.goto('http://localhost:4200/knowledge-base/user-guide/docs/');
await page.locator('#sidebar > a:nth-child(2)').click(); // -> Customising
// h1: 36px / weight 800 — correct
await page.locator('#sidebar > a:nth-child(2)').click(); // -> Customising again, same route
// h1: 16px / weight 400 — Tailwind Preflight wins, docs styling lostWhat changes on the second hopDiffing the fragment's shadow tree before/after:
This is consistent with reframed re-parenting content into a synthetic Suggested next stepExtend Happy to open a PR with the failing test + fix if useful. |
…ed host Reproduced the report — the mkdocs pages losing their CSS on client navigation, fine on reload — with server-side piercing on, the mode a production Angular SSR gateway runs in. It shows on the very first navigation, not only on a same-route repeat. Cause: on a pierced page the sub-app's <link rel="stylesheet"> has already been moved once, by reframed portalling the server-rendered host into the <web-fragment>'s shadow root with moveBefore(). The head swap in embedded-transitions.js then moved it again with moveBefore() to keep the new page's order, and Chromium drops the sheet from the shadow root's applied stylesheets on that second move: the element keeps its .sheet, styleSheets no longer lists it, the page renders unstyled until a reload. The client-rendered harness never hit it because there the link had only been inserted, never moved. The swap now leaves every reused head node exactly where it is and inserts only new nodes, each ahead of the next reused one. Head order no longer matters for the cascade — every stylesheet declares the layer order itself — so nothing is lost by keeping the old anchors' order. tests/host/server.mjs gains KB_PIERCING=true, playwright.config.js a chromium-pierced project on :4202, so every embedded test now runs against both hosts. The host stand-in's router script is deferred: a <web-fragment> created before web-fragment-host is defined cannot adopt a pierced host (portalHost is not a function) and silently falls back to client rendering — the same ordering an Angular host gets from calling initializeWebFragments() in main.ts. The css-isolation sheet inventory compares applied/not-applied rather than rule counts, since a pierced page carries every inline rule twice. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reproduced and fixed in 0f0c769 — piercing was the missing variable, and it is
|
… fix stale pierced CSS (#100) Follow-up to #99. There are three commits, and each one can be reviewed on its own. ## 1. `fix`: absolute CSS URLs, and no stale portal copies in a pierced fragment Measured with the embedded harness in all three modes: pierced, bound and unbound. - **The first app's CSS followed the visitor everywhere (pierced only).** When reframed portals a server-rendered fragment, it copies every linked sub-app stylesheet into an adopted constructed sheet. It drops the copy only once it sees the sheet fetched again. On the `moveBefore()` path the `<link>` is never re-fetched, so the copy stays for the life of the fragment. `embedded-transitions.js` now drops the knowledge base's own copies before the first swap. It recognises them by the layer-order statement that every built sub-app stylesheet opens with. - **Relative `url()` in sub-app CSS returned 404s.** The copy resolves URLs against the host document, and the host router strips the trailing slash. So `showcase.css` requested `/knowledge-base/docs/assets/images/hero.png`. `copyAssets()` now makes every `url()` and `@import` absolute, resolved against the stylesheet's own URL. It uses `rewriteCssUrls()`, shared with the inline-style rewrite. - **Considered and rejected: serving scripts and styles from a separate path prefix.** The gateway routes requests by `sec-fetch-dest`, not by path. A build retargeted to `/__wf/knowledge-base/` reproduced both failures unchanged. ## 2. `feat(contract)`: rule IDs and a checker for published docs - **`contract/RULES.md`** lists every rule a published docs site is checked against, by ID (`KB-HTML-003` …), with its severity, the reason and the fix. `actions/lib/rules.js` is the code side, and the self-test fails if the two disagree. - **`actions/lib/check.js`** replaces `verify-html.js`. It parses HTML with htmlparser2 and CSS with postcss. The existing checks keep their severity and wording, now prefixed with the rule ID. - **New rules, all warnings**, so no repo pinned to `@v1` starts failing: - inline `on*` handlers and `javascript:` URLs, which never run under `script-src 'self'`; - a site-level `<header>`; - links to non-HTML files in the app; - `!important`; - dark mode; - scripts, stylesheets or fonts from another origin; - fetches of page-relative URLs. - **`actions/lib/check-cli.js`** runs the same checks without publishing, for a docs repo's PR CI or an agent. It supports `--json` and `--strict`. ## 3. `feat(skills)`: audit mode for `kb-docs-add` An already-onboarded repo now takes the audit path in `references/audit.md`: 1. Check the workflow setup. 2. Build what the workflow publishes. 3. Run the checker from a scratch clone. 4. Fix each finding at its source, by rule ID. 5. Report what the checker cannot see. ## Testing - **`npm test`: 372 of 372 passed** on the tree for commits 1 and 2. Commit 3 only touches the skill, its fixtures and `CLAUDE.md`, and `tests/skill.spec.js` passes on it (30 of 30). - **The new tests fail without the fix in commit 1.** In the pierced project: `build-integrity` (relative `url()`) and `css-isolation` ("starting on an app page"). - **`npm run selftest` in `actions/` passes.** It includes the new `check.selftest.js`: one case per rule, a clean site that must produce no findings, the CLI, and the RULES.md ↔ rules.js sync check. - **The checker on real artifacts:** - the vendored `docs-example` fixture: 0 errors, 28 genuine warnings; - the single-page bundle: no findings. - **Eval 4:** `tests/fixtures/kb-docs-add/onboarded-mkdocs` is an onboarded mkdocs repo with problems planted in it. - Built with mkdocs 1.6.1, it gives 3 errors and 18 warnings. The fixes the skill prescribes bring it to no findings. - Two runs of an agent following only the skill met 12 of 13 and then 14 of 14 assertions. The miss in the first run was an assertion that was too strict. ## Notes for reviewers - **Nothing reaches docs repos until a new `v1.x` tag moves `v1`.** Until then, `audit.md` tells agents to clone without `--branch`. - **`htmlparser2` and `postcss` become direct dependencies of `actions/`,** at the versions its lockfile already carried. - **The knowledge base build does not run the checker yet.** It doesn't install the `actions/` dependencies, so that needs a decision on how. Refs #67. The checker now reports inline handlers (`KB-HTML-005`), but the build still does not strip them. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Closes #98.
What
Embedded as a web fragment, Library → app → Library left the catalog rendered under the app's stylesheet, and the masthead on the next app wore two themes. reframed owns the fragment's
<head>and has lost, relocated and duplicated head<link>/<style>nodes across ClientRouter swaps (web-fragments/web-fragments#297). The knowledge base stylesheet was one of those links; a sub-app's link outliving its page restyled the knowledge base's own markup.Two defences, neither depending on what the runtime does to
<head>:<body>(?inlineimport inBase.astro), never a head<link>. The body is replaced whole on every navigation, so the CSS is present exactly when its page is.build.inlineStylesheets: 'always'folds Astro's own route-announcer rule in too — no page links a stylesheet of its own any more.dist/style.cssis still published for outside consumers, now written from the inline block.copyAssets(), inline<style>intransform.js) is wrapped in@layer kb-app, below the knowledge base's rules and utilities. The masthead and catalog (.kb-shell) sit behind anall: revertfence in@layer kb-reset, with the few Preflight defaults they rely on restated; SVG is excluded becauserevertdiscards presentation attributes (every icon went solid black in the first cut). The ordertheme, base, kb-app, kb-reset, components, utilitiesis emitted by the layout head, atop every rewritten sub-app stylesheet and inknowledge-base.css, so it holds whichever the browser parses first, and it keeps Tailwind's Preflight below the app's own CSS (docs headings keep their theme size).src/utils/css-layers.jsowns the contract.Known limit:
!importantin a sub-app sheet outranks the fence (importance inverts layer order).Note on reproduction
The Express test host here (both embeddings,
piercing: false) does not reproduce the mangling — the swap from #97 keeps the head clean in it. The production host is a different runtime, so this makes the knowledge base independent of head handling rather than fixing one runtime's behaviour.Tests
tests/css-isolation.spec.js: the round trip in bound and unbound embeddings leaves catalog + masthead computed styles identical to first load (baseline pins the icon outline); a leak injected on purpose — the fixture stylesheets plus a hostile layered sheet appended to the fragment head — applies outside the fence and changes nothing inside; a docs page keeps its heading sizes.transform.spec.js:layerSubAppCss()—@charsetdropped, leading@imports hoisted and layered, later ones untouched, idempotent.build-integrity.spec.js: no page links an Astro stylesheet from its head, every page opens its body with the inline stylesheet, the layer order opens every head, every served sub-app stylesheet is wrapped.standalone.spec.jsassertions follow the new shape.Embedded suite 180/180, standalone 22/22,
astro checkclean.Docs
CLAUDE.md gains a CSS Isolation section, AGENTS.md a rule for the invariant,
astro.config.mjscomments updated.🤖 Generated with Claude Code