Content templates

Full design · Content

Recipe planner

Trestle, a recipe collection and weekly meal planner for home cooks, built Australian and metric throughout.

Live preview. Click around, it works.

About this template

Trestle, a recipe collection and weekly meal planner for home cooks, built Australian and metric throughout. Cooks browse a recipe library with search and filters for diet, total time and what is already in the pantry, open a recipe that scales its ingredient quantities and converts units correctly for any serving count, and step through a cook mode with one timer per method step that can keep the screen awake while it runs. A weekly plan drags recipes onto days, and the shopping list is built straight from that plan: every ingredient across the week's recipes is combined by unit into one correctly totalled line and grouped by supermarket aisle, checked off while shopping. A pantry screen tracks what is already on hand and feeds back into the recipe filters. The household owner side, reached through a visible Viewing as switch, adds and edits recipes with ingredients as structured rows (quantity, unit, aisle), organises them into collections, and manages a household of members who share the same weekly plan and shopping list, each unable to touch another household's plan.

What the agents are asked

The first message sent when you use this template. You can edit the plan before anything is built.

Adapt Trestle to my kitchen or my food business. Keep every screen and working rule: the cook side (Recipes with search, diet, time and pantry filters, Recipe page with a servings scaler that converts units correctly, Cook mode with per step timers that can keep the screen awake, Weekly plan, Shopping list combined and totalled from the week's plan and grouped by aisle, Pantry) and the household owner side (Recipes editor with structured ingredient rows, Collections, Household with shared plans, reached through Viewing as). Change the brand, colours, recipes, quantities and household members to mine, and keep every quantity, time and temperature genuinely cookable. Keep the sample data marked Sample and removable in one step, keep the general information line wherever the site touches dietary or nutrition claims, and never present a health claim as medical fact. When I want real accounts, wire in sign in and the tables and functions in schema.sql as the README describes, with every secret key on the server.

A recipe collection and weekly meal planner for home cooks, Australian and metric throughout. Cooks search and filter a library of original recipes, scale servings and switch between metric and imperial, cook with step by step timers, plan a week and let the shopping list build itself, correctly combined, from whatever that week's plan actually needs.

Built on the NovaBuild template kit (docs/guardrails/TEMPLATE-KIT.md): one CONFIG block, one design token block, and a shared data layer that runs in demo mode with no backend or against a real Supabase project once one is connected, with no code change either way.

Screens

Cook side:

  • Recipes (#/recipes): search, filter by diet, total time and whether it uses what is already in the pantry, sort, a card per recipe.
  • Recipe (#/recipes/<slug>): a servings stepper that scales every quantity, a metric or imperial toggle that converts mass, volume and the oven temperature correctly, structured ingredients and method, an "Add to plan" action.
  • Cook mode (#/recipes/<slug>/cook): one step at a time, a countdown timer where a step has one, and an optional "keep screen awake" toggle (feature detected: the toggle only appears where the Wake Lock API actually exists, and cooking still works without it).
  • Plan (#/plan): the current week, a day at a time, recipes added per meal.
  • Shopping list (#/shopping-list): every ingredient across the week's plan combined by unit and grouped the way an Australian supermarket runs, checkable, printable.
  • Pantry (#/pantry, behind CONFIG.features.pantry): what is already on hand, feeding the Recipes filter and a "have" marker on every ingredient list.

Household owner side ("Viewing as: Household owner", behind CONFIG.features.householdSide):

  • Recipes (#/manage/recipes, #/manage/recipes/new, #/manage/recipes/<slug>/edit): add and edit recipes with ingredients and method as structured, ordered rows, not a block of text.
  • Collections (#/manage/collections): group recipes the way the household actually cooks.
  • Household (#/manage/household): everyone who shares this plan and list, and one action to remove all sample data.

"Viewing as" is a preview toggle in demo mode, the way every NovaBuild template shows both sides without a separate sign up flow. Turning CONFIG.features.householdSide off removes the toggle and every #/manage/... route, including a direct visit to the address; turning CONFIG.features.pantry off does the same for #/pantry and removes the "uses what I have" filter from Recipes.

Signature function

The shopping list (buildShoppingList in script.js) reads every recipe in the current week's plan, scales each ingredient to the servings actually planned (not the recipe's base servings), and combines matching ingredients onto one line: mass units add up in grams, volume units in millilitres, then round back to a sensible display unit (750 ml rather than 1 tablespoon plus 2 teaspoons plus 3 tablespoons); a count unit like a clove or an each only combines with the exact same unit, because two cloves of garlic and three onions are never one number. The list is grouped by aisle and checked off per household, not per visitor.

Data model (schema.sql)

households, household_members, recipes, recipe_ingredients, recipe_steps, collections, collection_recipes, plans, plan_items, pantry_items, shopping_checks. Row level security on every table, scoped to the caller's own household through tr_household_id() / tr_is_member() / tr_is_owner(). Recipes, collections and the member roster are owner writable directly through row level security. Plans, plan items and shopping checks have no insert, update or delete policy at all: every write to them goes through add_plan_item, remove_plan_item and toggle_shopping_check, each a SECURITY DEFINER function that resolves the caller's household itself and never accepts one as an argument, so a member cannot even name another household's plan or list, let alone change it (proved in the schema's own VERIFY block against a second seeded household). add_plan_item upserts on (plan_id, recipe_id, day, meal): calling it twice with the same recipe, day and meal combines the servings instead of duplicating or overwriting the row. "Remove all sample data" is a fourth function, remove_sample_data(), checked owner only inside the function itself rather than trusted from the client: recipes, collections, pantry items and the household roster all carry a delete grant an owner could use directly, but a demo shaped action like this runs as one definer call so a real deployment and the demo mirror it exactly (every store write in script.js is checked against these grants; see docs/ops/LESSONS.md L47).

Sample data

Every household, recipe, collection and pantry item is marked is_sample: true and carries SAMPLE_VERSION (script.js). A returning visitor's own recipes, plan changes and pantry additions are never sample rows, so a version bump only replaces the sample cast and keeps what the visitor made. Household owner &rarr; Household has a "Remove all sample data" action that clears every sample row (recipes and their ingredients and steps, collections, pantry items, and every non owner sample member) in one step.

Going live

  1. Run schema.sql on a Supabase project (rehearsed first with tools/rehearse-template-schema.mjs, never applied directly).
  2. Set CONFIG.backend in script.js to { mode: 'supabase', url: '<project url>', anonKey: '<anon key>' }. The anon key is public by design; row level security is the actual boundary. Never put a service_role or sb_secret_... key here; the kit refuses to start with one and the checker refuses to publish a template that contains one.
  3. Call tr_create_household(householdName, ownerName) for the first signed in user to provision their household and make them its owner; this template's UI does not include a "create my household" form yet, so that first call is made by hand (or from your own sign up flow) rather than through a screen.
  4. Everything the customer would change (brand, contact details, locale, units, which screens are on) comes from CONFIG at the top of script.js. Colours, fonts, radii and spacing come from the nova:tokens block at the top of styles.css.

What is not built yet

  • A "create my household" screen: the database already supports it (tr_create_household, see "Going live" above), the template's UI just does not offer the form yet.
  • Inviting a member by email: today "Add a household member" in the owner's Household screen adds a named row with no login of their own; wiring a real invitation (and matching it to auth.uid() on their first sign in) is a follow on piece of work.
  • A dedicated ingredient database with fixed units per ingredient: today each recipe's own ingredient rows carry their own unit, which is what the combine logic relies on; a shared ingredient catalogue would let the owner catch a typo'd unit before it ships.

Honesty

Trestle and every recipe, household and person in it are fictional, for demonstration only, and were not modelled on any specific existing product or publication. Every recipe's quantities, times and temperatures are written to actually work; nothing here states a nutrition or health claim as medical fact. Two dishes worth noting for a home cook adapting this: the miso salmon and the maple soy tofu bowl use ordinary soy sauce, not gluten free by default, and each ingredient row says so; swap in tamari to make either dish gluten free.

Art direction

One shoot, one grade: warm, real kitchen light (a benchtop or table, not a studio backdrop), natural steam and texture, tight crops with a little negative space, no readable text, no logos, no branded plates, trays or packaging in any frame, one dish per photograph and never the same dish twice. The one video (ragu-steam-loop.mp4) is a locked off, static shot of the same bowl as beef-cheek-ragu-pappardelle.webp, with only its steam moving, so it reads as one continuous photograph rather than a separate piece of footage; it never autoplays under reduced motion and pauses whenever it scrolls off screen or the tab is hidden.