feat: add cook build static-site generator#344
Conversation
Adds spec for --pantry and --ignore-pantry flags on the shopping-list command, mirroring the existing --aisle handling.
Introduces the `cook build` subcommand stub with `BuildArgs` (output_dir, --base-path, --base-url), wires it into args.rs/main.rs/lib.rs, and adds the integration test plus updated help-output snapshot.
Two static-mode fixes discovered during verification: - renderer wrote menu pages at "menu/<name>.menu.html" because it only stripped ".cook"; the search index pointed at "menu/<name>.html", so every menu URL 404'd. Strip ".menu" too. - base.html had two CSS rule sets that referenced the shopping-list and preferences nav hrefs unconditionally; the rules are dead in static mode but the strings leaked into the output. Gate the extra selectors behind `static_mode` so the rendered HTML actually omits them. Adds two integration tests: one regression for the menu URL bug, and one safety net asserting the static index omits dynamic nav links and includes the static search.js link.
…c URLs Static-site directory listings linked to recipe/<name>.cook.html and recipe/<name>.menu.html, both of which 404 because the renderer writes to recipe/<name>.html and menu/<name>.html respectively. - Strip .cook/.menu from item paths in build_recipes_template so URLs use the bare stem in both server and static modes. - Strip .menu from TodaysMenu.menu_path returned by find_todays_menu. - In recipes.html, route menu items to /menu/ (static mode only) for the Today's Menu CTA and the recipe/menu cards; server mode still uses /recipe/ which already handles both extensions. - Drop hardcoded .cook from recipe-reference URLs in menu.html so they match the new convention. - Add an integration test that parses directory/Breakfast.html and asserts every recipe/menu/directory href resolves to a real file.
The search.js IIFE snapshots window.__PREFIX__ at script-tag execution. The prefix assignment was emitted *after* the script tag, so the IIFE saw undefined and fell back to ".", which produced URLs like /directory/static/search-index.json (404) from any non-root page. Move the assignment ahead of the script tag in static mode. The original post-script assignment is kept for non-static (server) mode where other scripts may read it later. Add regression test asserting __PREFIX__ appears before search.js in generated directory pages.
The keyboard-shortcuts modal listed shopping list, pantry, preferences, edit, scale, and clear-list entries even on static sites where those pages and actions don't exist. The g s/g p/g x bindings would also navigate to nonexistent URLs. Expose `window.__STATIC_MODE__` in base.html and let the shortcuts JS: - omit dynamic-only rows from the help modal - skip the corresponding key handlers (g s/p/x, e, a, +, -, [, ]) Add a build test asserting __STATIC_MODE__ is set to true in static output.
# Conflicts: # src/server/ui.rs # templates/recipes.html
Copies each .cook file to recipe/<path>.cook in the output and renders a download button on the recipe page so visitors can grab the canonical source alongside the rendered HTML.
The static renderer passed recipe_path with the `.cook`/`.menu` suffix
still attached, so the template's `{{ recipe_path }}.cook` produced
`Recipe.cook.cook` — which 404s and the browser saved the response as
an .html file. Trim the suffix before passing in, matching the server
convention. Tighten the test to assert the exact href.
- Add `--lang` CLI flag to `cook build` so the generated site can be rendered in any of the supported UI locales (en-US, de-DE, nl-NL, fr-FR, es-ES, eu-ES, sv-SE). Re-exports `parse_supported_language` from the server module so the build path validates the same set. - Emit a `<script type="application/ld+json">` block on every recipe page with schema.org Recipe markup (name, description, image, author, recipeYield, prepTime/cookTime/totalTime as ISO 8601, recipeIngredient, recipeInstructions, recipeCategory, recipeCuisine, keywords). Helps search engines surface recipes from sites built with `cook build`. - Point the "Built with CookCLI" footer link to https://cooklang.org/cli/ so users land on the docs rather than the GitHub repo. Tests cover --lang en/de happy path, unsupported tag rejection, and the presence of the JSON-LD block with @type Recipe and core fields.
Commit 627a637 dropped the .cook/.menu suffix from recipe URLs in both server and static modes (e.g. /recipe/lamb-chops, not /recipe/lamb-chops.cook). This navigation test still asserted the old href, so the link locator timed out on the home page. The intent of the test — confirming URLs use the file stem rather than the title — is preserved: 'lamb-chops' in the URL still proves it isn't 'sicilian-style-scottadito-lamb-chops'.
Code Review — PR #344:
|
- search.js: escape `href` when rendering search results. `prefix` and `m.path` flow into raw HTML; while both are build-time controlled today, defense in depth is cheap and the existing helper makes the fix a one-liner. - copy_all_images: replace the hand-rolled walkdir_utf8 with the walkdir crate. The previous implementation followed symlinks via `path.is_dir()` and would infinite-loop on cycles; walkdir defaults to not following symlinks, so loops are impossible. - run(): drop the duplicate `tracing::info!` so `RUST_LOG=info` doesn't print the "Building static site..." banner twice. - Drop the static-site and shopping-list-pantry-flags planning/spec artifacts under docs/superpowers/. These were development scaffolding, not user-facing docs.
Code Review:
|
When the output directory lives inside the source directory (the common case: `cook build` from the recipe root with default `_site`), every subsequent build discovered the previous run's generated files and copied them one level deeper. After a handful of runs the path got long enough that the OS rejected it with ENAMETOOLONG. Prune the output subtree from the recipe tree before walking, and filter the image walker so it never descends into the output directory.
Code Review:
|
| The static site is read-only. The following dynamic features from `cook server` are intentionally omitted: | ||
|
|
||
| - Shopping list and pantry pages | ||
| - Preferences and sync |
There was a problem hiding this comment.
I hide the language section
https://gitlab.com/pinage404/moku/-/blob/c780cfeb36ff172f3fcea9ade32a66e19c3c159e/custom.css#L5
I kept the rest of the page to keep the links to cooklang to promote the project with backlinks https://pinage404.gitlab.io/moku/preferences/

Summary
Adds a new
cook buildcommand that generates a self-contained static website from a Cooklang recipe collection. Output mirrors the existing web UI for browsing, recipe pages, menus, and search, but excludes dynamic features (shopping list, pantry, edit, preferences, sync, scaling). The result is hostable on any static-file host or browsable viafile://.Approach
src/build/module withmod.rs,renderer.rs,writer.rs,links.rs,index.rs.static_mode: boolfield on template structs. Server and build share template-builder code insrc/server/builders.rs.menu/<name>.html; recipes render torecipe/<path>.html.static/js/search.js+ a generatedstatic/search-index.json(title=3, tags=2, ingredients=1 scoring).api/static/<rel>/to match existing image URL conventions.Tests
11 integration tests in
tests/build.rs:.menu.htmlsuffix), title-metadata regression/api/searchfetch)Full suite green:
cargo test,cargo fmt,cargo clippyall clean.Test plan
cook build _site --base-path ./seedproduces a browsable site_site/index.htmlin a browser; navigation, search, and recipe scaling-free pages workshopping-list,pantry,preferences, or/api/searchreferences remainmenu/<name>.htmland link correctly from listingsKnown follow-ups (non-blocking)
static/js/search.js: addescapeHtml(href)for defense-in-depth XSS hardeningsrc/build/mod.rs::walkdir_utf8: add symlink loop guard or switch towalkdircrate.cook/.menustem-stripping into a single helper