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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`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 5f71e3e

opened this

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.ts and is routed in src/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 to http://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 set FORGE_URL wherever the forge is served.
  • Reserved name: I added opensearch.xml to src/core/reserved.ts so no owner can claim it.
  • Tests: test/opensearch.test.ts has 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

commented

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.ts and is routed in src/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 to http://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 set FORGE_URL wherever the forge is served.
  • Reserved name: I added opensearch.xml to src/core/reserved.ts so no owner can claim it.
  • Tests: test/opensearch.test.ts has 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

commented

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.

commented

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.

Approved

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs