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).
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)
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 main2026-07-13 01:37:13 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Follow-up to PR #3.
Head code end-to-end (was: never reached published sites)
useWhpApi.ts):save()now sendshead_code+ fulldesigntokens;load()restores them viaupdateDesign(backward-compatible with old project.json).web-files/api/site-builder.php, shipped separately):generateCompiledHTML()gains a head-code slot;handlePublishreadshead_code/design.headCodefrom project.json and injects it raw into each published page's<head>(site owner's own markup, by design).php -lclean.6 Minors from the adversarial review
addPageids unique even within one millisecond.scopeId→ djb2 hash (collision-free across case/punctuation; 50k-id no-collision verified).safeUrlblocksdata:image/svg+xml(otherdata:image/*still allowed).sandboxon iframes (hook added+removed per call, no leak).531 tests green (from 503); build clean.
🤖 Generated with Claude Code
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>