update recipe rendering engine to use JSX components for recipe card and frontmatter for title bar

This commit is contained in:
Jake Runyan 2026-02-13 11:16:44 -08:00
parent 9f25ce4ae7
commit b95f5c1aeb
49 changed files with 497 additions and 394 deletions

View File

@ -8,7 +8,7 @@ A personal recipe website. Content-first, no-nonsense. The name of the third hom
- **Next.js 15** with App Router, TypeScript, Tailwind CSS - **Next.js 15** with App Router, TypeScript, Tailwind CSS
- **MDX** for recipe content with YAML frontmatter - **MDX** for recipe content with YAML frontmatter
- **ReactMarkdown + remark-gfm** for rendering recipe sections client-side - **next-mdx-remote/rsc + remark-gfm** for compiling MDX content server-side
- **Static site generation (SSG)** — all pages are prerendered 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 MDX files on disk
@ -30,13 +30,12 @@ components/
RecipesSidebar.tsx # Search + category + tag filters RecipesSidebar.tsx # Search + category + tag filters
SelectedTags.tsx # Active tag chips SelectedTags.tsx # Active tag chips
TagSelector.tsx # Tag dropdown picker TagSelector.tsx # Tag dropdown picker
RecipeCard.tsx # Recipe grid card RecipeGridCard.tsx # Recipe grid card for listing page
RecipePageClient.tsx # Recipe detail page wrapper RecipeCard.tsx # MDX component — splits h2 children into tab sections
RecipeTabs.tsx # Section tabs (Photos/Ingredients/Instructions/Notes/References) RecipePageClient.tsx # Recipe detail page wrapper (server component)
lib/ 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 public/recipes/
parseRecipe.ts # Splits MDX content into ## sections for tabs
public/ public/
assets/ # Site-level images (homepage SVGs) assets/ # Site-level images (homepage SVGs)

View File

@ -5,12 +5,12 @@
- **RecipeLayout** owns sidebar open/close state; passes `handleFilterChange` (memoised with `useCallback`) down to RecipesSidebar - **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` - **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` - **RecipesClient** owns filtered recipe list (memoised with `useMemo`) and passes `setFilters` as `onFilterChange`
- **RecipeTabs** renders content with ReactMarkdown; custom `img` component rewrites `./` paths to `/recipes/[folderPath]/` - **RecipeCard** (MDX component) receives compiled children, splits by h2 into tab sections; custom `img` component in page.tsx rewrites `./` paths to `/recipes/[folderPath]/`
## Known Constraints ## Known Constraints
- `folderPath` in recipe metadata uses backslashes on Windows (from `path.join`) — always `.replace(/\\/g, '/')` before using in URLs - `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 `<p>` by ReactMarkdown — use `<img>` not `<figure>` to avoid invalid HTML nesting (`<p><figure>` is invalid) - Images in recipe MDX are wrapped in `<p>` by MDX compilation — use `<img>` not `<figure>` to avoid invalid HTML nesting (`<p><figure>` is invalid)
- `lib/recipes.ts` uses Node.js `fs` — server-side only; never import in client components - `lib/recipes.ts` uses Node.js `fs` — server-side only; never import in client components
- Build warning about `<img>` vs `<Image />` in RecipeTabs is intentional — markdown images can't use Next.js Image component - Build warning about `<img>` vs `<Image />` in recipe pages is intentional — markdown images can't use Next.js Image component
- Never add redundant ARIA roles on semantic elements (`<main>`, `<aside>`, `<footer>` already carry implicit roles) - Never add redundant ARIA roles on semantic elements (`<main>`, `<aside>`, `<footer>` already carry implicit roles)

View File

@ -21,9 +21,17 @@ displayPhoto: "./assets/hero.jpg"
--- ---
``` ```
## Content Sections ## Content Structure
Content uses `## ` (h2) headings to define tabs rendered in the UI: Content after frontmatter is compiled as MDX using `next-mdx-remote/rsc`. The `<RecipeCard>` JSX component wraps recipe sections and renders them as tabs in the UI.
- Markdown **before** `<RecipeCard>` renders as intro prose above the recipe card
- Markdown **after** `</RecipeCard>` renders as outro prose below the recipe card
- **Important**: a blank line after `<RecipeCard>` is required for MDX to parse the content inside as markdown
### `<RecipeCard>` Sections
`## ` (H2) headings inside `<RecipeCard>` define tabs:
- `## Photos` — images with italic captions (`*caption text*`) - `## Photos` — images with italic captions (`*caption text*`)
- `## Ingredients` — bullet lists, optionally grouped with h3 subheadings - `## Ingredients` — bullet lists, optionally grouped with h3 subheadings
@ -31,6 +39,41 @@ Content uses `## ` (h2) headings to define tabs rendered in the UI:
- `## Notes` — tips, variations, storage (optional) - `## Notes` — tips, variations, storage (optional)
- `## References` — credits and sources (optional) - `## References` — credits and sources (optional)
### Example
```mdx
---
title: "Lentils"
description: "A neutral lentil dish."
...
---
This recipe uses brown lentils (whole Masoor Dal)...
<RecipeCard>
## Photos
![Hero](./assets/hero.jpg)
*Finished lentils*
## Ingredients
- 1 cup brown lentils
...
## Instructions
1. Rinse lentils...
...
## Notes
### Tips
- Try with different lentils!
## References
- Reference Recipe **[HERE](https://example.com)**
</RecipeCard>
```
## Image Paths ## Image Paths
Images use relative paths: `./assets/image.jpg` Images use relative paths: `./assets/image.jpg`

View File

@ -15,7 +15,9 @@
"Read(//c/Users/runya/Documents/repositories/recipes/recipes/docs/**)", "Read(//c/Users/runya/Documents/repositories/recipes/recipes/docs/**)",
"Bash(node scripts/import-recipes.js:*)", "Bash(node scripts/import-recipes.js:*)",
"Bash(powershell.exe -Command \"Test-Path ''c:\\\\Users\\\\runya\\\\Documents\\\\repositories\\\\cooking\\\\.next\\\\standalone\\\\server.js''\")", "Bash(powershell.exe -Command \"Test-Path ''c:\\\\Users\\\\runya\\\\Documents\\\\repositories\\\\cooking\\\\.next\\\\standalone\\\\server.js''\")",
"Bash(powershell.exe -Command:*)" "Bash(powershell.exe -Command:*)",
"Bash(node migrate-mdx.mjs:*)",
"Bash(node migrate-mdx.js:*)"
] ]
} }
} }

View File

@ -1,7 +1,9 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { compileMDX } from 'next-mdx-remote/rsc';
import remarkGfm from 'remark-gfm';
import { getRecipeByCategoryAndSlug, getAllRecipePaths } from '@/lib/recipes'; import { getRecipeByCategoryAndSlug, getAllRecipePaths } from '@/lib/recipes';
import { parseRecipeSections } from '@/lib/parseRecipe'; import RecipeCard from '@/components/RecipeCard';
import RecipePageClient from '@/components/RecipePageClient'; import RecipePageClient from '@/components/RecipePageClient';
interface RecipePageProps { interface RecipePageProps {
@ -49,12 +51,48 @@ export default async function RecipePage({ params }: RecipePageProps) {
notFound(); notFound();
} }
const sections = parseRecipeSections(recipe.content); const folderPath = recipe.folderPath;
const { content } = await compileMDX({
source: recipe.content,
components: {
RecipeCard,
img: ({ src, alt }: { src?: string; alt?: string }) => {
const srcString = typeof src === 'string' ? src : '';
const imageSrc = srcString.startsWith('./')
? `/recipes/${folderPath}/${srcString.replace('./', '')}`.replace(/\\/g, '/')
: srcString;
return ( return (
<RecipePageClient <img
recipe={recipe} src={imageSrc}
sections={sections} alt={alt || 'Recipe image'}
className="rounded-lg shadow-md w-full max-w-2xl mx-auto my-6 block"
loading="lazy"
/> />
); );
},
a: ({ href, children, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
<a
href={href}
className="text-blue-600 dark:text-blue-400 hover:underline"
target={href?.startsWith('http') ? '_blank' : undefined}
rel={href?.startsWith('http') ? 'noopener noreferrer' : undefined}
{...props}
>
{children}
</a>
),
},
options: {
mdxOptions: {
remarkPlugins: [remarkGfm],
},
},
});
return (
<RecipePageClient recipe={recipe}>
{content}
</RecipePageClient>
);
} }

View File

@ -1,74 +1,109 @@
import Link from 'next/link'; 'use client';
import Image from 'next/image';
import type { RecipeMetadata } from '@/lib/recipes';
interface RecipeCardProps { import React, { useState, type ReactNode } from 'react';
recipe: RecipeMetadata & { folderPath: string };
interface Section {
title: string;
children: ReactNode[];
} }
export default function RecipeCard({ recipe }: RecipeCardProps) { function extractText(node: ReactNode): string {
// Convert relative path to public URL if (typeof node === 'string') return node;
const imageSrc = recipe.displayPhoto && recipe.displayPhoto.startsWith('./') if (typeof node === 'number') return String(node);
? `/recipes/${recipe.folderPath}/${recipe.displayPhoto.replace('./', '')}`.replace(/\\/g, '/') if (Array.isArray(node)) return node.map(extractText).join('');
: recipe.displayPhoto || null; if (React.isValidElement(node)) {
const props = node.props as { children?: ReactNode };
if (props.children) return extractText(props.children);
}
return '';
}
export default function RecipeCard({ children }: { children: ReactNode }) {
const childArray = React.Children.toArray(children);
const sections: Section[] = [];
let currentSection: Section | null = null;
for (const child of childArray) {
if (React.isValidElement(child) && child.type === 'h2') {
const title = extractText((child.props as { children?: ReactNode }).children);
currentSection = { title, children: [] };
sections.push(currentSection);
} else if (currentSection) {
currentSection.children.push(child);
}
}
const tabOrder = ['Photos', 'Ingredients', 'Instructions', 'Notes', 'References'];
const orderedSections = tabOrder
.map(name => sections.find(s => s.title === name))
.filter((s): s is Section => s !== undefined);
const [activeTab, setActiveTab] = useState(orderedSections[0]?.title || '');
if (orderedSections.length === 0) return null;
const activeSection = orderedSections.find(s => s.title === activeTab);
return ( return (
<Link <div
href={`/recipes/${recipe.category}/${recipe.slug}`} className="not-prose bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 overflow-hidden my-8"
className="group block bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden hover:shadow-lg transition-shadow" role="region"
aria-label={`View recipe: ${recipe.title}`} aria-label="Recipe sections"
> >
<div className="aspect-video bg-gray-200 dark:bg-gray-700 relative overflow-hidden"> <div className="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
{imageSrc ? ( <nav className="flex overflow-x-auto" role="tablist" aria-label="Recipe section tabs">
<Image {orderedSections.map((section) => (
src={imageSrc} <button
alt={`${recipe.title} - Recipe photo`} key={section.title}
fill onClick={() => setActiveTab(section.title)}
className="object-cover" role="tab"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" aria-selected={activeTab === section.title}
unoptimized aria-controls={`tab-panel-${section.title.toLowerCase()}`}
/> id={`tab-${section.title.toLowerCase()}`}
) : ( className={`flex-shrink-0 px-6 py-4 text-sm font-medium whitespace-nowrap transition-colors border-b-2 ${
<div className="flex items-center justify-center h-full text-4xl text-gray-400 dark:text-gray-600"> activeTab === section.title
🍽 ? 'border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400 bg-white dark:bg-gray-800'
</div> : 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800'
)} }`}
</div>
<div className="p-4 space-y-2">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
{recipe.title}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
{recipe.description}
</p>
<div className="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-500" role="list" aria-label="Recipe details">
<span className="flex items-center gap-1" role="listitem">
<span aria-hidden="true">👥</span>
<span className="sr-only">Servings: </span>
{recipe.servings} servings
</span>
</div>
<div className="flex flex-wrap gap-1" role="list" aria-label="Recipe tags">
{recipe.tags.slice(0, 3).map((tag) => (
<span
key={tag}
role="listitem"
className="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded"
> >
{tag} {section.title}
</span> </button>
))} ))}
{recipe.tags.length > 3 && ( </nav>
<span role="listitem" className="px-2 py-1 text-xs text-gray-500 dark:text-gray-500"> </div>
+{recipe.tags.length - 3} more
</span> <div className="p-8">
{activeSection && (
<div
role="tabpanel"
id={`tab-panel-${activeSection.title.toLowerCase()}`}
aria-labelledby={`tab-${activeSection.title.toLowerCase()}`}
className={`prose prose-lg dark:prose-invert max-w-none
prose-headings:text-gray-900 dark:prose-headings:text-white
prose-p:text-gray-700 dark:prose-p:text-gray-300
prose-strong:text-gray-900 dark:prose-strong:text-white
prose-ol:list-decimal prose-ol:pl-6 prose-ol:space-y-2
prose-ul:list-disc prose-ul:pl-6 prose-ul:space-y-2
prose-li:text-gray-700 dark:prose-li:text-gray-300
prose-li:marker:text-gray-500 dark:prose-li:marker:text-gray-400
prose-code:text-gray-900 dark:prose-code:text-gray-100
prose-pre:bg-gray-100 dark:prose-pre:bg-gray-900
prose-table:border-collapse
prose-th:border prose-th:border-gray-300 dark:prose-th:border-gray-600
prose-td:border prose-td:border-gray-300 dark:prose-td:border-gray-600
${activeSection.title === 'Photos'
? 'prose-img:w-full prose-img:max-w-2xl prose-img:mx-auto [&_em]:block [&_em]:text-center [&_em]:text-sm [&_em]:text-gray-600 dark:[&_em]:text-gray-400 [&_em]:italic [&_em]:my-2'
: ''
}
`}
>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
{activeSection.title}
</h2>
{activeSection.children}
</div>
)} )}
</div> </div>
</div> </div>
</Link>
); );
} }

View File

@ -0,0 +1,74 @@
import Link from 'next/link';
import Image from 'next/image';
import type { RecipeMetadata } from '@/lib/recipes';
interface RecipeCardProps {
recipe: RecipeMetadata & { folderPath: string };
}
export default function RecipeGridCard({ recipe }: RecipeCardProps) {
// Convert relative path to public URL
const imageSrc = recipe.displayPhoto && recipe.displayPhoto.startsWith('./')
? `/recipes/${recipe.folderPath}/${recipe.displayPhoto.replace('./', '')}`.replace(/\\/g, '/')
: recipe.displayPhoto || null;
return (
<Link
href={`/recipes/${recipe.category}/${recipe.slug}`}
className="group block bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden hover:shadow-lg transition-shadow"
aria-label={`View recipe: ${recipe.title}`}
>
<div className="aspect-video bg-gray-200 dark:bg-gray-700 relative overflow-hidden">
{imageSrc ? (
<Image
src={imageSrc}
alt={`${recipe.title} - Recipe photo`}
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
unoptimized
/>
) : (
<div className="flex items-center justify-center h-full text-4xl text-gray-400 dark:text-gray-600">
🍽
</div>
)}
</div>
<div className="p-4 space-y-2">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
{recipe.title}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
{recipe.description}
</p>
<div className="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-500" role="list" aria-label="Recipe details">
<span className="flex items-center gap-1" role="listitem">
<span aria-hidden="true">👥</span>
<span className="sr-only">Servings: </span>
{recipe.servings} servings
</span>
</div>
<div className="flex flex-wrap gap-1" role="list" aria-label="Recipe tags">
{recipe.tags.slice(0, 3).map((tag) => (
<span
key={tag}
role="listitem"
className="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded"
>
{tag}
</span>
))}
{recipe.tags.length > 3 && (
<span role="listitem" className="px-2 py-1 text-xs text-gray-500 dark:text-gray-500">
+{recipe.tags.length - 3} more
</span>
)}
</div>
</div>
</Link>
);
}

View File

@ -1,16 +1,12 @@
'use client';
import Link from 'next/link'; import Link from 'next/link';
import RecipeTabs from './RecipeTabs';
import type { Recipe } from '@/lib/recipes'; import type { Recipe } from '@/lib/recipes';
import type { RecipeSection } from '@/lib/parseRecipe';
interface RecipePageClientProps { interface RecipePageClientProps {
recipe: Recipe; recipe: Recipe;
sections: RecipeSection[]; children: React.ReactNode;
} }
export default function RecipePageClient({ recipe, sections }: RecipePageClientProps) { export default function RecipePageClient({ recipe, children }: RecipePageClientProps) {
return ( return (
<div> <div>
<article className="max-w-4xl mx-auto"> <article className="max-w-4xl mx-auto">
@ -93,7 +89,10 @@ export default function RecipePageClient({ recipe, sections }: RecipePageClientP
</div> </div>
</header> </header>
<RecipeTabs sections={sections} folderPath={recipe.folderPath} /> {/* MDX content: intro prose + RecipeCard + outro prose */}
<div className="prose prose-lg dark:prose-invert max-w-none prose-p:text-gray-700 dark:prose-p:text-gray-300">
{children}
</div>
</article> </article>
</div> </div>
); );

View File

@ -1,156 +0,0 @@
'use client';
import { useState } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import type { RecipeSection } from '@/lib/parseRecipe';
interface RecipeTabsProps {
sections: RecipeSection[];
folderPath: string;
}
export default function RecipeTabs({ sections, folderPath }: RecipeTabsProps) {
const tabOrder = ['Photos', 'Ingredients', 'Instructions', 'Notes', 'References'];
const orderedSections = tabOrder
.map(tabName => sections.find(s => s.title === tabName))
.filter((s): s is RecipeSection => s !== undefined);
const [activeTab, setActiveTab] = useState(orderedSections[0]?.title || '');
if (orderedSections.length === 0) {
return null;
}
const activeSection = orderedSections.find(s => s.title === activeTab);
return (
<div
className="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 overflow-hidden"
role="region"
aria-label="Recipe sections"
>
<div className="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
<nav className="flex overflow-x-auto" role="tablist" aria-label="Recipe section tabs">
{orderedSections.map((section) => (
<button
key={section.title}
onClick={() => setActiveTab(section.title)}
role="tab"
aria-selected={activeTab === section.title}
aria-controls={`tab-panel-${section.title.toLowerCase()}`}
id={`tab-${section.title.toLowerCase()}`}
className={`flex-shrink-0 px-6 py-4 text-sm font-medium whitespace-nowrap transition-colors border-b-2 ${
activeTab === section.title
? 'border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400 bg-white dark:bg-gray-800'
: 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800'
}`}
>
{section.title}
</button>
))}
</nav>
</div>
<div className="p-8">
{activeSection && (
<div
role="tabpanel"
id={`tab-panel-${activeSection.title.toLowerCase()}`}
aria-labelledby={`tab-${activeSection.title.toLowerCase()}`}
className={`prose prose-lg dark:prose-invert max-w-none
prose-headings:text-gray-900 dark:prose-headings:text-white
prose-p:text-gray-700 dark:prose-p:text-gray-300
prose-strong:text-gray-900 dark:prose-strong:text-white
prose-ol:list-decimal prose-ol:pl-6 prose-ol:space-y-2
prose-ul:list-disc prose-ul:pl-6 prose-ul:space-y-2
prose-li:text-gray-700 dark:prose-li:text-gray-300
prose-li:marker:text-gray-500 dark:prose-li:marker:text-gray-400
prose-code:text-gray-900 dark:prose-code:text-gray-100
prose-pre:bg-gray-100 dark:prose-pre:bg-gray-900
prose-table:border-collapse
prose-th:border prose-th:border-gray-300 dark:prose-th:border-gray-600
prose-td:border prose-td:border-gray-300 dark:prose-td:border-gray-600
${activeSection.title === 'Photos' ? 'prose-img:w-full prose-img:max-w-2xl prose-img:mx-auto' : ''}
`}>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
{activeSection.title}
</h2>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
ol: ({ children, ...props }) => (
<ol className="list-decimal pl-6 space-y-2 my-4" {...props}>
{children}
</ol>
),
ul: ({ children, ...props }) => (
<ul className="list-disc pl-6 space-y-2 my-4" {...props}>
{children}
</ul>
),
li: ({ children, ...props }) => (
<li className="text-gray-700 dark:text-gray-300" {...props}>
{children}
</li>
),
img: ({ src, alt }) => {
// Convert relative paths to public URLs
const srcString = typeof src === 'string' ? src : '';
const imageSrc = srcString.startsWith('./')
? `/recipes/${folderPath}/${srcString.replace('./', '')}`.replace(/\\/g, '/')
: srcString;
// Use img directly to avoid nesting issues with paragraphs
return (
<img
src={imageSrc}
alt={alt || 'Recipe image'}
className="rounded-lg shadow-md w-full max-w-2xl mx-auto my-6 block"
loading="lazy"
/>
);
},
em: ({ children }) => {
if (activeSection.title === 'Photos') {
return (
<span className="block text-center text-sm text-gray-600 dark:text-gray-400 italic my-2">
{children}
</span>
);
}
return <em className="italic">{children}</em>;
},
h3: ({ children }) => (
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
{children}
</h3>
),
h4: ({ children }) => (
<h4 className="text-base font-semibold text-gray-900 dark:text-white mt-4 mb-2">
{children}
</h4>
),
code: ({ inline, children, ...props }: any) => {
if (inline) {
return (
<code className="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded text-sm" {...props}>
{children}
</code>
);
}
return (
<code className="block p-4 bg-gray-100 dark:bg-gray-900 rounded-lg overflow-x-auto" {...props}>
{children}
</code>
);
},
}}
>
{activeSection.content}
</ReactMarkdown>
</div>
)}
</div>
</div>
);
}

View File

@ -3,7 +3,7 @@
import { useState, useMemo, useEffect, useCallback, useRef } from 'react'; import { useState, useMemo, useEffect, useCallback, useRef } from 'react';
import { useSearchParams, useRouter, usePathname } from 'next/navigation'; import { useSearchParams, useRouter, usePathname } from 'next/navigation';
import RecipeLayout from './RecipeLayout'; import RecipeLayout from './RecipeLayout';
import RecipeCard from './RecipeCard'; import RecipeGridCard from './RecipeGridCard';
import type { Recipe } from '@/lib/recipes'; import type { Recipe } from '@/lib/recipes';
import type { FilterState } from '@/lib/types'; import type { FilterState } from '@/lib/types';
@ -101,7 +101,7 @@ export default function RecipesClient({ recipes, categories, tags }: RecipesClie
{filteredRecipes.length > 0 ? ( {filteredRecipes.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{filteredRecipes.map((recipe) => ( {filteredRecipes.map((recipe) => (
<RecipeCard key={recipe.slug} recipe={recipe} /> <RecipeGridCard key={recipe.slug} recipe={recipe} />
))} ))}
</div> </div>
) : ( ) : (

View File

@ -1,45 +0,0 @@
export interface RecipeSection {
title: string;
content: string;
}
export function parseRecipeSections(markdownContent: string): RecipeSection[] {
const lines = markdownContent.split('\n');
const sections: RecipeSection[] = [];
let currentSection: RecipeSection | null = null;
let inFrontmatter = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.trim() === '---') {
inFrontmatter = !inFrontmatter;
continue;
}
if (inFrontmatter) {
continue;
}
if (line.startsWith('## ')) {
if (currentSection) {
sections.push(currentSection);
}
currentSection = {
title: line.replace('## ', '').trim(),
content: '',
};
} else if (currentSection) {
currentSection.content += line + '\n';
}
}
if (currentSection) {
sections.push(currentSection);
}
return sections.map(section => ({
...section,
content: section.content.trim(),
}));
}

91
package-lock.json generated
View File

@ -14,6 +14,7 @@
"@types/mdx": "^2.0.13", "@types/mdx": "^2.0.13",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"next": "^15.1.6", "next": "^15.1.6",
"next-mdx-remote": "^6.0.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
@ -44,6 +45,29 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@babel/code-frame": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.28.5",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/core": { "node_modules/@emnapi/core": {
"version": "1.8.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz",
@ -4659,7 +4683,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
@ -6050,6 +6073,28 @@
} }
} }
}, },
"node_modules/next-mdx-remote": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/next-mdx-remote/-/next-mdx-remote-6.0.0.tgz",
"integrity": "sha512-cJEpEZlgD6xGjB4jL8BnI8FaYdN9BzZM4NwadPe1YQr7pqoWjg9EBCMv3nXBkuHqMRfv2y33SzUsuyNh9LFAQQ==",
"license": "MPL-2.0",
"dependencies": {
"@babel/code-frame": "^7.23.5",
"@mdx-js/mdx": "^3.0.1",
"@mdx-js/react": "^3.0.1",
"unist-util-remove": "^4.0.0",
"unist-util-visit": "^5.1.0",
"vfile": "^6.0.1",
"vfile-matter": "^5.0.0"
},
"engines": {
"node": ">=14",
"npm": ">=7"
},
"peerDependencies": {
"react": ">=16"
}
},
"node_modules/next/node_modules/postcss": { "node_modules/next/node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@ -8055,6 +8100,21 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/unist-util-remove": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-4.0.0.tgz",
"integrity": "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0",
"unist-util-visit-parents": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-stringify-position": { "node_modules/unist-util-stringify-position": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
@ -8194,6 +8254,20 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/vfile-matter": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/vfile-matter/-/vfile-matter-5.0.1.tgz",
"integrity": "sha512-o6roP82AiX0XfkyTHyRCMXgHfltUNlXSEqCIS80f+mbAyiQBE2fxtDVMtseyytGx75sihiJFo/zR6r/4LTs2Cw==",
"license": "MIT",
"dependencies": {
"vfile": "^6.0.0",
"yaml": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/vfile-message": { "node_modules/vfile-message": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
@ -8330,6 +8404,21 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/yaml": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/yocto-queue": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@ -15,6 +15,7 @@
"@types/mdx": "^2.0.13", "@types/mdx": "^2.0.13",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"next": "^15.1.6", "next": "^15.1.6",
"next-mdx-remote": "^6.0.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Apple Pie <RecipeCard>
Just like grandma used to make?
## Photos ## Photos
@ -66,3 +64,5 @@ Just like grandma used to make?
## References ## References
- Reference Recipe **[HERE](https://sallysbakingaddiction.com/apple-pie-recipe/)** - Reference Recipe **[HERE](https://sallysbakingaddiction.com/apple-pie-recipe/)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Chicken, Sausage, and Shrimp Gumbo <RecipeCard>
A dish with Cajun and Creole roots made with chicken, sausage, and shrimp.
## Photos ## Photos
@ -75,3 +73,5 @@ The roux should be a dark brown color and have a nutty aroma.
## References ## References
- Reference Recipe **[HERE](https://southernbite.com/chicken-sausage-and-shrimp-gumbo/)** - Reference Recipe **[HERE](https://southernbite.com/chicken-sausage-and-shrimp-gumbo/)**
</RecipeCard>

View File

@ -15,9 +15,9 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Broccoli Beef in Oyster Sauce lasdfkjlkejflwkejflwkejflk
Broccoli Beef! Just like the restaurant! <RecipeCard>
## Photos ## Photos
@ -64,3 +64,5 @@ Broccoli Beef! Just like the restaurant!
## References ## References
- Reference Recipe **[HERE](https://www.youtube.com/watch?v=fSO83XlKcPI)** - Reference Recipe **[HERE](https://www.youtube.com/watch?v=fSO83XlKcPI)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Egg Flower Soup <RecipeCard>
The classic egg drop soup. Tastes pretty similar to the store, and super easy to make!
## Photos ## Photos
@ -64,3 +62,5 @@ Also give the cooking wine addition a try (thanks, mom)
## References ## References
- Reference Recipe [HERE](https://thewoksoflife.com/egg-drop-soup/) - Reference Recipe [HERE](https://thewoksoflife.com/egg-drop-soup/)
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Tangy Runyan Eggroll <RecipeCard>
A delicious fusion-style eggroll that can be fried as an eggroll or adapted into wontons.
## Photos ## Photos
@ -74,3 +72,5 @@ These make excellent additions to soup!
## References ## References
- Thanks to my sister for providing the recipe! - Thanks to my sister for providing the recipe!
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Peach Simple Syrup <RecipeCard>
A wonderfully fruity, sweet, and versatile syrup perfect for adding a peachy twist to cocktails, mocktails, iced tea, and lemonade.
## Photos ## Photos
@ -54,3 +52,5 @@ This peach syrup is perfect for cocktails (like a Peach Mojito or Bellini), mock
## References ## References
- Reference Recipe **[HERE](https://www.alphafoodie.com/how-to-make-peach-simple-syrup/)** - Reference Recipe **[HERE](https://www.alphafoodie.com/how-to-make-peach-simple-syrup/)**
</RecipeCard>

View File

@ -15,10 +15,10 @@ display: false
displayPhoto: "./assets/not-found.svg" displayPhoto: "./assets/not-found.svg"
--- ---
# Classic Chicken Parmesan
A beloved Italian-American comfort food that combines crispy breaded chicken cutlets with rich marinara sauce and gooey melted cheese. Perfect for a family dinner or special occasion. A beloved Italian-American comfort food that combines crispy breaded chicken cutlets with rich marinara sauce and gooey melted cheese. Perfect for a family dinner or special occasion.
<RecipeCard>
## Photos ## Photos
![Finished chicken parmesan on a white plate](./assets/not-found.svg) ![Finished chicken parmesan on a white plate](./assets/not-found.svg)
@ -96,3 +96,5 @@ A beloved Italian-American comfort food that combines crispy breaded chicken cut
- Adapted from traditional Italian-American recipes - Adapted from traditional Italian-American recipes
- Inspired by *The Silver Spoon* Italian cookbook - Inspired by *The Silver Spoon* Italian cookbook
- Reference Recipe **[HERE](https://www.example.com)** - Reference Recipe **[HERE](https://www.example.com)**
</RecipeCard>

View File

@ -15,10 +15,10 @@ display: false
displayPhoto: "./assets/not-found.svg" displayPhoto: "./assets/not-found.svg"
--- ---
# Perfect Chocolate Chip Cookies
The ultimate chocolate chip cookie recipe that delivers crispy edges, chewy centers, and loads of melty chocolate chips in every bite. This recipe has been tested and perfected to create bakery-style cookies at home. The ultimate chocolate chip cookie recipe that delivers crispy edges, chewy centers, and loads of melty chocolate chips in every bite. This recipe has been tested and perfected to create bakery-style cookies at home.
<RecipeCard>
## Photos ## Photos
![Stack of chocolate chip cookies](./assets/not-found.svg) ![Stack of chocolate chip cookies](./assets/not-found.svg)
@ -109,3 +109,5 @@ The ultimate chocolate chip cookie recipe that delivers crispy edges, chewy cent
- Based on the classic Nestlé Toll House recipe - Based on the classic Nestlé Toll House recipe
- Reference Recipe **[HERE](https://www.example.com)** - Reference Recipe **[HERE](https://www.example.com)**
</RecipeCard>

View File

@ -1,6 +1,6 @@
--- ---
title: "Lentils" title: "Lentils"
slug: "brown-lentils" slug: "lentils"
date: "2026-02-10" date: "2026-02-10"
lastUpdated: "2026-02-10" lastUpdated: "2026-02-10"
category: "indian" category: "indian"
@ -9,15 +9,17 @@ cookTime: 60
prepTime: 30 prepTime: 30
servings: 10 servings: 10
author: "jake" author: "jake"
description: "A neutral, lentil dish that complements any other curry or gravy. This recipe uses brown lentils (Whole Masoor Dal), as well as their hulled (red dal) and split (split red dal) forms. The whole dal retain their consistency, and the hulled and split dal thicken the gravy. Both are recommended to be used. Some substitutes for the whole dal can be done, I have used whole mung beans (Green Moong Dal) as a replacement, and any split dal can be used instead of the split red dal." description: "A neutral lentil dish that can complement other curries or be a meal on its own."
featured: false featured: false
display: true display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Lentils This recipe uses brown lentils (whole Masoor Dal), as well as their hulled (red Dal) and split (split red Dal) forms.
A neutral, lentil dish that complements any other curry or gravy. This recipe uses brown lentils (Whole Masoor Dal), as well as their hulled (red dal) and split (split red dal) forms. The whole dal retain their consistency, and the hulled and split dal thicken the gravy. Both are recommended to be used. Some substitutes for the whole dal can be done, I have used whole mung beans (Green Moong Dal) as a replacement, and any split dal can be used instead of the split red dal. The whole dal retain their consistency, and the hulled and split dal thicken the gravy. Both are recommended to be used. Some substitutes for the whole dal can be done, I have used whole mung beans (Green Moong Dal) as a replacement, and any split dal can be used instead of the split red dal.
<RecipeCard>
## Photos ## Photos
@ -75,3 +77,5 @@ A neutral, lentil dish that complements any other curry or gravy. This recipe us
## References ## References
- Reference Recipe **[HERE](https://www.indianhealthyrecipes.com/brown-lentils/)** - Reference Recipe **[HERE](https://www.indianhealthyrecipes.com/brown-lentils/)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Chana Masala <RecipeCard>
A classic chickpea recipe with a rich, tomato-based gravy.
## Photos ## Photos
@ -84,3 +82,5 @@ A classic chickpea recipe with a rich, tomato-based gravy.
## References ## References
- Reference Recipe **[HERE](https://www.indianhealthyrecipes.com/chana-masala/)** - Reference Recipe **[HERE](https://www.indianhealthyrecipes.com/chana-masala/)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "./assets/chicken-tikka-masala.jpg" displayPhoto: "./assets/chicken-tikka-masala.jpg"
--- ---
# Chicken Tikka Masala <RecipeCard>
A classic chicken recipe with a rich, tomato-based gravy.
## Photos ## Photos
@ -76,3 +74,5 @@ A classic chicken recipe with a rich, tomato-based gravy.
## References ## References
- Reference Recipe **[HERE](https://cafedelites.com/chicken-tikka-masala/)** - Reference Recipe **[HERE](https://cafedelites.com/chicken-tikka-masala/)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "./assets/palak-paneer.jpg" displayPhoto: "./assets/palak-paneer.jpg"
--- ---
# Palak Paneer <RecipeCard>
A great vegetarian option with a lot of fiber.
## Photos ## Photos
@ -74,3 +72,5 @@ A great vegetarian option with a lot of fiber.
## References ## References
- Reference Recipe **[HERE](https://www.indianhealthyrecipes.com/palak-paneer-recipe-easy-paneer-recipes-step-by-step-pics/)** - Reference Recipe **[HERE](https://www.indianhealthyrecipes.com/palak-paneer-recipe-easy-paneer-recipes-step-by-step-pics/)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Classic Chicken Parmigiana (Parmesan) <RecipeCard>
Classic chicken parm can be made with panko breadcrumbs or homemade ones.
## Photos ## Photos
@ -75,3 +73,5 @@ Classic chicken parm can be made with panko breadcrumbs or homemade ones.
## References ## References
- Reference Recipe **[HERE](https://www.homemadeitaliancooking.com/chicken-parmesan/)** - Reference Recipe **[HERE](https://www.homemadeitaliancooking.com/chicken-parmesan/)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Vodka Pasta <RecipeCard>
A recipe similar to the vodka pasta at Fiorella Polk.
## Photos ## Photos
@ -64,3 +62,5 @@ A recipe similar to the vodka pasta at Fiorella Polk.
## References ## References
- Reference Recipe **[HERE](https://www.bonappetit.com/recipe/fusilli-alla-vodka-basil-parmesan)** - Reference Recipe **[HERE](https://www.bonappetit.com/recipe/fusilli-alla-vodka-basil-parmesan)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Pizza Dough <RecipeCard>
A standard pizza dough for pizza night.
## Photos ## Photos
@ -51,3 +49,5 @@ A standard pizza dough for pizza night.
## References ## References
- Reference Recipe **[HERE](https://www.crazyforcrust.com/the-ultimate-pizza-crust-recipe/)** - Reference Recipe **[HERE](https://www.crazyforcrust.com/the-ultimate-pizza-crust-recipe/)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Pizza Sauce <RecipeCard>
A standard pizza sauce for pizza night.
## Photos ## Photos
@ -53,3 +51,5 @@ A standard pizza sauce for pizza night.
## References ## References
- Reference Recipe **[HERE](https://joyfoodsunshine.com/easy-homemade-pizza-sauce-recipe/)** - Reference Recipe **[HERE](https://joyfoodsunshine.com/easy-homemade-pizza-sauce-recipe/)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Ajitsuke Tamago (Ramen Egg) <RecipeCard>
Great on any kind of ramen!
## Photos ## Photos
@ -55,3 +53,5 @@ Great on any kind of ramen!
## References ## References
- Reference Recipe **[HERE](https://www.aspicyperspective.com/easy-ramen-egg-recipe-ajitsuke-tamago/)** - Reference Recipe **[HERE](https://www.aspicyperspective.com/easy-ramen-egg-recipe-ajitsuke-tamago/)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Golden Curry Beef Stew <RecipeCard>
The classic way to make Golden Curry packets into a rich beef stew.
## Photos ## Photos
@ -58,3 +56,5 @@ Macaroni salad goes great alongside the rice and curry stew.
## References ## References
- Reference Recipe **[HERE](https://www.waiyeehong.com/food-ingredients/sauces-oils/curry-sauces-and-pastes/golden-curry-mild)** - Reference Recipe **[HERE](https://www.waiyeehong.com/food-ingredients/sauces-oils/curry-sauces-and-pastes/golden-curry-mild)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Golden Curry Chicken <RecipeCard>
A variant of the Golden Curry Beef recipe, this version uses chicken instead of beef, and coconut cream instead of beef stock for a Thai curry flavor.
## Photos ## Photos
@ -57,3 +55,5 @@ The coconut cream settles to the bottom of the can, make sure to re-mix it befor
## References ## References
- Reference Recipe **[HERE](https://www.recipetineats.com/golden-coconut-chicken-curry/)** - Reference Recipe **[HERE](https://www.recipetineats.com/golden-coconut-chicken-curry/)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Beef Bourguignon <RecipeCard>
A classic French dish made with beef braised in red wine, often served with mushrooms and pearl onions.
## Photos ## Photos
@ -66,3 +64,5 @@ Serve with crusty bread or over mashed potatoes for a hearty meal.
## References ## References
- Reference Recipe **[HERE](https://cafedelites.com/beef-bourguignon/)** - Reference Recipe **[HERE](https://cafedelites.com/beef-bourguignon/)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Garlic Herb Butter Roast Turkey <RecipeCard>
Happy Thanksgiving!
## Photos ## Photos
@ -66,3 +64,5 @@ Happy Thanksgiving!
## References ## References
- Reference Recipe **[HERE](https://cafedelites.com/roast-turkey/)** - Reference Recipe **[HERE](https://cafedelites.com/roast-turkey/)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Grilled Lemon Pepper Salmon <RecipeCard>
Super simple! This recipe entry is more or less just a formality.
## Photos ## Photos
@ -49,3 +47,5 @@ Super simple! This recipe entry is more or less just a formality.
## References ## References
- Idk talk to my mom... - Idk talk to my mom...
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Jamaican Jerk Chicken <RecipeCard>
Great chicken on a recommendation from a friend!
## Photos ## Photos
@ -55,3 +53,5 @@ Great chicken on a recommendation from a friend!
## References ## References
- Reference Recipe **[HERE](https://www.foodandwine.com/recipes/jamaican-jerk-chicken)** - Reference Recipe **[HERE](https://www.foodandwine.com/recipes/jamaican-jerk-chicken)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Instant Pot Ribs <RecipeCard>
Juicy and smoky slow-cooked ribs for your grill to bless you with.
## Photos ## Photos
@ -63,3 +61,5 @@ Juicy and smoky slow-cooked ribs for your grill to bless you with.
## References ## References
- Reference Recipe **[HERE](https://www.wellplated.com/instant-pot-ribs/)** - Reference Recipe **[HERE](https://www.wellplated.com/instant-pot-ribs/)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Turkey Gravy <RecipeCard>
A great gravy recipe for Thanksgiving dinner!
## Photos ## Photos
@ -46,3 +44,5 @@ A great gravy recipe for Thanksgiving dinner!
## References ## References
- Reference Recipe **[HERE](https://cafedelites.com/turkey-gravy/)** - Reference Recipe **[HERE](https://cafedelites.com/turkey-gravy/)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Birria <RecipeCard>
A rich and flavorful Mexican stew that can be enjoyed as a comforting dish or in delicious tacos.
## Photos ## Photos
@ -69,3 +67,5 @@ Straining after blending is important for a smooth texture in the consumé. Howe
## References ## References
- Reference Recipe **[HERE](https://www.isabeleats.com/authentic-birria/)** - Reference Recipe **[HERE](https://www.isabeleats.com/authentic-birria/)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Elote (Mexican Street Corn) <RecipeCard>
Classic Mexican street corn featuring grilled corn on the cob or in a cup.
## Photos ## Photos
@ -56,3 +54,5 @@ You can achieve a similar result by broiling the corn in your oven or carefully
## References ## References
- Reference Recipe **[HERE](https://www.seriouseats.com/mexican-street-corn-elotes-recipe)** - Reference Recipe **[HERE](https://www.seriouseats.com/mexican-street-corn-elotes-recipe)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Horchata <RecipeCard>
A cool, creamy, and spicy Agua Fresca thats perfect over ice.
## Photos ## Photos
@ -56,3 +54,5 @@ Heed the instructions and blend in batches. Water easily escapes from the blende
## References ## References
- Reference Recipe **[HERE](https://www.muydelish.com/traditional-mexican-horchata/)** - Reference Recipe **[HERE](https://www.muydelish.com/traditional-mexican-horchata/)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Mexican Rice <RecipeCard>
A classic, fluffy, and flavorful restaurant-style Mexican rice, simmered with tomato sauce and spices. The perfect side dish for any Mexican meal.
## Photos ## Photos
@ -56,3 +54,5 @@ This rice is the perfect accompaniment to tacos, burritos, enchiladas, or any gr
## References ## References
- Reference Recipe **[HERE](https://www.allrecipes.com/recipe/27072/mexican-rice-ii/)** - Reference Recipe **[HERE](https://www.allrecipes.com/recipe/27072/mexican-rice-ii/)**
</RecipeCard>

View File

@ -15,10 +15,10 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Pinto Beans
Pinto beans that are great in a burrito on on their own. Pinto beans that are great in a burrito on on their own.
<RecipeCard>
## Photos ## Photos
![Image Coming Soon](./assets/not-found.svg) ![Image Coming Soon](./assets/not-found.svg)
@ -47,3 +47,13 @@ Pinto beans that are great in a burrito on on their own.
## References ## References
- Reference Recipe **[HERE](https://www.rachelcooks.com/instant-pot-pinto-beans/)** - Reference Recipe **[HERE](https://www.rachelcooks.com/instant-pot-pinto-beans/)**
</RecipeCard>
Stuff after recipe RecipeCard
# Another H1
is this renered
## another h2
is this h2 rendered
![Image asdf Soon](./assets/not-found.svg)
*Image Comasdfing Soon*asdf

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Caesar Salad Dressing <RecipeCard>
A good Caesar salad dressing goes a long way, and making it yourself saves some money.
## Photos ## Photos
@ -52,3 +50,5 @@ A good Caesar salad dressing goes a long way, and making it yourself saves some
## References ## References
- Reference Recipe **[HERE](https://www.onceuponachef.com/recipes/caesar-salad-dressing.html)** - Reference Recipe **[HERE](https://www.onceuponachef.com/recipes/caesar-salad-dressing.html)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Cauliflower Mash <RecipeCard>
Silky mashed cauliflower that eats like mashed potatoes but healthier (?).
## Photos ## Photos
@ -56,3 +54,5 @@ Silky mashed cauliflower that eats like mashed potatoes but healthier (?).
## References ## References
- Reference Recipe **[HERE](https://www.seriouseats.com/cauliflower-puree-recipe)** - Reference Recipe **[HERE](https://www.seriouseats.com/cauliflower-puree-recipe)**
</RecipeCard>

View File

@ -15,10 +15,10 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Pickled Red Onions
These are a great topping for ramen, sandwiches, charcuterie, salads, or anything that could use a fermented kick! These are a great topping for ramen, sandwiches, charcuterie, salads, or anything that could use a fermented kick!
<RecipeCard>
## Photos ## Photos
![Image Coming Soon](./assets/not-found.svg) ![Image Coming Soon](./assets/not-found.svg)
@ -53,3 +53,5 @@ These are a great topping for ramen, sandwiches, charcuterie, salads, or anythin
## References ## References
- Reference Recipe **[HERE](https://www.gimmesomeoven.com/quick-pickled-red-onions/)** - Reference Recipe **[HERE](https://www.gimmesomeoven.com/quick-pickled-red-onions/)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Sous Vide Garlic Mashed Potatoes <RecipeCard>
Perfect mashed potatoes, with no cleanup!
## Photos ## Photos
@ -57,3 +55,5 @@ Perfect mashed potatoes, with no cleanup!
## References ## References
- Reference Recipe **[HERE](https://www.youtube.com/watch?app=desktop&v=WRrtw9NwcIU&t=72s)** - Reference Recipe **[HERE](https://www.youtube.com/watch?app=desktop&v=WRrtw9NwcIU&t=72s)**
</RecipeCard>

View File

@ -15,10 +15,10 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Instant Pot Chicken Noodle Soup
A comforting chicken noodle soup perfect for rainy or sick days. Made easy in the Instant Pot but can be adapted for stovetop cooking A comforting chicken noodle soup perfect for rainy or sick days. Made easy in the Instant Pot but can be adapted for stovetop cooking
<RecipeCard>
## Photos ## Photos
![Image Coming Soon](./assets/not-found.svg) ![Image Coming Soon](./assets/not-found.svg)
@ -68,3 +68,5 @@ This recipe can be made on the stovetop by simmering the ingredients in a large
## References ## References
- Reference Recipe **[HERE](https://www.jocooks.com/recipes/instant-pot-chicken-noodle-soup/)** - Reference Recipe **[HERE](https://www.jocooks.com/recipes/instant-pot-chicken-noodle-soup/)**
</RecipeCard>

View File

@ -15,9 +15,7 @@ display: true
displayPhoto: "" displayPhoto: ""
--- ---
# Pho (Vietnamese Noodle Soup) <RecipeCard>
A traditional Vietnamese noodle soup with aromatic broth, rice noodles, and tender beef.
## Photos ## Photos
@ -74,3 +72,5 @@ Pho is traditionally served piping hot with plenty of fresh herbs and condiments
## References ## References
- Reference Recipe **[HERE](https://thewoksoflife.com/pho-vietnamese-noodle-soup/)** - Reference Recipe **[HERE](https://thewoksoflife.com/pho-vietnamese-noodle-soup/)**
</RecipeCard>