mirror of
https://github.com/runyanjake/cooking.git
synced 2026-03-26 09:53:17 -07:00
update recipe rendering engine to use JSX components for recipe card and frontmatter for title bar
This commit is contained in:
parent
9f25ce4ae7
commit
b95f5c1aeb
@ -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
|
||||
- **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
|
||||
- **No database** — recipes are MDX files on disk
|
||||
|
||||
@ -30,13 +30,12 @@ components/
|
||||
RecipesSidebar.tsx # Search + category + tag filters
|
||||
SelectedTags.tsx # Active tag chips
|
||||
TagSelector.tsx # Tag dropdown picker
|
||||
RecipeCard.tsx # Recipe grid card
|
||||
RecipePageClient.tsx # Recipe detail page wrapper
|
||||
RecipeTabs.tsx # Section tabs (Photos/Ingredients/Instructions/Notes/References)
|
||||
RecipeGridCard.tsx # Recipe grid card for listing page
|
||||
RecipeCard.tsx # MDX component — splits h2 children into tab sections
|
||||
RecipePageClient.tsx # Recipe detail page wrapper (server component)
|
||||
|
||||
lib/
|
||||
recipes.ts # Recipe file loader with in-memory cache; reads from public/recipes/
|
||||
parseRecipe.ts # Splits MDX content into ## sections for tabs
|
||||
|
||||
public/
|
||||
assets/ # Site-level images (homepage SVGs)
|
||||
|
||||
@ -5,12 +5,12 @@
|
||||
- **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`
|
||||
- **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
|
||||
|
||||
- `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
|
||||
- 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)
|
||||
|
||||
@ -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*`)
|
||||
- `## 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)
|
||||
- `## References` — credits and sources (optional)
|
||||
|
||||
### Example
|
||||
|
||||
```mdx
|
||||
---
|
||||
title: "Lentils"
|
||||
description: "A neutral lentil dish."
|
||||
...
|
||||
---
|
||||
|
||||
This recipe uses brown lentils (whole Masoor Dal)...
|
||||
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||

|
||||
*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
|
||||
|
||||
Images use relative paths: `./assets/image.jpg`
|
||||
|
||||
@ -15,7 +15,9 @@
|
||||
"Read(//c/Users/runya/Documents/repositories/recipes/recipes/docs/**)",
|
||||
"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:*)"
|
||||
"Bash(powershell.exe -Command:*)",
|
||||
"Bash(node migrate-mdx.mjs:*)",
|
||||
"Bash(node migrate-mdx.js:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import type { Metadata } from 'next';
|
||||
import { compileMDX } from 'next-mdx-remote/rsc';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { getRecipeByCategoryAndSlug, getAllRecipePaths } from '@/lib/recipes';
|
||||
import { parseRecipeSections } from '@/lib/parseRecipe';
|
||||
import RecipeCard from '@/components/RecipeCard';
|
||||
import RecipePageClient from '@/components/RecipePageClient';
|
||||
|
||||
interface RecipePageProps {
|
||||
@ -49,12 +51,48 @@ export default async function RecipePage({ params }: RecipePageProps) {
|
||||
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 (
|
||||
<img
|
||||
src={imageSrc}
|
||||
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}
|
||||
sections={sections}
|
||||
/>
|
||||
<RecipePageClient recipe={recipe}>
|
||||
{content}
|
||||
</RecipePageClient>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,74 +1,109 @@
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import type { RecipeMetadata } from '@/lib/recipes';
|
||||
'use client';
|
||||
|
||||
interface RecipeCardProps {
|
||||
recipe: RecipeMetadata & { folderPath: string };
|
||||
import React, { useState, type ReactNode } from 'react';
|
||||
|
||||
interface Section {
|
||||
title: string;
|
||||
children: ReactNode[];
|
||||
}
|
||||
|
||||
export default function RecipeCard({ 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;
|
||||
function extractText(node: ReactNode): string {
|
||||
if (typeof node === 'string') return node;
|
||||
if (typeof node === 'number') return String(node);
|
||||
if (Array.isArray(node)) return node.map(extractText).join('');
|
||||
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 (
|
||||
<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="not-prose bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 overflow-hidden my-8"
|
||||
role="region"
|
||||
aria-label="Recipe sections"
|
||||
>
|
||||
<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 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 [&_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 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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
74
components/RecipeGridCard.tsx
Normal file
74
components/RecipeGridCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -1,16 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import RecipeTabs from './RecipeTabs';
|
||||
import type { Recipe } from '@/lib/recipes';
|
||||
import type { RecipeSection } from '@/lib/parseRecipe';
|
||||
|
||||
interface RecipePageClientProps {
|
||||
recipe: Recipe;
|
||||
sections: RecipeSection[];
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function RecipePageClient({ recipe, sections }: RecipePageClientProps) {
|
||||
export default function RecipePageClient({ recipe, children }: RecipePageClientProps) {
|
||||
return (
|
||||
<div>
|
||||
<article className="max-w-4xl mx-auto">
|
||||
@ -93,7 +89,10 @@ export default function RecipePageClient({ recipe, sections }: RecipePageClientP
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -3,7 +3,7 @@
|
||||
import { useState, useMemo, useEffect, useCallback, useRef } from 'react';
|
||||
import { useSearchParams, useRouter, usePathname } from 'next/navigation';
|
||||
import RecipeLayout from './RecipeLayout';
|
||||
import RecipeCard from './RecipeCard';
|
||||
import RecipeGridCard from './RecipeGridCard';
|
||||
import type { Recipe } from '@/lib/recipes';
|
||||
import type { FilterState } from '@/lib/types';
|
||||
|
||||
@ -101,7 +101,7 @@ export default function RecipesClient({ recipes, categories, tags }: RecipesClie
|
||||
{filteredRecipes.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{filteredRecipes.map((recipe) => (
|
||||
<RecipeCard key={recipe.slug} recipe={recipe} />
|
||||
<RecipeGridCard key={recipe.slug} recipe={recipe} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@ -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
91
package-lock.json
generated
@ -14,6 +14,7 @@
|
||||
"@types/mdx": "^2.0.13",
|
||||
"gray-matter": "^4.0.3",
|
||||
"next": "^15.1.6",
|
||||
"next-mdx-remote": "^6.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
@ -44,6 +45,29 @@
|
||||
"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": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz",
|
||||
@ -4659,7 +4683,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"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": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
@ -8055,6 +8100,21 @@
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
|
||||
@ -8330,6 +8404,21 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
"@types/mdx": "^2.0.13",
|
||||
"gray-matter": "^4.0.3",
|
||||
"next": "^15.1.6",
|
||||
"next-mdx-remote": "^6.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Apple Pie
|
||||
|
||||
Just like grandma used to make?
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -66,3 +64,5 @@ Just like grandma used to make?
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://sallysbakingaddiction.com/apple-pie-recipe/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Chicken, Sausage, and Shrimp Gumbo
|
||||
|
||||
A dish with Cajun and Creole roots made with chicken, sausage, and shrimp.
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -75,3 +73,5 @@ The roux should be a dark brown color and have a nutty aroma.
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://southernbite.com/chicken-sausage-and-shrimp-gumbo/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,9 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Broccoli Beef in Oyster Sauce
|
||||
lasdfkjlkejflwkejflwkejflk
|
||||
|
||||
Broccoli Beef! Just like the restaurant!
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -64,3 +64,5 @@ Broccoli Beef! Just like the restaurant!
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.youtube.com/watch?v=fSO83XlKcPI)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Egg Flower Soup
|
||||
|
||||
The classic egg drop soup. Tastes pretty similar to the store, and super easy to make!
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -64,3 +62,5 @@ Also give the cooking wine addition a try (thanks, mom)
|
||||
## References
|
||||
|
||||
- Reference Recipe [HERE](https://thewoksoflife.com/egg-drop-soup/)
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Tangy Runyan Eggroll
|
||||
|
||||
A delicious fusion-style eggroll that can be fried as an eggroll or adapted into wontons.
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -74,3 +72,5 @@ These make excellent additions to soup!
|
||||
## References
|
||||
|
||||
- Thanks to my sister for providing the recipe!
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Peach Simple Syrup
|
||||
|
||||
A wonderfully fruity, sweet, and versatile syrup perfect for adding a peachy twist to cocktails, mocktails, iced tea, and lemonade.
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -54,3 +52,5 @@ This peach syrup is perfect for cocktails (like a Peach Mojito or Bellini), mock
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.alphafoodie.com/how-to-make-peach-simple-syrup/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,10 +15,10 @@ display: false
|
||||
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.
|
||||
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||

|
||||
@ -96,3 +96,5 @@ A beloved Italian-American comfort food that combines crispy breaded chicken cut
|
||||
- Adapted from traditional Italian-American recipes
|
||||
- Inspired by *The Silver Spoon* Italian cookbook
|
||||
- Reference Recipe **[HERE](https://www.example.com)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,10 +15,10 @@ display: false
|
||||
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.
|
||||
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||

|
||||
@ -109,3 +109,5 @@ The ultimate chocolate chip cookie recipe that delivers crispy edges, chewy cent
|
||||
|
||||
- Based on the classic Nestlé Toll House recipe
|
||||
- Reference Recipe **[HERE](https://www.example.com)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Lentils"
|
||||
slug: "brown-lentils"
|
||||
slug: "lentils"
|
||||
date: "2026-02-10"
|
||||
lastUpdated: "2026-02-10"
|
||||
category: "indian"
|
||||
@ -9,15 +9,17 @@ cookTime: 60
|
||||
prepTime: 30
|
||||
servings: 10
|
||||
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
|
||||
display: true
|
||||
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
|
||||
|
||||
@ -75,3 +77,5 @@ A neutral, lentil dish that complements any other curry or gravy. This recipe us
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.indianhealthyrecipes.com/brown-lentils/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Chana Masala
|
||||
|
||||
A classic chickpea recipe with a rich, tomato-based gravy.
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -84,3 +82,5 @@ A classic chickpea recipe with a rich, tomato-based gravy.
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.indianhealthyrecipes.com/chana-masala/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: "./assets/chicken-tikka-masala.jpg"
|
||||
---
|
||||
|
||||
# Chicken Tikka Masala
|
||||
|
||||
A classic chicken recipe with a rich, tomato-based gravy.
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -76,3 +74,5 @@ A classic chicken recipe with a rich, tomato-based gravy.
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://cafedelites.com/chicken-tikka-masala/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: "./assets/palak-paneer.jpg"
|
||||
---
|
||||
|
||||
# Palak Paneer
|
||||
|
||||
A great vegetarian option with a lot of fiber.
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -74,3 +72,5 @@ A great vegetarian option with a lot of fiber.
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.indianhealthyrecipes.com/palak-paneer-recipe-easy-paneer-recipes-step-by-step-pics/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Classic Chicken Parmigiana (Parmesan)
|
||||
|
||||
Classic chicken parm can be made with panko breadcrumbs or homemade ones.
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -75,3 +73,5 @@ Classic chicken parm can be made with panko breadcrumbs or homemade ones.
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.homemadeitaliancooking.com/chicken-parmesan/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Vodka Pasta
|
||||
|
||||
A recipe similar to the vodka pasta at Fiorella Polk.
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -64,3 +62,5 @@ A recipe similar to the vodka pasta at Fiorella Polk.
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.bonappetit.com/recipe/fusilli-alla-vodka-basil-parmesan)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Pizza Dough
|
||||
|
||||
A standard pizza dough for pizza night.
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -51,3 +49,5 @@ A standard pizza dough for pizza night.
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.crazyforcrust.com/the-ultimate-pizza-crust-recipe/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Pizza Sauce
|
||||
|
||||
A standard pizza sauce for pizza night.
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -53,3 +51,5 @@ A standard pizza sauce for pizza night.
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://joyfoodsunshine.com/easy-homemade-pizza-sauce-recipe/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Ajitsuke Tamago (Ramen Egg)
|
||||
|
||||
Great on any kind of ramen!
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -55,3 +53,5 @@ Great on any kind of ramen!
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.aspicyperspective.com/easy-ramen-egg-recipe-ajitsuke-tamago/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Golden Curry Beef Stew
|
||||
|
||||
The classic way to make Golden Curry packets into a rich beef stew.
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -58,3 +56,5 @@ Macaroni salad goes great alongside the rice and curry stew.
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.waiyeehong.com/food-ingredients/sauces-oils/curry-sauces-and-pastes/golden-curry-mild)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Golden Curry Chicken
|
||||
|
||||
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.
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -57,3 +55,5 @@ The coconut cream settles to the bottom of the can, make sure to re-mix it befor
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.recipetineats.com/golden-coconut-chicken-curry/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Beef Bourguignon
|
||||
|
||||
A classic French dish made with beef braised in red wine, often served with mushrooms and pearl onions.
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -66,3 +64,5 @@ Serve with crusty bread or over mashed potatoes for a hearty meal.
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://cafedelites.com/beef-bourguignon/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Garlic Herb Butter Roast Turkey
|
||||
|
||||
Happy Thanksgiving!
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -66,3 +64,5 @@ Happy Thanksgiving!
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://cafedelites.com/roast-turkey/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Grilled Lemon Pepper Salmon
|
||||
|
||||
Super simple! This recipe entry is more or less just a formality.
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -49,3 +47,5 @@ Super simple! This recipe entry is more or less just a formality.
|
||||
## References
|
||||
|
||||
- Idk talk to my mom...
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Jamaican Jerk Chicken
|
||||
|
||||
Great chicken on a recommendation from a friend!
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -55,3 +53,5 @@ Great chicken on a recommendation from a friend!
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.foodandwine.com/recipes/jamaican-jerk-chicken)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Instant Pot Ribs
|
||||
|
||||
Juicy and smoky slow-cooked ribs for your grill to bless you with.
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -63,3 +61,5 @@ Juicy and smoky slow-cooked ribs for your grill to bless you with.
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.wellplated.com/instant-pot-ribs/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Turkey Gravy
|
||||
|
||||
A great gravy recipe for Thanksgiving dinner!
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -46,3 +44,5 @@ A great gravy recipe for Thanksgiving dinner!
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://cafedelites.com/turkey-gravy/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Birria
|
||||
|
||||
A rich and flavorful Mexican stew that can be enjoyed as a comforting dish or in delicious tacos.
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -69,3 +67,5 @@ Straining after blending is important for a smooth texture in the consumé. Howe
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.isabeleats.com/authentic-birria/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Elote (Mexican Street Corn)
|
||||
|
||||
Classic Mexican street corn featuring grilled corn on the cob or in a cup.
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -56,3 +54,5 @@ You can achieve a similar result by broiling the corn in your oven or carefully
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.seriouseats.com/mexican-street-corn-elotes-recipe)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Horchata
|
||||
|
||||
A cool, creamy, and spicy Agua Fresca that’s perfect over ice.
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -56,3 +54,5 @@ Heed the instructions and blend in batches. Water easily escapes from the blende
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.muydelish.com/traditional-mexican-horchata/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Mexican Rice
|
||||
|
||||
A classic, fluffy, and flavorful restaurant-style Mexican rice, simmered with tomato sauce and spices. The perfect side dish for any Mexican meal.
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -56,3 +54,5 @@ This rice is the perfect accompaniment to tacos, burritos, enchiladas, or any gr
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.allrecipes.com/recipe/27072/mexican-rice-ii/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,10 +15,10 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Pinto Beans
|
||||
|
||||
Pinto beans that are great in a burrito on on their own.
|
||||
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||

|
||||
@ -47,3 +47,13 @@ Pinto beans that are great in a burrito on on their own.
|
||||
## References
|
||||
|
||||
- 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 Comasdfing Soon*asdf
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Caesar Salad Dressing
|
||||
|
||||
A good Caesar salad dressing goes a long way, and making it yourself saves some money.
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -52,3 +50,5 @@ A good Caesar salad dressing goes a long way, and making it yourself saves some
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.onceuponachef.com/recipes/caesar-salad-dressing.html)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Cauliflower Mash
|
||||
|
||||
Silky mashed cauliflower that eats like mashed potatoes but healthier (?).
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -56,3 +54,5 @@ Silky mashed cauliflower that eats like mashed potatoes but healthier (?).
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.seriouseats.com/cauliflower-puree-recipe)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,10 +15,10 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Pickled Red Onions
|
||||
|
||||
These are a great topping for ramen, sandwiches, charcuterie, salads, or anything that could use a fermented kick!
|
||||
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||

|
||||
@ -53,3 +53,5 @@ These are a great topping for ramen, sandwiches, charcuterie, salads, or anythin
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.gimmesomeoven.com/quick-pickled-red-onions/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Sous Vide Garlic Mashed Potatoes
|
||||
|
||||
Perfect mashed potatoes, with no cleanup!
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -57,3 +55,5 @@ Perfect mashed potatoes, with no cleanup!
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.youtube.com/watch?app=desktop&v=WRrtw9NwcIU&t=72s)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,10 +15,10 @@ display: true
|
||||
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
|
||||
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||

|
||||
@ -68,3 +68,5 @@ This recipe can be made on the stovetop by simmering the ingredients in a large
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://www.jocooks.com/recipes/instant-pot-chicken-noodle-soup/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
@ -15,9 +15,7 @@ display: true
|
||||
displayPhoto: ""
|
||||
---
|
||||
|
||||
# Pho (Vietnamese Noodle Soup)
|
||||
|
||||
A traditional Vietnamese noodle soup with aromatic broth, rice noodles, and tender beef.
|
||||
<RecipeCard>
|
||||
|
||||
## Photos
|
||||
|
||||
@ -74,3 +72,5 @@ Pho is traditionally served piping hot with plenty of fresh herbs and condiments
|
||||
## References
|
||||
|
||||
- Reference Recipe **[HERE](https://thewoksoflife.com/pho-vietnamese-noodle-soup/)**
|
||||
|
||||
</RecipeCard>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user