diff --git a/.agents/skills/webjs/SKILL.md b/.agents/skills/webjs/SKILL.md index d58a9d93a..a79f216a0 100644 --- a/.agents/skills/webjs/SKILL.md +++ b/.agents/skills/webjs/SKILL.md @@ -9,7 +9,7 @@ Use this skill for end-to-end WebJs app work. It helps you choose the right laye ## Full Documentation -This skill is the quick guide. When you need the full API reference for a surface, load the matching file in `references/` (listed below). For even deeper framework detail, WebJs ships buildless, so the source you run IS the source you read: look in `node_modules/@webjsdev/{core,server,cli}/` (each package ships its own `AGENTS.md`). The complete hosted docs live at https://docs.webjs.dev. +This skill is the quick guide. When you need the full API reference for a surface, load the matching file in `references/` (listed below). For even deeper framework detail, WebJs ships buildless, so the source you run IS the source you read: look in `node_modules/@webjsdev/{core,server,cli}/` (each package ships its own `AGENTS.md`). The complete hosted docs live at https://webjs.dev/docs. ## What WebJs Is diff --git a/.agents/skills/webjs/references/data-and-actions.md b/.agents/skills/webjs/references/data-and-actions.md index 03892f208..c7fdac7c0 100644 --- a/.agents/skills/webjs/references/data-and-actions.md +++ b/.agents/skills/webjs/references/data-and-actions.md @@ -96,7 +96,7 @@ const [updated] = await db.update(posts).set({ title }).where(eq(posts.id, id)). await db.delete(posts).where(eq(posts.id, id)); ``` -A `.returning()` row is the table's own columns only, never `with` relations. When the caller wants a joined shape, re-read with `db.query.*` or splice the already-known related value in by hand. Full surface at https://docs.webjs.dev. +A `.returning()` row is the table's own columns only, never `with` relations. When the caller wants a joined shape, re-read with `db.query.*` or splice the already-known related value in by hand. Full surface at https://webjs.dev/docs. ## Input validation at the boundary @@ -201,4 +201,4 @@ import type { Post } from '#db/schema.server.ts'; import { posts } from '#db/schema.server.ts'; ``` -Keep the wire shape in a browser-safe `modules//types.ts` with NO runtime import from a `.server.ts` file or from `db/`. Define a hand-written DTO, or a type-only derivation (`import type { Post } ...; export type PostFormatted = Omit & { createdAt: string }`). Never `export *` or a value re-export from a `.server.ts` in `types.ts`; that carries the runtime table bindings and breaks any component importing the types. Full reference at https://docs.webjs.dev. +Keep the wire shape in a browser-safe `modules//types.ts` with NO runtime import from a `.server.ts` file or from `db/`. Define a hand-written DTO, or a type-only derivation (`import type { Post } ...; export type PostFormatted = Omit & { createdAt: string }`). Never `export *` or a value re-export from a `.server.ts` in `types.ts`; that carries the runtime table bindings and breaks any component importing the types. Full reference at https://webjs.dev/docs. diff --git a/.agents/skills/webjs/references/routing-and-pages.md b/.agents/skills/webjs/references/routing-and-pages.md index 470807227..ea7103969 100644 --- a/.agents/skills/webjs/references/routing-and-pages.md +++ b/.agents/skills/webjs/references/routing-and-pages.md @@ -94,6 +94,8 @@ export async function POST(req: Request) { Optional root-level plus per-segment. The default export is `async (req, next) => Response`. Return a Response to short-circuit, or call `next()` and post-process. Per-segment middleware applies to its subtree, outermost to innermost. +The root file sits beside `app/`, not inside it, and may be `middleware.ts` / `.js` / `.mts` / `.mjs` (`.ts` wins if more than one exists). A per-segment `app//middleware.*` takes any of the same extensions. + ## Metadata and `generateMetadata` A page exports `metadata` (static) or `generateMetadata(ctx)` (request-scoped, takes precedence). Values flow into `` at SSR and merge across nested layouts (deeper wins). Type both with `Metadata`; `MetadataContext` types the argument. The surface is Next.js-compatible. @@ -108,7 +110,7 @@ export async function generateMetadata(ctx: MetadataContext): Promise } ``` -Common fields: `title` (string or `{ template, default, absolute }`), `description`, `keywords`, `metadataBase` (resolves relative URLs in `openGraph` / `twitter` / `alternates` / `icons`), `openGraph`, `twitter`, `robots`, `alternates.canonical`, `icons`, `manifest`, and `jsonLd` (schema.org structured data, single object or array, HTML-safe-escaped automatically). `viewport`, `themeColor`, and `colorScheme` may also be set via a split `export const viewport = { ... }`. `cacheControl` is emitted as a response HEADER (not a ``); pages default to `no-store`, and a `public` value enables conditional GET (a weak `ETag` + `304`). See https://docs.webjs.dev for the full field list. +Common fields: `title` (string or `{ template, default, absolute }`), `description`, `keywords`, `metadataBase` (resolves relative URLs in `openGraph` / `twitter` / `alternates` / `icons`), `openGraph`, `twitter`, `robots`, `alternates.canonical`, `icons`, `manifest`, and `jsonLd` (schema.org structured data, single object or array, HTML-safe-escaped automatically). `viewport`, `themeColor`, and `colorScheme` may also be set via a split `export const viewport = { ... }`. `cacheControl` is emitted as a response HEADER (not a ``); pages default to `no-store`, and a `public` value enables conditional GET (a weak `ETag` + `304`). See https://webjs.dev/docs for the full field list. ## Control-flow throws diff --git a/.claude/hooks/require-docs-with-src.sh b/.claude/hooks/require-docs-with-src.sh index 51cc88002..cb296acdc 100755 --- a/.claude/hooks/require-docs-with-src.sh +++ b/.claude/hooks/require-docs-with-src.sh @@ -62,10 +62,16 @@ if [ -z "$src_touched" ]; then exit 0; fi # Any documentation surface staged in the same commit? The curated set of # real doc surfaces: AGENTS / CLAUDE / CONVENTIONS / README markdown at any # depth (root, per-package, per-example), the skill at .agents/skills/webjs/, -# the docs site, the marketing website, and the scaffold templates. NOT every -# *.md, so a stray note cannot be staged to slip the gate. +# the marketing website (which carries the docs site at website/app/docs/), +# and the scaffold templates. NOT every *.md, so a stray note cannot be +# staged to slip the gate. +# +# `^docs/` is deliberately absent. That path used to BE the docs site, but +# since #1098 it holds a three-file redirect-only host, so accepting it would +# let a staged tsconfig satisfy the documentation gate. Its AGENTS.md still +# counts, via the markdown alternative above. doc_staged=$(printf '%s\n' "$staged" | grep -E \ - '(^|/)(AGENTS|CLAUDE|CONVENTIONS|README)\.md$|^\.agents/skills/webjs/|^docs/|^website/|^packages/cli/templates/' || true) + '(^|/)(AGENTS|CLAUDE|CONVENTIONS|README)\.md$|^\.agents/skills/webjs/|^website/|^packages/cli/templates/' || true) if [ -n "$doc_staged" ]; then exit 0; fi cat >&2 <<'EOF' @@ -80,7 +86,7 @@ package.json webjs.* key, an html hole, a lifecycle hook, a convention, or the behaviour of an already-documented feature), invoke the webjs-doc-sync skill, then `git add` EVERY applicable surface and commit again: AGENTS.md + .agents/skills/webjs/ agent reference (SKILL.md + references/) - docs/app/docs/ the docs site + website/app/docs/ the docs site (served at webjs.dev/docs) README.md if a headline capability website/ marketing copy, if headline packages/cli/templates/ scaffold per-agent rule files diff --git a/.claude/hooks/route-skills.sh b/.claude/hooks/route-skills.sh index 7bbf89fdc..19b7d665d 100755 --- a/.claude/hooks/route-skills.sh +++ b/.claude/hooks/route-skills.sh @@ -125,7 +125,7 @@ if has '(doc|documentation) (gap|drift|sync|coverage|debt)' \ || has '(missing|stale|outdated|out-of-date|out of date).{0,20}(doc|docs|documentation)' \ || has 'did (we|you|i) (update|sync).{0,20}(doc|docs)' \ || has '(audit|sweep|check).{0,40}(doc|docs|documentation)'; then - add_match "webjs-doc-sync: the request is about documentation sync, drift, or a doc gap. Invoke the webjs-doc-sync skill BEFORE editing any doc. It holds the authoritative map of EVERY surface (AGENTS.md + the skill at .agents/skills/webjs/, README, the docs site under docs/app/docs/, the marketing website/, and the scaffold templates' per-agent rule files) and the change-type to surface mapping, so no surface is silently skipped. File each confirmed gap via webjs-file-issue." + add_match "webjs-doc-sync: the request is about documentation sync, drift, or a doc gap. Invoke the webjs-doc-sync skill BEFORE editing any doc. It holds the authoritative map of EVERY surface (AGENTS.md + the skill at .agents/skills/webjs/, README, the docs site under website/app/docs/, the rest of the marketing website/, and the scaffold templates' per-agent rule files) and the change-type to surface mapping, so no surface is silently skipped. File each confirmed gap via webjs-file-issue." fi # --- webjs-scaffold-sync: keep every scaffold surface in sync ----------- diff --git a/.claude/skills/webjs-blog-write/SKILL.md b/.claude/skills/webjs-blog-write/SKILL.md index 64b86619f..16d97c961 100644 --- a/.claude/skills/webjs-blog-write/SKILL.md +++ b/.claude/skills/webjs-blog-write/SKILL.md @@ -11,7 +11,7 @@ when_to_use: | editing an existing blog post's prose in a non-trivial way Do NOT trigger for: filing an issue (webjs-file-issue), syncing framework API docs or the docs site (webjs-doc-sync), or the docs site's own - `docs/app/docs/**` pages (those are reference docs, not blog posts). + `website/app/docs/**` pages (those are reference docs, not blog posts). --- # Write a webjs blog post @@ -99,5 +99,5 @@ The test before you ship: read the draft aloud and ask whether the author would ## Related skills - **webjs-file-issue**: file the tracking issue for the blog work before writing, and file a framework issue if dogfooding surfaces a real bug. -- **webjs-doc-sync**: for the framework's own reference docs (`AGENTS.md`, the skill at `.agents/skills/webjs/`, the docs site under `docs/app/docs/`). A blog post is not a doc-sync surface, and reference docs are not a blog. Keep them separate. +- **webjs-doc-sync**: for the framework's own reference docs (`AGENTS.md`, the skill at `.agents/skills/webjs/`, the docs site under `website/app/docs/`). A blog post is not a doc-sync surface, and reference docs are not a blog. Keep them separate. - **webjs-list-todos**: to see what shipped work is open or recently done when hunting for a topic. diff --git a/.claude/skills/webjs-doc-sync/SKILL.md b/.claude/skills/webjs-doc-sync/SKILL.md index 93e6276e6..50d9079ac 100644 --- a/.claude/skills/webjs-doc-sync/SKILL.md +++ b/.claude/skills/webjs-doc-sync/SKILL.md @@ -43,8 +43,8 @@ applies, then update or consciously skip each. relevant `references/` file. 2. **`README.md`** (repo root). Update when a headline capability changes (the feature list, the quickstart, the runtime/template matrix). -3. **The docs site: `docs/app/docs//page.tsx`.** This is the - user-facing documentation at docs.webjs.dev. Find the topic page(s) that cover +3. **The docs site: `website/app/docs//page.tsx`.** This is the + user-facing documentation at webjs.dev/docs. Find the topic page(s) that cover the area (`server-actions`, `routing`, `components`, `caching`, `configuration`, `client-router`, `data-fetching`, ...) and update them. `llms.txt` / `llms-full.txt` are generated LIVE from the doc pages (no build step), so they @@ -90,7 +90,7 @@ applies, then update or consciously skip each. be) described: ```sh git grep -n -iE '|' -- \ - AGENTS.md '.agents/skills/webjs/**' README.md 'docs/app/docs/**' \ + AGENTS.md '.agents/skills/webjs/**' README.md 'website/app/docs/**' \ 'website/**' 'packages/cli/templates/**' 'examples/**/CONVENTIONS.md' ``` 3. For each surface in the mapping that applies, update it. For a behaviour diff --git a/.claude/skills/webjs-scaffold-sync/SKILL.md b/.claude/skills/webjs-scaffold-sync/SKILL.md index 64505c666..75ccbe39a 100644 --- a/.claude/skills/webjs-scaffold-sync/SKILL.md +++ b/.claude/skills/webjs-scaffold-sync/SKILL.md @@ -151,7 +151,7 @@ it applies, then update or consciously skip each. here, including the counterfactual (a per-template exclusion test). 6. **The framework docs that DESCRIBE the scaffold** (shared with doc-sync): - Root `AGENTS.md` "Scaffolding" section. - - The docs site: `docs/app/docs/getting-started/page.ts` (+ `backend-only`, + - The docs site: `website/app/docs/getting-started/page.ts` (+ `backend-only`, `ai-first`, `conventions` where they describe generated structure). - `README.md` (the template matrix + the "scaffold is the tutorial" note). 7. **The CLI surface**: `packages/cli/` `--template` validation + `--help`/usage @@ -185,7 +185,7 @@ it applies, then update or consciously skip each. ```sh git grep -n -iE '|' -- \ 'packages/cli/lib/**' 'packages/cli/templates/**' 'test/scaffolds/**' \ - AGENTS.md README.md 'docs/app/docs/**' + AGENTS.md README.md 'website/app/docs/**' ``` 3. Update every surface the mapping says applies. For a SCOPING or wording change (e.g. "gallery is full-stack only" becoming "full-stack and saas"), the grep diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index ab42f15ce..0f79336e0 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -143,7 +143,7 @@ Doc drift is the #1 way a framework rots. Documentation MUST stay in sync with c - `CHANGELOG.md` (per-package, under `changelog//.md`). Generated automatically by the pre-commit hook on version-bump commits; review and add migration notes for breaking changes. Without a version bump, no changelog entry. - `CLAUDE.md` (only if a Claude Code rule is specifically added; framework conventions go in AGENTS.md). - `.github/*.md` (issue templates, PR templates, contributing) when a workflow rule shifts. -3. **User-facing docs site** under `docs/app/docs//page.ts` (these are `.ts` files, not markdown, so they're excluded by the markdown query but they're the canonical user-facing reference). If the change is visible to a user reading the docs site, update the matching topic page. Add a new page if the surface is new and there's no obvious home. +3. **User-facing docs site** under `website/app/docs//page.ts` (these are `.ts` files, not markdown, so they're excluded by the markdown query but they're the canonical user-facing reference). If the change is visible to a user reading the docs site, update the matching topic page. Add a new page if the surface is new and there's no obvious home. 4. **Scaffold templates** under `packages/cli/templates/` and the generators `packages/cli/lib/{create,api-gallery}.js`. Update if the change affects what `webjs create` generates. The scaffold ships a gallery index home + layout + db wiring, a densely-commented feature gallery (`packages/cli/templates/gallery/**`, demos under `app/features/` plus `app/examples/todo`) and the api showcase (`api-gallery.js`), plus one cross-agent skill at `.agents/skills/webjs/` (SKILL.md + references) that the agent grows in place; there are no per-agent rule files. A feature change that agents should know about lands in the skill; a generated-code change lands in the generators, verified with `generate + boot + webjs check`. 5. **The MCP server** (the standalone `@webjsdev/mcp` package, `packages/mcp/src/{mcp,mcp-docs,mcp-source}.js`, extracted from the CLI in #415; `webjs mcp` and `npx @webjsdev/mcp` both run it). The MCP is how AI agents learn and introspect webjs, so it must stay in lockstep with the surfaces it exposes. Update it whenever the change touches what it serves: - **Introspection tools** (`list_routes` / `list_actions` / `list_components` / `check`): if you change the route table shape, the action/RPC-hash scheme, component registration, or a `webjs check` rule, update the matching tool projection so the MCP reports reality. @@ -213,13 +213,13 @@ For each item above, explicitly answer one of: The "every markdown file" rule is generative because new markdown files appear over a project's lifetime. A closed enumeration silently excludes them; the git query is the source of truth. -If you find yourself writing "N/A" for every item except tests, that is a smell. Most user-visible code changes touch at least one markdown file and the relevant `docs/app/docs//page.ts` page. +If you find yourself writing "N/A" for every item except tests, that is a smell. Most user-visible code changes touch at least one markdown file and the relevant `website/app/docs//page.ts` page. ### Concrete examples from recent PRs -- PR #110 (`fs.watch` + Web Crypto): updated `AGENTS.md` (no-build claim wording), `packages/server/AGENTS.md` (file watcher mention), `docs/app/docs/{configuration,deployment,no-build}/page.ts` (chokidar → fs.watch). Tests covered the watcher, the boundary, and the migration. -- PR #111 (module-graph asset gate): updated `AGENTS.md` (new invariant about the gate), `packages/server/AGENTS.md` (gate + guardrail invariants), `docs/app/docs/no-build/page.ts` (new "authorisation gate" subsection). Tests covered the gate end-to-end. -- PR #117 (core dist bundles): updated `docs/app/docs/no-build/page.ts` (the @webjsdev/core exception note), `packages/core/README.md` (tarball layout), `packages/core/AGENTS.md` (invariant 1 rewording), `packages/server/AGENTS.md` (importmap.js module-map entry). +- PR #110 (`fs.watch` + Web Crypto): updated `AGENTS.md` (no-build claim wording), `packages/server/AGENTS.md` (file watcher mention), `website/app/docs/{configuration,deployment,no-build}/page.ts` (chokidar → fs.watch). Tests covered the watcher, the boundary, and the migration. +- PR #111 (module-graph asset gate): updated `AGENTS.md` (new invariant about the gate), `packages/server/AGENTS.md` (gate + guardrail invariants), `website/app/docs/no-build/page.ts` (new "authorisation gate" subsection). Tests covered the gate end-to-end. +- PR #117 (core dist bundles): updated `website/app/docs/no-build/page.ts` (the @webjsdev/core exception note), `packages/core/README.md` (tarball layout), `packages/core/AGENTS.md` (invariant 1 rewording), `packages/server/AGENTS.md` (importmap.js module-map entry). If a PR ships without ANY of those touches and the change is user-visible, the PR is incomplete; do not mark it ready for review (leave it draft until the surfaces are addressed). diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c86ce4b3..4f3364c71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -146,13 +146,14 @@ jobs: # a node:crypto digest, so it must be byte-identical on the Bun.serve path. - name: App-source deploy signal on Bun run: bun test/bun/app-source-signal.mjs - # Boot website + docs + ui-website on Bun and GET real routes (#542). All - # four in-repo apps deploy on Bun, but only examples/blog had a Bun boot in - # CI, so a per-route break only on Bun (the #526 ui-website 500) could reach - # production. The script runs each app's webjs.start.before presteps (the - # ui-website registry copy is the #526 root cause) and probes an ui-website - # component detail page. Node runs it too in the "In-repo app tests" job. - - name: App boot-check on Bun (website + docs + ui-website) + # Boot website (which serves /docs) + ui-website on Bun and GET real + # routes (#542). Every in-repo app deploys on Bun, but only examples/blog + # had a Bun boot in CI, so a per-route break only on Bun (the #526 + # ui-website 500) could reach production. The script runs each app's + # webjs.start.before presteps (the ui-website registry copy is the #526 + # root cause) and probes an ui-website component detail page. Node runs it + # too in the "In-repo app tests" job. + - name: App boot-check on Bun (website incl. /docs + ui-website) run: bun test/bun/app-boot.mjs # SSR action-result seeding on Bun (#529): seeding rode Node's # module.registerHooks, which Bun lacks; it now installs via a Bun.plugin @@ -195,6 +196,13 @@ jobs: # a route handler's raw req.url both carry the forwarded scheme + host. - name: Forwarded proto/host on Bun run: bun test/bun/forwarded-proto.mjs + # Root middleware resolution on Bun (#1098). A root `middleware.ts` was + # never loaded at all: the lookup was the single literal `middleware.js`, + # with no error to notice. The proof is a module-LOAD path (a bare + # import() of a .ts file), and the TS strip differs per runtime, so it + # needs its own step rather than riding the Node suite. + - name: Root middleware resolution on Bun + run: bun test/bun/root-middleware.mjs # Light-DOM slot SSR projection on Bun (#1021): slot substitution # (injectDSD / substituteSlotsInRender) is on the SSR hot path, so the # projection must be byte-consistent across runtimes: authored children @@ -373,11 +381,12 @@ jobs: # suite. This job runs each IN-REPO app's OWN test suite (its `webjs test` # script), which the root runners do not discover, so a regression in an # app's tests gates the merge (issue #342). The website's `test` runs both - # its node + browser suites (hence Playwright); the blog is node-only and - # touches its SQLite DB (the same setup the unit + e2e jobs do). docs and - # the ui-website ship no `webjs test` suite, so they are covered by the - # `node test/bun/app-boot.mjs` boot-check step below (#627), which boots all - # three via createRequestHandler and asserts each serves real routes with no + # its node + browser suites (hence Playwright), and covers the docs too + # since they are its own /docs routes; the blog is node-only and touches + # its SQLite DB (the same setup the unit + e2e jobs do). The ui-website + # ships no `webjs test` suite, so it is covered by the + # `node test/bun/app-boot.mjs` boot-check step below (#627), which boots it + # via createRequestHandler and asserts it serves real routes with no # broken modulepreload; the same script runs on Bun in the `bun` job (#542). steps: - uses: actions/checkout@v6 @@ -398,11 +407,14 @@ jobs: run: npm test --workspace=@webjsdev/website - name: blog tests (node) run: npm test --workspace=@webjsdev/example-blog - # docs + ui-website ship no `webjs test` suite (#627): boot all three - # non-blog apps on Node and assert each serves real routes with no broken - # modulepreload. Runs the apps' `webjs.start.before` presteps first (the - # ui-website registry copy). The blog is covered by the e2e job. - - name: App boot-check on Node (website + docs + ui-website) + # The ui-website ships no `webjs test` suite (#627): boot the non-blog + # apps on Node and assert each serves real routes with no broken + # modulepreload, including the website's /docs routes. Runs the apps' + # `webjs.start.before` presteps first (the ui-website registry copy). The + # blog is covered by the e2e job. The docs.webjs.dev redirect host is not + # here on purpose: every route on it is an empty 301, which would pass + # vacuously; it is covered by test/docs/docs-host-redirect.test.mjs. + - name: App boot-check on Node (website incl. /docs + ui-website) run: node test/bun/app-boot.mjs docker: diff --git a/AGENTS.md b/AGENTS.md index 1b6671c47..2e2358195 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,7 +72,7 @@ When interactive approval is disabled, never block on questions. Auto-decide: on Every code change MUST include, automatically: 1. **Tests, every applicable layer (not just unit).** Ship the tests that prove the change across EVERY layer it touches: **unit** (`packages/*/test/**`, `test/**`, including the counterfactual that fails when reverted), **browser** (`*/test/**/browser/*` via `npm run test:browser`, for hydration / DOM / slots / client router / custom-element upgrade), **e2e** (`test/e2e/*.test.mjs` via `WEBJS_E2E=1`, including network probes / navigation / streaming), and **smoke** (`test/examples/*/smoke/*`). A unit test is NECESSARY BUT NOT SUFFICIENT for any client-router / component / browser-facing change (the headline behaviour is a browser/e2e assertion). **Bun parity is part of the task, not an afterthought:** WebJs runs on Node 24+ AND Bun (#508), so a change to a runtime-sensitive surface (the serializer, the node:http vs `Bun.serve` listener + request path, SSR / action / CSRF dispatch, streams, `node:crypto`, the TS stripper, auth / session / cors) MUST be proven on Bun (`node scripts/run-bun-tests.js` + the touched `test/bun/*.mjs` under `bun`) AND ship an added/updated `test/bun/.mjs` cross-runtime assertion. `npm test` does NOT run browser, e2e, or Bun; run them yourself and report the result. Never report work done with failing or missing tests. See `references/testing.md`. Enforced by `.claude/hooks/require-tests-with-src.sh` (the scaffold variant WARNS unless `WEBJS_TEST_GATE=block`) and `.claude/hooks/require-bun-parity-with-runtime-src.sh` (BLOCKS a commit that stages runtime-sensitive source with no `test/bun/**` test; escape hatch `WEBJS_BUN_VERIFIED=1`). -2. **Documentation, part of the definition of done (not optional).** A task is NOT done until EVERY doc surface its change touches is in sync: `AGENTS.md` + the skill at `.agents/skills/webjs/` (SKILL.md + references/) for new API surface, `CONVENTIONS.md` (and per-package `AGENTS.md`) for new conventions, the docs site (`docs/app/docs/`), the marketing `website/`, the scaffold templates (`packages/cli/templates/` per-agent rule files), and `README.md` for a headline capability. Updating `AGENTS.md` alone reproduces the #488 gap (docs site left stale). Invoke the `webjs-doc-sync` skill to sync every applicable surface. Enforced by `.claude/hooks/require-docs-with-src.sh`, which BLOCKS a commit that stages public `packages/*/src` source with no doc surface alongside it (a genuinely internal refactor / CI / release / perf change with no behaviour change bypasses with `WEBJS_NO_DOC_GATE=1`). +2. **Documentation, part of the definition of done (not optional).** A task is NOT done until EVERY doc surface its change touches is in sync: `AGENTS.md` + the skill at `.agents/skills/webjs/` (SKILL.md + references/) for new API surface, `CONVENTIONS.md` (and per-package `AGENTS.md`) for new conventions, the docs site (`website/app/docs/`), the marketing `website/`, the scaffold templates (`packages/cli/templates/` per-agent rule files), and `README.md` for a headline capability. Updating `AGENTS.md` alone reproduces the #488 gap (docs site left stale). Invoke the `webjs-doc-sync` skill to sync every applicable surface. Enforced by `.claude/hooks/require-docs-with-src.sh`, which BLOCKS a commit that stages public `packages/*/src` source with no doc surface alongside it (a genuinely internal refactor / CI / release / perf change with no behaviour change bypasses with `WEBJS_NO_DOC_GATE=1`). 3. **Scaffold + skill sync (when a feature changes what apps should do).** The scaffold `webjs create` emits is a gallery index home + a root layout + db wiring, a densely-commented feature gallery (`packages/cli/templates/gallery/**`, single-concept demos under `app/features/` plus the `app/examples/todo` app, shipped in every UI template) and the api backend-features showcase (`packages/cli/lib/api-gallery.js`), plus the one cross-agent skill at `packages/cli/templates/.agents/skills/webjs/` (SKILL.md + references). So when a WebJs feature is added or changed, ask: does the generator (`packages/cli/lib/{create,api-gallery}.js`), a gallery demo (`packages/cli/templates/gallery/`), or the agent skill (`.agents/skills/webjs/SKILL.md` + its `references/`) need to move so a freshly scaffolded app and the skill teach the new reality? Verify by generating an app and running `generate + boot + webjs check` (the generators emit strings, so an escaping bug only shows in a freshly generated app). See `framework-dev.md`. 4. **Convention validation.** Run `webjs check` and fix violations. @@ -157,7 +157,7 @@ app/ ROUTING ONLY (thin adapters importing from modules/; /route.js HTTP handler at / /middleware.js per-segment middleware /loading.js auto Suspense boundary -middleware.js root middleware (every request) +middleware.js root middleware (every request; .ts/.mts/.mjs too) readiness.js optional /__webjs/ready check (return false/throw = 503) env.js optional boot-time env validation (schema or validator fn; fails fast) instrumentation.js optional boot-time hook (register(); wire APM via setOnError, #848) @@ -286,7 +286,7 @@ Named async exports per method (`GET`/`POST`/`PUT`/`PATCH`/`DELETE`), each `(Req ### Middleware (`middleware.{js,ts}`) -Optional top-level + per-segment. Default export `async (req, next) => Response`. Return a Response to short-circuit, or call `next()` then post-process. Per-segment applies to its subtree, outermost to innermost. +Optional top-level + per-segment. Default export `async (req, next) => Response`. Return a Response to short-circuit, or call `next()` then post-process. Per-segment applies to its subtree, outermost to innermost. The root file sits beside `app/` and may be `middleware.ts` / `.js` / `.mts` / `.mjs` (`.ts` wins if more than one exists). ### Env validation (`env.{js,ts}`) @@ -430,7 +430,7 @@ const result = await optimistic(liked, true, () => likePost(postId)); ## Invariants (for both humans and agents) -> Hit one of these as a runtime error? The [Troubleshooting page](https://docs.webjs.dev/docs/troubleshooting) is keyed by symptom (the throw-at-load server import, the backtick-in-template 500, the TypeScript strip failure, the SSR browser-global crash, the missing-frame swap) and maps each back to the invariant and the `webjs check` rule below. +> Hit one of these as a runtime error? The [Troubleshooting page](https://webjs.dev/docs/troubleshooting) is keyed by symptom (the throw-at-load server import, the backtick-in-template 500, the TypeScript strip failure, the SSR browser-global crash, the missing-frame swap) and maps each back to the invariant and the `webjs check` rule below. 1. **Server-only code goes in `.server.{js,ts}` files, `route.ts` handlers, or `middleware.ts`. Never in pages, layouts, or components.** The `.server.{js,ts}` extension is the path-level boundary (the file router refuses to serve the source); a `'use server'` directive additionally makes exports RPC-callable, else the file is a server-only utility whose browser import is a throw-at-load stub. Importing a DB driver (`pg`), `node:*`, or any server-only dep from a component or an `app/**` page / layout / loading / error / not-found file crashes the browser at module load. 2. **Every `*.server.{js,ts}` file with `'use server'` exports must be `async` functions returning serializer-safe values.** Args and results round-trip via webjs's wire. Files without `'use server'` (server-only utilities) can export anything, including singletons. @@ -451,7 +451,7 @@ const result = await optimistic(liked, true, () => likePost(postId)); Two scaffolds exist (do not invent template names): `webjs create ` (full-stack: layout, page, components, modules, Drizzle+SQLite, plus a browsable feature gallery), and `webjs create --template api` (backend-only routes + modules + Drizzle, no SSR). Auth is one of the full-stack gallery cards (login + a signed session + a real protected route), so a full-stack app already carries a promotable auth baseline (this replaced the former `--template saas`). The `--db sqlite|postgres` flag (default sqlite) picks the dialect; the schema/queries/actions are identical across dialects (see #563). The `--runtime node|bun` flag (default node, #541) is ORTHOGONAL to the template and re-flavors either for Bun (`bun --bun` dev/start scripts so the SERVER runs on Bun, `bun.lock`, a pure `oven/bun:1` Dockerfile (#595; safe since cli@0.10.20's npx-free `webjs db migrate` (#570) needs no Node in the image) + bun-install CI, bun-command agent docs; the test/db/check tooling stays on Node); `bun create webjs ` auto-detects it. Pick from the request: default (full-stack) for any product with UI (todo, blog, dashboard, marketplace, social, e-commerce, a SaaS with accounts/login), `api` for an HTTP/JSON API with no UI; default to full-stack when ambiguous. -Rules: **always scaffold via `webjs create`** (never hand-roll). **Default to a real database (Drizzle + SQLite); NEVER use JSON files, in-memory arrays, or localStorage for persistence.** Update `db/schema.server.ts` to real models FIRST, then `webjs db generate` + `webjs db migrate`, then build pages/actions/queries. **The scaffold ships a gallery index home, a neutral-palette root layout, db wiring, and a densely-commented feature gallery** (single-concept demos under `app/features/` plus the `app/examples/todo` app, with logic in `modules/`), alongside the one cross-agent skill at `.agents/skills/webjs/` (a routing `SKILL.md` + on-demand `references/`) that teaches how to build. Treat the gallery as browsable reference: read the demos to learn the idioms, then build the app the user asked for by growing the scaffold (add routes under `app/`, components under `components/`, features under `modules//`, keep server-only code behind `.server.ts`) and prune the demos the app does not use (delete the `app/features/` route AND its `modules/`). Give a UI app its own palette by setting the token values in `app/layout.ts`. There is no design gate and no placeholder gate: taste is the agent's job, not the checker's. Docs at https://docs.webjs.dev. +Rules: **always scaffold via `webjs create`** (never hand-roll). **Default to a real database (Drizzle + SQLite); NEVER use JSON files, in-memory arrays, or localStorage for persistence.** Update `db/schema.server.ts` to real models FIRST, then `webjs db generate` + `webjs db migrate`, then build pages/actions/queries. **The scaffold ships a gallery index home, a neutral-palette root layout, db wiring, and a densely-commented feature gallery** (single-concept demos under `app/features/` plus the `app/examples/todo` app, with logic in `modules/`), alongside the one cross-agent skill at `.agents/skills/webjs/` (a routing `SKILL.md` + on-demand `references/`) that teaches how to build. Treat the gallery as browsable reference: read the demos to learn the idioms, then build the app the user asked for by growing the scaffold (add routes under `app/`, components under `components/`, features under `modules//`, keep server-only code behind `.server.ts`) and prune the demos the app does not use (delete the `app/features/` route AND its `modules/`). Give a UI app its own palette by setting the token values in `app/layout.ts`. There is no design gate and no placeholder gate: taste is the agent's job, not the checker's. Docs at https://webjs.dev/docs. --- diff --git a/Dockerfile b/Dockerfile index 59127226f..cee40acce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -117,13 +117,14 @@ RUN npm run build:dist --workspace=@webjsdev/core # build-safe. RUN cd packages/ui/packages/website && node scripts/copy-registry.js -# Tailwind: compile per-app CSS (all four use the CLI, no browser runtime). +# Tailwind: compile per-app CSS (every app with a stylesheet uses the CLI, no +# browser runtime). The `docs` service is absent on purpose: it renders no HTML +# at all now, only redirects to webjs.dev/docs, so it has no stylesheet. # Each compose service's command invokes `webjs.js start` directly, which # bypasses the per-package `prestart: css:build` hook in npm; the CSS has # to be ready in the image. Keep this list in sync with the apps that # have a public/input.css and a `css:build` script in their package.json. RUN npx tailwindcss -i website/public/input.css -o website/public/tailwind.css --minify \ - && npx tailwindcss -i docs/public/input.css -o docs/public/tailwind.css --minify \ && npx tailwindcss -i examples/blog/public/input.css -o examples/blog/public/tailwind.css --minify \ && npx tailwindcss -i packages/ui/packages/website/public/input.css -o packages/ui/packages/website/public/tailwind.css --minify diff --git a/README.md b/README.md index 8e2dcfc2f..0079cef90 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ older, unrelated Java framework also used the name WebJS. - **No build step you run.** `.ts` files served directly. Node 24+ or Bun is the runtime (run a Bun app with `bun --bun run dev` / `start`), and the dev server strips types via Node's built-in `module.stripTypeScriptTypes` (or `amaro` on Bun, byte-identical), position-preserving, no sourcemap, near-zero overhead. TypeScript must be erasable. Non-erasable constructs (enums, value-carrying namespaces, constructor parameter properties, legacy decorators with `emitDecoratorMetadata`) fail at strip time with a 500 pointing at the `no-non-erasable-typescript` lint rule, since WebJs is buildless end-to-end with no bundler fallback. Edit, refresh, done. - **Web components, light DOM by default.** Pages and components render as light DOM so global CSS and Tailwind utilities apply directly: no `::part`, no `:host`, no CSS-var plumbing. Shadow DOM is opt-in (`static shadow = true`) when you need scoped styles or third-party-embed isolation. `` projection (named slots, fallback content, `assignedNodes` / `slotchange`) works identically in both modes. Both modes SSR fully, no hydration runtime. - **Progressive enhancement, built in.** Pages *and* components are SSR'd to real HTML. Every web component's `render()` runs on the server, so its initial markup is in the response before any script loads. Content reads, links navigate, forms submit (server actions are plain HTML POSTs), and display-only custom elements look right, all without JavaScript. JS is opt-in *per interactive behavior*, not per component: a counter renders as "0" without JS, and only the +/- click handling needs scripts. The HTML is the floor, and the client router and `@click` / signal interactivity are layered on top. -- **Tailwind CSS by default.** The scaffold compiles a static Tailwind stylesheet (`css:build`, run automatically by dev / start) with shadcn-style `@theme` design tokens, so the app is fully styled with JavaScript disabled. Prefer hand-written CSS? Opt out entirely, and the framework works just as well with vanilla CSS when you follow the wrapper-scoping convention (`.page-`, `.layout-`, component-tag scoped). Full recipe in the [Styling docs](./docs/app/docs/styling/page.ts). +- **Tailwind CSS by default.** The scaffold compiles a static Tailwind stylesheet (`css:build`, run automatically by dev / start) with shadcn-style `@theme` design tokens, so the app is fully styled with JavaScript disabled. Prefer hand-written CSS? Opt out entirely, and the framework works just as well with vanilla CSS when you follow the wrapper-scoping convention (`.page-`, `.layout-`, component-tag scoped). Full recipe in the [Styling docs](./website/app/docs/styling/page.ts). - **Full-stack type safety.** Import a `.server.ts` function from a component, and TypeScript sees the real signature. webjs's built-in ESM serializer on the wire preserves `Date`, `Map`, `Set`, `BigInt`, `TypedArray`, `Blob`, `File`, `FormData`, and reference cycles. - **Server-file source is unreachable from the browser.** Framework invariant: any file ending `.server.{js,ts}` is source-protected. With `'use server'` it serves an RPC stub (server action); without, a throw-at-load stub (server-only utility). Either way the real source never reaches the browser. Enforced in the HTTP layer with regression tests. - **NextJs-style routing.** `page.ts`, `layout.ts`, `route.ts`, `error.ts`, `middleware.ts`, `[params]`, `(groups)`, `_private`. Layouts persist across navigations. @@ -69,7 +69,7 @@ older, unrelated Java framework also used the name WebJS. > **The scaffold is a starting point, grown in place.** It ships a gallery index home, a neutral-palette root layout, database wiring, and a densely-commented feature gallery (single-concept demos under `app/features/` plus the `app/examples/todo` app, with logic in `modules/`). The framework context ships as one cross-agent skill, `.agents/skills/webjs/SKILL.md` (a routing skill), alongside `AGENTS.md` and `.agents/rules/workflow.md`. Read the skill and browse the gallery, then build the app the user asked for on top of the scaffold, pruning the demos it does not use. > > Full rules: [`AGENTS.md` → How AI agents must scaffold](./AGENTS.md#how-ai-agents-must-scaffold). -> Full framework docs (every API, every recipe): **https://docs.webjs.dev**. +> Full framework docs (every API, every recipe): **https://webjs.dev/docs**. ```sh # Get started in one command (no global install required) @@ -123,8 +123,8 @@ packages/ webjsdev/ # unscoped npm name for @webjsdev/cli (so `npm i -g webjsdev` works without a scope) examples/ blog/ # full-featured reference app (auth, posts, comments, chat) -docs/ # documentation site (built on webjs itself) -website/ # landing site (built on webjs itself) +website/ # landing site AND the documentation at /docs +docs/ # docs.webjs.dev, a redirect-only host (kept forever) AGENTS.md # AI-agent contract for the framework CLAUDE.md # Claude Code quick-reference ``` @@ -150,14 +150,14 @@ because macOS reserves it for the AirPlay Receiver / Control Center): | App | Dir | Port | Env override | |---|---|---|---| | Landing site | `website/` | 5001 | `WEBSITE_PORT` | -| Docs | `docs/` | 5002 | `DOCS_PORT` | +| docs.webjs.dev redirect | `docs/` | 5002 | `DOCS_PORT` | | UI registry site | `packages/ui/packages/website/` | 5003 | `UI_PORT` | | Example blog | `examples/blog/` | 5004 | `BLOG_PORT` | **Run a single app** (from its directory). Each honors a `PORT` env var: ```sh -cd docs && npm run dev # docs on 5002 +cd website && npm run dev # landing site + docs on 5001 PORT=8080 npm run dev # ...or on 8080 ``` @@ -174,10 +174,11 @@ WEBSITE_PORT=8001 DOCS_PORT=8002 UI_PORT=8003 BLOG_PORT=8004 npm run dev > env var is the supported interface (and the conventional one: Railway, > Heroku, Fly, etc. all drive port via `PORT`). -The apps cross-link by URL (the landing site links to docs/demo/UI, -the UI site links back). Those default to the localhost ports above and -are overridable via `DOCS_URL` / `EXAMPLE_BLOG_URL` / `UI_URL` / `WEBSITE_URL` -(this is also how deploys point them at real domains). +The apps cross-link by URL (the landing site links to the demo and the UI +kit, the UI site links back). Those default to the localhost ports above and +are overridable via `EXAMPLE_BLOG_URL` / `UI_URL` / `WEBSITE_URL` (this is also +how deploys point them at real domains). The docs are not in that list: they +are served by the landing site itself at `/docs`, so they are a plain path. ## Example @@ -273,12 +274,16 @@ const resp = await app.handle(new Request('http://x/api/hello')); ## Documentation -The docs site is built on WebJs itself: +The docs are part of the landing site, served at `/docs` and built on WebJs +itself. They live in `website/app/docs/`, so running the site runs them: ```sh -cd docs && npm run dev # webjs dev; compiles Tailwind, then recompiles on request (see AGENTS.md) +cd website && npm run dev # webjs dev; compiles Tailwind, then recompiles on request (see AGENTS.md) ``` +`docs.webjs.dev` still resolves and path-preservingly redirects here, because +error messages in already-published npm packages point at it. + 37 pages covering: getting started, AI-first development, routing, components, SSR, styling, Suspense, loading states, error handling, client router, server actions, REST endpoints, API routes, diff --git a/compose.yaml b/compose.yaml index 69c7f3893..bc221b50d 100644 --- a/compose.yaml +++ b/compose.yaml @@ -3,7 +3,7 @@ # # docker compose up --build # website → http://localhost:15001 -# docs → http://localhost:15002 +# docs → http://localhost:15002 (redirect-only; the docs live at website/docs) # blog → http://localhost:15004 # ui-website → http://localhost:15003 (registry host + @webjsdev/ui docs) # @@ -25,10 +25,12 @@ services: - "15001:5001" environment: PORT: "5001" - DOCS_URL: ${DOCS_URL:-http://localhost:15002} EXAMPLE_BLOG_URL: ${EXAMPLE_BLOG_URL:-http://localhost:15004} UI_URL: ${UI_URL:-http://localhost:15003} + # The documentation itself is served by the `website` service at /docs. This + # service is the docs.webjs.dev host, which redirects every request there and + # must keep resolving indefinitely (shipped npm packages link to it). docs: image: webjs build: . @@ -38,6 +40,7 @@ services: - "15002:5002" environment: PORT: "5002" + SITE_URL: ${SITE_URL:-http://localhost:15001} blog: image: webjs diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 9820af024..6673b9936 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -1,90 +1,42 @@ -# AGENTS.md for the docs site +# AGENTS.md for docs.webjs.dev (redirect-only) -The WebJs documentation site, built on WebJs itself (eating our own -dogfood). All framework-wide rules (file conventions, public API, -workflow, scaffold rules, persistence rules, autonomous-mode behaviour) -live in the **framework root [`../AGENTS.md`](../AGENTS.md)** and apply -here. Read that first. +**The documentation itself no longer lives here.** It is served by the +marketing site at `webjs.dev/docs`, from `website/app/docs/**`. To add or +edit a doc page, go to [`../website/AGENTS.md`](../website/AGENTS.md). -This file only covers what's specific to the docs app. +This directory is now a tiny redirect-only service, and it exists for one +reason: **`docs.webjs.dev` must keep resolving forever.** Framework error +messages in ALREADY-PUBLISHED npm packages point at that host, and a +published version can never be retroactively corrected, so old installs will +keep sending people here for as long as they run. Do not delete this service +and do not repoint its domain at something that 404s. -## Layout +## What it does -``` -docs/ - app/ - layout.ts root layout (head, theme, GA, fonts) - page.ts / → docs homepage / landing - docs/ - layout.ts sub-layout: sidebar + content shell. - **The sidebar source-of-truth lives here.** - /page.ts one page per doc topic - api/ lightweight endpoints (search index, etc.) - components/ - doc-search.ts search palette - theme-toggle.ts light/dark cycle - public/ static assets (favicon, og image) -``` - -## How to add a new doc page - -1. Create `docs/app/docs//page.ts`. Export a default - function returning `html\`…\`` and export a `metadata` object with - at least `title`. -2. Register it in the sidebar in `docs/app/docs/layout.ts`. Find the - `sections` array (look for entries like - `{ href: '/docs/getting-started', label: 'Introduction' }`) and - add a new entry in the correct section. The href must match the - folder name, and the label is the visible sidebar text. -3. If the page covers a NEW API surface, also update the framework - root `../AGENTS.md` (the API reference) per the framework workflow. - -That's it. No separate manifest, no rebuild. +`middleware.ts` answers every request with a path-preserving permanent +redirect to the same path on `webjs.dev`, so `docs.webjs.dev/docs/routing` +lands on `webjs.dev/docs/routing` rather than dumping the visitor on a hub +page. A bare `/` goes to `/docs` (someone visiting this host wants the +documentation, not the marketing home page). `/__webjs/*` is exempt, because +`/__webjs/ready` is the healthcheck the deploy gates on. -## Machine-readable agent entrypoints (llms.txt) +The target origin comes from `SITE_URL`, falling back to `https://webjs.dev`. -The docs site serves the open llms.txt standard (llmstxt.org) so AI -agents can read the docs as plain text. Three surfaces, all generated -**live at request time** from the doc pages under `app/docs/**`, so they -stay in sync with zero build step (add a doc page and it appears -automatically): +## Why the docs moved -| URL | What it serves | -|---|---| -| `/llms.txt` | A structured INDEX. An `# WebJs documentation` H1, a one-line blurb, then a markdown bullet list of every doc page (title, blurb, absolute link). Ordered to match the sidebar nav. | -| `/llms-full.txt` | The full prose CORPUS. Every doc page concatenated as lightweight markdown. In the monorepo it also folds in the skill at `.agents/skills/webjs/` (SKILL.md + references/); a standalone deploy that lacks the repo root simply skips that (try/catch read). | -| `/docs//llms.txt` | One page's raw markdown. Every topic gets one via the `app/docs/[topic]/llms.txt/route.ts` dynamic route. | - -The generators live in `lib/llms.server.ts` (server-only `.server.ts` -infix, reads doc pages with node:fs, reuses the search route's title / -heading / template-stripping approach). The routes are thin -`route.ts` GET handlers under `app/llms.txt/`, `app/llms-full.txt/`, and -`app/docs/[topic]/llms.txt/`. The folder named `llms.txt` maps to the -`/llms.txt` URL because a `route.ts` handler matches before the -static-asset gate. Absolute links derive from the request origin (so -they are correct in dev and prod). Integration test: -`../test/docs/llms.test.mjs`. - -## Style - -- Light DOM throughout. Tailwind utilities. Design tokens via `@theme` - in the root layout. -- Doc pages return plain HTML in `html\`…\``: `

`, `

`, `

`, - `

`, ``, `
    `. No custom components per page. Consistency - comes from the layout's global styles. +Two costs, both structural. 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, the docs carried their own +layout, nav, and footer, so a reader crossing over from the marketing site +left one design system and entered another. Serving the docs as a path on +the main domain removes both at once. See webjsdev/webjs#1098. ## Run ```sh -cd docs && npm run dev # http://localhost:5002 +cd docs && npm run dev # http://localhost:5002, redirects everything ``` -`npm run dev` and `webjs dev` behave identically (#550): `webjs.dev.before` -compiles `public/tailwind.css`, and `webjs.dev.regenerate` (#967) recompiles it -on request when a source changes, so it never goes stale without a live -watcher. In prod, `npm start` and `webjs start` are equivalent too: -`webjs.start.before` runs `npm run css:build` before serving. - --- Framework-wide rules and full API reference: diff --git a/docs/app/api/search/route.ts b/docs/app/api/search/route.ts deleted file mode 100644 index 862e3dce9..000000000 --- a/docs/app/api/search/route.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { readFile } from 'node:fs/promises'; -import { join, basename, dirname } from 'node:path'; - -// Build search index lazily on first request, cache in memory. -let index: SearchEntry[] | null = null; - -type SearchEntry = { - path: string; - title: string; - headings: string[]; - text: string; // plain text, lowercase, for matching -}; - -async function buildIndex(): Promise { - if (index) return index; - - const { walk } = await import('../../../../packages/server/src/fs-walk.js'); - const docsRoot = join(process.cwd(), 'app', 'docs'); - const entries: SearchEntry[] = []; - - for await (const file of walk(docsRoot)) { - if (!file.endsWith('page.ts') && !file.endsWith('page.js')) continue; - const raw = await readFile(file, 'utf8'); - - // Extract title from metadata - const titleMatch = raw.match(/title:\s*['"]([^'"]+)['"]/); - const title = titleMatch?.[1] || basename(dirname(file)); - - // Extract headings from html template - const headings: string[] = []; - for (const m of raw.matchAll(/]*>([^<]+)]+>/g, ' ') - .replace(/\$\{[^}]*\}/g, ' ') - .replace(/html`|css`|`/g, ' ') - .replace(/\s+/g, ' ') - .toLowerCase(); - - // Derive URL path from file path - const rel = file.slice(docsRoot.length).replace(/\/page\.(ts|js)$/, ''); - const path = '/docs' + (rel || ''); - - entries.push({ path, title, headings, text }); - } - - index = entries; - return entries; -} - -export async function GET(req: Request) { - const url = new URL(req.url); - const q = (url.searchParams.get('q') || '').trim().toLowerCase(); - if (!q || q.length < 2) { - return Response.json([]); - } - - const entries = await buildIndex(); - const terms = q.split(/\s+/); - - const results = entries - .map(entry => { - let score = 0; - for (const term of terms) { - if (entry.title.toLowerCase().includes(term)) score += 10; - for (const h of entry.headings) { - if (h.toLowerCase().includes(term)) score += 5; - } - if (entry.text.includes(term)) score += 1; - } - // Extract a snippet around the first match - let snippet = ''; - const idx = entry.text.indexOf(terms[0]); - if (idx >= 0) { - const start = Math.max(0, idx - 60); - const end = Math.min(entry.text.length, idx + 120); - snippet = (start > 0 ? '…' : '') + entry.text.slice(start, end).trim() + (end < entry.text.length ? '…' : ''); - } - return { path: entry.path, title: entry.title, score, snippet }; - }) - .filter(r => r.score > 0) - .sort((a, b) => b.score - a.score) - .slice(0, 8); - - return Response.json(results); -} diff --git a/docs/app/docs/layout.ts b/docs/app/docs/layout.ts deleted file mode 100644 index c5dd11d00..000000000 --- a/docs/app/docs/layout.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { html } from '@webjsdev/core'; -import '#components/theme-toggle.ts'; -import '#components/doc-search.ts'; - -/** - * Docs sub-layout: sidebar + content shell. Light DOM throughout. - * Styling via Tailwind utility classes. Content typography for doc - * pages lives in the .prose-docs rules in the - - - - - -
    - -
    -
    ${children}
    -
    -
    - `; -} diff --git a/docs/app/error.ts b/docs/app/error.ts deleted file mode 100644 index 1c2874783..000000000 --- a/docs/app/error.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { html } from '@webjsdev/core'; - -/** - * Root error boundary. Any uncaught error thrown while rendering a page - * (or layout, or async hole) that is NOT a notFound() / redirect() - * sentinel lands here. Receives the thrown value as ctx.error. - * - * Keep this file's import graph minimal: it MUST stay importable when - * the rest of the app is broken, otherwise the framework falls through - * to its built-in generic 500 ("Server error / Something went wrong"). - * Only @webjsdev/core; no helpers, no custom elements, no shared - * components. - * - * (Do not put U+0060 GRAVE ACCENT characters in comments inside the - * html template body below: they close the tagged template literal - * at JS-parse time and re-introduce the very 500 this file exists to - * catch. See [[feedback-html-template-no-backticks]].) - */ -export default function ErrorBoundary({ error }: { error: unknown }) { - const message = error instanceof Error ? error.message : String(error); - return html` -
    -
    500 · server error
    -

    Something went wrong.

    -

    We hit an unexpected error while rendering this page. The full stack is logged on the server; only the short message is shown here.

    -
    ${message}
    - ← Back home -
    - `; -} diff --git a/docs/app/layout.ts b/docs/app/layout.ts deleted file mode 100644 index 1ed09e534..000000000 --- a/docs/app/layout.ts +++ /dev/null @@ -1,255 +0,0 @@ -import { html, cspNonce } from '@webjsdev/core'; - -/** - * Root layout for the docs site: Tailwind CSS browser runtime + - * @theme design tokens. Light DOM everywhere. Shell chrome (sidebar + - * content) lives in app/docs/layout.ts so the sidebar only renders on - * documentation pages. - */ - -const TITLE = 'WebJs - Documentation'; -const DESCRIPTION = 'Getting started, routing, components, server actions, deployment, and more.'; - -export function generateMetadata(ctx: { url: string }) { - const origin = new URL(ctx.url).origin; - const image = `${origin}/public/og.png`; - // Site-wide canonical, derived here so every docs page gets one from a single - // place (the docs site had none at all). Built from origin plus pathname, so - // tracking query strings and a stray trailing slash collapse onto one URL - // instead of splitting ranking signals across near-duplicate addresses. - // Mirrors website/app/layout.ts. - const { pathname } = new URL(ctx.url); - const canonical = origin + (pathname === '/' ? '' : pathname.replace(/\/+$/, '')); - return { - // Docs pages are identical for every visitor, so cache at the CDN. Set on - // the root layout so it applies to every doc page (a page could override). - cacheControl: 'public, max-age=0, s-maxage=600, stale-while-revalidate=86400', - alternates: { canonical }, - title: TITLE, - description: DESCRIPTION, - openGraph: { - type: 'website', - title: TITLE, - description: DESCRIPTION, - url: origin, - image, - 'image:width': '1200', - 'image:height': '630', - 'image:alt': 'WebJs documentation', - 'site_name': 'WebJs · docs', - }, - twitter: { - card: 'summary_large_image', - title: TITLE, - description: DESCRIPTION, - image, - }, - }; -} - -export default function RootLayout({ children }: { children: unknown }) { - const nonce = cspNonce(); - return html` - - - - - - - - - - - - - ${children} - - `; -} diff --git a/docs/app/llms.txt/route.ts b/docs/app/llms.txt/route.ts deleted file mode 100644 index cf6302559..000000000 --- a/docs/app/llms.txt/route.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * GET /llms.txt - * - * The llms.txt INDEX (llmstxt.org standard): a structured list of every - * doc page with title, one-line description, and absolute link. Served - * as text/plain and generated live from the doc pages, so it never - * drifts (no build step). - * - * The folder is literally named `llms.txt`, so the file router maps it - * to the `/llms.txt` URL. This routes cleanly because a WebJs route.ts - * handler is matched BEFORE the static-asset / source-file gate. - */ -import { renderLlmsIndex, textResponse } from '#lib/llms.server.ts'; - -export async function GET(req: Request) { - return textResponse(await renderLlmsIndex(req)); -} diff --git a/docs/app/not-found.ts b/docs/app/not-found.ts deleted file mode 100644 index c6f70b6b0..000000000 --- a/docs/app/not-found.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { html } from '@webjsdev/core'; - -/** - * Root 404 boundary. Renders when a page or server action throws - * notFound() AND when no route matches the requested URL. - * - * Keep this file's import graph minimal: same reason as error.ts: - * if this file fails to load, the framework falls through to its - * generic 404 page. Only @webjsdev/core, nothing else. - * - * (Do not put U+0060 GRAVE ACCENT characters in comments inside the - * html template body below. See [[feedback-html-template-no-backticks]].) - */ -export default function NotFound() { - return html` -
    -
    404 · not found
    -

    Page not found.

    -

    The page you were looking for does not exist.

    - ← Back home -
    - `; -} diff --git a/docs/app/page.ts b/docs/app/page.ts index 5441685fb..2ca837c5d 100644 --- a/docs/app/page.ts +++ b/docs/app/page.ts @@ -1,5 +1,11 @@ import { redirect } from '@webjsdev/core'; -export default function DocsRoot() { - redirect('/docs/getting-started'); +/** + * Unreachable in practice: middleware.ts catches every request to this host + * and 301s it to webjs.dev. This page exists so the app still has a route + * (and so a bare visit is handled if the middleware is ever bypassed), not + * because anything is expected to render it. + */ +export default function DocsHostRoot() { + redirect('https://webjs.dev/docs', 301); } diff --git a/docs/components/theme-toggle.ts b/docs/components/theme-toggle.ts deleted file mode 100644 index b770379ad..000000000 --- a/docs/components/theme-toggle.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { WebComponent, html, signal } from '@webjsdev/core'; - -/** - * ``: three-state theme switcher: system → light → dark → system. - * - * State is mirrored to localStorage (`webjs_theme`) and reflected as - * ``. The initial theme is set by the synchronous bootstrap - * script in layout.js so there's no FOUC on page load. - */ -type Theme = 'system' | 'light' | 'dark'; - -export class ThemeToggle extends WebComponent { - theme = signal('system'); - - connectedCallback() { - super.connectedCallback(); - let saved: string | null = null; - try { saved = localStorage.getItem('webjs_theme'); } catch {} - this.theme.set(saved === 'light' || saved === 'dark' ? saved : 'system'); - } - - cycle() { - const t = this.theme.get(); - const next: Theme = - t === 'system' ? 'light' - : t === 'light' ? 'dark' : 'system'; - this.theme.set(next); - try { - if (next === 'system') localStorage.removeItem('webjs_theme'); - else localStorage.setItem('webjs_theme', next); - } catch {} - if (next === 'system') delete document.documentElement.dataset.theme; - else document.documentElement.dataset.theme = next; - } - - render() { - const t = this.theme.get(); - const label = t === 'system' ? 'AUTO' : t === 'light' ? 'LIGHT' : 'DARK'; - const icon = t === 'light' ? ICONS.sun : t === 'dark' ? ICONS.moon : ICONS.system; - return html` - - `; - } -} - -const ICONS = { - sun: html``, - moon: html``, - system: html``, -}; - -ThemeToggle.register('theme-toggle'); diff --git a/docs/middleware.ts b/docs/middleware.ts new file mode 100644 index 000000000..c338352c5 --- /dev/null +++ b/docs/middleware.ts @@ -0,0 +1,49 @@ +/** + * docs.webjs.dev is now a redirect-only host. + * + * The documentation moved onto the main domain at webjs.dev/docs. A + * subdomain accrues its own authority in search rather than contributing to + * webjs.dev, and it carried a second layout that drifted from the marketing + * site's. Both problems go away by serving the docs as a path. + * + * This host MUST keep resolving indefinitely, not just through a migration + * window. Framework error messages in ALREADY-PUBLISHED npm packages point + * at docs.webjs.dev (see packages/core/src/component.js and + * packages/server/src/actions.js), and a published version can never be + * retroactively corrected, so old installs will keep sending people here for + * as long as they run. Every one of those must land on real documentation. + * + * The redirect is path-preserving and permanent, so /docs/routing lands on + * webjs.dev/docs/routing rather than dumping every visitor at a hub page and + * making them find their topic again. A 301 also passes the accumulated + * ranking signal to the new URL, which is the point of the move. + */ +const TARGET = (process.env.SITE_URL || 'https://webjs.dev').replace(/\/$/, ''); + +export default async function redirectToSite( + req: Request, + next: () => Promise, +): Promise { + const { pathname, search } = new URL(req.url); + + // The framework's own endpoints stay local, handled by the framework rather + // than answered here. The health and readiness probes are actually served + // before app middleware runs, so they never reach this line, but the rest of + // the namespace does, and answering it with a canned body would break it + // rather than leave it alone. + if (pathname.startsWith('/__webjs/')) return next(); + + // A bare visit to the host means "the documentation", not "the marketing + // home page", so root lands on the docs hub rather than at /. + const target = pathname === '/' ? `${TARGET}/docs` : `${TARGET}${pathname}${search}`; + + return new Response(null, { + status: 301, + headers: { + location: target, + // Long-lived and public: this host now has exactly one behaviour, so + // an intermediary caching the redirect is correct and saves the hop. + 'cache-control': 'public, max-age=86400', + }, + }); +} diff --git a/docs/package.json b/docs/package.json index c0cc4c3dd..cdf709a5b 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,6 +1,6 @@ { - "name": "@webjsdev/docs", - "version": "0.1.0", + "name": "@webjsdev/docs-redirect", + "version": "0.2.0", "type": "module", "private": true, "imports": { @@ -8,12 +8,7 @@ }, "scripts": { "dev": "webjs dev --port ${PORT:-5002}", - "start": "webjs start", - "css:build": "tailwindcss -i ./public/input.css -o ./public/tailwind.css --minify" - }, - "webjs": { - "dev": { "before": ["npm run css:build"], "regenerate": [{ "output": "public/tailwind.css", "command": "tailwindcss -i ./public/input.css -o ./public/tailwind.css --minify", "inputs": ["app", "components", "lib", "public/input.css"] }] }, - "start": { "before": ["npm run css:build"] } + "start": "webjs start" }, "dependencies": { "@webjsdev/cli": "^0.10.0", diff --git a/docs/public/apple-touch-icon.png b/docs/public/apple-touch-icon.png deleted file mode 100644 index 62c768515..000000000 Binary files a/docs/public/apple-touch-icon.png and /dev/null differ diff --git a/docs/public/favicon-192.png b/docs/public/favicon-192.png deleted file mode 100644 index 855b723eb..000000000 Binary files a/docs/public/favicon-192.png and /dev/null differ diff --git a/docs/public/favicon.ico b/docs/public/favicon.ico deleted file mode 100644 index 847ee6a73..000000000 Binary files a/docs/public/favicon.ico and /dev/null differ diff --git a/docs/public/favicon.png b/docs/public/favicon.png deleted file mode 100644 index ebcc07205..000000000 Binary files a/docs/public/favicon.png and /dev/null differ diff --git a/docs/public/favicon.svg b/docs/public/favicon.svg deleted file mode 100644 index 830c28833..000000000 --- a/docs/public/favicon.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/docs/public/fonts/inter-tight.woff2 b/docs/public/fonts/inter-tight.woff2 deleted file mode 100644 index d8f79a69e..000000000 Binary files a/docs/public/fonts/inter-tight.woff2 and /dev/null differ diff --git a/docs/public/fonts/inter.woff2 b/docs/public/fonts/inter.woff2 deleted file mode 100644 index d15208de0..000000000 Binary files a/docs/public/fonts/inter.woff2 and /dev/null differ diff --git a/docs/public/fonts/jetbrains-mono.woff2 b/docs/public/fonts/jetbrains-mono.woff2 deleted file mode 100644 index cd5102a44..000000000 Binary files a/docs/public/fonts/jetbrains-mono.woff2 and /dev/null differ diff --git a/docs/public/input.css b/docs/public/input.css deleted file mode 100644 index 677a7afcd..000000000 --- a/docs/public/input.css +++ /dev/null @@ -1,47 +0,0 @@ -/* Tailwind CSS v4 input - compiled to public/tailwind.css. */ -@import "tailwindcss"; - -@theme { - --color-fg: var(--fg); - --color-fg-muted: var(--fg-muted); - --color-fg-subtle: var(--fg-subtle); - - --color-bg: var(--bg); - --color-bg-elev: var(--bg-elev); - --color-bg-subtle: var(--bg-subtle); - --color-bg-sunken: var(--bg-sunken); - - --color-border: var(--border); - --color-border-strong: var(--border-strong); - - --color-accent: var(--accent); - --color-accent-hover: var(--accent-hover); - --color-accent-fg: var(--accent-fg); - --color-accent-tint: var(--accent-tint); - --color-accent-live: var(--accent-live); - - --font-display: var(--font-display); - --font-sans: var(--font-sans); - --font-serif: var(--font-serif); - --font-mono: var(--font-mono); - - --text-display: clamp(2.6rem, 1.6rem + 3vw, 4rem); - --text-h1: clamp(2rem, 1.5rem + 1.6vw, 2.6rem); - --text-h2: clamp(1.3rem, 1.1rem + 0.7vw, 1.6rem); - --text-lede: clamp(1.05rem, 0.95rem + 0.3vw, 1.18rem); - - --duration-fast: 140ms; - --duration-slow: 220ms; -} - -/* - * Self-hosted Inter set (latin subset woff2 in public/fonts/), matching the - * marketing website. One VARIABLE file per family covers its whole weight - * range, so a font-weight range here means every used weight resolves from a - * single download. No Google Fonts CDN, so no render-blocking third-party - * stylesheet. font-display:swap paints immediately with the fallback, then - * swaps. - */ -@font-face{font-family:'Inter Tight';font-style:normal;font-weight:100 900;font-display:swap;src:url('/public/fonts/inter-tight.woff2') format('woff2');} -@font-face{font-family:'Inter';font-style:normal;font-weight:100 900;font-display:swap;src:url('/public/fonts/inter.woff2') format('woff2');} -@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:100 800;font-display:swap;src:url('/public/fonts/jetbrains-mono.woff2') format('woff2');} diff --git a/docs/public/og.png b/docs/public/og.png deleted file mode 100644 index 935eed1b8..000000000 Binary files a/docs/public/og.png and /dev/null differ diff --git a/docs/tsconfig.json b/docs/tsconfig.json index de86d7ebd..8e7f27232 100644 --- a/docs/tsconfig.json +++ b/docs/tsconfig.json @@ -7,12 +7,10 @@ "strict": true, "types": ["node"], "noEmit": true, - "allowJs": true, - "checkJs": true, "allowImportingTsExtensions": true, "skipLibCheck": true, "erasableSyntaxOnly": true }, - "include": ["app/**/*", "components/**/*"], + "include": ["app/**/*", "middleware.ts"], "exclude": ["node_modules", ".webjs"] } diff --git a/examples/blog/.agents/rules/workflow.md b/examples/blog/.agents/rules/workflow.md index 662b60d49..b2bbef9e0 100644 --- a/examples/blog/.agents/rules/workflow.md +++ b/examples/blog/.agents/rules/workflow.md @@ -3,7 +3,7 @@ You are working on a webjs app, an AI-first, no-build, web-components-first framework. Read AGENTS.md for the full API reference and CONVENTIONS.md for project-specific conventions before writing any code. When AGENTS.md does not -cover what you need, the full hosted docs are at **https://docs.webjs.dev**. +cover what you need, the full hosted docs are at **https://webjs.dev/docs**. ## Persistence + scaffold rules (non-negotiable) diff --git a/examples/blog/.cursorrules b/examples/blog/.cursorrules index bf69dd129..c67687766 100644 --- a/examples/blog/.cursorrules +++ b/examples/blog/.cursorrules @@ -3,7 +3,7 @@ You are working on a webjs app, an AI-first, no-build, web-components-first framework. Read AGENTS.md for the full API reference and CONVENTIONS.md for project-specific conventions before writing any code. When AGENTS.md doesn't -cover what you need, the full hosted docs are at **https://docs.webjs.dev**. +cover what you need, the full hosted docs are at **https://webjs.dev/docs**. ## Persistence + scaffold rules (non-negotiable) diff --git a/examples/blog/.github/copilot-instructions.md b/examples/blog/.github/copilot-instructions.md index ce2988b9f..56b0659db 100644 --- a/examples/blog/.github/copilot-instructions.md +++ b/examples/blog/.github/copilot-instructions.md @@ -3,7 +3,7 @@ You are working on a webjs app, an AI-first, no-build, web-components-first framework. Read AGENTS.md for the full API reference and CONVENTIONS.md for project-specific conventions. When AGENTS.md doesn't cover what you need, -the full hosted docs are at **https://docs.webjs.dev**. +the full hosted docs are at **https://webjs.dev/docs**. ## Persistence + scaffold rules (non-negotiable) diff --git a/framework-dev.md b/framework-dev.md index 350e6f7e0..f4d38e379 100644 --- a/framework-dev.md +++ b/framework-dev.md @@ -1,12 +1,12 @@ # Framework development (editing WebJs itself) -Read this only when editing the WebJs monorepo (this repo), not a scaffolded app. The repo is buildless: `packages/` is plain `.js` with JSDoc (never add `.ts` there); TypeScript is fine in `examples/`, `docs/`, `website/`. Each in-repo app (`website/`, `docs/`, `examples/blog/`, `packages/ui/packages/website/`) is run from its OWN dir via `npm run dev` / `npm start`; as of #550 a bare `webjs dev` / `webjs start` is equivalent (each app's per-environment orchestration, the Tailwind watcher, `webjs db migrate`, the registry copy, moved into its `webjs.dev` / `webjs.start` tasks config, which `webjs dev`/`start` run). The sections below cover the repo-health git config, the changelog flow, and the dev error overlay. +Read this only when editing the WebJs monorepo (this repo), not a scaffolded app. The repo is buildless: `packages/` is plain `.js` with JSDoc (never add `.ts` there); TypeScript is fine in `examples/`, `docs/`, `website/`. Each in-repo app (`website/`, which serves the docs at `/docs`, `examples/blog/`, `packages/ui/packages/website/`, plus the tiny redirect-only `docs/` host) is run from its OWN dir via `npm run dev` / `npm start`; as of #550 a bare `webjs dev` / `webjs start` is equivalent (each app's per-environment orchestration, the Tailwind watcher, `webjs db migrate`, the registry copy, moved into its `webjs.dev` / `webjs.start` tasks config, which `webjs dev`/`start` run). The sections below cover the repo-health git config, the changelog flow, and the dev error overlay. --- ### Deploying the in-repo apps (Docker image + readiness gate) -The four in-repo apps (`website`, `docs`, `examples/blog`, `packages/ui/packages/website`) deploy from ONE image built by the root `Dockerfile`, each run as a separate service with its own `PORT` (compose sets it locally, the platform injects it in prod). `compose.yaml` is local parity for that setup; the platform never reads it. +The four in-repo apps (`website`, which serves the documentation at `/docs`, the redirect-only `docs` host, `examples/blog`, `packages/ui/packages/website`) deploy from ONE image built by the root `Dockerfile`, each run as a separate service with its own `PORT` (compose sets it locally, the platform injects it in prod). `compose.yaml` is local parity for that setup; the platform never reads it. The readiness gate is the same `/__webjs/ready` endpoint the framework ships and documents (503 until fully warm, then 200, see the deployment docs page). Two seams carry it, because no single file configures every platform: diff --git a/package-lock.json b/package-lock.json index 0c5d5cf17..d11afdee2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,8 +32,8 @@ } }, "docs": { - "name": "@webjsdev/docs", - "version": "0.1.0", + "name": "@webjsdev/docs-redirect", + "version": "0.2.0", "dependencies": { "@webjsdev/cli": "^0.10.0", "@webjsdev/core": "^0.7.0", @@ -2413,7 +2413,7 @@ "resolved": "packages/core", "link": true }, - "node_modules/@webjsdev/docs": { + "node_modules/@webjsdev/docs-redirect": { "resolved": "docs", "link": true }, diff --git a/packages/cli/bin/webjs.js b/packages/cli/bin/webjs.js index 9c5d963ac..11ed22394 100755 --- a/packages/cli/bin/webjs.js +++ b/packages/cli/bin/webjs.js @@ -875,7 +875,7 @@ components/schema with the actual app the user requested. Use Drizzle + SQLite for persistence (already wired up). Never store app data in JSON files. -Full docs: https://docs.webjs.dev`); +Full docs: https://webjs.dev/docs`); process.exit(1); } const noInstall = rest.includes('--no-install'); diff --git a/packages/cli/lib/create.js b/packages/cli/lib/create.js index e6827fc71..505f23ac6 100644 --- a/packages/cli/lib/create.js +++ b/packages/cli/lib/create.js @@ -1292,7 +1292,7 @@ export default function RootLayout({ children }: { children: unknown }) { WebJs Gallery @@ -1363,7 +1363,7 @@ export default function Home() {