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 (`
`, `