From d601895982179e8b115615c04cdc4261108e282a Mon Sep 17 00:00:00 2001 From: Jake Runyan Date: Sun, 13 Sep 2026 14:50:11 -0700 Subject: [PATCH] Markdown file update + recipe cards --- .claude/CLAUDE.md | 46 +- .claude/rules/architecture.md | 16 +- .claude/rules/recipe-format.md | 79 +- README.md | 54 +- app/about/page.tsx | 2 +- app/page.tsx | 2 +- .../[slug]/assets/[...file]/route.ts | 26 + .../[category]/[slug]/card/[image]/route.ts | 37 + app/recipes/[category]/[slug]/page.tsx | 75 +- app/recipes/page.tsx | 11 +- components/ActiveFilters.tsx | 58 ++ components/FacetGroup.tsx | 49 + components/PrintCardButton.tsx | 44 + components/RecipeCard.tsx | 110 --- components/RecipeCardPanel.tsx | 51 + components/RecipeGridCard.tsx | 8 +- components/RecipeLayout.tsx | 94 +- components/RecipeMarkdown.tsx | 39 + components/RecipePageLayout.tsx | 2 +- components/RecipeTabs.tsx | 92 ++ components/RecipesClient.tsx | 100 +- components/RecipesSidebar.tsx | 249 +++-- components/SelectedTags.tsx | 44 - components/TagSelector.tsx | 119 --- lib/recipe-card.tsx | 476 +++++++++ lib/recipe-content.ts | 159 +++ lib/recipe-urls.ts | 17 + lib/recipes.ts | 86 +- lib/site.ts | 2 + lib/types.ts | 6 + next.config.ts | 2 + package-lock.json | 908 ++++++------------ package.json | 15 +- {public/recipes => recipes}/README.md | 100 +- .../african/shakshuka/assets/shakshuka.jpg | Bin .../african/shakshuka/assets/shakshuka2.jpg | Bin .../african/shakshuka/shakshuka.md | 26 +- .../baking/apple-pie/apple-pie.md | 23 +- .../baking/apple-pie/assets/not-found.svg | 0 .../cajun/gumbo/assets/not-found.svg | 0 .../gumbo.mdx => recipes/cajun/gumbo/gumbo.md | 23 +- .../assets/not-found.svg | 0 .../broccoli-beef-in-oyster-sauce.md | 24 +- .../assets/egg-flower-soup.jpg | Bin .../egg-flower-soup/egg-flower-soup.md | 23 +- .../chinese/fried-rice/assets/fried-rice.jpg | Bin .../chinese/fried-rice/fried-rice.md | 23 +- .../chinese/mabu-tofu/assets/mapo-tofu.jpg | Bin .../chinese/mabu-tofu/mapo-tofu.md | 23 +- .../assets/tangy-runyan-eggroll.jpg | Bin .../tangy-runyan-eggroll.md | 23 +- .../desserts/ice-cream/assets/ice-cream.jpg | Bin .../desserts/ice-cream/ice-cream.md | 24 +- .../assets/banana-chocolate-smoothie.jpg | Bin .../banana-chocolate-smoothie.md | 19 +- .../berry-smoothie/assets/berry-smoothie.jpg | Bin .../drinks/berry-smoothie/berry-smoothie.md | 19 +- .../peach-simple-syrup/assets/not-found.svg | 0 .../peach-simple-syrup/peach-simple-syrup.md | 23 +- .../assets/strawberry-smoothie.jpg | Bin .../strawberry-smoothie.md | 19 +- .../example-recipe-1/assets/not-found.svg | 0 .../example-recipe-1/example-recipe-1.md | 26 +- .../example-recipe-2/assets/not-found.svg | 0 .../example-recipe-2/example-recipe-2.md | 26 +- .../indian/brown-lentils/assets/not-found.svg | 0 .../indian/brown-lentils/brown-lentils.md | 27 +- .../chana-masala/assets/chana-masala.jpg | Bin .../indian/chana-masala/chana-masala.md | 29 +- .../assets/chicken-tikka-masala.jpg | Bin .../chicken-tikka-masala.md | 23 +- .../lamb-vindaloo/assets/lamb-vindaloo.jpg | Bin .../indian/lamb-vindaloo/lamb-vindaloo.md | 23 +- .../palak-paneer/assets/palak-paneer.jpg | Bin .../indian/palak-paneer/palak-paneer.md | 27 +- .../assets/corned-beef-and-cabbage.jpg | Bin .../corned-beef-and-cabbage.md | 23 +- .../chicken-parmesan/assets/not-found.svg | 0 .../chicken-parmesan/chicken-parmesan.md | 31 +- .../assets/not-found.svg | 0 .../fusilli-with-vodka-basil-parmesan.md | 23 +- .../pizza-dough/assets/elote-pizza.jpg | Bin .../pizza-dough/assets/pizza-dough.jpg | Bin .../italian/pizza-dough/pizza-dough.md | 26 +- .../italian/pizza-sauce/assets/not-found.svg | 0 .../italian/pizza-sauce/pizza-sauce.md | 23 +- .../ajitsuke-tamago-ramen-egg.md | 23 +- .../assets/not-found.svg | 0 .../assets/not-found.svg | 0 .../golden-curry-beef-stew.md | 23 +- .../golden-curry-chicken/assets/not-found.svg | 0 .../golden-curry-chicken.md | 23 +- .../beef-bourguignon/assets/not-found.svg | 0 .../meat/beef-bourguignon/beef-bourguignon.md | 23 +- .../assets/not-found.svg | 0 .../garlic-herb-butter-roast-turkey.md | 23 +- .../assets/not-found.svg | 0 .../grilled-lemon-pepper-salmon.md | 23 +- .../assets/not-found.svg | 0 .../jamaican-jerk-chicken.md | 21 +- .../ribs-instant-pot/assets/not-found.svg | 0 .../meat/ribs-instant-pot/ribs-instant-pot.md | 23 +- .../meat/turkey-gravy/assets/not-found.svg | 0 .../meat/turkey-gravy/turkey-gravy.md | 23 +- .../mexican/birria/assets/not-found.svg | 0 .../mexican/birria/birria.md | 23 +- .../assets/creamy-jalapeno-sauce.jpg | Bin .../creamy-jalapeno-sauce.md | 23 +- .../mexican/elote/assets/elote.jpg | Bin .../mexican/elote/elote.md | 23 +- .../assets/fajita-vegetables.jpg | Bin .../fajita-vegetables/fajita-vegetables.md | 20 +- .../mexican/horchata/assets/not-found.svg | 0 .../mexican/horchata/horchata.md | 23 +- .../mexican/mexican-rice/assets/not-found.svg | 0 .../mexican/mexican-rice/mexican-rice.md | 23 +- .../mexican/pinto-beans/assets/not-found.svg | 0 .../mexican/pinto-beans/pinto-beans.md | 23 +- .../taco-bell-meat/assets/crunchwrap.jpg | Bin .../taco-bell-meat/assets/taco-bell-meat.jpg | Bin .../mexican/taco-bell-meat/taco-bell-meat.md | 26 +- .../assets/not-found.svg | 0 .../caesar-salad-dressing.md | 23 +- .../cauliflower-mash/assets/not-found.svg | 0 .../cauliflower-mash/cauliflower-mash.md | 23 +- .../pickled-red-onions/assets/not-found.svg | 0 .../pickled-red-onions/pickled-red-onions.md | 23 +- .../assets/not-found.svg | 0 .../sous-vide-mashed-potatoes.md | 23 +- .../chicken-noodle-soup/assets/not-found.svg | 0 .../chicken-noodle-soup.md | 23 +- .../soup/chili/assets/chili.jpg | Bin .../chili.mdx => recipes/soup/chili/chili.md | 23 +- .../vietnamese/pho/assets/not-found.svg | 0 .../pho.mdx => recipes/vietnamese/pho/pho.md | 23 +- 135 files changed, 2580 insertions(+), 1747 deletions(-) create mode 100644 app/recipes/[category]/[slug]/assets/[...file]/route.ts create mode 100644 app/recipes/[category]/[slug]/card/[image]/route.ts create mode 100644 components/ActiveFilters.tsx create mode 100644 components/FacetGroup.tsx create mode 100644 components/PrintCardButton.tsx delete mode 100644 components/RecipeCard.tsx create mode 100644 components/RecipeCardPanel.tsx create mode 100644 components/RecipeMarkdown.tsx create mode 100644 components/RecipeTabs.tsx delete mode 100644 components/SelectedTags.tsx delete mode 100644 components/TagSelector.tsx create mode 100644 lib/recipe-card.tsx create mode 100644 lib/recipe-content.ts create mode 100644 lib/recipe-urls.ts create mode 100644 lib/site.ts rename {public/recipes => recipes}/README.md (66%) rename {public/recipes => recipes}/african/shakshuka/assets/shakshuka.jpg (100%) rename {public/recipes => recipes}/african/shakshuka/assets/shakshuka2.jpg (100%) rename public/recipes/african/shakshuka/shakshuka.mdx => recipes/african/shakshuka/shakshuka.md (93%) rename public/recipes/baking/apple-pie/apple-pie.mdx => recipes/baking/apple-pie/apple-pie.md (95%) rename {public/recipes => recipes}/baking/apple-pie/assets/not-found.svg (100%) rename {public/recipes => recipes}/cajun/gumbo/assets/not-found.svg (100%) rename public/recipes/cajun/gumbo/gumbo.mdx => recipes/cajun/gumbo/gumbo.md (95%) rename {public/recipes => recipes}/chinese/broccoli-beef-in-oyster-sauce/assets/not-found.svg (100%) rename public/recipes/chinese/broccoli-beef-in-oyster-sauce/broccoli-beef-in-oyster-sauce.mdx => recipes/chinese/broccoli-beef-in-oyster-sauce/broccoli-beef-in-oyster-sauce.md (92%) rename {public/recipes => recipes}/chinese/egg-flower-soup/assets/egg-flower-soup.jpg (100%) rename public/recipes/chinese/egg-flower-soup/egg-flower-soup.mdx => recipes/chinese/egg-flower-soup/egg-flower-soup.md (92%) rename {public/recipes => recipes}/chinese/fried-rice/assets/fried-rice.jpg (100%) rename public/recipes/chinese/fried-rice/fried-rice.mdx => recipes/chinese/fried-rice/fried-rice.md (94%) rename {public/recipes => recipes}/chinese/mabu-tofu/assets/mapo-tofu.jpg (100%) rename public/recipes/chinese/mabu-tofu/mapo-tofu.mdx => recipes/chinese/mabu-tofu/mapo-tofu.md (94%) rename {public/recipes => recipes}/chinese/tangy-runyan-eggroll/assets/tangy-runyan-eggroll.jpg (100%) rename public/recipes/chinese/tangy-runyan-eggroll/tangy-runyan-eggroll.mdx => recipes/chinese/tangy-runyan-eggroll/tangy-runyan-eggroll.md (93%) rename {public/recipes => recipes}/desserts/ice-cream/assets/ice-cream.jpg (100%) rename public/recipes/desserts/ice-cream/ice-cream.mdx => recipes/desserts/ice-cream/ice-cream.md (93%) rename {public/recipes => recipes}/drinks/banana-chocolate-smoothie/assets/banana-chocolate-smoothie.jpg (100%) rename public/recipes/drinks/banana-chocolate-smoothie/banana-chocolate-smoothie.mdx => recipes/drinks/banana-chocolate-smoothie/banana-chocolate-smoothie.md (86%) rename {public/recipes => recipes}/drinks/berry-smoothie/assets/berry-smoothie.jpg (100%) rename public/recipes/drinks/berry-smoothie/berry-smoothie.mdx => recipes/drinks/berry-smoothie/berry-smoothie.md (80%) rename {public/recipes => recipes}/drinks/peach-simple-syrup/assets/not-found.svg (100%) rename public/recipes/drinks/peach-simple-syrup/peach-simple-syrup.mdx => recipes/drinks/peach-simple-syrup/peach-simple-syrup.md (93%) rename {public/recipes => recipes}/drinks/strawberry-smoothie/assets/strawberry-smoothie.jpg (100%) rename public/recipes/drinks/strawberry-smoothie/strawberry-smoothie.mdx => recipes/drinks/strawberry-smoothie/strawberry-smoothie.md (79%) rename {public/recipes => recipes}/example/example-recipe-1/assets/not-found.svg (100%) rename public/recipes/example/example-recipe-1/example-recipe-1.mdx => recipes/example/example-recipe-1/example-recipe-1.md (92%) rename {public/recipes => recipes}/example/example-recipe-2/assets/not-found.svg (100%) rename public/recipes/example/example-recipe-2/example-recipe-2.mdx => recipes/example/example-recipe-2/example-recipe-2.md (92%) rename {public/recipes => recipes}/indian/brown-lentils/assets/not-found.svg (100%) rename public/recipes/indian/brown-lentils/brown-lentils.mdx => recipes/indian/brown-lentils/brown-lentils.md (93%) rename {public/recipes => recipes}/indian/chana-masala/assets/chana-masala.jpg (100%) rename public/recipes/indian/chana-masala/chana-masala.mdx => recipes/indian/chana-masala/chana-masala.md (93%) rename {public/recipes => recipes}/indian/chicken-tikka-masala/assets/chicken-tikka-masala.jpg (100%) rename public/recipes/indian/chicken-tikka-masala/chicken-tikka-masala.mdx => recipes/indian/chicken-tikka-masala/chicken-tikka-masala.md (94%) rename {public/recipes => recipes}/indian/lamb-vindaloo/assets/lamb-vindaloo.jpg (100%) rename public/recipes/indian/lamb-vindaloo/lamb-vindaloo.mdx => recipes/indian/lamb-vindaloo/lamb-vindaloo.md (95%) rename {public/recipes => recipes}/indian/palak-paneer/assets/palak-paneer.jpg (100%) rename public/recipes/indian/palak-paneer/palak-paneer.mdx => recipes/indian/palak-paneer/palak-paneer.md (93%) rename {public/recipes => recipes}/irish/corned-beef-and-cabbage/assets/corned-beef-and-cabbage.jpg (100%) rename public/recipes/irish/corned-beef-and-cabbage/corned-beef-and-cabbage.mdx => recipes/irish/corned-beef-and-cabbage/corned-beef-and-cabbage.md (90%) rename {public/recipes => recipes}/italian/chicken-parmesan/assets/not-found.svg (100%) rename public/recipes/italian/chicken-parmesan/chicken-parmesan.mdx => recipes/italian/chicken-parmesan/chicken-parmesan.md (93%) rename {public/recipes => recipes}/italian/fusilli-with-vodka-basil-parmesan/assets/not-found.svg (100%) rename public/recipes/italian/fusilli-with-vodka-basil-parmesan/fusilli-with-vodka-basil-parmesan.mdx => recipes/italian/fusilli-with-vodka-basil-parmesan/fusilli-with-vodka-basil-parmesan.md (93%) rename {public/recipes => recipes}/italian/pizza-dough/assets/elote-pizza.jpg (100%) rename {public/recipes => recipes}/italian/pizza-dough/assets/pizza-dough.jpg (100%) rename public/recipes/italian/pizza-dough/pizza-dough.mdx => recipes/italian/pizza-dough/pizza-dough.md (89%) rename {public/recipes => recipes}/italian/pizza-sauce/assets/not-found.svg (100%) rename public/recipes/italian/pizza-sauce/pizza-sauce.mdx => recipes/italian/pizza-sauce/pizza-sauce.md (88%) rename public/recipes/japanese/ajitsuke-tamago-ramen-egg/ajitsuke-tamago-ramen-egg.mdx => recipes/japanese/ajitsuke-tamago-ramen-egg/ajitsuke-tamago-ramen-egg.md (89%) rename {public/recipes => recipes}/japanese/ajitsuke-tamago-ramen-egg/assets/not-found.svg (100%) rename {public/recipes => recipes}/japanese/golden-curry-beef-stew/assets/not-found.svg (100%) rename public/recipes/japanese/golden-curry-beef-stew/golden-curry-beef-stew.mdx => recipes/japanese/golden-curry-beef-stew/golden-curry-beef-stew.md (91%) rename {public/recipes => recipes}/japanese/golden-curry-chicken/assets/not-found.svg (100%) rename public/recipes/japanese/golden-curry-chicken/golden-curry-chicken.mdx => recipes/japanese/golden-curry-chicken/golden-curry-chicken.md (91%) rename {public/recipes => recipes}/meat/beef-bourguignon/assets/not-found.svg (100%) rename public/recipes/meat/beef-bourguignon/beef-bourguignon.mdx => recipes/meat/beef-bourguignon/beef-bourguignon.md (93%) rename {public/recipes => recipes}/meat/garlic-herb-butter-roast-turkey/assets/not-found.svg (100%) rename public/recipes/meat/garlic-herb-butter-roast-turkey/garlic-herb-butter-roast-turkey.mdx => recipes/meat/garlic-herb-butter-roast-turkey/garlic-herb-butter-roast-turkey.md (95%) rename {public/recipes => recipes}/meat/grilled-lemon-pepper-salmon/assets/not-found.svg (100%) rename public/recipes/meat/grilled-lemon-pepper-salmon/grilled-lemon-pepper-salmon.mdx => recipes/meat/grilled-lemon-pepper-salmon/grilled-lemon-pepper-salmon.md (88%) rename {public/recipes => recipes}/meat/jamaican-jerk-chicken/assets/not-found.svg (100%) rename public/recipes/meat/jamaican-jerk-chicken/jamaican-jerk-chicken.mdx => recipes/meat/jamaican-jerk-chicken/jamaican-jerk-chicken.md (90%) rename {public/recipes => recipes}/meat/ribs-instant-pot/assets/not-found.svg (100%) rename public/recipes/meat/ribs-instant-pot/ribs-instant-pot.mdx => recipes/meat/ribs-instant-pot/ribs-instant-pot.md (93%) rename {public/recipes => recipes}/meat/turkey-gravy/assets/not-found.svg (100%) rename public/recipes/meat/turkey-gravy/turkey-gravy.mdx => recipes/meat/turkey-gravy/turkey-gravy.md (88%) rename {public/recipes => recipes}/mexican/birria/assets/not-found.svg (100%) rename public/recipes/mexican/birria/birria.mdx => recipes/mexican/birria/birria.md (94%) rename {public/recipes => recipes}/mexican/creamy-jalapeno-sauce/assets/creamy-jalapeno-sauce.jpg (100%) rename public/recipes/mexican/creamy-jalapeno-sauce/creamy-jalapeno-sauce.mdx => recipes/mexican/creamy-jalapeno-sauce/creamy-jalapeno-sauce.md (89%) rename {public/recipes => recipes}/mexican/elote/assets/elote.jpg (100%) rename public/recipes/mexican/elote/elote.mdx => recipes/mexican/elote/elote.md (94%) rename {public/recipes => recipes}/mexican/fajita-vegetables/assets/fajita-vegetables.jpg (100%) rename public/recipes/mexican/fajita-vegetables/fajita-vegetables.mdx => recipes/mexican/fajita-vegetables/fajita-vegetables.md (95%) rename {public/recipes => recipes}/mexican/horchata/assets/not-found.svg (100%) rename public/recipes/mexican/horchata/horchata.mdx => recipes/mexican/horchata/horchata.md (92%) rename {public/recipes => recipes}/mexican/mexican-rice/assets/not-found.svg (100%) rename public/recipes/mexican/mexican-rice/mexican-rice.mdx => recipes/mexican/mexican-rice/mexican-rice.md (92%) rename {public/recipes => recipes}/mexican/pinto-beans/assets/not-found.svg (100%) rename public/recipes/mexican/pinto-beans/pinto-beans.mdx => recipes/mexican/pinto-beans/pinto-beans.md (86%) rename {public/recipes => recipes}/mexican/taco-bell-meat/assets/crunchwrap.jpg (100%) rename {public/recipes => recipes}/mexican/taco-bell-meat/assets/taco-bell-meat.jpg (100%) rename public/recipes/mexican/taco-bell-meat/taco-bell-meat.mdx => recipes/mexican/taco-bell-meat/taco-bell-meat.md (86%) rename {public/recipes => recipes}/salad/caesar-salad-dressing/assets/not-found.svg (100%) rename public/recipes/salad/caesar-salad-dressing/caesar-salad-dressing.mdx => recipes/salad/caesar-salad-dressing/caesar-salad-dressing.md (88%) rename {public/recipes => recipes}/sides/cauliflower-mash/assets/not-found.svg (100%) rename public/recipes/sides/cauliflower-mash/cauliflower-mash.mdx => recipes/sides/cauliflower-mash/cauliflower-mash.md (93%) rename {public/recipes => recipes}/sides/pickled-red-onions/assets/not-found.svg (100%) rename public/recipes/sides/pickled-red-onions/pickled-red-onions.mdx => recipes/sides/pickled-red-onions/pickled-red-onions.md (91%) rename {public/recipes => recipes}/sides/sous-vide-mashed-potatoes/assets/not-found.svg (100%) rename public/recipes/sides/sous-vide-mashed-potatoes/sous-vide-mashed-potatoes.mdx => recipes/sides/sous-vide-mashed-potatoes/sous-vide-mashed-potatoes.md (92%) rename {public/recipes => recipes}/soup/chicken-noodle-soup/assets/not-found.svg (100%) rename public/recipes/soup/chicken-noodle-soup/chicken-noodle-soup.mdx => recipes/soup/chicken-noodle-soup/chicken-noodle-soup.md (94%) rename {public/recipes => recipes}/soup/chili/assets/chili.jpg (100%) rename public/recipes/soup/chili/chili.mdx => recipes/soup/chili/chili.md (94%) rename {public/recipes => recipes}/vietnamese/pho/assets/not-found.svg (100%) rename public/recipes/vietnamese/pho/pho.mdx => recipes/vietnamese/pho/pho.md (94%) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 6b1e70b..7f761e6 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -7,10 +7,11 @@ A personal recipe website. Content-first, no-nonsense. The name of the third hom ## Tech Stack - **Next.js 15** with App Router, TypeScript, Tailwind CSS -- **MDX** for recipe content with YAML frontmatter -- **next-mdx-remote/rsc + remark-gfm** for compiling MDX content server-side +- **Markdown** (`.md`) for recipe content with YAML frontmatter; sections are markdown directives (`:::ingredients`, `::card`) +- **unified + remark-gfm + remark-directive** to parse content server-side; `mdast-util-to-hast` + `hast-util-to-jsx-runtime` to render React +- **satori + sharp** to generate printable recipe card PNGs at build time - **Static site generation (SSG)** — all pages are prerendered at build time -- **No database** — recipes are MDX files on disk +- **No database** — recipes are markdown files on disk ## Project Structure @@ -21,32 +22,43 @@ app/ # Next.js App Router pages recipes/ page.tsx # Recipe listing (server, passes data to RecipesClient) [category]/[slug]/ - page.tsx # Recipe detail (server, compiles MDX and renders via RecipePageLayout) + page.tsx # Recipe detail (server, renders sections as tabs via RecipePageLayout) + assets/[...file]/ + route.ts # Serves recipe images from recipes/ (prerendered at build) + card/[image]/ + route.ts # Recipe card PNGs (card.png, or front.png + back.png) components/ Header.tsx / Footer.tsx # Site chrome RecipesClient.tsx # Recipe listing with filter state RecipeLayout.tsx # Sidebar layout (mobile drawer, desktop persistent) - RecipesSidebar.tsx # Search + category + tag filters - SelectedTags.tsx # Active tag chips - TagSelector.tsx # Tag dropdown picker + RecipesSidebar.tsx # Search + category/tag facet lists with result counts + FacetGroup.tsx # Collapsible sidebar facet section + ActiveFilters.tsx # Removable chips for active filters (above results) RecipeGridCard.tsx # Recipe grid card for listing page - RecipeCard.tsx # MDX component — splits h2 children into tab sections (client) + RecipeTabs.tsx # Tabbed recipe sections (client) + RecipeMarkdown.tsx # Renders a parsed markdown tree to React (server) + RecipeCardPanel.tsx # Recipe Card tab: preview, download, print (server) + PrintCardButton.tsx # Prints card images at 5×7 in (client) RecipePageLayout.tsx # Recipe detail page layout (server component) lib/ - recipes.ts # Recipe file loader with in-memory cache; reads from public/recipes/ + recipes.ts # Recipe file loader with in-memory cache; reads from recipes/ + recipe-content.ts # Parses a recipe body into intro / sections / card / outro; validates directives + recipe-card.tsx # Card layout (measured with satori) and PNG rendering + recipe-urls.ts # Client-safe helper that maps ./assets/ paths to served URLs + +recipes/ # ALL recipe content lives here (markdown + images together) + [category]/ + recipe-slug/ + recipe-slug.md + assets/ + hero.jpg + ... public/ assets/ # Site-level images (homepage SVGs) authors.json # Author metadata - recipes/ # ALL recipe content lives here (MDX + images together) - [category]/ - recipe-slug/ - recipe-slug.mdx - assets/ - hero.jpg - ... ``` ## Design Principles @@ -54,4 +66,4 @@ public/ - **Content first**: recipe pages are minimal — no sidebar, just the recipe - **Server components by default**: only add `'use client'` when interactivity is needed - **No taxonomy file**: categories and tags are derived directly from frontmatter across all recipes — no external registry to keep in sync -- **Single content location**: MDX and images are colocated in `public/recipes/` so they can be served directly without a copy step +- **Content is top level**: markdown and images are colocated in `recipes/`; images are served by a statically prerendered route handler, so there's no copy step. `public/` is only for site branding and data diff --git a/.claude/rules/architecture.md b/.claude/rules/architecture.md index c89b878..37d339a 100644 --- a/.claude/rules/architecture.md +++ b/.claude/rules/architecture.md @@ -3,14 +3,18 @@ ## State and Data Flow - **RecipeLayout** owns sidebar open/close state; passes `handleFilterChange` (memoised with `useCallback`) down to RecipesSidebar -- **RecipesSidebar** owns filter state (search, category, selectedTags) and reports changes via `useEffect` → `onFilterChange` -- **RecipesClient** owns filtered recipe list (memoised with `useMemo`) and passes `setFilters` as `onFilterChange` -- **RecipeCard** (MDX component) receives compiled children, splits by h2 into tab sections; custom `img` component in page.tsx rewrites `./` paths to `/recipes/[folderPath]/` +- **RecipesSidebar** owns local UI state (debounced search input, tag filter text, show-more toggles) and reports filter changes via `onFilterChange` +- **RecipesClient** owns filter state (synced to the URL), the filtered recipe list, and facet counts (all memoised with `useMemo`). Each facet's counts apply every filter except its own; options with zero results are hidden +- **ActiveFilters** renders removable chips for the active filters above the results grid +- **Recipe page** gets the parsed `RecipeContent` from `getRecipeContent` (cached), renders each section with **RecipeMarkdown** (server), and passes them as tabs to **RecipeTabs** (client). All panels are in the HTML; inactive ones are `hidden` +- **Recipe card**: `getRecipeCardImages` computes the layout once per recipe; the page and the `card/[image]` route both use it, so the tab always matches the generated files. Layout measures real block heights with satori's `onNodeDetected`, so fitting is exact, not estimated ## Known Constraints -- `folderPath` in recipe metadata uses backslashes on Windows (from `path.join`) — always `.replace(/\\/g, '/')` before using in URLs -- Images in recipe MDX are wrapped in `

` by MDX compilation — use `` not `

` to avoid invalid HTML nesting (`

` is invalid) +- Recipe image URLs are `/recipes/[category]/[slug]/assets/...` (from frontmatter, not the folder on disk). Use `resolveRecipeAssetUrl` from `lib/recipe-urls.ts` — never build them from `folderPath`, which can differ from category/slug and uses backslashes on Windows +- Recipe detail and asset routes set `dynamicParams = false`; everything is prerendered and the runtime image doesn't ship `recipes/` +- Paragraphs containing only images are converted to `
` elements during parsing (caption from the image title), so there's no `

` nesting +- Card fonts are read from `node_modules/@fontsource/*` at build time; satori needs woff/ttf (not woff2) and doesn't support variable fonts - `lib/recipes.ts` uses Node.js `fs` — server-side only; never import in client components -- Build warning about `` vs `` in recipe pages is intentional — markdown images can't use Next.js Image component +- Build warnings about `` vs `` in RecipeMarkdown and RecipeCardPanel are intentional - Never add redundant ARIA roles on semantic elements (`
`, `