chore(blog): sync examples/blog with scaffold templates#15
Merged
Conversation
Mirrors what scaffolded apps get via webjs create. Three hooks plus the project-level Claude Code settings: * .claude/hooks/block-prose-punctuation.sh: enforces the em-dash / pause-punctuation rule * .claude/hooks/guard-branch-context.sh: prompts before edits on main * .claude/hooks/nudge-uncommitted.sh: commit-frequency nudge at threshold 4 * .claude/settings.json: registers all three on the right PreToolUse / PostToolUse events * .claude.json: project-level Claude Code config (Playwright MCP, etc.) The blog example serves as a reference for what a real webjs app looks like, so it should match the scaffold output.
Same nudge-uncommitted enforcement as Claude Code, adapted for Gemini CLI's AfterTool hook contract. Output shape is the same hookSpecificOutput.additionalContext format both tools accept. Plus .gemini/settings.json registering the hook on AfterTool for write_file and replace tools.
Same nudge-uncommitted enforcement, adapted for Cursor 1.7+'s afterFileEdit contract. Output uses Cursor's snake_case additional_context field at the top level. Plus .cursor/hooks.json registering the hook.
For agents without a programmatic hooks API (Windsurf, Copilot) and as a baseline source of truth for all agents: * .cursorrules: Cursor's rule file * .windsurfrules: Windsurf's rule file * .github/copilot-instructions.md: GitHub Copilot's rule file * .github/pull_request_template.md: PR template * .editorconfig: editor settings (indent, line endings) All copied verbatim from packages/cli/templates/, including the strengthened commit-frequency wording and hook references from PRs #12 and #13.
Copied from packages/cli/templates/CONVENTIONS.md and
substituted the {{APP_NAME}} placeholder with 'blog'. The
markdown covers architectural conventions (modules layout,
styling, testing, git workflow) and points readers at
webjs check --rules for the canonical lint rule catalogue.
For the case where someone copies the blog example out as a standalone webjs app and runs git init: the pre-commit fires and blocks direct commits to main, runs webjs test, and runs webjs check. Within this monorepo, git's core.hooksPath is set at the root (.hooks/pre-commit at the framework level), so this file serves the standalone-copy scenario, not commits made within the monorepo.
vivek7405
added a commit
that referenced
this pull request
May 21, 2026
Third slice. Wires the slot runtime into the WebComponent lifecycle and
refines render-client.js's slot bind so fallback content is captured
once at compile time and cloned freshly per instance.
component.js:
- Light-DOM activation now runs in two phases:
Phase 1 (before _performRender): if not hydrating, call
captureAuthoredChildren(host) to move authored children into the
slot state's assignment table. The renderer's replaceChildren()
call would otherwise destroy them on first render.
Phase 2 (after _performRender, slots are now live DOM): if the
host was hydrating, call adoptSSRAssignments(host) to record
SSR-placed children so the first projection pass is a no-op.
Then call attachSlotObservers(host) so future authored-child
mutations and slot-name changes drive incremental projection.
- New __isHydrating() helper inspects this.firstChild for the
framework's <!--webjs-hydrate--> marker and records the result
on __hydratedAtActivate so phase two can branch correctly.
- disconnectedCallback now calls detachSlotObservers(host) for
light-DOM hosts. The per-host slot state (assignment table,
pending fragments, last snapshots) is preserved across disconnect,
so a re-attached element picks up where it left off.
render-client.js:
- discoverSlots() now MOVES the slot's authored children into a
fallbackTemplate DocumentFragment stored on the SLOT
PartDescriptor. The cached templateEl's slot becomes empty, so
every clone starts empty too. This eliminates the cloning-the-
fallback-out-of-the-slot dance the previous bind step did.
- bindPart for slot now clones the descriptor's fallbackTemplate
into a per-instance holding fragment and stamps it on the slot
via SLOT_FALLBACK_FRAG for slot.js to swap in. The slot itself
is left untouched at bind time, which makes hydration trivially
correct (SSR-projected children stay in place; the slot-part
just sets up its fallback supply for later transitions).
- PartDescriptor typedef gains an optional fallbackTemplate field.
Verified across 115 existing core unit tests (component, render-client,
render-server, directives, registry, css, html, context, task,
suspense, repeat, testing). All pass.
What remains: render-server.js injectDSD upgrade for SSR slot
substitution (Task #13), then the 62-case test suite (Task #14),
then docs (Task #15).
vivek7405
added a commit
that referenced
this pull request
May 21, 2026
Fourth slice. Upgrades injectDSD to project authored children into
<slot> positions during server-side rendering for light-DOM
WebComponents, with full parity to the client-side projection rules
and the shadow-DOM <slot> spec.
When a light-DOM component's render() output contains <slot> tags,
injectDSD now:
1. Finds the source HTML's matching closing tag for the custom
element by walking forward with depth tracking for nested
same-tag elements.
2. Extracts the authored inner HTML between the element's opening
and closing tags.
3. Partitions the authored HTML by each top-level child's slot=""
attribute. Text nodes, comment nodes, and elements without
slot="" all route to the default-slot bucket.
4. Walks the rendered template's <slot> tags in document order and
substitutes each with a framework-marked
<slot data-webjs-light data-projection="actual"|"fallback">
element carrying either the projected children or the slot's
authored fallback content. Multiple slots with the same name
follow the first-wins rule per spec.
5. Emits one edit spanning the entire opening-to-closing range of
the source element. Inner custom elements among authored
children are processed via the recursive injectDSD call on the
substituted output, not by the outer loop (a new sort + overlap
filter drops the duplicate inner edits that were enumerated
against the original html before substitution).
When a component's render() output has NO <slot> tags, the old SSR
shape is preserved unchanged: edit at the opening tag only, leave
authored children adjacent to the rendered template, closing tag
untouched. This keeps existing components that never used slots
behaving exactly as before.
Shadow DOM components are completely unaffected. Their native <slot>
elements live inside the DSD <template shadowrootmode="open"> block
and the browser handles projection from the host's light-DOM children
into the shadow tree natively. No framework substitution there.
New helpers in render-server.js:
isVoidElement(tag) tag is a void element (br, img...).
findClosingTagInString(html, ...) depth-tracked matching close tag.
extractSlotAttr(attrsRaw) pulls slot="..." value or null.
partitionAuthoredBySlot(html) groups authored inner HTML by slot.
appendStringToMap(map, k, v) concatenating map insert helper.
substituteSlotsInRender(...) walks <slot> tags, emits framework
marker variants with projection or
fallback content, first-wins per
name across the document.
End-to-end smoke (server-only):
<my-card>
<h2 slot="header">Title</h2>
<p>Body</p>
<span slot="footer">Foot</span>
</my-card>
renders to
<my-card><!--webjs-hydrate-->
<div class="card">
<header><slot data-webjs-light data-projection="actual" name="header">
<h2 slot="header">Title</h2>
</slot></header>
<main><slot data-webjs-light data-projection="actual">
<p>Body</p>
</slot></main>
<footer><slot data-webjs-light data-projection="actual" name="footer">
<span slot="footer">Foot</span>
</slot></footer>
</div>
</my-card>
Fallback content surfaces correctly when no children match; first-wins
holds across duplicate same-named slots; shadow DOM passthrough is
unchanged. 127 existing unit tests across component, render-client,
render-server, directives, registry, css, html, context, task,
suspense, repeat, testing, blog-smoke, json-negotiation, and
light-dom-ssr all pass.
What remains: the 62-case test suite (Task #14) and docs +
convention rule (Task #15).
vivek7405
added a commit
that referenced
this pull request
May 21, 2026
chore(blog): sync examples/blog with scaffold templates
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The
examples/blog/reference app was missing all the enforcement plumbing that scaffolded webjs apps now ship with. This PR mirrorspackages/cli/templates/intoexamples/blog/so the example accurately reflects whatwebjs createproduces.What got added
.claude/settings.json,.claude/hooks/block-prose-punctuation.sh,guard-branch-context.sh,nudge-uncommitted.sh,.claude.json.gemini/settings.json,.gemini/hooks/nudge-uncommitted.sh.cursor/hooks.json,.cursor/hooks/nudge-uncommitted.sh.cursorrules,.windsurfrules,.github/copilot-instructions.md,.github/pull_request_template.md,.editorconfigCONVENTIONS.md(with{{APP_NAME}}substituted toblog).hooks/pre-commitAll hook scripts are committed with the executable bit set.
Why per-commit, not one big blob
Six logical chunks, each a single ecosystem or concern:
CONVENTIONS.md.hooks/pre-commitMirrors the same enforcement story PR #13 set up for the scaffold templates.
Notes
core.hooksPathis.hooksat the framework root (set in PR WEBJS_PUBLIC_* env vars accessible as process.env.* in the browser #14). The blog's local.hooks/pre-commitis dormant during monorepo commits but activates if someone copies the blog out andgit inits..claude/hooks/,.gemini/hooks/,.cursor/hooks/DO fire when an AI agent edits withinexamples/blog/, because those tools read project-scoped settings.tailwind-browser.js,web-test-runner.config.js, scaffold's test/browser + test/e2e) are app-feature concerns, not enforcement, and out of scope for this PR.Test plan
npm test, 913/913)diff -rq packages/cli/templates examples/blognow only shows the app-feature differences, not enforcement gapsexamples/blog/out and runninggit init(not done in this PR)