My deployment of the Tandoor recipe app made me increasingly uneasy but I could not point out exactly why. When I found Cooklang it clicked: Just because something is self-hosted and open source does not make it a sustainable solution.
Tandoor comes with quite a complex deployment and more importantly the recipe data is stored in an SQL database. Even though there are many exporters to different file formats it makes securing the data already more complex. Cooklang, on the other hand, is just a specification for markup of recipe files that applications can build on.
A pattern that that has proven itself more and more in my setup is just using standardized files as data storage and keeping the application separate from that:
- Notes: Markdown Files + Obsidian
- Music: Audio Files + Symfonium
- Recipes: Cooklang Files + Cookcli
- Blog: Markdown Files + Hugo
Because the data consists of simple files, using proprietary apps has become totally fine. If the format is standardized and somewhat popular, I can just exchange the app. Any cloud storage tool like Nextcloud or Google Drive can handle the Backup and distribution of the data across devices.
For Cooklang, the only complex part was the sidecar for fetching data from Nextcloud. Originally, I planned to write an exporter for Tandoor but that turned out to be more of a challenge than I was up for. I decided to be lazy and let an LLM do the heavy lifting:
PYTHON
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "google-genai"
# ]
# ///
import argparse
import glob
import json
import os
import shutil
from google import genai
def convert_name_to_filename(recipe_name: str) -> str:
return recipe_name.lower().replace(" ", "-").replace("'", "")
def convert_to_cooklang(client, orig_recipe: dict) -> str:
return client.models.generate_content(
model="gemini-3-flash-preview", contents=f"""
Please translate into Cooklang. Make sure the meta information in the header uses this format:
---
title: "A recipe title"
source: "A cool cook book"
author: "Jamie Oliver"
---
In case the recipe is in English, please translate it into German and convert the units to metric as well.
{orig_recipe}
"""
).text
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("in_dir", type=str)
parser.add_argument("out_dir", type=str)
args = parser.parse_args()
client = genai.Client()
if not os.path.exists(args.out_dir):
print(f"{args.out_dir} does not exist, creating...")
os.makedirs(args.out_dir)
for dir in os.listdir(args.in_dir):
orig_recipe = ""
with open(os.path.join(args.in_dir, dir, "recipe.json"), "r") as fp:
orig_recipe = json.load(fp)
print(f"Converting {orig_recipe["name"]}...")
base_filename = convert_name_to_filename(orig_recipe["name"])
for file in glob.glob(os.path.join(args.in_dir, dir, "image.*")):
suffix = file.split(".", -1)[1]
new_thumbnail_filename = f"{base_filename}.{suffix}"
new_recipe_path = os.path.join(
args.out_dir, new_thumbnail_filename
)
shutil.copy(file, new_recipe_path)
recipe_content = convert_to_cooklang(client, orig_recipe)
with open(os.path.join(args.out_dir, f"{base_filename}.cook"), "w") as new_recipe:
new_recipe.writelines(recipe_content)
if __name__ == "__main__":
main()Software projects are often fast paced and short lived. On my not quite so long IT journey I have already experienced software projects becoming abandoned while relying on them. Knowing the underlying data of music, playlists, recipes, … is just plain standardized files gives me peace of mind: This is a solution I can rely on for the rest of my life.