Summary
Add a "Discover" page to the CookCLI web UI that searches the Cooklang Federation over its HTTP API, previews a result, and saves the .cook file into the user's local recipe collection.
Today the server's search box only covers recipes already on disk. The federation is where recipes from other people's published feeds are indexed, and there is currently no path from "someone published a recipe" to "it's in my collection" other than copy-paste.
Federation API used
Public instance: https://recipes.cooklang.org (see cooklang/federation).
GET /api/search?q=<query>&locale=<lang> — Tantivy query syntax: bare terms, field-scoped (title:, tags:, ingredients:, difficulty:, servings:, total_time:), boolean AND/OR, negation -tags:dessert, ranges total_time:[0 TO 30], quoted multi-word values.
GET /api/recipes/:id — recipe details, including locale and locale_source.
GET /api/recipes/:id/download — the raw .cook file.
Proposed UI
GET /discover — search form plus results list. Query and locale round-trip through URL params (/discover?q=...&locale=...) so results are linkable and back/forward work. Empty state explains what the federation is; no-results and API-error states are distinct.
- Result card — title, summary, tags, time/servings/difficulty badges, source feed, and a link to the recipe on
recipes.cooklang.org. Reuse the existing .recipe-card / .metadata-pill / badge component classes.
GET /discover/:id — preview a single federation recipe rendered with the normal recipe view (ingredients, steps, metadata), with a "Save to my recipes" form.
POST /discover/:id/save — fetches /api/recipes/:id/download, writes it under base_path at a user-supplied (sanitised) filename, redirects to the local recipe page. Reuse the path sanitisation and CSRF same-origin check already in create_recipe (src/server/ui.rs); refuse to overwrite an existing file and surface that as a form error.
Nav entry sits next to Shopping List / Pantry, hidden in static_mode (no writes, and outbound calls don't belong in a statically exported site).
Configuration
--federation-url flag on cook server (and COOK_FEDERATION_URL env), defaulting to https://recipes.cooklang.org, so self-hosted federation instances work.
--no-federation (or the URL set empty) hides the page entirely, for offline/air-gapped use.
- Reasonable client timeout on federation calls; a slow or down federation must degrade to an error banner, never hang a request.
Details / edge cases
- All federation-supplied text is untrusted: escape it, and validate that the downloaded body parses as Cooklang before writing it to disk.
- Cap the response size accepted from
/download (the federation itself caps recipes at 1 MB).
- Locale filter defaults to the UI language, with an "any language" option.
- New user-facing strings go through the existing Fluent setup — add keys to
locales/en-US/ (new discover.ftl), leaving other locales to fall back.
Out of scope
- A
cook search --federation CLI command (worth a separate ticket).
- Publishing local recipes to the federation as a feed.
- Adding federation recipes straight to the shopping list without saving.
Implementation notes
- Routes:
src/server/ui.rs (ui() router).
- Templates: new
templates/discover.html + templates/discover_recipe.html, data structs in src/server/templates.rs.
- Nav:
templates/base.html, gated the same way as shopping list / pantry.
- HTTP client:
reqwest is already in the dependency tree via cooklang-import.
Acceptance criteria
Summary
Add a "Discover" page to the CookCLI web UI that searches the Cooklang Federation over its HTTP API, previews a result, and saves the
.cookfile into the user's local recipe collection.Today the server's search box only covers recipes already on disk. The federation is where recipes from other people's published feeds are indexed, and there is currently no path from "someone published a recipe" to "it's in my collection" other than copy-paste.
Federation API used
Public instance:
https://recipes.cooklang.org(seecooklang/federation).GET /api/search?q=<query>&locale=<lang>— Tantivy query syntax: bare terms, field-scoped (title:,tags:,ingredients:,difficulty:,servings:,total_time:), booleanAND/OR, negation-tags:dessert, rangestotal_time:[0 TO 30], quoted multi-word values.GET /api/recipes/:id— recipe details, includinglocaleandlocale_source.GET /api/recipes/:id/download— the raw.cookfile.Proposed UI
GET /discover— search form plus results list. Query and locale round-trip through URL params (/discover?q=...&locale=...) so results are linkable and back/forward work. Empty state explains what the federation is; no-results and API-error states are distinct.recipes.cooklang.org. Reuse the existing.recipe-card/.metadata-pill/ badge component classes.GET /discover/:id— preview a single federation recipe rendered with the normal recipe view (ingredients, steps, metadata), with a "Save to my recipes" form.POST /discover/:id/save— fetches/api/recipes/:id/download, writes it underbase_pathat a user-supplied (sanitised) filename, redirects to the local recipe page. Reuse the path sanitisation and CSRF same-origin check already increate_recipe(src/server/ui.rs); refuse to overwrite an existing file and surface that as a form error.Nav entry sits next to Shopping List / Pantry, hidden in
static_mode(no writes, and outbound calls don't belong in a statically exported site).Configuration
--federation-urlflag oncook server(andCOOK_FEDERATION_URLenv), defaulting tohttps://recipes.cooklang.org, so self-hosted federation instances work.--no-federation(or the URL set empty) hides the page entirely, for offline/air-gapped use.Details / edge cases
/download(the federation itself caps recipes at 1 MB).locales/en-US/(newdiscover.ftl), leaving other locales to fall back.Out of scope
cook search --federationCLI command (worth a separate ticket).Implementation notes
src/server/ui.rs(ui()router).templates/discover.html+templates/discover_recipe.html, data structs insrc/server/templates.rs.templates/base.html, gated the same way as shopping list / pantry.reqwestis already in the dependency tree viacooklang-import.Acceptance criteria
/discover?q=pastareturns federation results rendered in the CookCLI UI.tags:italian AND total_time:[0 TO 30]) reach the API unmangled.static_modeand when federation is disabled.cargo fmt,cargo clippy,cargo testclean.