Site builder: ship head code to published pages + fix 6 adversarial-review Minors #4

Merged
jknapp merged 4 commits from headcode-and-minors into main 2026-07-13 01:37:13 +00:00
Owner

Follow-up to PR #3.

Head code end-to-end (was: never reached published sites)

  • Frontend (useWhpApi.ts): save() now sends head_code + full design tokens; load() restores them via updateDesign (backward-compatible with old project.json).
  • Backend (web-files/api/site-builder.php, shipped separately): generateCompiledHTML() gains a head-code slot; handlePublish reads head_code/design.headCode from project.json and injects it raw into each published page's <head> (site owner's own markup, by design). php -l clean.

6 Minors from the adversarial review

  • M1 Navbar ids/hover now node-scoped (no two-navbar collision).
  • M2 Gallery lightbox: focus trap + store/restore focus.
  • M3 addPage ids unique even within one millisecond.
  • M4 scopeId → djb2 hash (collision-free across case/punctuation; 50k-id no-collision verified).
  • M5 safeUrl blocks data:image/svg+xml (other data:image/* still allowed).
  • M6 HtmlBlock forces a restrictive sandbox on iframes (hook added+removed per call, no leak).

531 tests green (from 503); build clean.

🤖 Generated with Claude Code

Follow-up to PR #3. ## Head code end-to-end (was: never reached published sites) - Frontend (`useWhpApi.ts`): `save()` now sends `head_code` + full `design` tokens; `load()` restores them via `updateDesign` (backward-compatible with old project.json). - Backend (`web-files/api/site-builder.php`, shipped separately): `generateCompiledHTML()` gains a head-code slot; `handlePublish` reads `head_code`/`design.headCode` from project.json and injects it raw into each published page's `<head>` (site owner's own markup, by design). `php -l` clean. ## 6 Minors from the adversarial review - M1 Navbar ids/hover now node-scoped (no two-navbar collision). - M2 Gallery lightbox: focus trap + store/restore focus. - M3 `addPage` ids unique even within one millisecond. - M4 `scopeId` → djb2 hash (collision-free across case/punctuation; 50k-id no-collision verified). - M5 `safeUrl` blocks `data:image/svg+xml` (other `data:image/*` still allowed). - M6 HtmlBlock forces a restrictive `sandbox` on iframes (hook added+removed per call, no leak). 531 tests green (from 503); build clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
jknapp added 4 commits 2026-07-13 01:36:56 +00:00
Extend the save payload with head_code + design so the backend can
inject SiteDesign.headCode into published pages, and restore design
tokens on load() so the editor reflects the last-saved state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
M-1: Navbar.toHtml emitted a fixed id="navbar-links" and unscoped
.navbar-link/.navbar-cta :hover selectors -- two Navbars on one page
collided on the duplicate id and cross-applied each other's hover
colors (later <style> block wins in the cascade). Scope both on the
Craft node id via scopeId(), matching the Menu/Tabs pattern: the links
container gets a unique id, aria-controls/the hamburger toggle script
reference it, and the hover rules are prefixed with a per-instance
class on the <nav> root.

M-2: Gallery lightbox had no focus management -- opening it left focus
wherever it was (behind the now-visible overlay) and closing it never
restored it. The inline script now stashes document.activeElement on
open, moves focus to a new accessible close button, traps Tab on the
close button while the dialog is open, and restores the saved focus on
close (Escape, backdrop click, or the close button).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
M-3: PageContext.addPage minted ids from bare `page_${Date.now()}` --
two adds inside the same millisecond collided on id, so a subsequent
rename/delete/save silently acted on both pages at once. Added a
module-scoped monotonic counter combined with the timestamp
(nextPageId(), exported for direct unit testing) and used it
everywhere an addPage-style id is minted (addPage, replaceAllPages).

M-4: scopeId() lowercased + stripped non-alphanumeric characters from
the node id into a slug, so two node ids differing only by
case/punctuation (e.g. "AbC" vs "abc", or "a-b" vs "ab") collapsed
onto the same scope -- defeating the whole point of scoping ids per
node (M-1/Menu/Tabs/ColumnLayout/Gallery/etc. all rely on it). Now
hashes the raw node id via the existing djb2 stableHash() instead of
slugifying it: still deterministic (same id -> same scope) and a valid
CSS ident, but collision-resistant across case/punctuation. This
changes the exact scope strings Menu/Tabs/ColumnLayout/Gallery/etc.
emit -- expected and fine, since none of their tests pinned an exact
scope value (all already asserted structure/uniqueness).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
M-5: safeUrl() blocked javascript:/vbscript:/data:text/html but
allowed data:image/svg+xml, which can execute inline <script>/onload=
when loaded as a document/navigation target despite its "image" MIME
type (defense in depth -- not currently reachable to execution via
this sink, but closing it). Added `data:image/svg+xml` to the existing
DANGEROUS_SCHEME_PREFIXES check, so it's caught after the same
entity-decode/whitespace-strip/lowercase normalization used for the
other blocked schemes (obfuscated variants included). Other
data:image/* types (png/jpeg/gif/webp, ...) remain allowed unchanged.

M-6: HtmlBlock's purifyHtml() allowed <iframe src> through with no
`sandbox` attribute -- a clickjacking/phishing vector even with
DOMPurify already stripping script/on*=. Added a DOMPurify
afterSanitizeAttributes hook, scoped tightly to each purifyHtml() call
(added right before sanitize(), removed in a finally right after) so
it can't leak onto other DOMPurify uses or accumulate duplicates
across repeated calls, that force-sets a restrictive sandbox
(allow-scripts allow-same-origin allow-popups allow-forms -- no
allow-top-navigation) and referrerpolicy=no-referrer on every iframe
that survives sanitization.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jknapp merged commit 3e43aee6e9 into main 2026-07-13 01:37:13 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: cloud-hosting-platform/site-builder#4