feat(website): launch /blog with 11 grounded long-form posts#80
Merged
Conversation
Infrastructure: - `blog/<slug>.md` with frontmatter at the repo root, mirroring the changelog/ shape. Hand-rolled frontmatter parse + markdown renderer in the page handlers (no markdown library, no client runtime). - `website/app/blog/page.ts`: index page listing all posts sorted by date DESC. Layout boundary matches /changelog (max-w-[840px]). - `website/app/blog/[slug]/page.ts`: per-post page with full SEO metadata (title, description, og:title, og:description, og:type, og:url, twitter:card, publishedTime, author, tags). canonical URL per post. Custom-positioned bullets that stay inside the layout via `before:` pseudo-elements. Code blocks with internal padding so long lines do not stick to the left border when they overflow-x. - Nav: `/blog` link added to both desktop and mobile header. - Railway watch path: `/blog/**` added to the website service via the railway agent, so future blog edits trigger redeploys. Posts, each anchored in actual git history / PR descriptions / source-file docstrings (not invented details): - `why-webjs` (origin/thesis, derived from the author's existing post at heyvivek.com; tagline "tiny in size, not in power") - `betting-on-lits-mental-model` (the API parity rationale; 127 lit-ported tests from PR #31's title) - `strip-types-not-esbuild` (the Node 24 stripper migration in PR #9; cache details from packages/server/src/dev.js) - `signals-replaced-setstate` (PR #43, breaking change; TC39 Stage 1 shape; algorithm description from signal.js docstring) - `light-dom-slots-with-full-parity` (PR #8 / #44; polyfill design from slot.js docstring) - `the-naming-saga` (the wjs/webjscli/webjsdev/create-webjs arc from this PR's own development) - `ai-first-is-plumbing` (AGENTS.md + the multi-tool config files + hooks + lint rules, all verifiable in scaffold templates) - `file-based-routing` (router.js JSDoc lists the conventions; same Next.js shape, with the divergences spelled out) - `client-router-turbo-drive-style` (router-client.js docstring + ssr.js's X-Webjs-Have handling) - `why-not-lit-as-a-dependency` (SSR + decorators + the AI-reads-node_modules angle the user surfaced) - `built-ins-auth-session-cookies-cache` (the four-method cache store interface from cache.js's CacheStore typedef, the Remix- shaped Session class, the NextAuth-shaped createAuth()) Typography: - 17px paragraph at 1.8 leading, my-7 spacing. - Title at clamp(36px, 6vw, 56px), more presence on the page. - Description in serif italic at 19px. - Headings at clamp 21-34px with strong vertical rhythm. - Code blocks at 13px monospace with px-6 py-5 padding inside the code element (not the pre) so overflow-x scrolls cleanly. - Footer pad-top + mt-28 so the "All posts" link does not collide with the last paragraph. Markdown supported by the renderer: - # / ## / ### headings (h2 / h3 / h4 in output) - Paragraphs - Bulleted lists with custom-positioned markers - `> ` blockquotes with accent border - ```fenced``` code blocks - Inline: **bold**, *italic*, `code`, [link](url)
Three concrete formatting fixes that all stemmed from one root cause:
Tailwind named-scale utilities (mt-20, my-8, mt-14, my-7) were NOT
landing in the compiled tailwind.css. The dev-server's watcher had
not picked them up from the new blog/[slug]/page.ts. So the page
rendered with classes that resolved to no CSS at all, which is why
the user saw:
- Headings sticking to the previous paragraph (no mt-20).
- "All posts" footer link colliding with last paragraph.
- Code blocks with no vertical breathing room.
Fix: switch every spacing utility to its arbitrary-value form
(`mt-[80px]` instead of `mt-20`, `my-[28px]` instead of `my-7`,
etc.). Arbitrary-value classes get JIT-emitted from the literal
token in source, so they compile regardless of whether the named
scale has been brought into the build.
Also brings the [slug] page's max-width from 760px back up to
840px to match /changelog and /blog index, restoring the layout
boundary parity the user pointed out twice.
The compiled tailwind.css is gitignored (regenerated at deploy
time), so this commit ships only the source change. The classes
JIT correctly on the next `tailwindcss` invocation.
Bundle of post-grade improvements:
- Code block padding moved inside the <code> with px-[24px]
py-[20px], so overflow-x preserves padding on both sides.
- List items use `before:content-['•']` absolute-positioned
markers so bullets stay inside the layout column.
- Title at clamp(36px, 6vw, 56px), description in serif italic.
- Footer with mt-[128px] pt-[40px] for the "All posts" link.
…webjs's own convention
The website was stuffing file-reading, frontmatter parsing, and markdown
rendering directly inside `app/blog/page.ts`, `app/blog/[slug]/page.ts`,
and `app/changelog/page.ts`. That violates the layout we tell every
scaffolded webjs app to follow in AGENTS.md:
app/ ROUTING ONLY. Thin route adapters.
modules/<feature>/ Feature-scoped queries + utils + types.
lib/ App-wide helpers.
Dogfooding fix. New layout:
website/lib/frontmatter.ts Shared parser (browser-safe).
website/modules/blog/queries/
list-posts.server.ts Read all posts, return metadata.
get-post.server.ts Read one post by slug, return body.
website/modules/blog/utils/render-post.ts Long-form markdown renderer.
website/modules/blog/types.ts Post / PostWithBody.
website/modules/changelog/queries/
list-entries.server.ts Read all changelog entries.
website/modules/changelog/utils/
render-entry.ts Compact-card markdown renderer.
pkg-badge.ts Color-coded package pill.
website/modules/changelog/types.ts Entry.
The route files at `website/app/blog/page.ts`,
`website/app/blog/[slug]/page.ts`, and `website/app/changelog/page.ts`
are now thin adapters that import from the modules and render the
result. None of them do file IO or string-parsing directly.
Same routes, same output. Logic moved to where AGENTS.md says it
should live.
The `'use server'` directive on each query file makes the file
source-protected (browser imports get a throw-at-load stub) and
RPC-callable (so a client component could in principle import
`listPosts` if it needed to, and the dev server would rewrite the
import into an RPC stub). For the current pages, both query files
are only called server-side from the page's default export.
Sharing across the two features: `parseFrontmatter()` is identical
for both, so it lives in `website/lib/frontmatter.ts` (lib/ scope
because it's cross-feature). The inline-markdown regexes diverge
between the two renderers (different code-block sizes, different
heading typography), so each module has its own renderer rather
than parameterizing a shared one. Two callers, two short
implementations, no premature abstraction.
…myself'
The post leaned hard on critiquing other frameworks (stack traces in
minified bundles, convention drift between engineers, etc.). Reframed
per the user's direction: the story is "I wanted a framework close
to web standards with the Next.js-style DX I enjoy. Could not find
one I personally liked. Built one for myself. AI-first followed
naturally from building it from scratch in 2025."
New shape:
- Open with what I wanted (web standards + Next.js DX), the search,
the not-finding, the decision to build my own.
- "Close to web standards" section explains the platform-first
architecture (native web components, lit-shaped public API on
top), without comparing other frameworks unfavorably.
- "How small that lets the framework be" surfaces the concrete
5-10% of Next.js size claim, with the feature parity list and
the explanation: the platform does the heavy lifting (web
components, Node 24 strip-types, HTTP/2 multiplex, CSS vars).
- "Why AI-first followed naturally" reframes the AI-first content
as the consequence of building from scratch in 2025, not the
starting motivation. Same content, different positioning.
Removed:
- "watching AI agents try to write code in those frameworks ..."
paragraph that read as a critique of competitors.
- "stack traces that pointed at minified bundle positions the
agent could not read" line.
- "conventions that two engineers would interpret differently"
line, which read as a dig.
- The "why web components, not React/Vue/Svelte/Solid?" framing.
Replaced with "what close-to-standards means" which states the
positive case without the comparison.
Title and intro keep the AI-era angle for SEO and for the inaugural-
post role of why-webjs.md.
The two posts (`betting-on-lits-mental-model.md` and
`why-not-lit-as-a-dependency.md`) argued the same point from two
angles, with substantial content overlap. Merged the strongest
material from both into one post and deleted the redundant file.
Kept the `betting-on-lits-mental-model` slug (better SEO surface,
nuanced title). Retitled to "Lit-shaped, without depending on lit"
to flag the dual angle directly.
The merged post is now structured as:
1. The "minimal version: just re-export lit" code, and why I
considered it for a week before writing my own runtime.
2. What I wanted to KEEP from lit (the API surface the corpus
already knows, with the four-agents experiment as evidence).
3. Why I did NOT depend on lit, broken into four reasons in load-
bearing order:
a. SSR (the killer, with the four lit-ssr structural limits)
b. The decorator + erasable-TypeScript conflict
c. The AI-agent-reads-node_modules readability argument
d. Fine-grained control over edge cases
4. What an LLM sees when it reads webjs (the code-diff comparison)
5. What the runtime ownership cost (lost lit bug fixes,
lost cleverness, ~10 KB size delta)
6. The "what if lit ships SSR + slots tomorrow" hypothetical
7. Not a dig at lit
8. Reading the actual implementation
…ault
Most web-components frameworks default to shadow DOM (lit, Stencil,
FAST). webjs flips the default: every component renders in light DOM
unless it sets `static shadow = true`. The post walks through six
concrete benefits in load-bearing order:
1. Tailwind utility classes apply (the load-bearing one for webjs).
2. CSS stays cache-friendly: external stylesheet hit once by the
browser, instead of inline `static styles` shipped per page.
3. document.getElementById, querySelector, closest just work
without shadow-piercing.
4. Accessibility behaves the way ARIA + form association specs
assume (aria-labelledby across roots, form data carrying
light-DOM input names, no formAssociated/ElementInternals
ceremony).
5. Playwright / Puppeteer / Web Test Runner selectors work
without `>>>` pierce syntax. Agents writing tests reuse the
same selectors they write in components.
6. SEO + crawler reach is more reliable in initial HTML. Modern
Googlebot handles DSD correctly, but the long tail of
crawlers, social-card scrapers, RSS readers, and archival
bots is more variable. Light DOM is the lower-variance answer.
Counters the "but scoping!" argument by pointing at the two real
shadow-DOM use cases (third-party embeds, design-system primitives
meant to drop into hostile pages) and notes that Tailwind utilities
sidestep the leakage thought experiment for app code.
Links to the existing light-dom-slots-with-full-parity post for
the slot-projection story, which is what unblocks light DOM as a
serious default (most frameworks treat <slot> as a shadow-only
feature, webjs ships full parity in both modes).
Dated 2025-12-22, slotted between why-webjs (2025-12-15) and
light-dom-slots-with-full-parity (2025-12-30) so the
foundational decision lands before the slot deep-dive.
Native web components default to LIGHT DOM. If a custom element does
not call attachShadow(...), there is no shadow root. Lit picked a
different default for its LitElement class (it attaches a shadow root
in the constructor unless you override createRenderRoot to return
this), and because lit is what most developers and most AI training
data treat as canonical web-components style, the perception has
shifted toward "shadow DOM is the default."
The opening of the post implied the latter. Rewritten to state the
platform-level fact accurately:
- Native web components default to light DOM.
- lit defaults to shadow DOM by attaching a shadow root in
LitElement's constructor.
- webjs aligns with the platform default, not lit's default.
This is also a sharper framing for the rest of the post: the
"benefits of light DOM" become "benefits the platform already gives
you that lit's default opts out of."
Description in the frontmatter updated to match.
…framing
Web-verified the original claim: lit, Stencil, and FAST all default
to shadow DOM. With three independent data points, the framing is
sharper than "lit picked a different default" alone:
- lit: LitElement attaches a shadow root in its constructor
unless you override createRenderRoot to return this.
- Stencil: components default to shadow DOM. The stencil generate
CLI emits shadow-enabled components and the @component
decorator's shadow field defaults to true.
- FAST: FASTElement automatically attaches a ShadowRoot and
renders the template into it.
All three are cited inline with links to their official docs.
The reframing is "the three libraries developers actually learn
web components from all default to shadow, and that's where the
'shadow is the default' perception comes from. The platform itself
does not share that default."
That is a stronger argument for webjs's choice than the
single-library version.
Stencil's @component decorator defaults shadow: false (light DOM). You opt INTO shadow with @component({ shadow: true }). The earlier claim that Stencil "defaults to shadow DOM" conflated the CLI scaffolder default (the `stencil generate` template enables shadow) with the framework's actual decorator default (which does not). Verified by fetching both stencil.io/docs/styling and stencil.io/docs/component: - styling: "To use the Shadow DOM in a Stencil component, you can set the shadow option to true in the component decorator." - component: "If shadow is set to false, the component will not use native shadow DOM encapsulation." Default is false. The post now states the accurate picture: - lit defaults to shadow at the framework level - FAST defaults to shadow at the framework level - Stencil defaults to light at the framework level; the CLI scaffolder is what produces shadow-enabled components The "popular libraries pick shadow" framing is now scoped to lit and FAST. Stencil is called out as the precedent that backs webjs's choice: same underlying default (light), shadow as opt-in. This is actually a stronger argument for webjs than the previous "everyone defaults to shadow" framing, because it shows there is existing precedent in the ecosystem for light-DOM-by-default at the framework level.
…quirement The post was too Tailwind-centric. Easy to read it as "you need Tailwind to use webjs," which is wrong: the framework is agnostic about the styling story. Tailwind is the scaffold default because it pairs well with the rest of the stack, but vanilla CSS, CSS modules, BEM, hand-written stylesheets, or another utility framework all work the same way. Changes: - Added an explicit note after benefit #1: webjs does NOT require Tailwind, the benefit (external CSS cascades into light-DOM components) is general, Tailwind is just the concrete example in the post. - Benefit #2 ("CSS cache-friendly"): broadened from "tailwind.css" to "an external stylesheet (the scaffold's tailwind.css, or your own app.css, or whatever you write)." - Scoping section: added a mention of BEM / class-prefix discipline as a non-Tailwind way to avoid leakage. Linked to the framework's `light-dom-css-prefix` lint rule that catches unprefixed selectors in vanilla CSS for light-DOM components. - Summary bullets: "Tailwind utility classes apply" -> "External CSS applies without escape hatches: Tailwind, vanilla, CSS modules, BEM, whatever you bring." - Closing paragraph: "light DOM with Tailwind by default" -> "light DOM by default, with Tailwind as the scaffold default but no framework-level requirement to use it." The argument is now framed as light-DOM-vs-shadow, not as Tailwind-evangelism. Tailwind users still see Tailwind-flavored examples throughout, but non-Tailwind users see the framework working for them too.
…personal DX The opening listed three options (invent, Rails-shaped, Next.js). Rails was never seriously in the running and the inclusion read as filler. Stripped to two: custom or Next.js. The decision is now framed primarily around the Next.js DX I personally enjoy, with the corpus-priors argument as a secondary reason rather than the load-bearing one.
The post is bylined by Vivek and written in first-person voice. The 'the user pointed out' phrasing slipped through, treating someone else as the source of the insight. Now reads 'I realized,' matching the rest of the post's voice. Audited the other blog posts for similar third-person 'user' references. The remaining mentions are about end-users of the framework (package.json size, function-wrapping callers, store config etc.) which is the correct use of the word.
Reduced mt from 128px to 72px and pt from 40px to 32px, halving the gap between the last paragraph and the bottom 'All posts' link. Earlier value was overcompensating after the user pointed out the link sticking to the paragraph; this lands in the comfortable middle.
Previous change went from 168px to 104px which the user said was too aggressive. Dialed to 140px (mt-[104px] pt-[36px]), a modest ~17% reduction from the original 168px rather than the 38% cut.
4 tasks
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
A new /blog section on webjs.dev with 11 story-telling posts on the framework's design decisions. Long-form, first-person founder voice, anchored in real git history and source code (not invented details).
Infrastructure
blog/<slug>.mdat the repo root, frontmatter-driven, same shape aschangelog/.website/app/blog/page.tsfor the index,website/app/blog/[slug]/page.tsfor individual posts.#/##/###, paragraphs,>quotes, bulleted lists with custom-positioned markers,```code fences, inline**bold**,*italic*,`code`,[link](url))./bloglink added to both desktop and mobile nav inwebsite/app/layout.ts./blog/**added to@webjsdev/websiteservice so future blog edits trigger redeploys.Posts shipped (11)
Each anchored to actual evidence:
why-webjsOrigin/thesis. Derived from author's existing personal-blog post; tagline "tiny in size, not in power."betting-on-lits-mental-modelAPI-parity rationale. References PR feat(core): full lit-API parity (ReactiveController + lifecycle + directives + 127 lit-ported tests) #31 (127 lit-ported tests in the PR title).strip-types-not-esbuildPR feat(server): replace esbuild TS stripping with Node 24+ strip-types #9 migration. Cache details verified againstpackages/server/src/dev.js.signals-replaced-setstatePR feat(core)!: signals replace this.state / setState across the stack #43 breaking change. TC39 Stage 1 shape; algorithm description fromsignal.jsdocstring; test breakdown fromtest/signals/.light-dom-slots-with-full-parityPR feat(core): light-DOM <slot> with full shadow-DOM spec parity #8 / fix(core): filter framework records in light-DOM slot observer #44. Polyfill design fromslot.jsdocstring; cross-file coordination withrender-client.js.the-naming-sagaDirect from this PR's development arc: the wjs / webjscli / webjsdev / create-webjs path through npm's similarity filter.ai-first-is-plumbingAGENTS.md + multi-tool config files + hooks + lint rules, all verifiable inpackages/cli/templates/.file-based-routingConventions frompackages/server/src/router.jsJSDoc.client-router-turbo-drive-styleMechanism fromrouter-client.jsdocstring; X-Webjs-Have header verified inssr.js.why-not-lit-as-a-dependencySSR + decorators + the AI-reads-node_modules angle (the JSDoc readability point came up during PR review).built-ins-auth-session-cookies-cacheThe four-methodCacheStoreinterface fromcache.js's typedef; Remix-shaped Session class; NextAuth-shapedcreateAuth().Typography
<code>(not the<pre>) so overflow-x preserves padding.before:pseudo-elements at left-1 so they sit safely inside the layout (not the previouslist-discwhich leaked outside on narrow viewports).mt-28 pt-10 border-tso "← All posts" does not collide with the last paragraph.Test plan
npm testpasses (1151/1151)