Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,19 +272,19 @@ cook server --port 8080
cook server --open
```

### `cook build`
### `cook build web`

Generate a self-contained static website from your recipes. Hostable anywhere or browsable via `file://`.

```bash
# Build into ./_site from the current directory
cook build
cook build web

# Build a specific collection into a custom output
cook build dist --base-path ~/my-recipes
cook build web dist --base-path ~/my-recipes

# Build for hosting under a subpath
cook build --base-url /recipes/
cook build web --base-url /recipes/
```

### `cook search`
Expand Down
16 changes: 10 additions & 6 deletions docs/build.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# Build Command

The `cook build` command groups artifact-generation subcommands. Today it offers `web` for static-site generation; future targets (e.g. cookbooks) will live alongside it.

## `cook build web`

Generate a self-contained static website from your recipe collection. The output mirrors `cook server`'s browsing experience but ships as plain HTML, CSS, and JS — no Rust process needed at runtime, so it can be hosted on GitHub Pages, Netlify, S3, or opened directly via `file://`.

## Usage

```
cook build [OPTIONS] [OUTPUT_DIR]
cook build web [OPTIONS] [OUTPUT_DIR]
```

## Arguments
Expand All @@ -25,13 +29,13 @@ cook build [OPTIONS] [OUTPUT_DIR]

```bash
# Build into ./_site from the current directory
cook build
cook build web

# Build a specific recipe collection into a custom output directory
cook build dist --base-path ~/my-recipes
cook build web dist --base-path ~/my-recipes

# Build for hosting under /recipes/ on your domain
cook build --base-url /recipes/
cook build web --base-url /recipes/
```

## What gets generated
Expand Down Expand Up @@ -66,7 +70,7 @@ Because internal links default to page-relative paths, no configuration is neede

```bash
# GitHub Pages: push _site/ to gh-pages
cook build && git -C _site init && git -C _site add . && \
cook build web && git -C _site init && git -C _site add . && \
git -C _site commit -m "site" && \
git -C _site push -f git@github.com:user/repo gh-pages

Expand All @@ -85,5 +89,5 @@ Use `--base-url` only if your host serves the site under a fixed subpath and you

- The generated site has no server dependency — it works fully offline via `file://`.
- Search runs entirely in the browser by loading `static/search-index.json`.
- Re-run `cook build` after editing recipes; the command is idempotent.
- Re-run `cook build web` after editing recipes; the command is idempotent.
- For a live editing experience, use `cook server` instead.
18 changes: 9 additions & 9 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,18 +87,18 @@ pub enum Command {
)]
Server(server::ServerArgs),

/// Generate a self-contained static website from your recipe collection
/// Build artifacts from your recipe collection
///
/// Renders your recipes as static HTML files browsable on any static-file
/// host or directly from disk via file://. Excludes dynamic features
/// (shopping list, pantry, editing).
/// Container for build subcommands. Currently supports `web` (static
/// website generation). More targets (e.g. cookbooks) may be added in
/// future releases.
///
/// Examples:
/// cook build # Build to ./_site
/// cook build out # Build to ./out
/// cook build --base-path ~/recipes # Use specific source directory
/// cook build --base-url /recipes/ # Absolute URL prefix for subpath hosting
#[command(long_about = "Generate a static HTML website from your recipe collection")]
/// cook build web # Build a static website to ./_site
/// cook build web out # Build to ./out
/// cook build web --base-path ~/recipes # Use specific source directory
/// cook build web --base-url /recipes/ # Absolute URL prefix for subpath hosting
#[command(long_about = "Build artifacts (static website, etc.) from your recipe collection")]
Build(build::BuildArgs),

/// Generate a combined shopping list from multiple recipes
Expand Down
36 changes: 33 additions & 3 deletions src/build/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,33 @@ use crate::util::resolve_to_absolute_path;
use crate::Context;
use anyhow::{bail, Context as _, Result};
use camino::Utf8PathBuf;
use clap::Args;
use clap::{Args, Subcommand};
use unic_langid::LanguageIdentifier;

#[derive(Debug, Args)]
pub struct BuildArgs {
#[command(subcommand)]
pub command: BuildCommand,
}

#[derive(Debug, Subcommand)]
pub enum BuildCommand {
/// Generate a self-contained static website from your recipes
///
/// Renders your recipes as static HTML files browsable on any static-file
/// host or directly from disk via file://. Excludes dynamic features
/// (shopping list, pantry, editing).
///
/// Examples:
/// cook build web # Build to ./_site
/// cook build web out # Build to ./out
/// cook build web --base-path ~/recipes # Use specific source directory
/// cook build web --base-url /recipes/ # Absolute URL prefix for subpath hosting
Web(WebBuildArgs),
}

#[derive(Debug, Args)]
pub struct WebBuildArgs {
/// Output directory for the generated static site
///
/// Defaults to ./_site if not specified. The directory is created if
Expand Down Expand Up @@ -51,11 +73,19 @@ fn parse_lang_arg(s: &str) -> Result<LanguageIdentifier, String> {

impl BuildArgs {
pub fn get_base_path(&self) -> Option<Utf8PathBuf> {
self.base_path.clone()
match &self.command {
BuildCommand::Web(args) => args.base_path.clone(),
}
}
}

pub fn run(ctx: &Context, args: BuildArgs) -> Result<()> {
match args.command {
BuildCommand::Web(web_args) => run_web(ctx, web_args),
}
}

fn run_web(ctx: &Context, args: WebBuildArgs) -> Result<()> {
let source = resolve_to_absolute_path(ctx.base_path())?;
if !source.is_dir() {
bail!("Source base path is not a directory: {source}");
Expand All @@ -82,7 +112,7 @@ pub fn run(ctx: &Context, args: BuildArgs) -> Result<()> {
let mut tree = cooklang_find::build_tree(&source)
.map_err(|e| anyhow::anyhow!("Failed to build recipe tree: {e}"))?;
// If the user pointed the output directory inside the source directory
// (the common case: `cook build` with default `_site` next to recipes),
// (the common case: `cook build web` with default `_site` next to recipes),
// strip the output subtree so we don't re-process the previous run's
// generated files. Without this, every run would nest `_site/recipe/...`
// and `_site/api/static/...` one level deeper until the OS rejects the
Expand Down
2 changes: 1 addition & 1 deletion src/server/builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! struct ready to render (or be turned into an `axum::Response` by a handler).
//!
//! The builders intentionally avoid any axum / tokio-async types so they can be
//! reused from a non-async context (e.g. `cook build`).
//! reused from a non-async context (e.g. `cook build web`).

use crate::server::templates::*;
use anyhow::Result;
Expand Down
17 changes: 15 additions & 2 deletions tests/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ fn seed_dir() -> PathBuf {
#[test]
fn build_command_help_works() {
let mut cmd = Command::cargo_bin("cook").unwrap();
cmd.args(["build", "--help"]).assert().success();
cmd.args(["build", "web", "--help"]).assert().success();
}

#[test]
Expand All @@ -21,6 +21,7 @@ fn build_creates_output_dir() {
let mut cmd = Command::cargo_bin("cook").unwrap();
cmd.args([
"build",
"web",
out.to_str().unwrap(),
"--base-path",
seed.to_str().unwrap(),
Expand All @@ -41,6 +42,7 @@ fn build_writes_index_and_static_assets() {
.unwrap()
.args([
"build",
"web",
out.to_str().unwrap(),
"--base-path",
seed.to_str().unwrap(),
Expand Down Expand Up @@ -75,6 +77,7 @@ fn build_lang_arg_changes_ui_locale() {
.unwrap()
.args([
"build",
"web",
out.to_str().unwrap(),
"--base-path",
seed.to_str().unwrap(),
Expand Down Expand Up @@ -103,6 +106,7 @@ fn build_lang_arg_rejects_unsupported() {
.unwrap()
.args([
"build",
"web",
out.to_str().unwrap(),
"--base-path",
seed.to_str().unwrap(),
Expand All @@ -123,6 +127,7 @@ fn build_writes_recipe_pages() {
.unwrap()
.args([
"build",
"web",
out.to_str().unwrap(),
"--base-path",
seed.to_str().unwrap(),
Expand Down Expand Up @@ -194,6 +199,7 @@ fn build_renders_recipes_with_title_metadata() {
.unwrap()
.args([
"build",
"web",
out.to_str().unwrap(),
"--base-path",
seed.to_str().unwrap(),
Expand Down Expand Up @@ -223,6 +229,7 @@ fn build_writes_search_index() {
.unwrap()
.args([
"build",
"web",
out.to_str().unwrap(),
"--base-path",
seed.to_str().unwrap(),
Expand Down Expand Up @@ -266,6 +273,7 @@ fn build_copies_images_when_present() {
.unwrap()
.args([
"build",
"web",
out.to_str().unwrap(),
"--base-path",
seed.to_str().unwrap(),
Expand All @@ -291,6 +299,7 @@ fn build_writes_search_js() {
.unwrap()
.args([
"build",
"web",
out.to_str().unwrap(),
"--base-path",
seed.to_str().unwrap(),
Expand Down Expand Up @@ -339,6 +348,7 @@ fn build_writes_menu_pages_without_dotmenu_suffix() {
.unwrap()
.args([
"build",
"web",
out.to_str().unwrap(),
"--base-path",
seed.to_str().unwrap(),
Expand Down Expand Up @@ -368,6 +378,7 @@ fn static_output_omits_dynamic_ui() {
.unwrap()
.args([
"build",
"web",
out.to_str().unwrap(),
"--base-path",
seed.to_str().unwrap(),
Expand Down Expand Up @@ -415,6 +426,7 @@ fn build_internal_links_resolve_to_existing_files() {
.unwrap()
.args([
"build",
"web",
out.to_str().unwrap(),
"--base-path",
seed.to_str().unwrap(),
Expand Down Expand Up @@ -449,7 +461,7 @@ fn build_internal_links_resolve_to_existing_files() {
#[test]
fn build_twice_with_output_inside_source_does_not_recurse() {
// Regression: when the output dir lives inside the source dir
// (`cook build` from the recipe root with default `_site`), every run
// (`cook build web` from the recipe root with default `_site`), every run
// used to discover the previous run's generated files and copy them one
// level deeper, eventually hitting ENAMETOOLONG.
let tmp = TempDir::new().unwrap();
Expand Down Expand Up @@ -477,6 +489,7 @@ fn build_twice_with_output_inside_source_does_not_recurse() {
.unwrap()
.args([
"build",
"web",
out.to_str().unwrap(),
"--base-path",
source.to_str().unwrap(),
Expand Down
2 changes: 1 addition & 1 deletion tests/snapshots/snapshot_test__help_output.snap
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Usage: cook [OPTIONS] <COMMAND>
Commands:
recipe Parse, validate and display recipe files in various formats
server Start a local web server to browse and view your recipe collection
build Generate a self-contained static website from your recipe collection
build Build artifacts from your recipe collection
shopping-list Generate a combined shopping list from multiple recipes [aliases: sl]
seed Initialize a directory with example Cooklang recipes
search Search through your recipe collection for matching text
Expand Down
2 changes: 1 addition & 1 deletion tests/snapshots/snapshot_test__help_output_no_update.snap
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Usage: cook [OPTIONS] <COMMAND>
Commands:
recipe Parse, validate and display recipe files in various formats
server Start a local web server to browse and view your recipe collection
build Generate a self-contained static website from your recipe collection
build Build artifacts from your recipe collection
shopping-list Generate a combined shopping list from multiple recipes [aliases: sl]
seed Initialize a directory with example Cooklang recipes
search Search through your recipe collection for matching text
Expand Down
Loading