Repository navigation
docs(skills): app-structure — concrete urls() layouts for SaaS, multi-app, content and e-commerce apps - #1135
Merged
Merged
Conversation
commit: |
…-app, content and e-commerce apps A new skill, packages/rangojs-router/skills/app-structure/, puts the primitives together into whole apps. SKILL.md holds the rules every recipe shares (route tree in its own module, a named sync include() per section, identity in router.use() and gates in route middleware, shared chrome in a server layout, generated files), sync vs async include(), include naming (name nested includes through 0.24.2, #1104), where shared chrome lives, a growing-an-app path, and the minimal one-module app. Four companion recipes: - large-app.md: a server shell over auth, onboarding, app, settings and admin clientUrls() groups; session in global middleware, sign-in, onboarding and role guards around the includes; prefetch: false with its measured trade-off; actions + revalidate(), refetch()/refreshGroup and cache tags for updates; fetchable loaders guard themselves; the group layout() loader gap through 0.24.2 and its workaround. - multi-app.md: createHostRouter() with apex, app., admin. and docs. apps, what is shared and how, one TypeScript program per app, the host-override cookie in dev, cross-app links as document navigations. - content-site.md: docs with Prerender + ppr and a Static nav, a blog with Prerender onDemand + Passthrough, the publish webhook beside the entry, machine routes. - e-commerce.md: ppr category and product pages with tagged "use cache" reads and live loader holes, the cart as a layout loader, a guarded server-rendered checkout, the clientUrls() shop variant. Wiring: the rango index and README tables list the skill, catalog.json is regenerated, check-docs-api guards the five files, and check-skills allowlists the /app and /settings route examples.
… in 0.24.3 A clientUrls() layout() loader is held across the group's routes since #1137, so the e-commerce recipe no longer steers the cart loader to a server layout for that reason, and large-app loses its known-gap section. The nested unnamed include note now says 0.24.x: #1128 ships after 0.24.3.
ivogt
force-pushed
the
docs/skill-app-structure
branch
2 times, most recently
from
October 13, 2026 10:22
9363c95 to
9b7c2b1
Compare
…ith the generated map) and throw rewrite()
ivogt
force-pushed
the
docs/skill-app-structure
branch
from
October 13, 2026 10:26
9b7c2b1 to
856698a
Compare
4 tasks done
ivogt
marked this pull request as ready for review
October 13, 2026 11:24
2 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
devcorner: dc-t41 · session dc-s37
docs(skills): app-structure — concrete urls() layouts for SaaS, multi-app, content and e-commerce apps
Problem
An agent or a developer starting a Rango app finds one skill per primitive (
/composability,/router-setup,/client-urls,/host-router,/prerender,/ppr,/middleware, …) and no whole app. The questions they actually have, "where does the auth guard go relative to theclientUrls()group", "what does the rooturls.tsxof a dashboard look like", "how do I split marketing, app, admin and docs across hosts", "which pages getpprand whichPrerender", get answered by assembling five skills, and the assembly is where mistakes land: a guard expected to protect actions, a shared loader on a grouplayout(), a route module that imports the router.Before and after
Before: a consumer reads
/composabilityforinclude(),/client-urlsfor groups,/middlewarefor guards, and composes a tree, unverified.After:
/app-structuregives the tree, verified end to end:End-to-end consumer example
skills/app-structure/large-app.mdis the full app: file tree,router.tsx(router.use(loadSession),/api/*CSRF guard, optionalthrow rewrite()maintenance), the guard modules, the server shell and its user-menu loader, everyclientUrls()group with itsrevalidate()predicates, the marketing and API modules, loaders with"use cache"+cacheTag(), actions withupdateTag(), out-of-band refresh (refetch(),useRefreshLoaders(),invalidateClientCache()), and the generated files to commit.Outline
packages/rangojs-router/skills/app-structure/(2,086 lines):SKILL.md(214): not-this-skill-if; recipe picker; the six rules every recipe follows (route tree in its own module, named sync include per section, identity inrouter.use()and gates in routemiddleware(), shared chrome in a server layout, data in loaders, generated files); a small app in one module; sync vs asyncinclude()(table with measured costs, when async pays,rango generatelimits); naming includes (name nested includes through 0.24.2, Nested unnamed include() never matches: its routes answer 404 with no warning #1104); where shared chrome lives; growing an app (6 steps); related skills.large-app.md(823): the user's target architecture as the primary recipe. How navigations behave inside vs between groups; file tree;router.tsx; session, guards, fetch-lane guard, maintenance rewrite; rooturls.tsx; the server shell; the auth and app groups in full, settings and admin excerpts; marketing (ppr) and API (path.json, CSRF); loaders, caches, actions;prefetch: falsewith the measured trade-off and rules of thumb; keeping data current (actions +revalidate(),"use cache"tags,refetch()/useRefreshLoaders()/invalidateClientCache(),useRouter().refresh(),keepClientCache()); fetchable loaders guard themselves; names and generated files; placement table; the grouplayout()loader gap through 0.24.2 (dc-t38) with its workaround; mistakes the shape avoids.multi-app.md(280):createHostRouter()with apex,app.,admin.,docs.; Cloudflare entry and the node/vercel variant; each app'srouter.tsx/handler.ts/urls.tsx; what is shared and how (components, Document, CSS, theme config, cache store, parent-domain session; theme choice and client caches are not); one TypeScript program per app; the host-override cookie in dev; cross-app links as document navigations (fix(router): swap app shell on cross-router SPA navigation #476 → fix(router): redirect on cross-app navigation so the target document is fully re-established #555), absolute URLs vsreloadDocument+prefetch="none"; deploys.content-site.md(333): docs withPrerender+pprand aStaticnav in a parallel slot; a blog withPrerenderonDemand+Passthrough, drafts vianotFound()(and why notSkipin a refresh); the publish webhook beside the Worker entry; machine routes; locales in the URL.e-commerce.md(436):pprcategory (typed search, one shell per query, tracking params excluded) and product pages (tagged"use cache"handler output, live loader holes,prefetch: falsereviews); the cart as a store-layout loader narrowed to cart actions; per-segment action outcomes; checkout guarded and server-rendered with anexternalredirect; variants: prerendered products, aclientUrls()shop.Code-change summary
skills/app-structure/{SKILL.md,large-app.md,multi-app.md,content-site.md,e-commerce.md}.skills/rango/SKILL.md: a row in "Structure & routing" and in "Pick a primitive".skills/catalog.json: regenerated (node tools/generate-skills-catalog.mjs).README.md: one row in "Everything else, when you need it".tools/check-docs-api.mjs: the five app-structure files join the guarded docs (404 identifiers verified, up from the previous set).tools/check-skills.mjs:/appand/settingsadded toURL_EXAMPLES(route examples in the recipes).Verification
Every snippet was written first as a scratch app per recipe, typechecked against the package's built declarations with
rango generateroute maps, and exercised through@rangojs/router/testing/flight(serveShellRequest,router.prerender()). The final check extracted every full-file code block from the five markdown files over those apps and re-rantscand the probes: 5 programs plus 4 per-app programs clean, 25 probe tests passing. Blocks marked "(excerpt)" (settings and admin groups in recipe 1, the site urls in recipe 2) were not extracted; the full versions they abridge are in the scratch apps and typecheck.middleware(guard, () => [include(...)])scopes the guard to the include's routessrc/route-definition/dsl-helpers.ts:511-555(wrapping mode),:69-77(an include counts as routes); e2e test-app mountsclientUrlsVarsPatternsthis way/settings→/auth/login?next=%2Fsettings(requireUser before requireOnboarded)X-RSC-Redirect302 /auth/login?next=%2Fapp; partial →x-rsc-redirect: http://localhost/auth/login?next=%2Fapp/app→/app/projectsredirectsrouter.use()wraps actions and loader fetches; route middleware wraps neithersrc/rsc/handler.ts:706-727(global chain around the core handler),:1194(loader fetch inside it),src/rsc/loader-fetch.ts:18(executeLoaderMiddleware);skills/middleware/SKILL.mdrequest flowcreateLoader(fn, true)/createLoader(fn, { middleware })src/loader.ts:25-39;skills/hooks/data.md(403 for plain loaders)useLoader()returns{ data, isLoading, error, load, refetch };refreshGroup;useRefreshLoaders();loadmemoizedsrc/use-loader.tsx:129-158,:427-562,:735-747useRouter().refresh()refetches with no held segmentssrc/browser/navigation-bridge.ts:582-601invalidateClientCache,keepClientCache,cacheTag,updateTag,revalidateTag,rewrite,TRACKING_SEARCH_PARAMSexportssrc/index.rsc.ts:122-127, 209, 242-254; signaturessrc/cache/cache-tag.ts:310,src/cache/tag-invalidation.ts:241,291,src/server/cookie-store.ts:335throw rewrite("maintenance")renders at the visitor's URLx-rango-rewrite: /maintenance, "Back soon" in the payloadsrc/client-urls/navigation.ts:97-104, 138-142(only the mounted group matches)/app→/settingskeepsM0L0L0and its loader segment inmatched; viewer data absent from the payloadrenameProjectre-ran ProjectLoader + ActivityLoader (predicate), not ViewerLoader;updateProfilere-ran ViewerLoaderprefetch: falsedefers the loader in a prefetchreadDeferred()= 1 segment, ActivityLoader not calledlayout()loader re-runs on cross-route navs; a server layout loader is heldrevalidate(() => false)ran 1 → 2 on/g/a→/g/b; server layout loader 1 → 1; dc-t38notFound()from a group loader renders the shell'snotFoundBoundary()with a 404src/use-loader.tsx:166-213pprpages capture and HIT/pricingMISS → HIT<Link>and the interceptor;reloadDocumentskips the click handler but not prefetch;data-no-intercept="true"src/browser/react/Link.tsx:179-191, 290-293, 355-384;src/browser/link-interceptor.ts:183-218X-RSC-Reloadsrc/rsc/handler.ts:995-1000; dc-d21 (measured request sequence, e2emulti-router.test.tscross-app cases)example.com/www.→ site,app./admin./docs.→ their apps,localhost→ site, cookiex-rango-host=admin.example.com→ adminsrc/theme/ThemeProvider.tsx:66-72(noDomain)cookies().set(..., { domain })src/router/middleware-types.ts:27-35examples/cloudflare-multi-router/tsconfig.json; root + 4 app programs typecheckPrerender+pprdocs page with aStaticnav slotonDemand+Passthrough: baked, live fallback, refreshrouter.prerender()→renderednotFound()→removed;Skipin a refresh →render-failedonDemandandpprcannot combine?sort=price&utm_source=xHIT on?sort=price's shell;?sort=nameMISSupdateTag("product:mug")drops the shellget1 call over 2 requests,price2; MISS afterupdateTagnotOnActions; without the routerevalidate()the handler re-ranctx.set()in an action is visible to the revalidation renderskills/server-actions/SKILL.mdrequireCartredirect302 /cartCannot read properties of undefined (reading 'handler')atrouter.ts:946rango generateresolves() => import()(default export); a.then()provider fails without--runtimeskills/composability/SKILL.md(measured 2026-07)clientUrls()async include has no startup winskills/client-urls/SKILL.md"Defining and mounting"router.named-routes.gen.ts; per-module files come fromrango generatesrc/vite/discovery/route-types-writer.ts:83-140,src/build/route-types/per-module-writer.ts:121; #1127 commit messagesrc/vite/discovery/route-types-writer.ts:28,124,src/route-name.tsTest plan
node tools/check-skills.mjs: OK, 48 skills.node tools/generate-skills-catalog.mjs --check: OK.node tools/check-docs-api.mjs: 404 identifiers verified (the five new files included).node tools/check-doc-links.mjs: OK.node tools/generate-route-types.mjs --check: current (no app sources changed).oxfmt --check .,oxlint --deny-warnings: clean.tscclean in 9 programs.serveShellRequest/router.prerender()tests over the extracted snippets pass.pnpm run typecheck,pnpm run test:unit:all: not run; no TypeScript source changed (markdown, JSON, two.mjscheck scripts).Notes
/ux-architecturecross-reference to a missing directory. The four mentions read "theux-architectureskill"; once dc-t40 lands, switch them to/ux-architectureso the guard checks them (SKILL.md×3,large-app.md×1)./composability. That skill says "Prefer the async form for any route group that is a natural, independently-loadable unit". This skill defaults to sync and names when async pays. One of the two should move; follow-up.rango generateand an inline rooturls().rango generate src/router.tsxoncreateRouter({}).routes(urls(({ path }) => [...]))writes a per-modulerouter.gen.tsand norouter.named-routes.gen.ts, with no warning (the.routes(({ path }) => [...])andurls: urlpatternsforms both work). The recipes keep the root tree inurls.tsx, so they are unaffected; worth a ticket./private/tmp/claude-501/-Users-ivotodorov-Development-vite-rsc/b29807e8-f2f4-4afb-bc0b-4452071e8f6f/scratchpad/skill-structure/(large-app,multi-app,content-site,shop,small,extract.py,doc-*extractions).Update 2026-10-13
clientUrls()layout()loader is now held on navigation, as on a server layout. The defaults tables and recipes say so, the known gap is removed, and therevalidate(true|false)shorthand (feat(router): revalidate(true) and revalidate(false) shorthand in urls() and clientUrls() #1130) is used.ctx.reverse(".x"),useReverse(routes)with the module's generated.gen.ts). The module never knows its mount, and mount params (/:accountId/:projectId,/:locale,/:locale?) are filled automatically.useReverse(routes), or the layout gets arevalidate()that compares the param.throw rewrite()from middleware (same URL, no round trip), and its limits.