How to build a recipe manager
How to build a recipe manager turns a recipe page into ingredients you can scale, then merges a week of dinners into one grocery list you can check off.
How to build a recipe manager is a page turned into ingredients, a week of dinners, and one grocery list. Paprika Recipe Manager is that product. The catalog line is “Recipes without the life story, groceries without the chaos.”
The card lists $2.50–$5 a month. That is $30–$60 a year. The verdict is “Deliciously in scope”. The first estimate is 6 hours. The project name on the card is Stewpot.
A list with no recipes is AnyList. The pricing note says AnyList Complete is $9.99 a year for one person and $14.99 a year for a household, which the card shows as $0.83–$1.25 a month. The first estimate there is 4 hours, and recipes are on its month row. Calories are MyFitnessPal, $19.99–$24.99 a month, about 10 hours. Stewpot stops before calories.
The fixtures in this guide are three recipes you type if a URL is blocked. Soda bread at 4 servings: 1 1/2 cups flour, 1/2 tsp salt, a pinch of nutmeg. Friday beans at 4 servings: 200g onion, 1 cup flour, 2-3 cloves garlic. Wednesday stew at 4 servings: 300g onion. Those lines are the ones the product prompts already use for scaling and merging.
How to build a recipe manager from one URL
The Paprika page splits the work into three rows.
The first row is 6 hours. URL import, recipes, and a scaled grocery list. You paste a URL. You fix one line. You put dinners on a week. You shop from one list.
The second row is 2 days, 16 hours. Photos, cook timers, plan templates, and list sharing. A picture of the loaf can wait until the flour line scales.
The third row is a month and up, 160 hours. Native apps, cloud sync, and pantry tracking.
If you tell an agent to “clone Paprika”, it aims at the phone app and a sync account. Paste the prompts below and keep the three fixtures on the first row.
What the 6-hour build does
You paste a URL. The server fetches the page, with a timeout and a size cap, and looks for schema.org Recipe markup. You see the title, the ingredient lines, and the steps. Nothing is in the library yet. You fix a quantity, add a tag, and save. A page with no recipe markup explains that, and the library stays as it was. You can type a recipe by hand instead.
You open Sunday and assign soda bread at 6 servings. The stored recipe stays at 4. The plan holds the 6. Flour on that card reads 2 1/4 cups, because 1 1/2 times 1.5 is 2.25, and 2.25 cups renders as 2 1/4 cups. The pinch of nutmeg stays a pinch and is marked unscalable. You add Wednesday stew at 4 and Friday beans at 4. Generate the list. Onions merge to 500g. Flour in cups merges to 3 1/4 cups. A cup of flour and a gram of flour would stay on two lines, and this week you do not have that pair.
You drag onion from unsorted to produce. Next week’s list remembers produce. In the kitchen, cook mode shows one step in large type, the full scaled ingredient list, and a wake lock. Refresh offers to resume that step. A timer is the next row.
What it leaves out
No photo on the card. The product screen mentions one, and the 16-hour row is where the file lands. No countdown timer, no plan template, no second person on the list. No pantry, no nutrition, no native app. The same v1 also replaces Mela and Crouton if what you wanted was a week of your own recipes. The card names them and does not give them prices.
Where the recipe lives
One cook, one account. Node.js and Astro render the library, the review, the week, and the list. Use TanStack Start if React is your weekday language. The stack post is which tab you copy. Recipes, ingredients, plan entries, aisle memory, and the grocery lines live in D1. There is no object storage in this version, because the photo waits.
Most screens are ordinary forms. The browser runs a bit of script for three things: the serving scaler preview, dragging a recipe between days, and the wake lock in cook mode. With that script off, you can still save a recipe, type a serving count, and read the steps as a page.
Import is a server fetch. The cook’s browser never requests the recipe site, so you avoid a CORS wall and you can cap the body. This v1 stops at 10 seconds, follows at most 3 redirects, and stops reading at 1 MB. Those limits belong to this build. If the site returns a non-200, times out, or exceeds the cap, the review shows the status and writes nothing.
The parser looks for schema.org Recipe. JSON-LD may be one object, an array, or a node inside @graph next to a breadcrumb. @type may be a string or an array. Some pages still use microdata. Instructions may be a string, a list of strings, HowToStep objects, or HowToSection groups. Flatten sections in order. Images may be a URL, an object, or an array. You may keep one image URL on the review for the cook to see. You do not download it in this row.
You store the structured lines the cook saved, plus the original line text. You do not store the HTML. A page with no Recipe node gets a short explanation and a link to the blank form.
Scaling multiplies the stored amounts at render time. The stored numbers stay the original parse, so going back to 4 servings restores 1 1/2 cups with no rounding left behind. Grocery generation uses the servings on the plan entry. The recipe default stays 4.
If D1 is down, the library fails to load and an import review can still be on screen, unsaved. Saving then shows an error and keeps the form. You do not clear the review on a failed save.
Delete names the days that still point at the recipe, then removes the recipe, its lines, and those plan entries. The next grocery run omits them. Checks on lines that disappear are dropped. Checks on lines that remain, matched by name and unit, stay. A manual line such as butter survives a regenerate.
What a recipe actually stores
The card lists Recipe, Ingredient, PlanEntry, GroceryList, and GroceryItem. All five exist in these 6 hours.
A recipe is Soda bread. Title, servings (4), source URL, source domain, tags, steps in order, and the time you saved it. The original servings stay put when a plan entry uses 6.
An ingredient is one line. It stores the original text, a parsed amount, a unit, a name, and a flag when the parse failed. 1 1/2 cups flour becomes amount 1.5, unit cups, name flour. Unicode fractions count. 1/2 tsp salt becomes 0.5, tsp, salt. 2-3 cloves garlic stores a range, and both ends scale. a pinch of nutmeg stores the original text, amount empty, flag unscalable. A failed parse is never rewritten into a fake number.
A plan entry is one recipe on one day of the week that contains today in the cook’s timezone. The week starts Monday. The entry stores the day, the recipe id, the servings for that dinner, and a position so two recipes on Friday keep their order. Dragging to another day changes the day and the position together.
A grocery list is the latest run for that week. A grocery item is a merged line: name, unit, amount or range or the original text, aisle, checked, and whether you typed it by hand. Merge when the names match after case, extra space, and a simple plural (onion and onions), and the units are the same. 200g onion plus 300g onion is 500g onion. 1 cup flour plus 200g flour stays two lines. Red onion stays distinct from onion, so the normalizer does not strip adjectives.
An aisle is a remembered pair, ingredient name to aisle name, for this household. Unknown lines land in unsorted. Moving onion to produce writes that pair. The next generate uses it.
The step index lives in localStorage on that browser, keyed by recipe id. The Screen Wake Lock API is the integration the card names. The lock is dropped when the tab is hidden, so you reacquire it on visibilitychange. Browsers without the API still show the steps.
The screens you ship
The library, the import review, the recipe, the week, the grocery list, and cook mode. The photo and the timer are drawn so the next row has a shape.
Library
Each card shows the title, the tags, the source domain, and the original servings. Actions are open and add to this week. The empty state says “No recipes yet” and offers import and a blank recipe. Search matches title and tags. There is no photo placeholder pretending a file exists.
Import review
The URL you pasted, the status of the fetch, the parsed title, each ingredient as an editable line, and the steps. A banner says nothing is saved yet. Confirm writes the recipe. Discard returns to the library with no new row. A missing Recipe node replaces the form with the explanation and the blank-recipe button. A timeout names the limit, 10 seconds, and leaves the library alone.
Recipe
The title, the original servings, a scaler that previews another count, the lines with an unscalable badge where the flag is set, the steps, the source link, tags, add to plan, and cook. Changing the preview does not write. Saving the recipe writes the original lines only. If you edit 1 1/2 cups down to 1 cup and save, the stored amount becomes 1 and the old 1.5 is gone. That edit is the original now.
Week
Seven columns, Monday through Sunday, labeled with dates for the current week. Each assigned recipe shows its plan servings. Drag between days. A control on Sunday’s soda bread sets servings to 6. Generate grocery list sits at the top. An empty week says the list will be empty too.
Grocery list
Lines grouped by aisle, unsorted first until you move them. Each line has a check, the amount, and the name. Manual add is a text field. Export copies plain text. Regenerating keeps butter if you added it, keeps the onion check if onion is still 500g, and drops a check for a recipe you removed.
Cook mode
One step, large type, previous and next as large buttons. Every step shows the full scaled ingredient list, because the markup rarely names a line. The wake-lock indicator is on when the lock is held. After you switch away and come back, it reads on again. If the API is missing, the indicator says the screen may sleep, and the buttons still work. A refresh shows “Resume step 2” and waits for you to choose it.
Screens that wait
The photo is an image on the library card and the recipe, stored as a file, with a size cap of its own. The timer is a minute count on a step, started by the cook, and it does not change the stored step text. A plan template copies a week’s entries into another week. List sharing is a link for a second person, read and check only. Those four are the 16-hour row.
From a URL to 500 grams of onion
You paste a URL that has Recipe markup. The review shows ingredients. You change one quantity and save. The library has one card. You open a private window and the card is there, so the save was the server. You discard a second import and the count stays one.
The URL host refuses the fetch. The review shows the status. You open the blank form and type the three fixtures. Sunday gets soda bread at 6. Wednesday gets stew at 4. Friday gets beans at 4. You drag stew to Thursday and reload. Thursday still has it.
Generate. Onion reads 500g. Flour is one cups line: 2 1/4 from the scaled soda bread plus 1 cup from the beans, which is 3 1/4 cups. Garlic reads 2-3 cloves. Nutmeg is flagged. You move onion to produce, add butter, check onion, and generate again. Onion is still checked, still in produce. Butter is still on the list.
You open cook mode on soda bread, go to step 2, lock the screen indicator, switch to another app, and come back. The indicator returns. You refresh. The resume prompt names step 2.
Prompt 1. A recipe row and the three fixtures
Copy the Astro prompt from Build your own Paprika Recipe Manager. Paste this above it.
Ship the 6-hour row only. Project name: Stewpot.
Node.js and Astro. Recipes, ingredients, plan entries,
aisle memory, and grocery lines live in D1.
TanStack Start is the same screens and the same rules,
with server functions and a SQL database.
No photo file, no timer, no second shopper, no pantry,
no calories.
One cook. A recipe stores title, original servings,
source URL, source domain, tags, and ordered steps.
An ingredient stores the original line, amount, unit,
name, and an unscalable flag.
A blank form can create a recipe without a URL.
Empty library copy: No recipes yet.
Seed nothing. The cook types these fixtures if needed.
Soda bread, 4 servings: 1 1/2 cups flour, 1/2 tsp salt,
a pinch of nutmeg.
Friday beans, 4 servings: 200g onion, 1 cup flour,
2-3 cloves garlic.
Wednesday stew, 4 servings: 300g onion.
The pinch stays unscalable. The garlic range keeps both ends.
Do not convert cups to grams.
When the plan lists routes and tables, stop.
What to check after the library
Save soda bread from the blank form. Reload. The three lines are there, and nutmeg is flagged. A logged-out window does not see it. Delete it. The library is empty again. Search for “bread” finds it once the tag is bread. The tables are recipes, ingredients, plan_entries, aisle_map, grocery_items.
Prompt 2. Import that saves nothing until confirm
Add URL import on Stewpot.
The server fetches the URL. Timeout 10 seconds.
Follow at most 3 redirects. Stop reading at 1 MB.
On a non-200, a timeout, or a body over the cap,
show the status and write no recipe.
Parse schema.org Recipe only.
Accept JSON-LD as an object, an array, or a node in @graph.
Accept @type as a string or an array.
Accept microdata.
Flatten HowToSection and HowToStep into ordered steps.
Strip tags and HTML entities inside ingredient strings.
Show the review with editable lines.
Confirm is the only save. Discard writes nothing.
A page with no Recipe node explains that and links
to the blank form. Do not guess a recipe from prose.
Do not store the HTML.
Done when a marked-up page can be edited and then saved,
a discard leaves the library count unchanged,
and a blocked fetch shows the status.
What to check after import
Paste a URL you know has Recipe JSON-LD. The review appears and the library count is still the old number. Change one line. Confirm. The count goes up by one, and the edited line is what you stored. Discard a second import. The count stays. Paste a URL on a host that refuses you, or a page with no Recipe node. You get the explanation or the status, and no new card. Confirm the database has no HTML column.
Prompt 3. Scale, the week, and the merge
Add scaling, the week, and the grocery list on Stewpot.
Scaling multiplies stored amounts at render time.
1 1/2 cups at 1.5x renders as 2 1/4 cups.
1/2 tsp at 1.5x renders as 3/4 tsp.
Ranges scale both ends. Unscalable lines pass through
and stay flagged. Returning to the original servings
shows the original numbers again.
The preview on the recipe page does not write.
The week starts Monday, in the cook's timezone,
for the week that contains today.
A plan entry stores recipe, day, servings, and position.
Drag updates day and position together.
The recipe's original servings stay put.
Grocery generation reads plan servings.
Merge lines when the normalized name matches
and the unit matches.
onion and onions merge. red onion does not merge into onion.
200g onion plus 300g onion is 500g onion.
1 cup flour plus 200g flour stays two lines.
For the fixture week, Sunday soda bread at 6,
Wednesday stew at 4, Friday beans at 4:
onion is 500g, flour in cups is one line of 3 1/4 cups,
garlic is 2-3 cloves.
Unknown items start unsorted.
Moving a line to an aisle remembers that name.
Manual lines survive regenerate.
A check survives regenerate when the name and unit
still match. A vanished line drops its check.
Export is plain text.
Done when the fixture week matches those quantities
after a reload, and a cup-plus-grams pair stays two lines.
What to check after the list
Set the three plan entries. Read Sunday’s preview before you generate: 2 1/4 cups, 3/4 tsp, pinch flagged. Generate. Read 500g onion and 3 1/4 cups flour. Add a temporary recipe with 200g flour, generate, and confirm flour is now two lines. Delete that recipe and generate again. One cups line, and the grams line is gone. Move onion to produce. Generate again. It is still produce. Add butter, check onion, generate. Both the check and the butter remain. Drag stew to Thursday and reload.
Prompt 4. Cook mode that survives a refresh
Add cook mode on Stewpot.
One step at a time, large type, large previous and next.
Show the full scaled ingredient list on every step.
Do not guess which line belongs to the step.
Use the Screen Wake Lock API.
Reacquire the lock on visibilitychange.
Show whether the lock is held.
If the API is missing, the steps still work and the
indicator says the screen may sleep.
Store the step index in localStorage keyed by recipe id.
On load, offer Resume at that step.
Do not add a timer.
Done when a refresh on step 2 offers to resume step 2,
and leaving the tab and returning reacquires the lock
on a browser that has the API.
What to check after cook mode
Open soda bread, advance to step 2, refresh. The prompt says step 2. Choose it and the step matches. Switch apps and come back. The indicator is on again. In a browser without the API, or with the call forced to fail, the buttons still move and the indicator explains the screen may sleep. The recipe’s stored steps are unchanged.
Failures the agent ships anyway
A save on fetch
The library gains a card before you see the review. Then a bad parse, or a page that was a shop not a recipe, is already stored. The count check before confirm is the one that catches this. Discard must be a real path that writes zero rows.
Cups turned into grams
The list shows one flour line with a gram amount you did not type. The agent added a density. Delete that table. The fixture’s cup line and a 200g line stay apart.
The pinch became zero
Nutmeg reads 0 tsp, or it vanished. Unscalable lines keep their original text and a badge. The soda bread preview at 6 servings still says pinch.
Servings written onto the recipe
You set Sunday to 6 and the recipe page now says the original is 6. The plan entry holds 6. The recipe holds 4. Going back to 4 servings on the recipe page shows 1 1/2 cups again.
HTML in the database
A column holds the fetched page “for later”. The import prompt says to store the structured recipe. Drop the column. A private recipe site should not leave its HTML in your database.
A timer and a photo on day one
The recipe page grows a file input and a start button. Those are the 16-hour row. The library card in this row has title, tags, domain, and servings.
The 16-hour row, after the list matches
Shop once from the fixture list. Then add the photo, the timer, the template, and the share.
The photo is one image per recipe, size-capped, shown on the card. The timer is minutes on a step, in the browser, and it does not rewrite the step. A template duplicates plan entries onto a later week, with the same servings. Sharing is one link to the grocery list. The other person can check lines. They cannot edit recipes or see other weeks. Revoke uses the same not-available page for a bad token and a dead one.
The scope post is the three-row habit. The prompt post is the line that keeps a nutrition model out of the first prompt.
Deploy, then shop the fixture week
Stewpot wants Astro and D1. Put it on a hostname you already own, recipes.flaviocopes.com. The hosting post is the adapter and the domain. Take a backup of D1 before you delete a paid account. The backup is the recipes and the aisle memory.
Log an import with the host, the status, and whether a Recipe node was found. Leave the URL path and the HTML out of the log. A recipe page can be private. Log a save with the recipe id and the ingredient count. Log a generate with the week id and the line count. When a fetch fails, log the host and the status code.
Then use it. Import one real dinner, or type the three fixtures, generate the list, and read the onion line before you trust a Saturday shop.
What to skip on purpose
Leave native apps, cloud sync, and a pantry. Leave calories to MyFitnessPal. Leave a list with no recipes to AnyList, if that is the whole rent. Leave the cups-to-grams guess on the floor.
If the rent is “check off butter”, build the list. If the rent is “what did lunch cost in calories”, build the diary. If the rent is “this URL, this week, one shop”, build this.
Start now
Go to Build your own Paprika Recipe Manager. Copy the Astro prompt, or TanStack Start if that is what you write.
Tell the agent the first estimate. URL import, recipes, a scaled grocery list. Six hours. The original amounts stay on the recipe.
Put soda bread on Sunday at 6. Read 2 1/4 cups, then 500g of onion on the list.