Import recipes from original repo.

This commit is contained in:
Jake Runyan 2026-02-10 02:14:42 -08:00
parent 4bbb8e4cfe
commit e182dd0244
16 changed files with 180 additions and 664 deletions

View File

@ -11,7 +11,9 @@
"Bash(dir:*)", "Bash(dir:*)",
"Bash(findstr:*)", "Bash(findstr:*)",
"Bash(timeout 10 npm run dev:*)", "Bash(timeout 10 npm run dev:*)",
"Bash(grep:*)" "Bash(grep:*)",
"Read(//c/Users/runya/Documents/repositories/recipes/recipes/docs/**)",
"Bash(node scripts/import-recipes.js:*)"
] ]
} }
} }

View File

@ -1,3 +1,4 @@
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
export default function Home() { export default function Home() {
@ -26,27 +27,65 @@ export default function Home() {
</div> </div>
</section> </section>
<section className="grid md:grid-cols-3 gap-8 pt-12"> <section className="space-y-24 pt-16">
<div className="text-center space-y-3"> <div className="flex flex-col md:flex-row items-center gap-12">
<div className="text-4xl">🍳</div> <div className="w-full md:w-3/5 relative aspect-[3/2] rounded-2xl overflow-hidden shadow-lg">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">Easy Recipes</h3> <Image
<p className="text-gray-600 dark:text-gray-400"> src="/assets/want_to_cook.svg"
Step-by-step instructions for home cooks of all skill levels alt="Person deciding what to cook"
</p> fill
className="object-cover"
unoptimized
/>
</div>
<div className="w-full md:w-2/5 space-y-4">
<h2 className="text-3xl font-bold text-gray-900 dark:text-white">Easy Recipes</h2>
<p className="text-lg text-gray-600 dark:text-gray-400">
Step-by-step instructions for home cooks of all skill levels. Whether you&apos;re a beginner or an experienced cook, every recipe is written with clarity in mind.
</p>
<Link
href="/recipes"
className="inline-block px-5 py-2.5 bg-gray-900 dark:bg-white text-white dark:text-gray-900 rounded-lg hover:bg-gray-700 dark:hover:bg-gray-200 transition-colors font-medium"
>
Browse Recipes
</Link>
</div>
</div> </div>
<div className="text-center space-y-3">
<div className="text-4xl">📸</div> <div className="flex flex-col md:flex-row-reverse items-center gap-12">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">Beautiful Photos</h3> <div className="w-full md:w-3/5 relative aspect-[3/2] rounded-2xl overflow-hidden shadow-lg">
<p className="text-gray-600 dark:text-gray-400"> <Image
High-quality images to inspire your cooking journey src="/assets/online_cookbook.svg"
</p> alt="Online cookbook illustration"
fill
className="object-cover"
unoptimized
/>
</div>
<div className="w-full md:w-2/5 space-y-4">
<h2 className="text-3xl font-bold text-gray-900 dark:text-white">Beautiful Photos</h2>
<p className="text-lg text-gray-600 dark:text-gray-400">
High-quality images accompany every recipe to inspire your cooking journey. See exactly what you&apos;re making before you start.
</p>
</div>
</div> </div>
<div className="text-center space-y-3">
<div className="text-4xl">🏷</div> <div className="flex flex-col md:flex-row items-center gap-12">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">Organized</h3> <div className="w-full md:w-3/5 relative aspect-[3/2] rounded-2xl overflow-hidden shadow-lg">
<p className="text-gray-600 dark:text-gray-400"> <Image
Find recipes by tags, ingredients, and dietary preferences src="/assets/recipe_sites_suck.svg"
</p> alt="Organized recipe collection"
fill
className="object-cover"
unoptimized
/>
</div>
<div className="w-full md:w-2/5 space-y-4">
<h2 className="text-3xl font-bold text-gray-900 dark:text-white">No Nonsense</h2>
<p className="text-lg text-gray-600 dark:text-gray-400">
Just recipes. Find what you need quickly by browsing categories, filtering by tags, or searching by ingredient.
</p>
</div>
</div> </div>
</section> </section>
</div> </div>

View File

@ -1,3 +1,4 @@
import { Suspense } from 'react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { getAllRecipes, getAllCategories, getAllTags } from '@/lib/recipes'; import { getAllRecipes, getAllCategories, getAllTags } from '@/lib/recipes';
import RecipesClient from '@/components/RecipesClient'; import RecipesClient from '@/components/RecipesClient';
@ -23,7 +24,13 @@ export default function RecipesPage() {
</p> </p>
</div> </div>
<RecipesClient recipes={recipes} categories={categories} tags={tags} /> <Suspense fallback={
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
Loading recipes...
</div>
}>
<RecipesClient recipes={recipes} categories={categories} tags={tags} />
</Suspense>
</div> </div>
); );
} }

View File

@ -1,12 +1,14 @@
'use client'; 'use client';
import { useState, useCallback, ReactNode } from 'react'; import { useState, useCallback, ReactNode } from 'react';
import RecipesSidebar, { type FilterState } from './RecipesSidebar'; import RecipesSidebar from './RecipesSidebar';
import type { FilterState } from '@/lib/types';
interface RecipeLayoutProps { interface RecipeLayoutProps {
children: ReactNode; children: ReactNode;
categories: string[]; categories: string[];
tags: string[]; tags: string[];
filters?: FilterState;
onFilterChange?: (filters: FilterState) => void; onFilterChange?: (filters: FilterState) => void;
showFilters?: boolean; showFilters?: boolean;
} }
@ -15,14 +17,15 @@ export default function RecipeLayout({
children, children,
categories, categories,
tags, tags,
filters,
onFilterChange, onFilterChange,
showFilters = true showFilters = true
}: RecipeLayoutProps) { }: RecipeLayoutProps) {
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
const handleFilterChange = useCallback((filters: FilterState) => { const handleFilterChange = useCallback((newFilters: FilterState) => {
if (onFilterChange) { if (onFilterChange) {
onFilterChange(filters); onFilterChange(newFilters);
} }
}, [onFilterChange]); }, [onFilterChange]);
@ -76,10 +79,11 @@ export default function RecipeLayout({
</button> </button>
{/* Sidebar content */} {/* Sidebar content */}
{showFilters && ( {showFilters && filters && (
<RecipesSidebar <RecipesSidebar
categories={categories} categories={categories}
tags={tags} tags={tags}
filters={filters}
onFilterChange={handleFilterChange} onFilterChange={handleFilterChange}
/> />
)} )}

View File

@ -28,7 +28,12 @@ export default function RecipePageClient({ recipe, sections }: RecipePageClientP
<header className="mb-8 space-y-4"> <header className="mb-8 space-y-4">
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400"> <div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<span className="capitalize">{recipe.category}</span> <Link
href={`/recipes?category=${encodeURIComponent(recipe.category)}`}
className="capitalize hover:text-gray-900 dark:hover:text-white transition-colors"
>
{recipe.category}
</Link>
<span></span> <span></span>
<time dateTime={recipe.date}> <time dateTime={recipe.date}>
Published {new Date(recipe.date).toLocaleDateString('en-US', { Published {new Date(recipe.date).toLocaleDateString('en-US', {
@ -84,13 +89,14 @@ export default function RecipePageClient({ recipe, sections }: RecipePageClientP
<div className="flex flex-wrap gap-2" role="list" aria-label="Recipe tags"> <div className="flex flex-wrap gap-2" role="list" aria-label="Recipe tags">
{recipe.tags.map((tag) => ( {recipe.tags.map((tag) => (
<span <Link
key={tag} key={tag}
href={`/recipes?tags=${encodeURIComponent(tag)}`}
role="listitem" role="listitem"
className="px-3 py-1 text-sm bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-full" className="px-3 py-1 text-sm bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
> >
{tag} {tag}
</span> </Link>
))} ))}
</div> </div>
</header> </header>

View File

@ -127,7 +127,6 @@ export default function RecipeTabs({ sections, folderPath }: RecipeTabsProps) {
{children} {children}
</h4> </h4>
), ),
// Don't override <p> - let ReactMarkdown handle it to avoid hydration errors
code: ({ inline, children, ...props }: any) => { code: ({ inline, children, ...props }: any) => {
if (inline) { if (inline) {
return ( return (

View File

@ -1,10 +1,11 @@
'use client'; 'use client';
import { useState, useMemo } from 'react'; import { useState, useMemo, useEffect, useCallback, useRef } from 'react';
import { useSearchParams, useRouter, usePathname } from 'next/navigation';
import RecipeLayout from './RecipeLayout'; import RecipeLayout from './RecipeLayout';
import { FilterState } from './RecipesSidebar';
import RecipeCard from './RecipeCard'; import RecipeCard from './RecipeCard';
import type { Recipe } from '@/lib/recipes'; import type { Recipe } from '@/lib/recipes';
import type { FilterState } from '@/lib/types';
interface RecipesClientProps { interface RecipesClientProps {
recipes: Recipe[]; recipes: Recipe[];
@ -12,12 +13,52 @@ interface RecipesClientProps {
tags: string[]; tags: string[];
} }
function parseFiltersFromParams(searchParams: URLSearchParams): FilterState {
return {
search: searchParams.get('search') || '',
category: searchParams.get('category') || '',
selectedTags: searchParams.get('tags')
? [...new Set(searchParams.get('tags')!.split(',').filter(Boolean))]
: [],
};
}
function buildQueryString(filters: FilterState): string {
const params = new URLSearchParams();
if (filters.search) params.set('search', filters.search);
if (filters.category) params.set('category', filters.category);
if (filters.selectedTags.length > 0) params.set('tags', filters.selectedTags.join(','));
const qs = params.toString();
return qs ? `?${qs}` : '';
}
export default function RecipesClient({ recipes, categories, tags }: RecipesClientProps) { export default function RecipesClient({ recipes, categories, tags }: RecipesClientProps) {
const [filters, setFilters] = useState<FilterState>({ const searchParams = useSearchParams();
search: '', const router = useRouter();
category: '', const pathname = usePathname();
selectedTags: [],
}); const [filters, setFilters] = useState<FilterState>(() =>
parseFiltersFromParams(searchParams)
);
// Track internal updates to avoid reacting to our own URL changes
const isInternalUpdate = useRef(false);
// Sync URL → state on browser back/forward
useEffect(() => {
if (isInternalUpdate.current) {
isInternalUpdate.current = false;
return;
}
setFilters(parseFiltersFromParams(searchParams));
}, [searchParams]);
// Update filters and sync to URL
const updateFilters = useCallback((newFilters: FilterState) => {
isInternalUpdate.current = true;
setFilters(newFilters);
router.replace(`${pathname}${buildQueryString(newFilters)}`, { scroll: false });
}, [router, pathname]);
const filteredRecipes = useMemo(() => { const filteredRecipes = useMemo(() => {
return recipes.filter((recipe) => { return recipes.filter((recipe) => {
@ -47,7 +88,8 @@ export default function RecipesClient({ recipes, categories, tags }: RecipesClie
<RecipeLayout <RecipeLayout
categories={categories} categories={categories}
tags={tags} tags={tags}
onFilterChange={setFilters} filters={filters}
onFilterChange={updateFilters}
showFilters={true} showFilters={true}
> >
<div className="mb-4 text-sm text-gray-600 dark:text-gray-400"> <div className="mb-4 text-sm text-gray-600 dark:text-gray-400">

View File

@ -3,49 +3,54 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import SelectedTags from './SelectedTags'; import SelectedTags from './SelectedTags';
import TagSelector from './TagSelector'; import TagSelector from './TagSelector';
import type { FilterState } from '@/lib/types';
interface RecipesSidebarProps { interface RecipesSidebarProps {
categories: string[]; categories: string[];
tags: string[]; tags: string[];
filters: FilterState;
onFilterChange: (filters: FilterState) => void; onFilterChange: (filters: FilterState) => void;
} }
export interface FilterState { export default function RecipesSidebar({ categories, tags, filters, onFilterChange }: RecipesSidebarProps) {
search: string; const [searchInput, setSearchInput] = useState(filters.search);
category: string;
selectedTags: string[];
}
export default function RecipesSidebar({ categories, tags, onFilterChange }: RecipesSidebarProps) {
const [search, setSearch] = useState('');
const [category, setCategory] = useState('');
const [selectedTags, setSelectedTags] = useState<string[]>([]);
// Sync external search changes (e.g. "Clear All" or URL-driven) to local input
useEffect(() => { useEffect(() => {
onFilterChange({ search, category, selectedTags }); setSearchInput(filters.search);
}, [search, category, selectedTags, onFilterChange]); }, [filters.search]);
// Debounce local search input → parent
useEffect(() => {
const timer = setTimeout(() => {
if (searchInput !== filters.search) {
onFilterChange({ ...filters, search: searchInput });
}
}, 300);
return () => clearTimeout(timer);
}, [searchInput, filters, onFilterChange]);
const handleTagToggle = (tag: string) => { const handleTagToggle = (tag: string) => {
setSelectedTags((prev) => const selectedTags = filters.selectedTags.includes(tag)
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag] ? filters.selectedTags.filter((t) => t !== tag)
); : [...filters.selectedTags, tag];
onFilterChange({ ...filters, selectedTags });
}; };
const handleRemoveTag = (tag: string) => { const handleRemoveTag = (tag: string) => {
setSelectedTags((prev) => prev.filter((t) => t !== tag)); onFilterChange({ ...filters, selectedTags: filters.selectedTags.filter((t) => t !== tag) });
}; };
const handleClearTags = () => { const handleClearTags = () => {
setSelectedTags([]); onFilterChange({ ...filters, selectedTags: [] });
}; };
const handleClearFilters = () => { const handleClearFilters = () => {
setSearch(''); setSearchInput('');
setCategory(''); onFilterChange({ search: '', category: '', selectedTags: [] });
setSelectedTags([]);
}; };
const hasActiveFilters = search || category || selectedTags.length > 0; const hasActiveFilters = searchInput || filters.category || filters.selectedTags.length > 0;
return ( return (
<section aria-label="Recipe filters"> <section aria-label="Recipe filters">
@ -57,8 +62,8 @@ export default function RecipesSidebar({ categories, tags, onFilterChange }: Rec
<input <input
type="text" type="text"
id="search" id="search"
value={search} value={searchInput}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearchInput(e.target.value)}
placeholder="Search recipes..." placeholder="Search recipes..."
className="w-full px-2 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-2 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
/> />
@ -70,8 +75,8 @@ export default function RecipesSidebar({ categories, tags, onFilterChange }: Rec
</label> </label>
<select <select
id="category" id="category"
value={category} value={filters.category}
onChange={(e) => setCategory(e.target.value)} onChange={(e) => onFilterChange({ ...filters, category: e.target.value })}
className="w-full px-2 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-2 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
> >
<option value="">All Categories</option> <option value="">All Categories</option>
@ -89,14 +94,14 @@ export default function RecipesSidebar({ categories, tags, onFilterChange }: Rec
</label> </label>
<SelectedTags <SelectedTags
tags={selectedTags} tags={filters.selectedTags}
onRemove={handleRemoveTag} onRemove={handleRemoveTag}
onClear={handleClearTags} onClear={handleClearTags}
/> />
<TagSelector <TagSelector
availableTags={tags} availableTags={tags}
selectedTags={selectedTags} selectedTags={filters.selectedTags}
onToggleTag={handleTagToggle} onToggleTag={handleTagToggle}
/> />
</div> </div>

View File

@ -48,7 +48,7 @@ export default function TagSelector({ availableTags, selectedTags, onToggleTag }
{isOpen && ( {isOpen && (
<div <div
id="tag-selector-dropdown" id="tag-selector-dropdown"
className="absolute top-full left-0 right-0 mt-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg z-50 max-h-80 flex flex-col" className="mt-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg max-h-48 flex flex-col"
role="dialog" role="dialog"
aria-label="Tag selection" aria-label="Tag selection"
> >
@ -62,7 +62,6 @@ export default function TagSelector({ availableTags, selectedTags, onToggleTag }
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search tags..." placeholder="Search tags..."
className="w-full px-2 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-2 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
autoFocus
aria-describedby="tag-search-description" aria-describedby="tag-search-description"
/> />
<span id="tag-search-description" className="sr-only"> <span id="tag-search-description" className="sr-only">

View File

@ -17,8 +17,8 @@ public/recipes/
Each recipe follows this structure: Each recipe follows this structure:
``` ```
public/recipes/category/ public/recipes/category/
└── YYYY.MM.DD-recipe-slug/ └── recipe-slug/
├── YYYY.MM.DD-recipe-slug.mdx ├── recipe-slug.mdx
└── assets/ └── assets/
├── hero.jpg ├── hero.jpg
├── step1.jpg ├── step1.jpg
@ -64,21 +64,21 @@ displayPhoto: "./assets/hero.jpg"
- `slug` - URL-friendly identifier - `slug` - URL-friendly identifier
- `date` - Publication date (YYYY-MM-DD) - `date` - Publication date (YYYY-MM-DD)
- `lastUpdated` - Last modification date (YYYY-MM-DD) - `lastUpdated` - Last modification date (YYYY-MM-DD)
- `category` - Main category ID (references `../taxonomy.json`) - `category` - Main category (free-form string, e.g. "mains", "soups")
- `tags` - Array of tag IDs (references `../taxonomy.json`) - `tags` - Array of tags (free-form strings, e.g. ["italian", "chicken"])
- `dietary` - Array of dietary tag IDs (references `../taxonomy.json`) - `dietary` - Array of dietary tags (free-form strings, e.g. ["gluten-free"])
- `cookTime` - Active cooking time in minutes - `cookTime` - Active cooking time in minutes
- `prepTime` - Preparation time in minutes - `prepTime` - Preparation time in minutes
- `totalTime` - Total time in minutes - `totalTime` - Total time in minutes
- `difficulty` - Difficulty ID: easy, medium, or hard (references `../taxonomy.json`) - `difficulty` - Difficulty level: easy, medium, or hard
- `servings` - Number of servings - `servings` - Number of servings
- `author` - Author ID (references `../authors.json`) - `author` - Author ID (references `public/authors.json`)
- `description` - Brief description for SEO and cards - `description` - Brief description for SEO and cards
- `featured` - Boolean for homepage featuring - `featured` - Boolean for homepage featuring
- `display` - Boolean to control visibility (set to false to hide recipe) - `display` - Boolean to control visibility (set to false to hide recipe)
- `displayPhoto` - Relative path to display photo (e.g., "./assets/hero.jpg") - `displayPhoto` - Relative path to display photo (e.g., "./assets/hero.jpg")
**Note:** Use IDs from the reference files (`data/authors.json` and `data/taxonomy.json`) to ensure consistency and enable validation. **Note:** Author IDs must match entries in `public/authors.json`. Categories, tags, dietary, and difficulty are free-form strings — there is no taxonomy registry file.
#### Content Sections #### Content Sections
@ -197,8 +197,8 @@ Choose from these categories:
## Adding New Recipes ## Adding New Recipes
1. Create recipe folder: `public/recipes/[category]/YYYY.MM.DD-recipe-name/` 1. Create recipe folder: `public/recipes/[category]/recipe-name/`
2. Create `YYYY.MM.DD-recipe-name.mdx` with frontmatter and content 2. Create `recipe-name.mdx` with frontmatter and content
3. Create `assets/` subfolder for images 3. Create `assets/` subfolder for images
4. Add images to the `assets/` folder 4. Add images to the `assets/` folder
5. Reference images in MDX using relative paths: `./assets/image.jpg` 5. Reference images in MDX using relative paths: `./assets/image.jpg`
@ -207,7 +207,6 @@ Choose from these categories:
## Best Practices ## Best Practices
- Use consistent date formatting (YYYY.MM.DD)
- Keep slugs URL-friendly (lowercase, hyphens) - Keep slugs URL-friendly (lowercase, hyphens)
- Optimize images before adding (compress, resize) - Optimize images before adding (compress, resize)
- Test recipes before publishing - Test recipes before publishing

View File

@ -1,101 +0,0 @@
---
title: "Classic Chicken Parmesan"
slug: "chicken-parmesan"
date: "2026-02-05"
lastUpdated: "2026-02-05"
category: "mains"
tags: ["italian", "chicken", "comfort-food", "family-friendly"]
dietary: ["gluten-free-option"]
cookTime: 45
prepTime: 20
totalTime: 65
difficulty: "medium"
servings: 4
author: "pws"
description: "A classic Italian-American dish featuring crispy breaded chicken topped with marinara sauce and melted cheese."
featured: true
display: true
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.
## Photos
![Finished chicken parmesan on a white plate](./assets/not-found.svg)
*Golden-brown chicken topped with bubbling cheese*
![Close-up of the crispy breading](./assets/not-found.svg)
*Perfectly crispy coating with melted mozzarella*
![Plated with pasta and garnish](./assets/not-found.svg)
*Served alongside spaghetti with fresh basil*
## Ingredients
### For the Chicken
- 4 boneless, skinless chicken breasts (about 6-8 oz each)
- 1 cup all-purpose flour
- 2 large eggs, beaten
- 2 cups Italian-style breadcrumbs
- 1 cup grated Parmesan cheese (divided)
- 1 teaspoon garlic powder
- 1 teaspoon dried oregano
- Salt and freshly ground black pepper to taste
- 1/2 cup olive oil (for frying)
### For the Sauce & Topping
- 2 cups marinara sauce (homemade or quality store-bought)
- 1 1/2 cups shredded mozzarella cheese
- 1/4 cup fresh basil leaves, torn
- Extra Parmesan for serving
## Instructions
### Prep the Chicken
1. Place **chicken breasts** between two sheets of **plastic wrap** and pound to an even 1/2-inch thickness using a meat mallet.
2. Season both sides generously with **salt** and **pepper**.
### Set Up Breading Station
3. Prepare three shallow dishes:
- Dish 1: **All-purpose flour**
- Dish 2: Beaten **eggs** with 1 tablespoon **water**
- Dish 3: **Breadcrumbs** mixed with 1/2 cup **Parmesan**, **garlic powder**, and **oregano**
### Bread and Fry
4. Dredge each breast in **flour** (shake off excess), dip in **egg** mixture, then press firmly into **breadcrumb** mixture, coating both sides thoroughly.
5. In a large skillet, heat **olive oil** over medium-high heat until shimmering.
6. Cook **chicken** for 4-5 minutes per side until golden brown and cooked through (internal temp 165°F). Work in batches if needed. Transfer to a paper towel-lined plate.
### Assemble and Bake
7. Preheat oven to 400°F (200°C) or turn on broiler.
8. Place fried **chicken** in a baking dish. Spoon **marinara sauce** over each piece, then top with **mozzarella** and remaining **Parmesan**.
9. Bake for 10-12 minutes (or broil for 3-4 minutes) until cheese is melted and bubbly.
10. Top with fresh torn **basil** and serve immediately with **pasta** or a side salad.
## Notes
### Tips for Success
- **Even thickness**: Pounding the chicken ensures even cooking
- **Don't overcrowd**: Fry in batches to maintain oil temperature
- **Quality ingredients**: Use good marinara sauce for best flavor
- **Make ahead**: Bread chicken up to 4 hours ahead and refrigerate
- **Gluten-free option**: Substitute with gluten-free flour and breadcrumbs
### Storage
- **Refrigerate**: Store leftovers in an airtight container for up to 3 days
- **Reheat**: Warm in a 350°F oven for 15-20 minutes to maintain crispiness
- **Freeze**: Freeze breaded (uncooked) chicken for up to 2 months
### Variations
- **Baked version**: Skip frying and bake breaded chicken at 425°F for 20-25 minutes
- **Spicy**: Add red pepper flakes to the breadcrumb mixture
- **Extra crispy**: Use panko breadcrumbs instead of Italian breadcrumbs
## References
- Adapted from traditional Italian-American recipes
- Inspired by *The Silver Spoon* Italian cookbook
- Breading technique from classic French *cotoletta* method

View File

@ -1,148 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
id="svg2"
sodipodi:docname="_svgclean2.svg"
viewBox="0 0 260 310"
version="1.1"
inkscape:version="0.48.3.1 r9886"
>
<sodipodi:namedview
id="base"
bordercolor="#666666"
inkscape:pageshadow="2"
inkscape:window-y="19"
fit-margin-left="0"
pagecolor="#ffffff"
fit-margin-top="0"
inkscape:window-maximized="0"
inkscape:zoom="1.4142136"
inkscape:window-x="0"
inkscape:window-height="768"
showgrid="false"
borderopacity="1.0"
inkscape:current-layer="layer1"
inkscape:cx="-57.248774"
inkscape:cy="-575.52494"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1024"
inkscape:pageopacity="0.0"
inkscape:document-units="px"
>
<inkscape:grid
id="grid3769"
originy="-752.36218px"
enabled="true"
originx="-35.03125px"
visible="true"
snapvisiblegridlinesonly="true"
type="xygrid"
empspacing="4"
/>
</sodipodi:namedview
>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer"
transform="translate(-242.06 -371.19)"
>
<path
id="rect3757"
style="stroke:#636363;stroke-width:6;fill:#ffffff"
inkscape:connector-curvature="0"
d="m38 53v304h254v-226.22l-77.78-77.78h-176.22z"
transform="translate(207.06 321.19)"
/>
<g
id="g3915"
transform="translate(0 17.678)"
>
<rect
id="rect3759"
style="stroke-linejoin:round;stroke:#a6a6a6;stroke-linecap:round;stroke-width:6;fill:#ffffff"
height="150"
width="130"
y="451.19"
x="307.06"
/>
<path
id="path3761"
d="m343.53 497.65 57.065 57.065"
style="stroke-linejoin:round;stroke:#e00000;stroke-linecap:round;stroke-width:12;fill:none"
inkscape:connector-curvature="0"
/>
<path
id="path3763"
style="stroke-linejoin:round;stroke:#e00000;stroke-linecap:round;stroke-width:12;fill:none"
inkscape:connector-curvature="0"
d="m400.6 497.65-57.065 57.065"
/>
</g
>
<path
id="rect3911"
sodipodi:nodetypes="ccc"
style="stroke-linejoin:round;stroke:#636363;stroke-width:6;fill:none"
inkscape:connector-curvature="0"
d="m499.06 451.97h-77.782v-77.782"
/>
</g
>
<metadata
id="metadata13"
>
<rdf:RDF
>
<cc:Work
>
<dc:format
>image/svg+xml</dc:format
>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage"
/>
<cc:license
rdf:resource="http://creativecommons.org/licenses/publicdomain/"
/>
<dc:publisher
>
<cc:Agent
rdf:about="http://openclipart.org/"
>
<dc:title
>Openclipart</dc:title
>
</cc:Agent
>
</dc:publisher
>
</cc:Work
>
<cc:License
rdf:about="http://creativecommons.org/licenses/publicdomain/"
>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
/>
</cc:License
>
</rdf:RDF
>
</metadata
>
</svg
>

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -1,116 +0,0 @@
---
title: "Perfect Chocolate Chip Cookies"
slug: "chocolate-chip-cookies"
date: "2026-02-08"
lastUpdated: "2026-02-08"
category: "desserts"
tags: ["cookies", "chocolate", "baking", "dessert", "american"]
dietary: ["vegetarian"]
cookTime: 12
prepTime: 15
totalTime: 27
difficulty: "easy"
servings: 24
author: "pws"
description: "Classic chewy chocolate chip cookies with crispy edges and gooey centers, loaded with chocolate chips."
featured: true
display: true
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.
## Photos
![Stack of chocolate chip cookies](./assets/not-found.svg)
*A stack of golden-brown cookies showing the perfect texture*
![Cookie broken in half showing chocolate](./assets/not-found.svg)
*Gooey chocolate chips and perfect chewy texture*
![Cookies cooling on a wire rack](./assets/not-found.svg)
*Fresh from the oven with melted chocolate*
![Single cookie on plate with milk](./assets/not-found.svg)
*The perfect cookie and milk pairing*
## Ingredients
### Dry Ingredients
- 2 1/4 cups (280g) all-purpose flour
- 1 teaspoon baking soda
- 1 teaspoon fine sea salt
### Wet Ingredients
- 1 cup (2 sticks / 226g) unsalted butter, softened to room temperature
- 3/4 cup (150g) granulated sugar
- 3/4 cup (165g) packed light brown sugar
- 2 large eggs, room temperature
- 2 teaspoons pure vanilla extract
### Mix-ins
- 2 cups (340g) semi-sweet chocolate chips
- 1 cup (170g) milk chocolate chips (optional, for extra chocolate)
- Flaky sea salt for topping (optional)
## Instructions
### Prepare
1. Preheat oven to 375°F (190°C). Line two baking sheets with **parchment paper**.
2. In a medium bowl, whisk together **flour**, **baking soda**, and **salt**. Set aside.
### Make the Dough
3. In a large bowl or stand mixer, beat softened **butter** with both **sugars** on medium speed for 2-3 minutes until light and fluffy.
4. Beat in **eggs** one at a time, then add **vanilla extract**. Mix until well combined, scraping down the sides of the bowl.
5. With mixer on low speed, gradually add the **flour** mixture. Mix just until no flour streaks remain (don't overmix).
6. Use a spatula or wooden spoon to fold in **chocolate chips** until evenly distributed.
### Chill (Important!)
7. Cover bowl with **plastic wrap** and refrigerate for at least 30 minutes (or up to 72 hours for even better flavor). Cold dough = thicker cookies!
### Bake
8. Use a cookie scoop or tablespoon to form balls (about 2 tablespoons each). Place 2 inches apart on prepared baking sheets.
9. If using, sprinkle a tiny pinch of **flaky sea salt** on top of each dough ball.
10. Bake for 10-12 minutes until edges are golden brown but centers still look slightly underdone.
11. Let cookies cool on the baking sheet for 5 minutes (they'll continue to set), then transfer to a wire rack.
### Serve
12. Serve warm or at room temperature. Best enjoyed with cold **milk**!
## Notes
### Tips for Success
- **Room temperature ingredients**: Softened butter and eggs create the best texture
- **Don't skip chilling**: Cold dough prevents spreading and creates thicker cookies
- **Underbake slightly**: Cookies will look underdone but will set as they cool
- **Use parchment paper**: Prevents sticking and promotes even browning
- **Measure flour correctly**: Spoon and level flour, don't pack it
### Storage & Freezing
- **Room temperature**: Store in an airtight container for up to 5 days
- **Refresh**: Warm in a 300°F oven for 3-4 minutes to restore chewiness
- **Freeze dough**: Scoop dough balls and freeze for up to 3 months. Bake from frozen, adding 1-2 minutes
- **Freeze baked cookies**: Freeze baked cookies for up to 2 months
### Variations
- **Brown butter cookies**: Brown the butter for a nutty, caramel flavor
- **Thick and bakery-style**: Increase flour to 2 1/2 cups and chill overnight
- **Double chocolate**: Add 1/4 cup cocoa powder to dry ingredients
- **Mix-ins**: Try nuts, toffee bits, or different chocolate varieties
- **Giant cookies**: Use 1/4 cup dough per cookie, bake 14-16 minutes
### Science Behind the Recipe
- **Brown sugar**: Creates chewiness and moisture
- **Granulated sugar**: Creates crispy edges
- **Baking soda**: Promotes spreading and browning
- **Chilling**: Allows flour to hydrate and flavors to develop
- **Underbaking**: Keeps centers soft and gooey
## References
- Based on the classic Nestlé Toll House recipe
- Chilling technique from *The Food Lab* by J. Kenji López-Alt
- Brown butter variation inspired by *BraveTart* by Stella Parks
- Tested with feedback from family and friends over multiple batches

View File

@ -1,148 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
id="svg2"
sodipodi:docname="_svgclean2.svg"
viewBox="0 0 260 310"
version="1.1"
inkscape:version="0.48.3.1 r9886"
>
<sodipodi:namedview
id="base"
bordercolor="#666666"
inkscape:pageshadow="2"
inkscape:window-y="19"
fit-margin-left="0"
pagecolor="#ffffff"
fit-margin-top="0"
inkscape:window-maximized="0"
inkscape:zoom="1.4142136"
inkscape:window-x="0"
inkscape:window-height="768"
showgrid="false"
borderopacity="1.0"
inkscape:current-layer="layer1"
inkscape:cx="-57.248774"
inkscape:cy="-575.52494"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1024"
inkscape:pageopacity="0.0"
inkscape:document-units="px"
>
<inkscape:grid
id="grid3769"
originy="-752.36218px"
enabled="true"
originx="-35.03125px"
visible="true"
snapvisiblegridlinesonly="true"
type="xygrid"
empspacing="4"
/>
</sodipodi:namedview
>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer"
transform="translate(-242.06 -371.19)"
>
<path
id="rect3757"
style="stroke:#636363;stroke-width:6;fill:#ffffff"
inkscape:connector-curvature="0"
d="m38 53v304h254v-226.22l-77.78-77.78h-176.22z"
transform="translate(207.06 321.19)"
/>
<g
id="g3915"
transform="translate(0 17.678)"
>
<rect
id="rect3759"
style="stroke-linejoin:round;stroke:#a6a6a6;stroke-linecap:round;stroke-width:6;fill:#ffffff"
height="150"
width="130"
y="451.19"
x="307.06"
/>
<path
id="path3761"
d="m343.53 497.65 57.065 57.065"
style="stroke-linejoin:round;stroke:#e00000;stroke-linecap:round;stroke-width:12;fill:none"
inkscape:connector-curvature="0"
/>
<path
id="path3763"
style="stroke-linejoin:round;stroke:#e00000;stroke-linecap:round;stroke-width:12;fill:none"
inkscape:connector-curvature="0"
d="m400.6 497.65-57.065 57.065"
/>
</g
>
<path
id="rect3911"
sodipodi:nodetypes="ccc"
style="stroke-linejoin:round;stroke:#636363;stroke-width:6;fill:none"
inkscape:connector-curvature="0"
d="m499.06 451.97h-77.782v-77.782"
/>
</g
>
<metadata
id="metadata13"
>
<rdf:RDF
>
<cc:Work
>
<dc:format
>image/svg+xml</dc:format
>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage"
/>
<cc:license
rdf:resource="http://creativecommons.org/licenses/publicdomain/"
/>
<dc:publisher
>
<cc:Agent
rdf:about="http://openclipart.org/"
>
<dc:title
>Openclipart</dc:title
>
</cc:Agent
>
</dc:publisher
>
</cc:Work
>
<cc:License
rdf:about="http://creativecommons.org/licenses/publicdomain/"
>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
/>
</cc:License
>
</rdf:RDF
>
</metadata
>
</svg
>

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -1,73 +0,0 @@
---
title: "Instant Pot Chicken Noodle Soup"
slug: "chicken-noodle-soup"
date: "2026-02-08"
lastUpdated: "2026-02-08"
category: "soup"
tags: ["soup", "chicken", "comfort-food", "instant-pot", "one-pot", "sick-day", "meal-prep"]
dietary: []
cookTime: 20
prepTime: 10
totalTime: 30
difficulty: "easy"
servings: 8
author: "pws"
description: "A comforting chicken noodle soup perfect for rainy or sick days. Made easy in the Instant Pot but can be adapted for stovetop cooking."
featured: false
display: true
displayPhoto: "./assets/chicken-noodle-soup.png"
---
# Instant Pot Chicken Noodle Soup
A great soup for rainy or sick days. This comforting classic is made simple with the Instant Pot, though it can easily be adapted for stovetop cooking. The recipe makes enough to fuel a whole sick week!
## Photos
![Chicken Noodle Soup](./assets/chicken-noodle-soup.png)
*Comforting homemade chicken noodle soup*
## Ingredients
### Main Ingredients
- 2 tablespoons unsalted butter
- 1 large onion, chopped
- 2 medium carrots, chopped
- 2 stalks celery, chopped
- Kosher salt and fresh ground pepper to taste
- 1 teaspoon thyme
- 1 tablespoon parsley
- 1 tablespoon oregano
- 1 chicken bouillon cube or powder
- 4 cups chicken broth
- 2 pounds chicken (usually one Safeway or Costco chicken)
- 4 cups water
- 2 cups uncooked egg noodles
## Instructions
1. Turn your Instant Pot to the saute setting.
2. Add the **butter** and cook until melted. Add the **onion**, **carrots**, and **celery** and saute for 3 minutes until the **onion** softens and becomes translucent.
3. Season with **salt** and **pepper**, then add the **thyme**, **parsley**, **oregano**, and **chicken bouillon**. Stir to combine.
4. Pour in the **chicken broth**. Add the **chicken** pieces and another 4 cups of **water**.
5. Close the lid. Set the Instant Pot to the Soup setting and set the timer to 7 minutes on high pressure.
6. Once the Instant Pot cycle is complete, wait until the natural release cycle is complete before opening the instant pot.
7. Remove the **chicken** pieces from the soup and shred with two forks.
8. Add the **noodles** to the soup and set the Instant Pot to the saute setting again. Cook for another 6 minutes uncovered, or until the **noodles** are cooked.
9. Turn off the Instant Pot. Add the shredded **chicken** back to the pot, taste for seasoning and adjust as necessary. Garnish with additional **parsley** if preferred.
## Notes
### Meal Prep Tips
- **Make it last**: This recipe can make nearly a whole week of soup. Double it if you're feeling dangerous.
- **Noodle absorption**: If doing meal prep with lots of noodles or macaroni, they tend to soak up the broth. Double the water and broth amounts if you want it to keep in the fridge and retain liquid, versus becoming more of a soup-casserole.
### Seasoning Tips
- **Don't oversalt**: Watch the amount of salt in this recipe - it's pretty easy to over-salt if you're not careful, especially with the bouillon cube.
### Stovetop Adaptation
This recipe can be made on the stovetop by simmering the ingredients in a large pot for about 30-40 minutes until the chicken is cooked through, then following the same steps for shredding and adding noodles.
## References
- [Jo Cooks - Instant Pot Chicken Noodle Soup](https://www.jocooks.com/recipes/instant-pot-chicken-noodle-soup/)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB