Conversation
staticalize only rewrites self referencing absolute urls inside HTML, so these two resources hardcoded https://frontside.com/effection and told every reader to go to production no matter where they were served from. Locally, llms.txt sent agents away from the dev server they were reading. Add `useSiteUrl()`, which builds urls from `SITE_URL` when it is configured and falls back to `useAbsoluteUrlFactory()` otherwise, and use it for both routes. The www workflow sets `SITE_URL` per deploy, so a preview advertises its own alias url and production advertises the canonical one, and the dev task sets it to localhost so development exercises the same code path as a deploy.
commit: |
Merging this PR will improve performance by 20.25%
Performance Changes
Tip Curious why performance improved? Comment Comparing |
Contributor
|
🚀 Deploy Preview Ready!
|
taras
added a commit
that referenced
this pull request
Sep 20, 2026
llms.txt sent agents to raw.githubusercontent.com for the behavioral contract, so the one document that governs how they write Effection code came from a different origin than the documentation it describes, and from whatever `v4` happened to be rather than the snapshot the rest of the site was built from. Serve the repository's root AGENTS.md at `/AGENTS.md` instead. The route reads the file from the checkout on each request, so the repository root stays the only copy, and it is in the sitemap, so a static build captures it like any other resource. llms.txt and the footer link now point there through the site's own url generation, which keeps the production base path.
The root AGENTS.md mixed two audiences: the behavioral contract that governs how anyone writes Effection code, and the rules for contributing to this repository. An agent writing an application had to read past gitmoji and pre-commit commands to reach the invariants, and the website served the whole thing. Move the contract to docs/agents.md, alongside the other public documentation sources, and leave the root file with code style, commit and PR conventions, pre-commit workflow and the pull request template. The root file now opens by sending repository agents to the contract, both as the copy checked out on their branch and as its published url, so the two audiences stay distinct without a second copy of the rules. The route serves docs/agents.md and rewrites the canonical links in it to whichever site is answering, through the same `useSiteUrl()` that llms.txt uses, so a preview or a dev server keeps agents on itself rather than sending them to production. Urls that are not part of this site, such as the Frontside blog, are left alone.
These responses are rewritten per environment, and the header only ever reaches a browser talking to the dev server: a static build copies the body and netlify supplies its own caching headers. An hour of max-age meant an editor who had opened /AGENTS.md or /llms.txt before a change kept being shown the copy from before it, links and all. The etag plugin already answers a revalidation with a 304.
taras
added a commit
that referenced
this pull request
Sep 20, 2026
The last seven links in llms.txt pointed at raw.githubusercontent.com, pinned to the v4 branch. An agent reading a preview or a dev server was sent to GitHub for a copy of the docs that had nothing to do with the site it was reading, and the version it got was whatever v4 happened to be. Serve the source of each guide at `/guides/:series/:id.md`, from the same worktree the rendered pages are built from, and point llms.txt there. Agents still get markdown, now from the site they came from and matching the pages beside it. The route is registered before the page route so that `.md` is read as a suffix rather than part of a guide id, and it lists every guide in the sitemap so a static build captures them. The series comes from the site config rather than a hardcoded `v4`, which also settles the `[Guides]` link above it.
`[Browse all guides][docs/]` had no matching definition, so an agent reading llms.txt got the literal text instead of a link. The guides index is already defined as `[Guides]` two lines above it, so point the bullet there rather than define the same url twice. The new test collects the labels the document uses and the ones it defines and compares them, so the next reference that loses its definition fails rather than shipping as literal text.
The guides are readable as markdown now, but the packages were not: an agent that followed the catalog in llms.txt to /x/task-buffer got a rendered page, and the readme it was built from was only on GitHub. Serve it at /x/:workspacePath.md, from the same checkout the page is built from, through the getReadme() the page already uses. Registered before the page route so that `.md` is read as a suffix rather than part of a package name, and every package is listed in the sitemap so a static build captures them. The three markdown endpoints — the contract, the guides and now the readmes — build their responses through one helper, so the content type and the no-cache decision live in one place rather than three.
The catalog sent agents to the rendered package pages while everything else in llms.txt now leads to markdown. Point it at `/x/<package>.md`. A readme read on its own is missing what the page carries beside it: the package name to install. Append the npm command to the served markdown, unless the readme already gives it — six of them do, and the document should not say it twice.
llms.txt led agents to markdown everywhere except the API, where [API]
pointed at the rendered index and every symbol behind it was HTML.
Serve the index at /api.md and each symbol at /api/<series>/<symbol>.md,
experimental symbols under their own segment as their pages already are.
Both read the same pkg.docs() the HTML routes render: a symbol page is
its declarations, each one rendered through the existing Type component
and taken as text so the signature cannot drift from the page, its
documentation with {@link} resolved to markdown pages, and where the code
lives. The index lists what the HTML index lists, newest series first,
and llms.txt now points there rather than repeating the catalog.
An unknown symbol answers 404, where the page route throws.
Open
9 of 20 tasks
`import("d:/a/effection/effection/docs/structure.json")` fails on
windows, where deno reads the drive letter as an unsupported scheme. The
dev server never hits it because nobody runs the site from windows, but
the guides markdown test loads the structure, so it turned up on the
windows leg of the test matrix.
cowboyd
reviewed
Sep 25, 2026
cowboyd
left a comment
Member
There was a problem hiding this comment.
There are a couple things that I'm looking at
- there are just a lot of changes outside scope
- looking at this, I feel like we should ask "how can we fix this in staticalize?" because to be honest the SITE_URL was always a hack, and staticalize was written for the most part to solve that.
This was referenced Sep 26, 2026
Member
Author
This branch was successfully deployed
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.
Motivation
Three problems, all of them the same shape: a resource served by this site told its reader to go somewhere else.
llms.txtandfeed.xmlhardcodedhttps://frontside.com/effection, so every copy advertised production no matter where it was served from. Locallyllms.txtpointed agents away from the dev server they were reading it on; in a PR preview it pointed reviewers at production. The rest of the site does not have this problem because staticalize rewrites self-referencing absolute urls — but only inside HTML.downloader.tstakes that branch onContent-Type?.includes("html")and streams everything else to disk untouched, sotext/plainandapplication/rss+xmlare copied verbatim and their urls have to be right when the response is generated.llms.txtsent agents toraw.githubusercontent.comforAGENTS.mdand for all seven guides, pinned to thev4branch, and to rendered HTML pages for the packages. The documents that govern how an agent writes Effection code came from a different origin than the documentation they describe, and from whateverv4happened to be rather than the snapshot the rest of the site was built from.The root
AGENTS.mdmixed two audiences — the behavioral contract that governs anyone writing Effection code, and the rules for contributing to this repository — so an agent writing an application had to read past gitmoji and pre-commit commands to reach the invariants.Approach
Urls follow the site
useSiteUrl()inwww/plugins/current-request.ts. With noSITE_URLit returnsuseAbsoluteUrlFactory(), the existing origin-based helper; with one set it builds urls from that base, preserving its path.llms.txtandfeed.xmluse it in place of the hardcoded constant, and the series inllms.txtnow comes from the site config rather than a hardcodedv4.www.yamlsetsSITE_URLper deploy: production →https://frontside.com/effection, the canonical url, byte-identical to what these files contain today; preview →https://pr-<N>--effection.netlify.app, computed before the server starts, since alias deploys land on a predictable url, with fork PRs falling back to the netlify production url because they deploy anonymously to a url nobody knows in advance. A::warning::fires if the site name netlify reports back differs fromNETLIFY_SITE_NAME, so the prediction cannot silently drift.environment.urlfor previews now points at the alias deploy, so the link in the checks panel shares an origin with the urls baked into that build. The per-commit snapshot url is still in the sticky comment.deno task devsetsSITE_URL=http://localhost:8000, so development exercises the same code path as a deploy rather than the fallback.Everything llms.txt points at is markdown on this site
docs/agents.mdis the public behavioral contract, moved verbatim out of the rootAGENTS.md: core invariants, operations/futures/tasks, entry points,spawn(),Task.halt(), scope vs task, context, the concurrency operations, promise interoperability,resource(),ensure(),useAbortSignal(), streams and the rest. Only the three-line introduction changed, to address agents writing applications rather than agents editing this repository. The rootAGENTS.mdkeeps the repository-only rules and opens by sending repository agents to the contract, both as the copy checked out on their branch and as its published url.www/AGENTS.mdstays the scoped guide for the website; only its references to "the rootAGENTS.md" for API correctness were repointed at where that material now lives./AGENTS.mdserves that file, read from the checkout per request, and rewrites the canonical links inside it through the sameuseSiteUrl(), so an agent on a preview stays on the preview. Urls that are not part of this site, such as the Frontside blog post linked fromuseAbortSignal(), are left alone, and the pattern is anchored so…/effectionxis not caught./guides/:series/:id.mdserves each guide's markdown source from the same worktree the rendered pages are built from — the loader already keeps it, so nothing is read twice or copied./x/:workspacePath.mdserves each package'sREADME.mdthrough thegetReadme()the page already uses, and appends the npm install command, since a readme read on its own is missing what the page carries beside it. The six readmes that already give the command are left as they are./api.mdand/api/<series>/<symbol>.mdserve the API reference, experimental symbols under their own segment as their pages already are. Both read the samepkg.docs()the HTML routes render: a symbol page is its declarations, each rendered through the existingTypecomponent and taken as text so the signature cannot drift from the page, its documentation with{@link}resolved to markdown pages, and where the code lives. The index lists what the HTML index lists, newest series first, andllms.txtpoints[API]there rather than repeating the catalog. An unknown symbol answers404, where the page route throws..mdroutes are registered before their page routes, sincepath-to-regexpwould otherwise read.mdas part of the id, and both list their documents in the sitemap so a static build captures them. All three markdown endpoints build their responses through one helper inwww/lib/markdown-response.ts, which also settles the cache header:no-cacherather thanmax-age=3600, because that header only ever reaches a browser talking to the dev server — a static build copies the body and netlify supplies its own — and an hour of caching meant an editor who had opened one of these before a change kept being shown the copy from before it. The etag plugin already answers the revalidation with a 304.[Browse all guides][docs/], a reference that never had a definition, now resolves to the[Guides]index.Verification
Served the site in both configurations and read what came back. Every reference definition in
llms.txt:SITE_URLSITE_URL=https://frontside.com/effection[AGENTS.md]http://localhost:8000/AGENTS.mdhttps://frontside.com/effection/AGENTS.md[API]http://localhost:8000/api/https://frontside.com/effection/api/[Operations]http://localhost:8000/guides/v4/operations.mdhttps://frontside.com/effection/guides/v4/operations.md…and the same for the other six guides. The package catalog now reads
- [@effectionx/bdd](…/x/bdd.md): …. Nogithub.comanywhere in the document./AGENTS.mdreturns the contract astext/markdownwith its API link rewritten and the Frontside blog link untouched./guides/v4/operations.mdis byte-identical todocs/operations.mdx, all seven guides return 200, and the sitemap carries 33 guide entries./x/task-buffer.mdis its readme plus the install section; across all 27 packages every document contains exactly onenpm install @effectionx/<name>— none missing, none duplicated. Unknown ids return 404 on both routes, and the HTML pages at/guides/v4/operationsand/x/task-bufferstill returntext/html, so the route ordering does not shadow them./api.mdlists 99 symbols across 4.1.1 and 3.6.1, every one of which returns 200;/api/v4/main.mdcarries the signature, the documentation with{@link exit}resolved to/api/v4/exit.mdon the same host, and the source link;/api/v4/does-not-exist.mdreturns 404; the sitemap carries 156 API markdown entries including/api.md; and/api,/api/v4/main,/api/v3/mainand/api/v4/experimental/createApistill returntext/html.feed.xmlunder the production base is byte-identical to the current production feed, which matters most forguid, where a change would re-flag every post as unread in subscribers' readers.127.0.0.1underdeno task devstill returnslocalhost:8000, confirmingSITE_URLis in effect rather than the origin fallback.https://pr-42--effection.netlify.appfor a branch PR andhttps://effection.netlify.appfor a fork.pr-1241--effection.netlify.app/llms.txtpoints at itself rather than production.Tests cover the routes (status, exact body, content type, cache header, sitemap entries, 404), the contract's contents (the public sections it has, the repository sections it must not), link rewriting under both environments, the install-appending rule including the readmes that already give the command and the one that merely mentions
npm installin an example, thellms.txtdefinitions including that none point at GitHub and that every reference it uses is defined, the footer markup, and the shape of all three instruction files. The reference check was confirmed to bite by restoring the dangling label.deno fmt --check(252 files),deno lint(211 files),deno checkand the full suite (51 passed, 269 steps) are all clean.One gap worth naming:
/x/:workspacePath.mdhas no unit test, because exercising it means standing upuseWorkspaces, which clones effectionx over the network. Its pure part, the install-appending rule, is tested; the route itself is covered by the live checks above. #1242 addsEFFECTIONX_DIR, which would make a fixture-based test cheap once it lands.www/deno.json's test task gets--allow-env, because importing the route modules pulls in helpers that readOSTYPEat module load. Worth knowing for anything added towwwlater: the root task picks up thewwwworkspace and grants--allow-envbut not--allow-write.