Cookbook attempts to be a modern and simple website to display recipes. This means among other things:
- Lightweight websites with minimal javascript
- Necessary ingredients displayed next to each step
- Multi-language support
(TODO images)
Cookbook is written as a WSGI service. It is recommended to run it via uWSGI, though several other deployment options are available. Please see the Flask deployment instructions for in-depth information.
Please find example setup scenarios below:
Clone this repository to some location, e.g. /usr/local/cookbook. This means
you should find e.g. the following paths:
/usr/local/cookbook/README.md/usr/local/cookbook/cookbook/main.py/usr/local/cookbook/cookbook/static/
Create a virtual env in /usr/local/cookbook, i.e.
python3 -m venv /usr/local/cookbook/envActivate the virtual env and install cookbook into it:
source /usr/local/cookbook/env/bin/activate
python3 -m pip install .Create /var/cookbook/. In /var/cookbook/recipes/, put your recipes; in
/var/cookbook/recipes/images/, put your images.
In /var/cookbook/config.json, put:
{
"COOKBOOK_LOCATION": "/var/cookbook/recipes",
"SECRET_KEY": "YOUR-SECRET-KEY",
"DEFAULT_LANG": "en",
"BASE_URL": "https://your.website.name",
"SITE_NAME": "Cookbook"
}Replace all of these keys as necessary. SECRET_KEY should be any randomly
generated string, see the Flask config for more details.
If you prefer, you can also set all of these options as environment variables. Simply
prefix them with FLASK_ (e.g. FLASK_COOKBOOK_LOCATION). If both a config file
and environment variables are provided, environment variables take precedence.
Install uWSGI. Find (or create)
the configuration directory for uwsgi. We will assume /var/uwsgi for this
tutorial.
In /var/uwsgi/server.json, put
{
"uwsgi": {
"emperor": "/var/uwsgi/vassals"
}
}In /var/uwsgi/vassals/cookbook.json, put
{
"uwsgi": {
"chmod-socket": "664",
"chown-socket": "uwsgi:www-data",
"env": [
"PATH=/usr/local/cookbook/env/bin",
"COOKBOOK_CONFIG=/var/cookbook/config.json"
],
"master": true,
"module": "cookbook:app",
"plugin": "python3",
"plugins": ["python3"],
"pyhome": "/usr/local/cookbook/env",
"socket": "/run/uwsgi/cookbook.sock",
"workers": 5
}
}Change the value of chown-socket to the user that runs -- or will run -- the
uWSGI service and the group that runs -- or will run -- nginx service. In env,
you can also provide configuration keys (see above for details).
Set up a systemd service (if none comes with your distro) to start uwsgi. This
service should run as the user set in chown-socket. Start uwsgi with the option
--json /var/uwsgi/server.json, for example:
[Service]
User=uwsgi
Group=uwsgi
ExecStart=uwsgi --json /var/uwsgi/server.json
# More options here
Enable and start the uwsgi service:
systemctl enable uwsgi
systemctl start uwsgiSet up nginx. Amend the configuration for the server section where you intend to host the server as follows:
http {
include uwsgi_params;
server {
# other server config goes here...
server_name your.cookbook.url;
location / {
uwsgi_pass unix:/run/uwsgi/cookbook.sock;
}
location /images/ {
alias /var/cookbook/recipes/images/;
expires 7d;
add_header Cache-Control "public";
}
location /static/ {
alias /usr/local/cookbook/cookbook/static/;
}
}
}Enable and start nginx:
systemctl enable nginx
systemctl start nginxNavigate to the URL set in server_name to verify that cookbook is deployed and
running.
While the default setup of Cookbook assumes that it is deployed at the top-level of a webserver, it is possible to host it in a subfolder.
To this end:
- Make sure that BASE_URL in the configuration file contains the full path to Cookbook's index (e.g.
https://example.com/cookbook/) - Add the configuration key
APPLICATION_ROOTspecifiying the subfolder (e.g./cookbook/) - Prefix the three locations in the nginx config with the subfolder (e.g.
location /cookbook/ { ... },location /cookbook/images/ { ... }andlocation /cookbook/static/ { ... })
A nix module is being written. In the meantime, if you would like guidance on how to set up cookbook via nix, please see here for some hints.
Recipes are stored in yaml files. These files should all be stored in one folder,
which should be provided to the cookbook service in the config file (COOKBOOK_LOCATION).
The file names should follow the following format: <id>.<language>.<ending>.
The cookbook service will automatically aggregate recipes
with the same id but different language codes, see 'Localization' for more
information.
The following formats are supported:
.recipefor Recipe Schema V2.ymlor.yamlfor Recipe Schema V1
Recipe ids exist in two variants: Unnormalized, as found in the file name, and
normalized, for internal handling (e.g. the recipe in Chocolate Cake.en.yml
would have an unnormalized id of Chocolate Cake and a normalized id
of chocolate-cake).
Whenever you as the user need to specify ids (e.g. for translations or for linking related recipes), it is recommended you use unnormalized ids. This makes recipes more readable for you, and allows the normalized format to change without breaking your recipes.
Recipes can have linked images. For now these are only displayed in the search results. Images are loaded from a subfolder of the recipe folder. Specifically, the image is looked up in the following order:
<recipe-folder>/images/<unnormalized id>.png<recipe-folder>/images/<unnormalized id>.jpg<recipe-folder>/images/<normalized id>.png<recipe-folder>/images/<normalized id>.jpg
Note that the service performs no further processing of the image files, instead serving the files directly (in fact, it is recommended to configure your webserver such that the image folder is served without dispatching to the service). Therefore, images should be cropped to a 16:9 aspect ratio (the search listing scales the images to 250 x 141px) [n.b. subject to change].
The v2 schema is a freeform format designed to make writing recipes more convenient than the v1 format. A recipe consists of a metadata section and an arbitrary number of arbitrarily named recipe sections.
The metadata section is a sequence of lines that start with the following keywords:
name(mandatory) - The name of the recipe. Examples:name Chocolate Cake
serves(optional) - Servings information, in the format<default number of servings> <servings unit> <servings increment>. All values are optional but must be specified in left to right order. Default is a default serving of 1, no specified unit, and an increment of 1. Examples:serves 8serves 1 cakeserves 24 tartelettes 8(when the user presses the increment button, the recipe will be scaled for 32, then 40, and so on, tartelettes)
desc(optional) - A recipe description to display in the search overview. Multiline descriptions are supported by prefixing each subsequent line with two spaces.note(optional) - A note to display on the recipe itself, for example for serving suggestions. Multiline descriptions are supported by prefixing each subsequent line with two spaces.tags(optional) - A comma separated list of tags. Examplestags caketags main, vegan, my favorites
attrib(optional) - Attribution and provenance informationrelated(optional) - A comma separated list of recipe ids to link from the recipe view. Examples:related Chocolate Pecan Cake, White Chocolate Cake
hide(optional, default false) - If present as a keyword, hides the recipe from the all-recipes listing. Useful in combination withrelatedfor splitting out recipe variations without cluttering the main recipe listing. Specific searches will still turn up the recipe.version(optional, default 2) - Recipe schema version. Currently unused and only supported for future proofing. If present, must be 2.
An empty line terminates the metadata section.
name Egg Tartelettes with Raisins
serves 24 tartelettes 8
desc A raisin variant of the classic egg tartelettes.
note Soak the raisins in rum for a boozy variation.
tags sweet, cake, dessert, snack
attrib My Grandma
related Egg Tartelettes
hide
A recipe section is a sequence of logically-related instructions, for example
"Prep Ahead", "Mis-en-Place" or "For the Filling". A section starts with its title,
for example # Mis-en-Place. It is then followed by a number of steps (described
below), each delimited with an empty line. A new section heading ends the current
section and begins a new one.
A step consists of ingredients required at this step, inputs from previous steps, outputs for subsequent steps and the actual instructions, in the following format:
- A line beginning with
-is an ingredient. Depending on the number of words in the line, this has the following meaning:- Just one word: An ingredient without a specific amount of unit. For example
- salt - Two words: An ingredient in a specified amount, without unit. For example
- 2 eggs - Three or more words: An ingredient with a specified amount and unit. For example
- 50 ml white wine - If the default splitting behavior does not work for a multi-word ingredient, you can wrap it in quotes. For example
- "orange juice"
- Just one word: An ingredient without a specific amount of unit. For example
- A line beginning with
->is an output of a step, for use in subsequent steps. This can be listed multiple times for multiple outputs, for example- 2 eggs -> egg whites -> egg yolks - A line beginning with
=is an internal ingredient, that is, an input from a previous step. Internal ingredients are free-form strings, but they must match an output of a previous step. - A line beginning with
@is a hidden ingredient. These function like internal ingredients but are not displayed in the recipe view. Their purpose is for semantic linking of recipe steps. - Any other line is treated as part of the instructions. Inside the instructions you may wrap a
number in braces (
{}) to have it scale with the serving amount. For example:Zest {1} orange, then juice all of them.
Hidden ingredients? Internal ingredients? Semantic linking?
For purposes of building a graph view of the recipe steps, the parser builds a web of dependencies between the individual steps as follows:
- The first step does not depend on any other step.
- A step with internal or hidden ingredients depends on the steps that produce the corresponding outputs.
- A step with no internal or hidden ingredients implicitly depends on the
preceeding step. This behavior can be overwritten with the special line
@nodep, which causes the step to be a root step like the first one.
When writing recipes, you can use the following as a rule of thumb:
- If a step only depends on the previous step and this linking is clear from context, do not specify a dependency.
- If a step depends on more than one previous step, on a distantly removed step, or if the implicit linking might cause confusion, explicitly list the dependencies as internal or hidden ingredients. Use internal ingredients for more distant steps and major intermediate products and hidden ingredients for closer steps and minor intermediate products.
The following is an (imaginary, untested!) section for orange custard
# For the custard
- 2 eggs
-> egg yolks
Split the eggs. Discard the whites.
- 2 oranges
- 100 g sugar
- "vanilla extract"
-> orange zest
-> orange syrup
Zest {1} orange and juice all of them. Bring the orange juice to a boil and stir
in the sugar and the vanilla extract until dissolved.
= egg yolks
@ orange zest
@ orange syrup
-> custard
In a bain-marie, whip eggs until foamy and just starting to set. Stir in
the orange syrup and the orange zest and continue whipping until you get
a thick foam. Remove from heat and let cool.
Note: Unless you plan to process recipes further with other tools, it is not recommended to choose this format.
| Key | Description | Default | Mandatory? |
|---|---|---|---|
name |
Name of the recipe, as displayed in the search results and on the recipe. | Yes | |
serves |
Number of servings in this recipe. | Yes | |
servings_unit |
The "kind" of servings this recipe produces. Usually readers assume that servings refers to the number of people that you can feed with the given amounts. Override this here if it does not apply, (e.g. servings is the number of muffins this recipe produces) | n/a | |
servings_increment |
The change in servings when readers press the + or - buttons next to the servings. | 1 | |
descr |
A brief (one to two sentence) description of the recipe, displayed in the search results. | n/a | |
note |
Additional information about the recipe, displayed on the recipe page (e.g. suitable side dishes, possible variations). | ||
hide_from_all |
Hide the recipe from the 'all recipes' overview page. The recipe will still appear in more specific searches. | ||
related |
List of (normalized or unnormalized, see 'Recipe Ids') ids of related recipes. | [] | |
tags |
List or comma separated string of tags | [] | |
prep |
List of recipe steps (see 'Recipe Steps') for the prep stage: Things that can or should be prepared in advance. | [] | |
mis_en_place |
List of recipe steps for the mis-en-place phase: Preparing ingredients for assembly | [] | |
cooking |
List of recipe steps for the cooking phase: Active or semi-active assembly | [] | |
passive_cooking |
List of recipe steps for the passive cooking phase: Extended hands-off cooking times | [] | |
cooking2 |
List of recipe steps for the second cooking phase: Active or semi-active steps following a passive cooking phase | [] | |
passive_cooking2 |
List of recipe steps for the second passive cooking: Extended hands-off cooking times following a previous passive cooking phase | [] |
| Key | Description | Default | Mandatory? |
|---|---|---|---|
ingredients |
List of ingredients (see 'Ingredients') for this step | [] | |
internal_ingredients |
List of strings: Intermediate results (see yields field here) from previous steps, for semantic linking of steps. Displayed in the instructions. |
[] | |
hidden_ingredients |
List of strings: As internal_ingredients but only for semantic linking of steps, not displayed in the instructions. |
[] | |
yields |
String or list of strings: Intermediate result(s) produced by this step. Not mandatory, but recommend for all but the final step. | n/a | |
instructions |
Preparatory instructions for this step (we recommend yaml multiline strings, see examples for more information). | Yes |
| Key | Description | Default | Mandatory? |
|---|---|---|---|
ingredient |
Name of the ingredient | Yes | |
amount |
Amount of the ingredient to use (just the number, see unit field for unit information) |
n/a | |
unit |
Unit that the amount is given in |
The service attempts to serve one localized version of the website for each
language for which a recipe has been found. The recipe localization itself is
left up to the user. The website UI is localized with strings taken from
localization/<lang>.yml. If the language has no localization file, or a
localization key is not defined, the service first falls back on the DEFAULT_LANG
language (definable in the config, if undefined defaults to the language
with the most recipes), and barring that, English.
The default language of the website is DEFAULT_LANG. Specific languages can be
requested by prefixing all paths on the website by the desired language code.
Additionally, all recipes will link to versions of the same recipe in different
languages.
The service attempts to remember the preferred (i.e. last-used) language and serve that as a default in the future (needs work, might get removed in favor of explicit language paths everywhere except the default landing page)