Skip to content

feat(website): serve the documentation at webjs.dev/docs#1101

Merged
vivek7405 merged 16 commits into
mainfrom
feat/docs-on-webjs-dev
Jul 25, 2026
Merged

feat(website): serve the documentation at webjs.dev/docs#1101
vivek7405 merged 16 commits into
mainfrom
feat/docs-on-webjs-dev

Conversation

@vivek7405

@vivek7405 vivek7405 commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Closes #1098

The documentation moves from docs.webjs.dev onto the marketing app at webjs.dev/docs, rendering inside the site's own shell so the header, footer, theme toggle, fonts, and design tokens are literally the same code every other page uses. The only docs-specific chrome left is the page-tree sidebar.

Two reasons. In search, a subdomain accrues authority to itself rather than to webjs.dev, and the docs are the largest body of indexable content the project has (45 routes). In design, the docs carried a parallel layout that had already drifted from the marketing site, so a reader crossing over left one design system and entered another.

docs.webjs.dev becomes a redirect-only service with path-preserving 301s, and it has to keep resolving indefinitely: framework error messages in already-published npm packages point at it, and a published version can never be corrected after the fact.

What changed

  • docs/app/docs/** moved to website/app/docs/** (45 pages plus the sidebar layout, the search endpoint, the llms.txt routes). /docs 308s to /docs/getting-started rather than serving a second landing page at a near-duplicate URL.
  • The docs sub-layout is a non-root layout that writes no shell. It contributes the sidebar, the .prose-docs typography, and docs-scoped metadata, and reads the root layout's design tokens; the docs @theme block and its duplicate font and token declarations are deleted, not merged.
  • The docs read as part of the site, not a panel on it. The sidebar is transparent, so the page's warm glow runs behind both columns instead of stopping at the content edge; the shell shares the header's 1240px container, so the nav lines up with the wordmark above it; and the prose tracks the blog renderer's scale (serif headings in --fg, no rules between sections, code in a --bg-subtle card) instead of the old app's orange h1 and per-h2 rule. Only the nav scrolls, so the search box stays reachable on a long page.
  • One llms.txt. The two apps each published their own; webjs.dev/llms.txt now enumerates every doc page inline, in sidebar order derived from the sidebar itself, instead of pointing at a second index on another host. /llms-full.txt and the per-page markdown routes came along, and both carry X-Robots-Tag: noindex so 45 full-text copies of the docs do not compete with the pages themselves on the domain this change exists to consolidate.
  • The sitemap covers the docs, enumerated from the live pages on disk, so a new doc page is crawlable the moment it exists. docs.webjs.dev served no sitemap at all, which is why this migration was cheap.
  • docs.webjs.dev redirects path-preservingly with a 301, leaving /__webjs/* to the framework so the deploy healthcheck still passes.
  • Hardcoded docs URLs updated in packages/core, packages/server, packages/cli (bin, generator, templates), the VS Code extension, AGENTS.md, README.md, the skill references, and the scaffold's per-agent rule files.

Bugs found on the way

A root middleware.ts has never been loaded. loadMiddleware looked up the single literal middleware.js, so a root middleware written in TypeScript was silently skipped: no error, no warning, indistinguishable from an app that has none. examples/blog/middleware.ts ships in this repo, sets x-webjs: demo on every response, and that header has never been there. Fixed to resolve .ts / .js / .mts / .mjs, with a counterfactual and a cross-runtime proof. I did not work around it: shipping a workaround for a broken documented convention, in the PR about doc drift, is how drift gets normalized.

The search endpoint rooted its index at process.cwd() and imported its walker via ../../../../packages/server/src/fs-walk.js, four levels out of the app into the framework. It now indexes off getDocPages(), the same extraction the llms.txt routes use, so search and the corpus cannot disagree about a page title.

doc-search probed a window.navigate global the client router has never defined, so every search result did a full page reload.

Also fixed: three internal docs links that had been 404ing (/docs/route-handlers, /docs/caching, /docs/advanced), a dead #state fragment, and a counter sample on the progressive-enhancement page that rendered as @click=${'() => this.inc()'}, quoted, which is exactly what invariant 4 forbids, on the page teaching the model.

Deliberately not in this PR

A top-level <script> in a client-router swapped range is never reactivated, so a script emitted as a sibling of swapped content silently never runs. The docs hit this, and the first fix was on this branch. It is reverted and filed as #1102: nothing here depends on it any more (the highlighter moved to the root layout, which is never swapped), and it changes swap semantics, which AGENTS.md rule 1 says needs a browser or e2e assertion rather than the unit test it shipped with. packages/core is down to one changed line on this branch, a docs URL in an error message.

/docs/auth and /docs/authentication ship identical titles and contradict each other about whether the framework ships auth. Pre-existing content, but this PR is what starts submitting the pair to crawlers from a sitemap, so it is filed as #1103.

Test plan

  • Unit + integration: npm test 3323 pass, 0 fail.
  • Website suite: webjs test in website/ 102 pass, 0 fail. New: docs-chrome (header and footer compared byte for byte against a marketing page through the real request pipeline, the skip-link target, docs-scoped metadata, and the social-card fields a shallow merge would drop), docs-sitemap, docs-search, docs-links (every internal /docs href including the sidebar's, every doc page reachable from the nav, the llms ordering, the noindex set).
  • Browser: web-test-runner 24 pass, including a new doc-search suite (debounce, endpoint URL, result rendering, empty state, click interception, supersede).
  • E2E: WEBJS_E2E=1 78 pass, 0 fail.
  • Bun: node scripts/run-bun-tests.js 251 pass, 0 genuine failures. New test/bun/root-middleware.mjs passes under node 26 and bun 1.3, with its own CI step. app-boot.mjs probes real docs routes on both runtimes (it was probing the redirect host's empty 301s, which pass vacuously).
  • Counterfactuals: neutering the middleware resolution reds 5 of 7 tests; reintroducing a docs-only header reds the chrome test; dropping the docs from the sitemap reds 3; breaking the social-card merge reds the metadata test; pointing doc-search at a different endpoint reds the browser test; restoring a process.cwd() root reds the search cwd test; a typo in a sidebar href and an unlinked doc page each red the link test; narrowing the dev-supervisor watch list reds the drift test.
  • Dogfood: website, docs-redirect, and ui-website boot in prod mode with no broken modulepreloads; blog e2e green. Scaffold: webjs create then migrate, boot (/, /features/auth, /examples/todo all 200), and webjs check clean.
  • Manual: verified in a real browser that highlighting survives soft navigation into and within the docs while marketing pages keep their SSR spans, that the drawer opens below the header and its state does not leak across navigation, that no page overflows at 375px, and that the design holds in light and dark at 390, 860, and 1440.
  • webjs check clean on website/ and docs/; tsc --noEmit clean on both.

Doc surfaces

Updated: AGENTS.md, README.md, framework-dev.md, docs/AGENTS.md (rewritten for the redirect host), website/AGENTS.md, packages/server/AGENTS.md, packages/ui/packages/website/AGENTS.md, the skill (SKILL.md plus the routing-and-pages and data-and-actions references), the docs site's own middleware and architecture pages, the scaffold templates and generator, Dockerfile, compose.yaml, .github/workflows/ci.yml, scripts/dev-all.js, both asset generators, package-lock.json, and the .claude skills and hooks that reference doc-surface paths.

N/A: the MCP server (no introspection projection changed, and its knowledge layer reads agent-docs and AGENTS.md, which are updated). The editor plugins (no intellisense, grammar, snippet, or command surface changed; the one VS Code docs URL is updated but the bundle is otherwise untouched).

Follow-up, not in this PR

docs.webjs.dev needs its Railway service redeployed from this branch after merge, and the Cloudflare managed robots.txt block still needs turning off in the dashboard (tracked in #1088). The client-router script bug is #1102; the duplicate auth pages are #1103.

@vivek7405 vivek7405 self-assigned this Jul 25, 2026
@vivek7405

Copy link
Copy Markdown
Collaborator Author

Found while migrating: a root middleware.ts has never been loaded

The plan for docs.webjs.dev was a root middleware that 301s everything, so I wrote docs/middleware.ts and it did nothing. loadMiddleware in packages/server/src/dev.js looked up the single literal middleware.js, so a root middleware written in TypeScript was silently skipped.

Silently is the important part. There is no error and no warning, and an app with a middleware that never loads is indistinguishable from an app that has none. That is why it survived: examples/blog/middleware.ts ships in this repo, sets x-webjs: demo on every response, and I confirmed against the running handler that the header has never been there.

Every other routing convention matches on the file STEM (a per-segment app/x/middleware.ts has always worked), the api scaffold writes middleware.ts, the dev supervisor watches both extensions, and the docs page documents middleware.ts first. This lookup was the one place that disagreed with all of them.

Fixed in 2046bef8, resolving .ts / .js / .mts / .mjs with .ts first. I considered writing the redirect service as middleware.js and filing the bug separately, and decided against it: shipping a workaround around a broken documented convention, in the very PR whose subject is doc/site drift, is how the drift gets normalized. The counterfactual is in the log below.

Verified on both runtimes (test/bun/root-middleware.mjs passes under node 26 and bun 1.3), since this is a module-load path and the TS strip differs per runtime.

@vivek7405

Copy link
Copy Markdown
Collaborator Author

Decision: one llms.txt for the site, not two

The two apps each published their own /llms.txt, and merging them made the split visible. An agent that fetched webjs.dev/llms.txt got a ## Docs section containing a single link to docs.webjs.dev/docs, so reaching an actual doc page took two more hops and knowing that a second index existed.

webjs.dev/llms.txt is now the one index, with every doc page enumerated inline, each linked to its raw-markdown variant (/docs/<topic>/llms.txt) so a model following one gets prose rather than HTML it has to strip. /llms-full.txt moved over as the whole corpus, and the per-page markdown routes came with the pages.

I kept the enumeration derived from the live pages rather than a curated list. Adding a doc page puts it in the index, the sitemap, and the corpus with nothing else to remember, which is the same property the old docs-app index had and the main reason it was worth preserving.

Two things the merge exposed that were already broken:

The search endpoint rooted its index at process.cwd() and imported its walker via ../../../../packages/server/src/fs-walk.js, reaching four levels out of the app into the framework. It now indexes off getDocPages(), the same extraction the llms.txt routes use, so search and the corpus cannot disagree about what a page is called. The cwd bug is pinned by a test that moves the working directory out from under a fresh handler, since webjs test runs the suite from the app dir, the one cwd that could never catch it.

The progressive-enhancement page's counter sample rendered as @click=${'() => this.inc()'}, quoted, because the sample interpolated a real arrow function instead of escaping the hole. Invariant 4 says event holes must be unquoted, so the page teaching progressive enhancement was showing the counterexample.

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went through the whole migration: the moved routes, the shared-chrome layout, the redirect host, the middleware fix, and every surface that names the docs URL.

The migration itself holds up. The redirect host has no open-redirect or header-injection hole (WHATWG URL strips CR/LF before parsing and pathname always starts with /, so a //evil.com path resolves onto webjs.dev), invariants 8 and 9 hold in the new sub-layout, and every asset the moved pages need is present and reachable through the auth gate.

Two real bugs, both from the docs no longer owning their own document.

The mobile drawer paints under the site header, because the root layout wraps children in a stacking context and the drawer cannot escape it. The old docs app could not hit this: its header and its drawer were siblings in the same context. Worth remembering as a general hazard for anything the sub-layout wants to float.

code-highlight.js leaks a document.body MutationObserver per navigation into /docs, for the same structural reason: the script moved from a root layout (never swapped) into a sub-layout (inside the router's swap range), and the router re-executes scripts it swaps in. The script had no idempotence guard because it never needed one.

The rest is stale text, and more of it than I expected: five CTAs that kept their new-tab screen-reader cue after becoming same-origin links, plus DOCS_URL and docs.webjs.dev surviving in the agent skill, two hooks, README, and website/AGENTS.md. The .claude/skills/* files got updated but the two hooks that inject the same guidance did not, which is the kind of pair that is easy to half-do.

Comment thread website/app/docs/layout.ts
Comment thread website/public/code-highlight.js
Comment thread .agents/skills/webjs/SKILL.md
Comment thread .claude/hooks/route-skills.sh
Comment thread packages/cli/lib/dev-supervisor.js

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second pass, this time on the tests and the client-side behaviour rather than the file moves, plus the design feedback that the docs still read as pasted in.

The design half came down to background. The sidebar had an opaque fill and a divider rule, so the page's warm glow stopped dead at the content column and the two halves of every docs page had different backgrounds. It was also full-bleed while every other page is centred in the header's 1240px container, and it kept the old app's type voice (orange h1, a rule above every h2) where the site's other long-form surface uses serif in --fg with space instead of rules. All three now match, and only the mobile drawer keeps a fill because it floats over what it covers.

The behavioural half was more interesting, and both bugs share a root cause: the docs stopped owning their own document. Things that were safe in a root layout are not safe in a sub-layout inside the router's swap range, and things that were safe on their own origin are not safe on a shared body element. A top-level script in a swapped range was never reactivated, so entering the docs by soft navigation left every code sample unhighlighted and never installed the observer that would have fixed later navigations. And the drawer's open state, an attribute on body, rode along to whatever page you left for and locked its scrolling.

On the tests, two of mine were weaker than they read. The cwd-independence check could not fail: prod mode does not bust the module cache, so the chdir happened after the modules had already resolved their roots. It runs in a fresh process now. The header/footer equality is real but proves less than its comment claimed, since it always slices the root layout's header; the count assertion beside it is what actually catches a second one. I corrected the comment rather than dressing it up.

Also worth noting: the bun boot check was probing the redirect host's empty 301s, which pass the status check and read zero preloads, so it looked like docs coverage while providing none.

Comment thread packages/core/src/router-client.js Outdated
Comment thread website/app/layout.ts
Comment thread website/app/docs/layout.ts
Comment thread website/components/doc-search.ts
Comment thread docs/middleware.ts
Comment thread test/bun/app-boot.mjs
Comment thread website/test/ssr/docs-search.test.ts

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Third pass, zoomed out to whole-change coherence and the things a per-file read misses. This one was the most useful of the three, because most of what it found is invisible in the diff.

Two guards had gone quiet rather than red, which is worse than breaking. docs-pages-well-formed kept globbing the old app, so it matched one redirect stub with no template in it and ran its balanced-tag check on an empty string. Its tripwire asked for "more than zero" pages, which one satisfies, so nothing complained while all 45 real pages went uncovered. And they matter more than before: the bug that test was written against was an unclosed tag swallowing a client-router marker, and the docs are inside the marketing app's router for the first time. Separately, the doc-sync hook still accepted ^docs/ as a documentation surface, so staging a tsconfig from a three-file redirect host would have satisfied the gate.

The lockfile was the one thing that would have failed the merge outright rather than just rotting: the docs workspace was renamed and npm ci validates that.

The rest was drift I had propagated instead of finishing: both asset generators still wrote into the deleted docs/public, CI step names still advertised docs boot coverage this PR deliberately removed, and README plus the AGENTS.md app-layout block still described the docs as a separate app.

The client-router fix is out of this PR, filed as #1102. Nothing here depends on it any more, since moving code-highlight.js to the root layout means no swapped script needs reactivating. It is a real bug and the fix is small, but it changes swap semantics, and AGENTS.md rule 1 asks for a browser or e2e assertion on a client-router change rather than the unit test it shipped with. That is a PR of its own, not a rider on a docs migration. packages/core is now back to one changed line on this branch, the docs URL in an error message.

Comment thread package-lock.json
Comment thread test/docs/docs-pages-well-formed.test.js
Comment thread scripts/generate-og.mjs
Comment thread .github/workflows/ci.yml
Comment thread README.md
Comment thread .claude/hooks/require-docs-with-src.sh
Comment thread AGENTS.md

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fourth pass, the convergence check: re-verify the previous rounds' fixes, then look at the moved pages themselves and the SEO surface, which is the whole point of the change.

The worst finding is one the migration itself introduced. Deleting the docs' own root layout took its title and description with it, so every one of the 45 pages was serving the landing page's blurb as its search snippet and social card, differing only in <title>. On a change whose entire justification is search, that is the wrong regression to ship, and neither the chrome test nor the repo-health SEO test looked at description or og. The sub-layout carries docs-scoped metadata now.

Three mobile problems, all consequences of the new shell rather than anything inherited. A wide table on /docs/runtime pushed the document past the viewport and dragged the fixed header with it, because pre had an overflow container and table did not. The menu button only gained aria-expanded after its first click, so it announced as a plain button until then and never gained state at all with JS off. And the closed drawer was only translated off-screen, so 45 links sat in the tab order ahead of the content on every docs page.

Three internal docs links have been 404ing since before the move (/docs/route-handlers, /docs/caching, /docs/advanced, none of which are real slugs). Fixing them is a footnote; the guard is the point, so the next one fails in CI rather than shipping.

Last, the per-page markdown routes are full-text copies of pages that also exist as HTML, and text/plain cannot carry a canonical. Indexing both would put 45 near-duplicates of the docs on the exact domain this change exists to consolidate, which is the same reasoning the sitemap already uses to exclude the /docs redirect. They now carry X-Robots-Tag: noindex and stay fetchable, which is what an agent asking for llms.txt actually wants. /llms.txt itself is left indexable: it is a link list, not a copy of anything. (That one is in website/lib/docs-llms.server.ts, which GitHub renders as a rename with no diff, so it has no inline anchor.)

Comment thread website/app/docs/layout.ts Outdated
Comment thread website/app/docs/layout.ts Outdated
Comment thread website/app/docs/layout.ts
Comment thread website/app/docs/layout.ts
Comment thread website/app/docs/file-storage/page.ts
Comment thread website/app/docs/architecture/page.ts
Comment thread test/bun/app-boot.mjs
Comment thread test/docs/docs-pages-well-formed.test.js

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fifth pass, aimed at the most recent commits, because each round's fixes have been the source of the next round's findings and this one is the clearest example.

The metadata fix I shipped last round made the thing it was fixing worse. Metadata merges as a shallow spread per layer, so naming openGraph in the sub-layout REPLACES the root's object instead of merging into it. Every docs URL lost og:image, its dimensions, og:url, and the summary_large_image card, so they shared as bare text cards. The description regression I was fixing was real, but I traded it for a bigger one, and my own comment claimed "the og:image stays the site's, deliberately" while the rendered HTML said otherwise. Worse, the test I wrote alongside it asserted only the fields I had just set, so it passed throughout. That is the lesson worth writing down: a test written from the fix rather than from the risk will confirm whatever you just did.

The table fix had the same shape. It solved the mobile overflow and introduced a desktop one, because display: block also makes a table shrink to fit, so row rules stopped short of the column edge on 15 pages. It only ever needed to apply at the narrow breakpoint.

Two things four rounds had not looked at. At exactly 860 CSS pixels the docs had no navigation at all: my hand-written media query includes 860 and Tailwind's max-[860px]: variant excludes it, so the grid had already collapsed and the button was already hidden while the drawer rules had not taken over. And the link guard I added last round could not see the sidebar, which is the one surface linking all 45 pages, because those hrefs are single-quoted literals rendered through a template hole. It also dropped fragment links entirely instead of checking their path, which is how a dead /docs/components#state walked straight past it.

Smaller but real: the highlighter was the only script in the root layout without a CSP nonce, so turning on the opt-in nonce CSP would have blocked exactly it; the llms 404 path reused the 200's five-minute public cache, so a shared cache would pin a not-found whose body echoes the requested topic; and the corpus builder used raw NUL bytes as its sentinel, which makes git treat a 16KB shared server module as binary and therefore unreviewable.

Comment thread website/app/docs/layout.ts
Comment thread website/app/docs/layout.ts
Comment thread website/app/docs/layout.ts
Comment thread website/test/ssr/docs-links.test.ts
Comment thread website/app/layout.ts
vivek7405 added 12 commits July 25, 2026 23:56
The docs lived on docs.webjs.dev, which cost us twice. In search, a
subdomain accrues authority to itself rather than to webjs.dev, and the
docs are the largest body of indexable content the project has. In
design, they carried their own layout, nav, and footer, so a reader
crossing over from the marketing site left one design system and entered
another.

Moving them onto the marketing app as app/docs/** fixes both at once.
The pages now render inside the site's own shell, so the header, footer,
theme toggle, fonts, and design tokens are literally the same code the
rest of the site uses. The only docs-specific chrome left is the
page-tree sidebar.

docs.webjs.dev becomes a redirect-only service. It has to keep resolving
indefinitely, because framework error messages in already-published npm
packages point at it and a published version can never be corrected
after the fact.
The root middleware lookup was the single literal `middleware.js`, so an
app whose root middleware was written in TypeScript never had it loaded.
There was no error and no warning: a missing middleware looks exactly
like an app that has none, which is why this went unnoticed even though
examples/blog ships a root middleware.ts that has never run.

TypeScript is the documented default for an app, the scaffold writes
middleware.ts for the api template, the dev supervisor watches both
extensions, and every other routing convention matches on the file stem.
This was the one lookup that did not.

Resolves .ts, .js, .mts, and .mjs, .ts first to match the supervisor's
order. Found while making docs.webjs.dev a redirect-only host, whose
whole behaviour is a root middleware.
The documentation URL changes with the move, so every place that names
it has to move with it: the error messages the framework throws, the
scaffold's generated footer and agent rule files, AGENTS.md, README, the
skill references, and the tests that anchor on those strings.

The error-message URLs in core and server can only be corrected going
forward. Anyone running an already-published version keeps getting the
old host, which is exactly why docs.webjs.dev has to keep redirecting
rather than being retired.

Two smaller things fixed on the way. The UI website derived its docs
link from its own DOCS_URL env var, which is now one service variable
that can drift from WEBSITE_URL for no reason, so it reads
WEBSITE_URL + /docs instead. And the progressive-enhancement page's
counter sample rendered as @click=${'() => this.inc()'}, quoted, which
is the exact thing invariant 4 forbids, on the page teaching the model.
The move is easy to half-undo without anything looking wrong: someone
reintroduces a docs-only header, or adds a doc page that never reaches
the sitemap, and the pages still render fine. These are the assertions
that make those fail.

Shared chrome is asserted by comparing the rendered header and footer of
a doc page against a marketing page byte for byte, through the real
request pipeline rather than the layout function, since 'the docs end up
wrapped in the root layout' is the property under test.

The search endpoint gains its first tests. Its index was rewritten to
reuse the llms.txt extraction, which removed a process.cwd() root and an
import that reached four levels out of the app into packages/. The cwd
one is pinned by moving the working directory out from under a fresh
handler, because the suite otherwise runs from the one directory that
cannot catch it.

Also repoints the doc tests, the api-coverage corpus, and the knowledge
and preload sweeps at the app that serves the docs now.
The docs host renders no HTML now, only redirects, so it has no
public/input.css to compile and the image build step for it would fail.
The website carries the docs, so its DOCS_URL cross-link is gone, and
the docs service gains SITE_URL, which is where it sends every request.
Review turned up nine things. The two that matter to a reader:

The mobile docs drawer painted UNDER the site header. The root layout
wraps children in a relative z-1 element, which is a stacking context,
so nothing the docs layout renders can rise above the z-20 header no
matter what z-index it claims. The old docs app did not hit this because
its header lived in the same context as its drawer. The drawer now opens
below the header instead of trying to cover it, which sidesteps the
stacking context and leaves the header usable while it is open.

code-highlight.js attached a document.body MutationObserver every time
it ran, and it now sits inside the client router's swap range, so each
marketing-to-docs navigation leaked another one. Guarded.

The rest were stale text: five docs CTAs kept a screen-reader 'opens in
a new tab' cue after becoming same-origin links, and DOCS_URL and
docs.webjs.dev survived in the agent skill, two hooks, README, and
website/AGENTS.md.

Also widens the dev supervisor's middleware watch list to the four
extensions the server now loads, with a test that reads the server's
list rather than restating it, since a middleware that loads but never
triggers a dev restart is the same quiet failure in a new place.
The docs still read as a section pasted into the site rather than part
of it, and the reason was mostly background. The sidebar carried an
opaque fill and a divider rule, so the page's warm glow stopped at the
content column and the two halves had different backgrounds. It was also
full-bleed while every other page is centred in a 1240px container, and
it kept the old docs app's type voice: an orange h1 and a rule above
every h2, where the site's other long-form surface uses serif headings
in --fg with space instead of rules.

The sidebar is transparent now, the shell shares the header's container
so the nav lines up with the wordmark above it, and the prose tracks the
blog renderer's scale. Only the mobile drawer keeps a fill, because it
floats over the content it covers.

Also from review: code-highlight.js never ran when a reader entered the
docs by soft navigation. It sat in the docs sub-layout, inside the
router's swap range, and a top-level script in a swapped range was never
reactivated. Fixed in the router, and the script moved to the root
layout (never swapped) scoped to .prose-docs, so it no longer depends on
swap mechanics at all. Marketing pages keep the spans lib/highlight.ts
already emitted at SSR.

The drawer's open state lived on <body>, outside every swap range, so it
rode along to the next page and locked its scrolling. Cleared from the
root layout, which survives navigation.

Smaller: doc-search probed a window.navigate global the router has never
defined, so every search result did a full reload; the redirect host
answered /__webjs/core/* with a canned body; the docs main had no
skip-link target and then no tabindex; and the bun boot check probed the
redirect host's empty 301s while believing it covered the docs.
The lockfile still recorded the docs workspace under its old name, which
npm ci validates, so CI would have failed on it.

Two guards had gone quiet rather than red. docs-pages-well-formed still
globbed docs/app, which now matches one redirect stub with no template
in it, so the balanced-tag check ran on an empty string and passed while
covering none of the 45 real pages; its tripwire said 'more than zero',
which that satisfies, so it now asserts a real floor. And the doc gate
hook still accepted ^docs/ as a documentation surface, where staging a
tsconfig from a three-file redirect host would satisfy it.

Both favicon and OG generators still wrote into the deleted docs/public,
and CI step names, README, and the AGENTS.md app-layout block still
described the docs as a separate app.

The client-router fix is reverted out of this PR and filed as #1102.
Nothing here depends on it any more: code-highlight.js moved to the root
layout, so it no longer needs a swapped script to be reactivated. It is
a real bug, but it changes swap semantics and AGENTS.md rule 1 asks for
a browser or e2e assertion on a client-router change, which is more than
belongs in a docs migration.
Dropping the docs' own root layout took its title and description with
it, so all 45 pages were advertising the landing page's blurb as their
search snippet and social card, with only the title differing. That is a
regression the migration introduced, and on an SEO-motivated change it is
the wrong one to ship. The sub-layout now carries docs-scoped metadata;
pages still own their titles.

Three mobile problems, all from the new shell. A wide table pushed the
whole document sideways on /docs/runtime, taking the fixed header with
it, because pre had an overflow container and table did not. The menu
button gained aria-expanded only after its first click, so it announced
as a plain button until then and never gained state with JS off. And the
closed drawer was merely translated off-screen, so 45 links stayed in the
tab order in front of the content on every docs page.

Also: three internal docs links pointed at slugs that do not exist
(route-handlers, caching, advanced), shipping 404s since before the move.
Fixed, with a test that walks every internal /docs href through the real
handler so the next one fails instead of shipping.

And the per-page markdown routes now carry X-Robots-Tag: noindex. They
are full-text copies of pages that also exist as HTML, and text/plain
cannot carry a canonical, so indexing both would put 45 near-duplicates
on the exact domain this whole change exists to consolidate. They stay
fetchable, which is what an agent asking for llms.txt wants. The site
index at /llms.txt is deliberately left indexable.
The metadata fix in the last commit made things worse before better.
Metadata merges as a shallow spread per layer, so the sub-layout's
openGraph object REPLACED the root's rather than merging into it, and
every docs URL lost og:image, its dimensions, og:url, and the
summary_large_image card. Restating every field is the fix; the test now
asserts the image and card type survive, which is what it should have
checked in the first place rather than only the fields I had just set.

The table overflow fix was applied at every width, so a table also
shrank to fit its content on desktop and its row rules stopped short of
the column edge on 15 pages. Scoped to the narrow breakpoint, where it
is the only thing that stops a wide table dragging the fixed header off
screen.

At exactly 860px the docs had no navigation at all: the hand-written
media query includes 860 while Tailwind's max-[860px] variant excludes
it, so the grid had collapsed and the button was hidden while the drawer
rules had not taken over.

The link guard could not see the sidebar, which is the only surface that
links all 45 pages, because its hrefs are single-quoted literals in a
template hole. It also dropped fragment links whole rather than checking
their path, which is how a dead /docs/components#state survived it.

Smaller: the highlighter had no CSP nonce, so enabling the opt-in nonce
CSP would have blocked exactly it; the llms 404 path reused the
5-minute public cache, so a shared cache would pin a not-found whose
body echoes the requested topic; and the corpus builder used raw NUL
bytes as a sentinel, which made git treat a 16KB shared server module as
binary and therefore unreviewable.
The previous commit said the test covers og:image and the card type. It
did not; I added the assertion after the fact rather than before. It
asserts the fields a shallow metadata merge would silently drop, which
is the failure that actually happened, and the counterfactual confirms
it fails when the merge is broken again.
The whole sidebar scrolled, so the search field scrolled out of reach on
a long page and the scrollbar ran the column's full height, pressing
against the field. The aside is a column now: the search box is pinned
and only the link list below it scrolls, with the scrollbar inset off
the links.

The links also pulled themselves back out with a negative margin, so
their text sat flush against the container edge and the hover pill bled
past it. Dropping it indents the text by the link's own padding and
lines the pill up with the sidebar edge.
@vivek7405
vivek7405 force-pushed the feat/docs-on-webjs-dev branch from 6f4d996 to bc6f528 Compare July 25, 2026 18:27
The order was a hand-copied list and it had already drifted: it carried
a slug that no longer exists and was missing six real pages, so the
AI-facing index put Runtime and Security in an alphabetical tail rather
than in Getting Started and Infrastructure. Deriving it from the sidebar
removes the failure mode instead of correcting one instance of it. It
parses the layout source rather than importing it, since the layout
pulls in browser components and this module is server-only.

Also adds the assertion that actually rots in the other direction: the
sitemap and llms.txt both enumerate from disk, and /docs is a redirect
rather than a listing, so the sidebar is the only human path into a doc
page. A new page would land in both indexes while being unreachable by a
reader, with every test green.

The Bun proof gets its own CI step, matching the convention the other
listener proofs use, so its own comment is no longer describing a step
that did not exist. That matters because denylist entries in the Bun
runner justify skipping a file by pointing at exactly such a step.

The /docs/auth and /docs/authentication pages ship identical titles and
contradict each other about whether the framework ships auth. Pre-existing,
but this PR is what starts submitting the pair to crawlers, so it is
filed as #1103 rather than fixed here.

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sixth pass, late-stage: whole-diff coherence, whether the test SET covers the real risk surface, a functional sweep against the running server, and merge readiness.

The blocker was that the branch conflicted with main. 8ce50392 modified docs/app/docs/deployment/page.ts while this PR deletes it, and GitHub reported CONFLICTING. The content risk was the real part: resolving it the obvious way (delete wins, since the file moved) would have dropped the X-Forwarded-Proto parity line and the whole "embedding moves the proxy-header responsibility to you" caveat from the live docs, which is a security-relevant deploy warning. Rebased; git's rename detection carried the change onto the moved file, and I verified website/app/docs/deployment/page.ts is now byte-identical to main's version rather than assuming it.

The test-set gap was a direction I had not covered. The sitemap and llms.txt both enumerate topics from disk, and /docs is a redirect rather than a listing page, so the sidebar is the only surface a human can navigate from. A new doc page would appear in the sitemap and the AI index automatically while being unreachable by a reader, with every test green. That is now asserted, and the counterfactual (drop a page in without a nav entry) fails it.

The CURATED_ORDER list in the corpus builder had already drifted before this PR: one slug that no longer exists, six real pages missing, so /llms.txt listed Runtime and Security in an alphabetical tail. I derived it from the sidebar rather than correcting the list, since correcting it just resets the clock on the same drift. (That is in website/lib/docs-llms.server.ts, which GitHub renders as a rename with no diff, so it takes no inline anchor.)

Also fixed a comment that described a CI step which did not exist, and added the step, because the Bun runner's denylist entries justify skipping files by pointing at exactly that kind of dedicated step.

One thing I did not fix here: /docs/auth and /docs/authentication ship identical titles and open by contradicting each other about whether the framework ships auth. It is pre-existing content, but this PR is what starts submitting the pair to crawlers from a sitemap, so it is filed as #1103 rather than folded in.

Comment thread website/test/ssr/docs-links.test.ts
Comment thread test/bun/root-middleware.test.mjs

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seventh pass, and this one is clean.

Checked the rebase is honest rather than assuming it: the pre-rebase and post-rebase diffs are byte-identical apart from blob hashes and hunk offsets, 8ce5039 is an ancestor of HEAD, and both of its deployment-page edits survived the move into the website app. Its listener and forwarded-proto work is untouched by this branch.

Checked the newest commit's edge cases: an empty layout parse degrades to alphabetical and is caught by the ordering test, an unlinked topic ranks to the tail and is caught by the orphan test, and the two caches are independent. The live /llms.txt order matches the sidebar exactly.

Everything green: website 102 node plus 24 browser, test/docs 29, preload-subset, repo-health, the root-middleware resolution tests, app-boot on both runtimes, and webjs check in both apps.

On the deploy: the Dockerfile drops only the deleted docs stylesheet, and a real Tailwind build of the website picks up the docs-only classes, so the merged docs are styled in the image. Compose, the Railway start commands and the healthcheck paths are unchanged, and the docs service has no variables set so SITE_URL correctly falls back to the production origin.

One note for whoever picks this up next, not a defect: booting docs/ from this worktree through the symlinked CLI resolves @webjsdev/server from the primary checkout, which lacks the middleware fix, so deep paths 404. That is a worktree artifact; the behaviour is correct through createRequestHandler and in the deploy.

@vivek7405
vivek7405 marked this pull request as ready for review July 25, 2026 18:47
The section label had no horizontal padding while the links had 12px, so
the label sat further left than the items under it. Both carry the same
small inset now, which is what makes the label read as a heading for the
group rather than a separate column.
Every list in the docs rendered as indented plain text with no bullets
or numbers: Tailwind's preflight sets list-style: none on ul/ol, and the
prose rules re-added only the padding. Without a visible marker the 24px
inset read as an arbitrary, inconsistent indent (the report that exposed
it was 'why is this section indented', on a numbered how-it-works list
whose numbers were invisible).

With markers restored the indentation rule across the docs is simple and
uniform: text sits flush unless it carries a marker, and the inset exists
only to house that marker.

Also fixes the description extractor stopping dead at an escaped quote,
which published the garbage fragment 'WebJs pages and components are
SSR\' as the progressive-enhancement entry in /llms.txt and the search
index, plus a nav comment describing padding values that were never the
ones on the element.

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eighth pass, a fresh full read of the whole diff after the padding commit.

Three things, and the interesting one only looked cosmetic. The report was "why is this section indented", on the suspense page's how-it-works list. The actual cause: Tailwind's preflight sets list-style: none on every ul/ol, and the prose rules re-added only the padding, so every list in the docs has been rendering as indented plain text with NO bullets or numbers. The numbered steps on that page were a numbered list whose numbers were invisible, at which point the indent genuinely is arbitrary. Markers are restored now, which makes the indentation rule across the docs uniform: text sits flush unless it carries a marker, and the inset exists only to house the marker. A guard asserts the restatement stays, since deleting it renders fine, just wrong, and nothing else would notice.

The second is in the corpus extractor: the description regex stopped dead at an escaped quote, so the progressive-enhancement page, whose description contains SSR\'d, published the garbage fragment "WebJs pages and components are SSR" as its entry in /llms.txt and fed the same fragment to the search index. Fixed to cross escapes and unescape the capture, with a test asserting the known page is whole and that no index line anywhere ends in a backslash, so the next escaped quote fails loudly. (The extractor lives in website/lib/docs-llms.server.ts, a rename GitHub shows with no diff, so it takes no inline anchor.)

Third was a comment on the sidebar nav describing padding values that were never on the element, wrong at birth and made more wrong by the padding commit next to it. Corrected to describe what is actually there.

The padding commit itself is sound: the label and links share the inset, the utilities exist in the compiled stylesheet, and the live render matches.

Comment thread website/app/docs/layout.ts
Comment thread website/app/docs/layout.ts
The list was a ul with numbers hand-written into the item text, which
the invisible-marker era let pass as a plain numbered list. With markers
restored it rendered a bullet stacked on each number. A real ol is also
what a screen reader or crawler should have been given all along.

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ninth pass, converging. Verified the escape-crossing regexes (unescape order relative to entity decoding is safe, no metadata uses control escapes that would mis-fold), the marker CSS in both themes and on the one nested list, and the suites.

One finding: the routing page's route-resolution order was a ul with the numbers hand-written into the item text. The invisible-marker era let it pass as a plain numbered list; restoring markers rendered a bullet stacked on each number. It is now a real ol with the hand numbers dropped, which is also what a screen reader or a crawler should have been handed all along. Swept the rest of the site for the same pattern; this was the only instance.

Comment thread website/app/docs/routing/page.ts

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tenth pass, clean.

Confirmed the ol conversion on the live page: one ordered list, four decimal-numbered items, no hand-written numbers left, and a repo-wide sweep found no other page with the old pattern. Website suite green (103 server, 24 browser), the routing page renders with zero console errors, and the key routes (/docs redirect, /llms.txt, /sitemap.xml) all answer correctly.

Nothing left to flag. The two follow-ups on the board are #1102 (swapped-range script reactivation) and #1103 (the duplicate auth pages).

@vivek7405
vivek7405 merged commit e07ea3a into main Jul 25, 2026
10 checks passed
@vivek7405
vivek7405 deleted the feat/docs-on-webjs-dev branch July 25, 2026 19:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

seo: serve the docs at webjs.dev/docs with the marketing site chrome

1 participant