It's a cookbook!
A personal cookbook system built with Cooklang that generates both a beautiful static website and a professionally typeset PDF.
- Plain Text Recipes - Store recipes in Cooklang format, making them portable and version-controllable
- Static Website - Beautiful, responsive website with Schema.org markup for recipe discoverability
- Client-Side Search - Fast, accessible recipe search with keyboard navigation and weighted scoring
- PDF Cookbook - Professionally typeset PDF using LaTeX with elegant typography
- Dual Format Support - Separate handling for food recipes and cocktails
- Multiple Browse Options - Filter by category, cuisine, tags, and spirit base
- Customizable - Easy configuration via
.envfile and Markdown content - Responsive Design - Mobile-friendly website with hamburger menu and print-optimized recipe pages
- Modern CSS - Built with Tailwind CSS for rapid styling and consistency
- Python 3.8 or higher
- Node.js 16+ and npm (for TypeScript compilation)
- macOS:
brew install node - Ubuntu:
sudo apt-get install nodejs npm - Windows: Node.js installer
- macOS:
- LaTeX distribution (for PDF generation)
- macOS:
brew install --cask mactex - Ubuntu:
sudo apt-get install texlive-full - Windows: MiKTeX
- macOS:
-
Clone or fork this repository
git clone https://github.com/yourusername/to-serve-man.git cd to-serve-man -
Run the setup script
./setup.sh
The setup script will:
- Create a Python virtual environment
- Install all Python dependencies
- Create a
.envconfiguration file - Prompt you to configure your cookbook
- Validate your recipes
-
Install Node dependencies
npm install
This installs TypeScript for the search feature compilation.
-
Build your cookbook
source venv/bin/activate # Activate virtual environment python build.py all # Build both website and PDF
-
Preview your cookbook
python -m http.server -d docs 8000
Visit http://localhost:8000
Edit the .env file to customize your cookbook:
# Cookbook Information
COOKBOOK_TITLE=My Personal Cookbook
COOKBOOK_DESCRIPTION=Family recipes and culinary experiments
COOKBOOK_AUTHOR=Your Name
# Website Configuration
BASE_URL=/my-cookbook # For GitHub Pages, or leave empty for custom domain
SITE_URL=https://yourusername.github.io/my-cookbook
# PDF Configuration
PDF_AUTHOR=Your Name
PDF_TITLE=My Personal CookbookEdit Markdown files in the content/ directory:
content/hero.md- Homepage hero sectioncontent/about.md- About page content
- Tailwind CSS - Main styling framework (loaded via CDN)
static/css/custom.css- Custom CSS for components that need pseudo-elements or special stylinglatex/preamble.tex- PDF typography and layoutlatex/closing.tex- PDF back matter
Recipes are stored in the recipes/ directory using the Cooklang format with YAML frontmatter.
Create recipes/mains/pasta-carbonara.cook:
---
title: Pasta Carbonara
category: mains
cuisine: Italian
tags:
- pasta
- italian
- quick
servings: 4
prep_time: 10 minutes
cook_time: 20 minutes
description: A rich, creamy Roman classic made the authentic way—no cream needed.
source: https://www.example.com/recipe
author: Chef Name
---
Bring a large #pot of @water{4%liters} to boil. Season generously with @salt{2%tbsp}.
While waiting, cut @guanciale{200%g} into small strips.
Cook @spaghetti{400%g} in the boiling water for ~{2%minutes} less than package directions.
Meanwhile, cook the guanciale in a cold #skillet over medium heat for ~{8%minutes} until fat renders and meat is crispy.
Reserve @pasta water{1%cup}, then drain pasta. Off heat, add pasta to skillet, then quickly stir in egg mixture.Create recipes/cocktails/negroni.cook:
---
title: Negroni
type: cocktail
glass: rocks
spirit_base: gin
garnish: orange peel
tags:
- gin
- classic
- stirred
- bitter
description: The iconic Italian aperitif. Bold, bitter, beautiful.
---
Add @gin{1%oz}, @Campari{1%oz}, and @sweet vermouth{1%oz} to a #mixing glass with ice.
Stir for ~{30%seconds} until well chilled.
Strain into a #rocks glass over @large ice cube{1}.
Express the oils from an @orange peel over the drink, then drop it in.@ingredient{quantity}- Ingredients with quantities#cookwareor#cookware{}- Cookware items~{time}- Timers--- Comments (not shown in output)>> Section Name- Section headers in instructions
# Activate virtual environment first
source venv/bin/activate
# Build everything
python build.py all
# Build website only
python build.py site
# Build PDF only
python build.py pdf
# Generate LaTeX source only (no pdflatex compile — used by CI)
python build.py latex
# Validate recipes
python build.py validate
# Local dev server with auto-rebuild on file change
python build.py serve
# Run smoke tests
python -m unittest discover -s tests -t . -v
# Refresh snapshot fixtures after an intentional rendering change
UPDATE_SNAPSHOTS=1 python -m unittest discover -s tests -t .
# Lint and format
ruff check . && ruff format .
# Build with custom base URL
python build.py site --base-url /my-cookbookBoth output directories are gitignored and rebuilt on every CI run:
- Website - Generated in
docs/ - PDF - Generated as
output/cookbook.pdf(and copied todocs/cookbook.pdffor download)
Deployment is handled by .github/workflows/deploy.yml: every push to main validates recipes, runs the smoke tests, builds the site, compiles the PDF via xu-cheng/latex-action, and publishes to GitHub Pages via actions/deploy-pages.
-
Enable GitHub Pages with the workflow source
- Settings → Pages → Source: GitHub Actions
-
Set the site URL in the workflow
Edit the
env:block in.github/workflows/deploy.yml:env: BASE_URL: /your-repo-name SITE_URL: https://yourusername.github.io/your-repo-name
-
Push to
main— the workflow runs automatically.
Your cookbook will be live at https://yourusername.github.io/your-repo-name/.
- Configure your domain in GitHub Pages settings.
- Edit the workflow
env:block:env: BASE_URL: "" SITE_URL: https://your-domain.com
- Push to
main.
to-serve-man/
├── recipes/ # Recipe files (.cook)
│ ├── breakfast/
│ ├── basics/
│ ├── mains/
│ ├── sides/
│ ├── desserts/
│ └── cocktails/
├── content/ # Markdown content files
│ ├── hero.md # Homepage hero section
│ └── about.md # About page
├── templates/ # Jinja2 HTML templates
├── static/ # CSS and static assets
│ ├── css/
│ │ └── custom.css # Custom CSS overrides
│ └── js/ # Compiled JavaScript (gitignored)
├── src/ # TypeScript source files
│ └── search.ts # Client-side search implementation
├── latex/ # LaTeX templates for PDF
├── tests/ # Smoke tests (unittest)
├── .github/workflows/ # CI/CD (build + deploy to GitHub Pages)
├── docs/ # Generated website (gitignored, built by CI)
├── output/ # Generated PDF/LaTeX (gitignored, built by CI)
├── recipe_parser.py # Recipe parsing logic
├── site_generator.py # Static site generator
├── pdf_generator.py # PDF generator
├── config.py # Configuration management
├── build.py # Build script
├── setup.sh # Setup script
├── package.json # Node.js dependencies
├── tsconfig.json # TypeScript configuration
├── .env # Your configuration (gitignored)
└── .env.example # Configuration template
Required:
title- Recipe namecategory- One of: breakfast, basics, mains, sides, desserts
Optional:
cuisine- E.g., Italian, Indian, Americantags- List of tags (see "Tags vs. facets" below for the vocabulary policy)servings- Number of servingsprep_time- Preparation time (e.g., "15 minutes", "1 hour")cook_time- Cooking time (computedtotal_timeis auto-derived from these two)description- Brief descriptionheadnote- Cook's voice/story (markdown), rendered above ingredientssource- Source URLauthor- Original authoradapted_by- Your name if adaptedmake_ahead,storage,reheats,yield_notes- Standardized notes blockvariations- List of{name, swap, note}for substitutionsserve_with,pairs_with,uses- Lists of recipe slugs (cross-references)season- List of: spring, summer, fall, winter, year-roundoccasion- List of: weeknight, dinner-party, holiday, brunch, hangover, etc.hero_image- Path to hero photo, relative to site root (e.g.images/recipes/foo.jpg)hero_alt- Alt text — required ifhero_imageis set
Required:
title- Cocktail nametype- Must be "cocktail"
Optional:
glass- Glass type (rocks, coupe, highball, etc.)spirit_base- Primary spirit (gin, vodka, rum, etc.)garnish- Garnish descriptiontags- List of tags (see "Tags vs. facets")description- Brief description- All Phase 3 schema fields above (
headnote,variations, cross-refs, etc.) apply to cocktails too.
The site has multiple ways to slice recipes. To keep them from competing, each one has a distinct purpose:
| Field | Type | Purpose | Examples |
|---|---|---|---|
category |
controlled | Where it lives in the cookbook | mains, sides, desserts |
cuisine |
controlled (food only) | Culinary tradition | Italian, Indian, Italian-American |
spirit_base |
controlled (cocktails only) | Primary spirit | gin, rum, bourbon |
season |
controlled enum | When it's at its best | spring, summer, fall, winter, year-round |
occasion |
controlled enum | When you'd reach for it | weeknight, dinner-party, holiday, brunch |
tags |
freeform | Technique, dietary, mood | one-pot, vegetarian, no-knead, comfort-food |
Rule of thumb: if a value has its own dedicated field (cuisine, spirit,
season, occasion), don't also tag it. tags is reserved for cross-cuts the
named facets don't capture — e.g. cooking technique (braise, no-knead,
grill), dietary constraints (vegan, gluten-free), mood/feel (comfort-food,
celebratory, easy-cleanup).
If you find yourself adding the same tag to many recipes and treating it
structurally (e.g. filtering by it), promote it to a facet instead — add a
new field to Recipe in recipe_parser.py and a matching index page in
site_generator.py.
- Modify templates in
templates/ - Update styles with Tailwind utilities or
static/css/custom.css - Add TypeScript in
src/and compile withnpm run build:ts - Extend generators in
site_generator.pyorpdf_generator.py - Update LaTeX in
latex/preamble.texorlatex/closing.tex
Always activate the virtual environment before working:
source venv/bin/activateTo deactivate:
deactivatePython (in pyproject.toml):
- Jinja2 - Template engine
- PyYAML - YAML parsing
- python-slugify - URL-friendly slugs
- python-dotenv - Environment configuration
- markdown - Markdown processing
Dev extras (pip install -e ".[dev]"): ruff, mypy, watchfiles, pre-commit.
Node.js (in package.json):
- TypeScript - Type-safe JavaScript compilation
Problem: pdflatex not found
Solution: Install a LaTeX distribution:
- macOS:
brew install --cask mactex - Ubuntu:
sudo apt-get install texlive-full - Windows: Install MiKTeX
Problem: CSS not loading on GitHub Pages
Solution: Check that BASE_URL in .env matches your repository name:
BASE_URL=/repository-nameThen rebuild:
python build.py siteProblem: Recipes failing validation
Solution: Run validation to see specific errors:
python build.py validateCommon issues:
- Missing required metadata (title, category/type)
- Invalid category name
- Missing YAML frontmatter delimiters (
---)
"To Serve Man" references the classic 1962 Twilight Zone episode where an alien book titled "To Serve Man" turns out to be a cookbook. Here, we embrace the pun with affection—it is indeed a cookbook, and a delicious one at that.
This project is open source. Feel free to fork and customize for your own cookbook!
Built with:
- Cooklang - Recipe markup language
- Python - Programming language
- Jinja2 - Template engine
- LaTeX - Document typesetting system
- Tailwind CSS - Utility-first CSS framework
- TypeScript - Type-safe JavaScript
Typography:
- Web: EB Garamond (serif) + Inter (sans-serif)
- PDF: EB Garamond (serif) + Helvetica (sans-serif)
Issues and pull requests welcome! If you create something cool with this, we'd love to hear about it.