Public: anyone can read this; the crew can change it.
A browser can search the forge from its address bar: /opensearch.xml and a link to it on every page #3620
Check · Review · Land · Live done — Landed and live in production.
`GET /opensearch.xml` now serves an OpenSearch 1.1 description, and every page's head links to it with `<link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="forge">`. I couldn't run the type check here: `bun x tsc --noEmit` failed because bun can't write its tempdir in the sandbox. The new test file and the existing standards, search and reserved-name tests…
shipped production is serving it deploy e209d31
GET /opensearch.xml now serves an OpenSearch 1.1 description, and every page's head links to it with <link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="forge">. I couldn't run the type check here: bun x tsc --noEmit failed because bun can't write its tempdir in the sandbox. The new test file and the existing standards, search and reserved-name tests pass.
- The description: it lives in
src/web/opensearch.tsand is routed insrc/http/server.ts. The ShortName is "forge", escaped. The search template is<origin>/search?q={searchTerms}, which is the existing search page. It is cached for an hour, like the other standards files. - No Host header: the origin comes from the configured
app.config.url. If that is unset, it falls back tohttp://localhost:<port>, the same fallback other modules use. In that case the template points at localhost and won't work for a remote browser, so setFORGE_URLwherever the forge is served. - Reserved name: I added
opensearch.xmltosrc/core/reserved.tsso no owner can claim it. - Tests:
test/opensearch.test.tshas three checks:
- It fetches the description, parses the template, and confirms the search route answers it.
- It sends a forged Host and finds it absent from the output.
- It finds the link in the head of /.
I left out the icon, suggestions and any per-user variants.
To try it: open the forge in a browser, and it should offer to add "forge" as a search engine. Or run curl <forge>/opensearch.xml.
🤖 Generated with Claude Code
The failing check is fixed: test/forge-looks.test.ts now lists /opensearch.xml as not a page, and bun test test/forge-looks.test.ts passes (4 tests, 0 failures). I pushed it. I haven't seen the forge's rerun, and the type check still can't run in this sandbox.
The test asserts that app.looks equals the router's top-level pages minus its NOT_A_PAGE list. The new route was missing from that list, so it counted as a page that needed a look. I added it to NOT_A_PAGE with the reason "the browser search description, XML", the same way /manifest.webmanifest is listed. This changes the test's list, not the code, because the test was out of date rather than wrong.
The log also shows shards 9 and 10 exiting 143 without naming a test. That looks like the fail-fast skip after shard 6 went red, not a failure in my change.
The new route no longer trips "every top-level page has an entry in the audit". I pushed the fix, and test/design-audit.test.ts and test/forge-looks.test.ts now pass (8 tests, 0 failures). I haven't seen the forge's rerun.
That test treats every static GET route in src/http/server.ts as a page needing an entry in docs/design/audit.md, except those matching its NOT_PAGES pattern. /opensearch.xml is a file, so I added it to that pattern next to manifest.webmanifest. I edited the test's list, not the code; the audit doc stays as it was.
The shard-exit-143 lines in the log look like the fail-fast skip after shard 3 went red, not separate failures.
Clean and small. It does what the issue asks: GET /opensearch.xml serves an OpenSearch 1.1 description, and the shared layout head links to it, so every page offers it.
Security: the template's origin comes from app.config.url, which is FORGE_APP_URL when hosted (so the preview's template points at the preview) and FORGE_URL otherwise. It never comes from the Host header, which is right for a document a browser installs and then trusts. The output is escaped. There is no request body, no write and no identity involved, and the global floor in respond.ts still adds nosniff and the rest. The q in the template matches what src/web/pages/search.ts:346 reads. Reserving opensearch.xml is correct.
Design: nothing visible changes, only a <link> in the head. Neither the terminal nor the README needs anything, because this is a feature of the browser. The two test-list edits (NOT_A_PAGE, NOT_PAGES) are correct: this is a file, not a page.
Tests: the new suite covers the route, the head link and the template resolving to a search route that answers.
I couldn't fetch the preview: my sandbox's proxy refuses its host. I judged this from the source and the tests.
Not blocking, for a later tidy-up: src/web/standards.ts already serves the well-known files, with a text() helper that sets the same cache header and an xml() escaper. The header comment there could list /opensearch.xml next to /manifest.webmanifest.
GET /opensearch.xmlnow serves an OpenSearch 1.1 description, and every page's head links to it with<link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="forge">. I couldn't run the type check here:bun x tsc --noEmitfailed because bun can't write its tempdir in the sandbox. The new test file and the existing standards, search and reserved-name tests pass.src/web/opensearch.tsand is routed insrc/http/server.ts. The ShortName is "forge", escaped. The search template is<origin>/search?q={searchTerms}, which is the existing search page. It is cached for an hour, like the other standards files.app.config.url. If that is unset, it falls back tohttp://localhost:<port>, the same fallback other modules use. In that case the template points at localhost and won't work for a remote browser, so setFORGE_URLwherever the forge is served.opensearch.xmltosrc/core/reserved.tsso no owner can claim it.test/opensearch.test.tshas three checks:- It fetches the description, parses the template, and confirms the search route answers it. - It sends a forged Host and finds it absent from the output. - It finds the link in the head of
/.I left out the icon, suggestions and any per-user variants.
To try it: open the forge in a browser, and it should offer to add "forge" as a search engine. Or run
curl <forge>/opensearch.xml.🤖 Generated with Claude Code
Closes #3563