mirror of
https://github.com/runyanjake/cooking.git
synced 2026-03-26 01:43:17 -07:00
Formulate website
This commit is contained in:
parent
bc013860b0
commit
4133d2e342
@ -6,7 +6,12 @@
|
||||
"Bash(npm --version:*)",
|
||||
"Bash(where:*)",
|
||||
"Bash(npm install:*)",
|
||||
"Bash(npm run build:*)"
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(node scripts/copy-recipe-assets.js:*)",
|
||||
"Bash(dir:*)",
|
||||
"Bash(findstr:*)",
|
||||
"Bash(timeout 10 npm run dev:*)",
|
||||
"Bash(grep:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,4 +24,16 @@ body {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import type { Metadata } from 'next';
|
||||
import { getRecipeByCategoryAndSlug, getAllRecipePaths } from '@/lib/recipes';
|
||||
import { parseRecipeSections } from '@/lib/parseRecipe';
|
||||
import RecipePageClient from '@/components/RecipePageClient';
|
||||
|
||||
interface RecipePageProps {
|
||||
params: Promise<{
|
||||
@ -47,86 +49,12 @@ export default async function RecipePage({ params }: RecipePageProps) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const sections = parseRecipeSections(recipe.content);
|
||||
|
||||
return (
|
||||
<article className="max-w-4xl mx-auto">
|
||||
<header className="mb-8 space-y-4">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<span className="capitalize">{recipe.category}</span>
|
||||
<span>•</span>
|
||||
<time dateTime={recipe.date}>
|
||||
Published {new Date(recipe.date).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</time>
|
||||
{recipe.lastUpdated !== recipe.date && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<time dateTime={recipe.lastUpdated}>
|
||||
Updated {new Date(recipe.lastUpdated).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</time>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white">
|
||||
{recipe.title}
|
||||
</h1>
|
||||
|
||||
<p className="text-xl text-gray-600 dark:text-gray-400">
|
||||
{recipe.description}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">Prep:</span>
|
||||
<span>{recipe.prepTime} min</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">Cook:</span>
|
||||
<span>{recipe.cookTime} min</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">Total:</span>
|
||||
<span>{recipe.totalTime} min</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">Servings:</span>
|
||||
<span>{recipe.servings}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">Difficulty:</span>
|
||||
<span className="capitalize">{recipe.difficulty}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{recipe.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-3 py-1 text-sm bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-full"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="prose dark:prose-invert max-w-none">
|
||||
<div className="bg-gray-50 dark:bg-gray-900 p-8 rounded-lg">
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Recipe content will be rendered here from MDX
|
||||
</p>
|
||||
<pre className="text-xs mt-4 overflow-auto">
|
||||
{recipe.content.substring(0, 500)}...
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<RecipePageClient
|
||||
recipe={recipe}
|
||||
sections={sections}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,22 +7,21 @@ interface RecipeCardProps {
|
||||
}
|
||||
|
||||
export default function RecipeCard({ recipe }: RecipeCardProps) {
|
||||
const imageRelativePath = recipe.displayPhoto.startsWith('./')
|
||||
? `${recipe.folderPath}/${recipe.displayPhoto.replace('./', '')}`
|
||||
// Convert relative path to public URL
|
||||
const imageSrc = recipe.displayPhoto.startsWith('./')
|
||||
? `/recipes/${recipe.folderPath}/${recipe.displayPhoto.replace('./', '')}`.replace(/\\/g, '/')
|
||||
: recipe.displayPhoto;
|
||||
|
||||
const normalizedPath = imageRelativePath.replace(/\\/g, '/');
|
||||
const imageSrc = `/api/recipe-image?path=${encodeURIComponent(normalizedPath)}`;
|
||||
|
||||
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">
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={recipe.title}
|
||||
alt={`${recipe.title} - Recipe photo`}
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
@ -39,32 +38,43 @@ export default function RecipeCard({ recipe }: RecipeCardProps) {
|
||||
{recipe.description}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-500">
|
||||
<span className="flex items-center gap-1">
|
||||
⏱️ {recipe.totalTime} min
|
||||
<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">Total time: </span>
|
||||
{recipe.totalTime} min
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
👥 {recipe.servings} servings
|
||||
<span className="flex items-center gap-1" role="listitem">
|
||||
<span aria-hidden="true">👥</span>
|
||||
<span className="sr-only">Servings: </span>
|
||||
{recipe.servings} servings
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span
|
||||
className="flex items-center gap-1"
|
||||
role="listitem"
|
||||
aria-label={`Difficulty: ${recipe.difficulty}`}
|
||||
>
|
||||
<span aria-hidden="true">
|
||||
{recipe.difficulty === 'easy' && '⭐'}
|
||||
{recipe.difficulty === 'medium' && '⭐⭐'}
|
||||
{recipe.difficulty === 'hard' && '⭐⭐⭐'}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<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 className="px-2 py-1 text-xs text-gray-500 dark:text-gray-500">
|
||||
+{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>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import RecipesSidebar, { FilterState } from './RecipesSidebar';
|
||||
import RecipeLayout from './RecipeLayout';
|
||||
import { FilterState } from './RecipesSidebar';
|
||||
import RecipeCard from './RecipeCard';
|
||||
import type { Recipe } from '@/lib/recipes';
|
||||
|
||||
@ -17,7 +18,6 @@ export default function RecipesClient({ recipes, categories, tags }: RecipesClie
|
||||
category: '',
|
||||
selectedTags: [],
|
||||
});
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
const filteredRecipes = useMemo(() => {
|
||||
return recipes.filter((recipe) => {
|
||||
@ -44,18 +44,12 @@ export default function RecipesClient({ recipes, categories, tags }: RecipesClie
|
||||
}, [recipes, filters]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
|
||||
<div className="lg:col-span-1">
|
||||
<RecipesSidebar
|
||||
<RecipeLayout
|
||||
categories={categories}
|
||||
tags={tags}
|
||||
onFilterChange={setFilters}
|
||||
isOpen={sidebarOpen}
|
||||
onToggle={() => setSidebarOpen(!sidebarOpen)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-4">
|
||||
showFilters={true}
|
||||
>
|
||||
<div className="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{filteredRecipes.length === recipes.length
|
||||
? `Showing all ${recipes.length} recipes`
|
||||
@ -79,7 +73,6 @@ export default function RecipesClient({ recipes, categories, tags }: RecipesClie
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</RecipeLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import SelectedTags from './SelectedTags';
|
||||
import TagSelector from './TagSelector';
|
||||
|
||||
interface RecipesSidebarProps {
|
||||
categories: string[];
|
||||
tags: string[];
|
||||
onFilterChange: (filters: FilterState) => void;
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
export interface FilterState {
|
||||
@ -16,13 +16,12 @@ export interface FilterState {
|
||||
selectedTags: string[];
|
||||
}
|
||||
|
||||
export default function RecipesSidebar({ categories, tags, onFilterChange, isOpen, onToggle }: RecipesSidebarProps) {
|
||||
export default function RecipesSidebar({ categories, tags, onFilterChange }: RecipesSidebarProps) {
|
||||
const [search, setSearch] = useState('');
|
||||
const [category, setCategory] = useState('');
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const [showAllTags, setShowAllTags] = useState(false);
|
||||
|
||||
useMemo(() => {
|
||||
useEffect(() => {
|
||||
onFilterChange({ search, category, selectedTags });
|
||||
}, [search, category, selectedTags, onFilterChange]);
|
||||
|
||||
@ -32,6 +31,14 @@ export default function RecipesSidebar({ categories, tags, onFilterChange, isOpe
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveTag = (tag: string) => {
|
||||
setSelectedTags((prev) => prev.filter((t) => t !== tag));
|
||||
};
|
||||
|
||||
const handleClearTags = () => {
|
||||
setSelectedTags([]);
|
||||
};
|
||||
|
||||
const handleClearFilters = () => {
|
||||
setSearch('');
|
||||
setCategory('');
|
||||
@ -39,18 +46,10 @@ export default function RecipesSidebar({ categories, tags, onFilterChange, isOpe
|
||||
};
|
||||
|
||||
const hasActiveFilters = search || category || selectedTags.length > 0;
|
||||
const displayedTags = showAllTags ? tags : tags.slice(0, 10);
|
||||
|
||||
return (
|
||||
<aside>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="lg:hidden w-full mb-3 px-3 py-2 text-sm font-medium bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
{isOpen ? '✕ Hide Filters' : '⚙️ Show Filters'}
|
||||
</button>
|
||||
|
||||
<div className={`space-y-3 ${!isOpen ? 'hidden lg:block' : ''}`}>
|
||||
<section aria-label="Recipe filters">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label htmlFor="search" className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Search
|
||||
@ -84,55 +83,34 @@ export default function RecipesSidebar({ categories, tags, onFilterChange, isOpe
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="space-y-2">
|
||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
Tags
|
||||
</label>
|
||||
{selectedTags.length > 0 && (
|
||||
<button
|
||||
onClick={() => setSelectedTags([])}
|
||||
className="text-xs text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-0.5 max-h-48 overflow-y-auto">
|
||||
{displayedTags.map((tag) => (
|
||||
<label
|
||||
key={tag}
|
||||
className="flex items-center gap-1.5 py-0.5 px-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedTags.includes(tag)}
|
||||
onChange={() => handleTagToggle(tag)}
|
||||
className="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
|
||||
|
||||
<SelectedTags
|
||||
tags={selectedTags}
|
||||
onRemove={handleRemoveTag}
|
||||
onClear={handleClearTags}
|
||||
/>
|
||||
|
||||
<TagSelector
|
||||
availableTags={tags}
|
||||
selectedTags={selectedTags}
|
||||
onToggleTag={handleTagToggle}
|
||||
/>
|
||||
<span className="text-xs text-gray-700 dark:text-gray-300">{tag}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{tags.length > 10 && (
|
||||
<button
|
||||
onClick={() => setShowAllTags(!showAllTags)}
|
||||
className="text-xs text-blue-600 dark:text-blue-400 hover:underline mt-1"
|
||||
>
|
||||
{showAllTags ? 'Show less' : `Show ${tags.length - 10} more`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasActiveFilters && (
|
||||
<button
|
||||
onClick={handleClearFilters}
|
||||
className="w-full px-3 py-1.5 text-xs bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
|
||||
aria-label="Clear all filters"
|
||||
>
|
||||
Clear All Filters
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
140
data/README.md
140
data/README.md
@ -1,140 +0,0 @@
|
||||
# Data Directory
|
||||
|
||||
This directory contains all content and reference data for the cooking website.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
data/
|
||||
├── README.md # This file
|
||||
├── authors.json # Author profiles and information
|
||||
├── taxonomy.json # Categories, tags, and classification system
|
||||
└── recipes/ # Recipe content
|
||||
├── README.md
|
||||
├── examples/
|
||||
├── appetizers/
|
||||
├── mains/
|
||||
├── desserts/
|
||||
├── sides/
|
||||
├── beverages/
|
||||
└── breads/
|
||||
```
|
||||
|
||||
## Reference Files
|
||||
|
||||
### authors.json
|
||||
|
||||
Contains author profiles with biographical information, social links, and specialties.
|
||||
|
||||
**Structure:**
|
||||
```json
|
||||
{
|
||||
"authors": [
|
||||
{
|
||||
"id": "pws",
|
||||
"name": "PWS",
|
||||
"fullName": "Paul Wilson Smith",
|
||||
"bio": "Short biography",
|
||||
"email": "email@example.com",
|
||||
"website": "https://...",
|
||||
"avatar": "/images/authors/pws.jpg",
|
||||
"social": {
|
||||
"github": "username"
|
||||
},
|
||||
"joinDate": "2026-01-01",
|
||||
"specialties": ["Italian cuisine", "Baking"],
|
||||
"favoriteIngredient": "Garlic"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Usage in recipes:**
|
||||
- Reference the author by their `id` in the recipe frontmatter
|
||||
- Example: `author: "pws"`
|
||||
- The site will look up full author details from this file
|
||||
|
||||
### taxonomy.json
|
||||
|
||||
Defines the complete taxonomy system for organizing recipes.
|
||||
|
||||
**Contains:**
|
||||
1. **Categories** - Main recipe classifications (appetizers, mains, desserts, etc.)
|
||||
2. **Tags** - Organized by type:
|
||||
- `cuisine` - Italian, Mexican, Asian, etc.
|
||||
- `protein` - Chicken, beef, vegetarian, etc.
|
||||
- `dietary` - Gluten-free, vegan, keto, etc.
|
||||
- `meal-type` - Breakfast, lunch, dinner, etc.
|
||||
- `occasion` - Weeknight, holiday, party, etc.
|
||||
- `cooking-method` - Baking, grilling, slow-cooker, etc.
|
||||
- `speed` - Quick-meals, one-pot, make-ahead, etc.
|
||||
- `flavor-profile` - Spicy, sweet, savory, etc.
|
||||
- `special` - Chocolate, pasta, soup, etc.
|
||||
3. **Difficulty** - Easy, medium, hard with descriptions
|
||||
|
||||
**Usage in recipes:**
|
||||
- Use tag IDs from this file in recipe frontmatter
|
||||
- Example: `tags: ["italian", "chicken", "comfort-food"]`
|
||||
- Example: `category: "mains"`
|
||||
- Example: `difficulty: "medium"`
|
||||
- The site can validate tags against this taxonomy
|
||||
|
||||
## Benefits of Reference Files
|
||||
|
||||
### Consistency
|
||||
- Ensures uniform spelling and naming across all recipes
|
||||
- Prevents duplicate tags with slight variations
|
||||
- Maintains standardized author information
|
||||
|
||||
### Validation
|
||||
- Can validate recipe frontmatter against these schemas
|
||||
- Catch typos and invalid values during build
|
||||
- Provide helpful error messages
|
||||
|
||||
### UI Generation
|
||||
- Generate filter dropdowns from taxonomy
|
||||
- Display tag descriptions and icons
|
||||
- Show author bios and links automatically
|
||||
|
||||
### Scalability
|
||||
- Easy to add new authors, tags, or categories
|
||||
- Central place to update descriptions and metadata
|
||||
- Supports multiple authors and contributors
|
||||
|
||||
### SEO & Discovery
|
||||
- Structured data for search engines
|
||||
- Consistent taxonomy improves findability
|
||||
- Tag descriptions enhance metadata
|
||||
|
||||
## Adding New Entries
|
||||
|
||||
### Adding an Author
|
||||
|
||||
1. Open `data/authors.json`
|
||||
2. Add a new author object to the `authors` array
|
||||
3. Ensure the `id` is unique and lowercase-hyphenated
|
||||
4. Add avatar image to `/public/images/authors/`
|
||||
5. Reference the author ID in recipe frontmatter
|
||||
|
||||
### Adding a Tag
|
||||
|
||||
1. Open `data/taxonomy.json`
|
||||
2. Find the appropriate tag category (cuisine, protein, etc.)
|
||||
3. Add a new tag object with `id`, `name`, and `description`
|
||||
4. Use the tag ID in recipe frontmatter
|
||||
|
||||
### Adding a Category
|
||||
|
||||
1. Open `data/taxonomy.json`
|
||||
2. Add a new category to the `categories` array
|
||||
3. Create the corresponding folder in `data/recipes/`
|
||||
4. Use the category ID in recipe frontmatter
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Always reference tags/categories/authors by their `id`
|
||||
- Keep IDs lowercase with hyphens (kebab-case)
|
||||
- Write clear, helpful descriptions
|
||||
- Add emojis to categories for visual interest
|
||||
- Update both reference files and recipes together
|
||||
- Validate changes before committing
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"authors": [
|
||||
{
|
||||
"id": "pws",
|
||||
"name": "PWS",
|
||||
"fullName": "Paul Wilson Smith",
|
||||
"bio": "Home cook and food enthusiast sharing tested recipes and cooking techniques.",
|
||||
"email": "pws@example.com",
|
||||
"website": "https://github.com/runyanjake/cooking",
|
||||
"avatar": "/images/authors/pws.jpg",
|
||||
"social": {
|
||||
"github": "runyanjake"
|
||||
},
|
||||
"joinDate": "2026-01-01",
|
||||
"specialties": ["Italian cuisine", "Baking", "Comfort food"],
|
||||
"favoriteIngredient": "Garlic"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,212 +0,0 @@
|
||||
# Recipe Content Structure
|
||||
|
||||
This directory contains all recipe content for the cooking website.
|
||||
|
||||
## Folder Organization
|
||||
|
||||
```
|
||||
data/recipes/
|
||||
├── appetizers/
|
||||
├── mains/
|
||||
├── desserts/
|
||||
├── sides/
|
||||
├── beverages/
|
||||
└── breads/
|
||||
```
|
||||
|
||||
Each recipe follows this structure:
|
||||
```
|
||||
category/
|
||||
└── YYYY.MM.DD-recipe-slug/
|
||||
├── YYYY.MM.DD-recipe-slug.mdx
|
||||
└── assets/
|
||||
├── hero.jpg
|
||||
├── step1.jpg
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Recipe Format
|
||||
|
||||
### MDX File Structure
|
||||
|
||||
The `.mdx` file contains all recipe metadata and content in one place.
|
||||
|
||||
#### Frontmatter (Required)
|
||||
|
||||
All recipe metadata is stored in YAML frontmatter at the top of the MDX file:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: "Recipe Title"
|
||||
slug: "recipe-slug"
|
||||
date: "YYYY-MM-DD"
|
||||
lastUpdated: "YYYY-MM-DD"
|
||||
category: "mains"
|
||||
tags: ["tag1", "tag2", "tag3"]
|
||||
dietary: ["vegetarian", "gluten-free"]
|
||||
cookTime: 45
|
||||
prepTime: 20
|
||||
totalTime: 65
|
||||
difficulty: "easy"
|
||||
servings: 4
|
||||
author: "Author Name"
|
||||
description: "Short description for SEO and previews"
|
||||
featured: true
|
||||
display: true
|
||||
displayPhoto: "./assets/hero.jpg"
|
||||
---
|
||||
```
|
||||
|
||||
**Frontmatter Fields:**
|
||||
- `title` - Display title of the recipe
|
||||
- `slug` - URL-friendly identifier
|
||||
- `date` - Publication date (YYYY-MM-DD)
|
||||
- `lastUpdated` - Last modification date (YYYY-MM-DD)
|
||||
- `category` - Main category ID (references `../taxonomy.json`)
|
||||
- `tags` - Array of tag IDs (references `../taxonomy.json`)
|
||||
- `dietary` - Array of dietary tag IDs (references `../taxonomy.json`)
|
||||
- `cookTime` - Active cooking time in minutes
|
||||
- `prepTime` - Preparation time in minutes
|
||||
- `totalTime` - Total time in minutes
|
||||
- `difficulty` - Difficulty ID: easy, medium, or hard (references `../taxonomy.json`)
|
||||
- `servings` - Number of servings
|
||||
- `author` - Author ID (references `../authors.json`)
|
||||
- `description` - Brief description for SEO and cards
|
||||
- `featured` - Boolean for homepage featuring
|
||||
- `display` - Boolean to control visibility (set to false to hide recipe)
|
||||
- `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.
|
||||
|
||||
#### Content Sections
|
||||
|
||||
The following `## ` (h2) sections are parsed into tabs in the UI:
|
||||
|
||||
1. **## Photos** - Recipe images with captions
|
||||
2. **## Ingredients** - Lists of ingredients (can use h3 subsections)
|
||||
3. **## Instructions** - Step-by-step cooking instructions
|
||||
4. **## Notes** - Tips, variations, storage info (optional)
|
||||
5. **## References** - Sources, inspirations, credits (optional)
|
||||
|
||||
#### Example MDX Structure
|
||||
|
||||
```mdx
|
||||
---
|
||||
title: "Recipe Name"
|
||||
slug: "recipe-name"
|
||||
date: "2026-02-08"
|
||||
lastUpdated: "2026-02-08"
|
||||
category: "mains"
|
||||
tags: ["italian", "chicken"]
|
||||
dietary: ["gluten-free-option"]
|
||||
cookTime: 45
|
||||
prepTime: 20
|
||||
totalTime: 65
|
||||
difficulty: "medium"
|
||||
servings: 4
|
||||
author: "PWS"
|
||||
description: "Short description for SEO and previews"
|
||||
featured: false
|
||||
display: true
|
||||
displayPhoto: "./assets/hero.jpg"
|
||||
---
|
||||
|
||||
# Recipe Name
|
||||
|
||||
Introduction paragraph about the recipe.
|
||||
|
||||
## Photos
|
||||
|
||||

|
||||
*Caption describing the image*
|
||||
|
||||

|
||||
*Another helpful image*
|
||||
|
||||
## Ingredients
|
||||
|
||||
### For the Main Component
|
||||
- 2 cups ingredient one
|
||||
- 1 tablespoon ingredient two
|
||||
- Salt and pepper to taste
|
||||
|
||||
### For the Sauce
|
||||
- 1 cup sauce base
|
||||
- Seasonings
|
||||
|
||||
## Instructions
|
||||
|
||||
### Preparation
|
||||
1. **Step name**: Detailed instruction with technique.
|
||||
2. **Another step**: More details here.
|
||||
|
||||
### Cooking
|
||||
3. **Heat and cook**: Continue with numbered steps.
|
||||
4. **Finish**: Final steps.
|
||||
|
||||
## Notes
|
||||
|
||||
### Tips for Success
|
||||
- Helpful tip one
|
||||
- Helpful tip two
|
||||
|
||||
### Storage
|
||||
- How to store leftovers
|
||||
- Freezing instructions
|
||||
|
||||
### Variations
|
||||
- How to adapt the recipe
|
||||
|
||||
## References
|
||||
|
||||
- Source credits
|
||||
- Inspiration mentions
|
||||
- Cookbook references
|
||||
```
|
||||
|
||||
## Content Guidelines
|
||||
|
||||
### Writing Style
|
||||
- Use clear, conversational language
|
||||
- Include helpful tips and context
|
||||
- Explain techniques for beginners
|
||||
- Add timing and temperature details
|
||||
|
||||
### Photography
|
||||
- Include hero shot (main finished dish)
|
||||
- Add process shots for complex steps
|
||||
- Use descriptive alt text for accessibility
|
||||
- Optimize images (web-friendly sizes)
|
||||
|
||||
### Tags
|
||||
Choose from these categories:
|
||||
- **Cuisine**: italian, mexican, asian, american, mediterranean, etc.
|
||||
- **Protein**: chicken, beef, pork, seafood, vegetarian, vegan
|
||||
- **Meal Type**: breakfast, lunch, dinner, snack, appetizer
|
||||
- **Occasion**: weeknight, holiday, party, comfort-food
|
||||
- **Dietary**: gluten-free, dairy-free, low-carb, keto, paleo
|
||||
- **Cooking Method**: baking, grilling, slow-cooker, instant-pot
|
||||
- **Speed**: quick-meals, one-pot, make-ahead, no-cook
|
||||
|
||||
### Difficulty Levels
|
||||
- **easy**: Beginner-friendly, simple techniques, common ingredients
|
||||
- **medium**: Some cooking experience needed, multiple steps
|
||||
- **hard**: Advanced techniques, precise timing, specialty equipment
|
||||
|
||||
## Adding New Recipes
|
||||
|
||||
1. Create folder: `data/recipes/[category]/YYYY.MM.DD-recipe-name/`
|
||||
2. Create `YYYY.MM.DD-recipe-name.mdx` with frontmatter and content
|
||||
3. Add images to `assets/` folder
|
||||
4. Build locally to verify rendering
|
||||
5. Commit and push
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Use consistent date formatting (YYYY.MM.DD)
|
||||
- Keep slugs URL-friendly (lowercase, hyphens)
|
||||
- Optimize images before adding (compress, resize)
|
||||
- Test recipes before publishing
|
||||
- Include metric and imperial measurements when possible
|
||||
- Credit sources and inspirations
|
||||
- Update featured flag sparingly (limit to 3-5 recipes)
|
||||
@ -1,187 +0,0 @@
|
||||
{
|
||||
"categories": [
|
||||
{
|
||||
"id": "appetizers",
|
||||
"name": "Appetizers",
|
||||
"slug": "appetizers",
|
||||
"description": "Small dishes to start your meal",
|
||||
"icon": "🥖"
|
||||
},
|
||||
{
|
||||
"id": "mains",
|
||||
"name": "Main Dishes",
|
||||
"slug": "mains",
|
||||
"description": "Hearty entrees and main courses",
|
||||
"icon": "🍽️"
|
||||
},
|
||||
{
|
||||
"id": "desserts",
|
||||
"name": "Desserts",
|
||||
"slug": "desserts",
|
||||
"description": "Sweet treats and baked goods",
|
||||
"icon": "🍰"
|
||||
},
|
||||
{
|
||||
"id": "sides",
|
||||
"name": "Side Dishes",
|
||||
"slug": "sides",
|
||||
"description": "Complementary dishes and accompaniments",
|
||||
"icon": "🥗"
|
||||
},
|
||||
{
|
||||
"id": "beverages",
|
||||
"name": "Beverages",
|
||||
"slug": "beverages",
|
||||
"description": "Drinks, smoothies, and cocktails",
|
||||
"icon": "🥤"
|
||||
},
|
||||
{
|
||||
"id": "breads",
|
||||
"name": "Breads & Baking",
|
||||
"slug": "breads",
|
||||
"description": "Homemade breads, rolls, and baked goods",
|
||||
"icon": "🍞"
|
||||
}
|
||||
],
|
||||
"tags": {
|
||||
"cuisine": {
|
||||
"name": "Cuisine",
|
||||
"description": "Regional and cultural cooking styles",
|
||||
"tags": [
|
||||
{ "id": "italian", "name": "Italian", "description": "Italian cuisine and cooking traditions" },
|
||||
{ "id": "mexican", "name": "Mexican", "description": "Mexican and Latin American dishes" },
|
||||
{ "id": "asian", "name": "Asian", "description": "Asian cuisine including Chinese, Japanese, Thai, etc." },
|
||||
{ "id": "american", "name": "American", "description": "Classic American cooking" },
|
||||
{ "id": "mediterranean", "name": "Mediterranean", "description": "Mediterranean diet and cuisine" },
|
||||
{ "id": "french", "name": "French", "description": "French culinary techniques and dishes" },
|
||||
{ "id": "indian", "name": "Indian", "description": "Indian spices and cooking methods" },
|
||||
{ "id": "middle-eastern", "name": "Middle Eastern", "description": "Middle Eastern flavors and dishes" }
|
||||
]
|
||||
},
|
||||
"protein": {
|
||||
"name": "Protein",
|
||||
"description": "Main protein sources",
|
||||
"tags": [
|
||||
{ "id": "chicken", "name": "Chicken", "description": "Chicken-based recipes" },
|
||||
{ "id": "beef", "name": "Beef", "description": "Beef and red meat dishes" },
|
||||
{ "id": "pork", "name": "Pork", "description": "Pork and ham recipes" },
|
||||
{ "id": "seafood", "name": "Seafood", "description": "Fish and shellfish dishes" },
|
||||
{ "id": "lamb", "name": "Lamb", "description": "Lamb-based recipes" },
|
||||
{ "id": "vegetarian", "name": "Vegetarian", "description": "No meat, may include dairy and eggs" },
|
||||
{ "id": "vegan", "name": "Vegan", "description": "Plant-based, no animal products" }
|
||||
]
|
||||
},
|
||||
"dietary": {
|
||||
"name": "Dietary",
|
||||
"description": "Special dietary considerations",
|
||||
"tags": [
|
||||
{ "id": "gluten-free", "name": "Gluten-Free", "description": "No gluten-containing ingredients" },
|
||||
{ "id": "gluten-free-option", "name": "Gluten-Free Option", "description": "Can be made gluten-free with substitutions" },
|
||||
{ "id": "dairy-free", "name": "Dairy-Free", "description": "No dairy products" },
|
||||
{ "id": "low-carb", "name": "Low-Carb", "description": "Reduced carbohydrate content" },
|
||||
{ "id": "keto", "name": "Keto", "description": "Ketogenic diet friendly" },
|
||||
{ "id": "paleo", "name": "Paleo", "description": "Paleo diet compliant" },
|
||||
{ "id": "whole30", "name": "Whole30", "description": "Whole30 program approved" },
|
||||
{ "id": "nut-free", "name": "Nut-Free", "description": "No nuts or nut products" }
|
||||
]
|
||||
},
|
||||
"meal-type": {
|
||||
"name": "Meal Type",
|
||||
"description": "When to serve this dish",
|
||||
"tags": [
|
||||
{ "id": "breakfast", "name": "Breakfast", "description": "Morning meals and brunch" },
|
||||
{ "id": "lunch", "name": "Lunch", "description": "Midday meals" },
|
||||
{ "id": "dinner", "name": "Dinner", "description": "Evening meals" },
|
||||
{ "id": "snack", "name": "Snack", "description": "Small bites between meals" },
|
||||
{ "id": "brunch", "name": "Brunch", "description": "Late morning combination meal" }
|
||||
]
|
||||
},
|
||||
"occasion": {
|
||||
"name": "Occasion",
|
||||
"description": "Special events and settings",
|
||||
"tags": [
|
||||
{ "id": "weeknight", "name": "Weeknight", "description": "Quick meals for busy evenings" },
|
||||
{ "id": "weekend", "name": "Weekend", "description": "Recipes with more time investment" },
|
||||
{ "id": "holiday", "name": "Holiday", "description": "Special holiday dishes" },
|
||||
{ "id": "party", "name": "Party", "description": "Entertaining and gatherings" },
|
||||
{ "id": "comfort-food", "name": "Comfort Food", "description": "Cozy, satisfying dishes" },
|
||||
{ "id": "family-friendly", "name": "Family-Friendly", "description": "Kid-approved recipes" },
|
||||
{ "id": "date-night", "name": "Date Night", "description": "Romantic or impressive dishes" }
|
||||
]
|
||||
},
|
||||
"cooking-method": {
|
||||
"name": "Cooking Method",
|
||||
"description": "Primary cooking technique",
|
||||
"tags": [
|
||||
{ "id": "baking", "name": "Baking", "description": "Oven-baked dishes" },
|
||||
{ "id": "grilling", "name": "Grilling", "description": "Outdoor or indoor grilling" },
|
||||
{ "id": "slow-cooker", "name": "Slow Cooker", "description": "Crock-pot and slow cooker meals" },
|
||||
{ "id": "instant-pot", "name": "Instant Pot", "description": "Pressure cooker recipes" },
|
||||
{ "id": "stovetop", "name": "Stovetop", "description": "Cooked on the stove" },
|
||||
{ "id": "no-cook", "name": "No-Cook", "description": "No cooking required" },
|
||||
{ "id": "air-fryer", "name": "Air Fryer", "description": "Air fryer recipes" },
|
||||
{ "id": "sous-vide", "name": "Sous Vide", "description": "Precision cooking method" }
|
||||
]
|
||||
},
|
||||
"speed": {
|
||||
"name": "Speed & Convenience",
|
||||
"description": "Time and effort indicators",
|
||||
"tags": [
|
||||
{ "id": "quick-meals", "name": "Quick Meals", "description": "30 minutes or less" },
|
||||
{ "id": "one-pot", "name": "One-Pot", "description": "Minimal cleanup required" },
|
||||
{ "id": "make-ahead", "name": "Make-Ahead", "description": "Can be prepared in advance" },
|
||||
{ "id": "meal-prep", "name": "Meal Prep", "description": "Great for batch cooking" },
|
||||
{ "id": "leftovers", "name": "Leftovers", "description": "Tastes great reheated" },
|
||||
{ "id": "freezer-friendly", "name": "Freezer-Friendly", "description": "Can be frozen" }
|
||||
]
|
||||
},
|
||||
"flavor-profile": {
|
||||
"name": "Flavor Profile",
|
||||
"description": "Dominant tastes and characteristics",
|
||||
"tags": [
|
||||
{ "id": "spicy", "name": "Spicy", "description": "Hot and spicy flavors" },
|
||||
{ "id": "sweet", "name": "Sweet", "description": "Sweet or dessert-like" },
|
||||
{ "id": "savory", "name": "Savory", "description": "Rich savory flavors" },
|
||||
{ "id": "tangy", "name": "Tangy", "description": "Acidic or citrus notes" },
|
||||
{ "id": "rich", "name": "Rich", "description": "Indulgent and decadent" },
|
||||
{ "id": "light", "name": "Light", "description": "Fresh and light dishes" },
|
||||
{ "id": "smoky", "name": "Smoky", "description": "Smoky flavors" },
|
||||
{ "id": "umami", "name": "Umami", "description": "Deep savory umami taste" }
|
||||
]
|
||||
},
|
||||
"special": {
|
||||
"name": "Special Features",
|
||||
"description": "Notable characteristics",
|
||||
"tags": [
|
||||
{ "id": "chocolate", "name": "Chocolate", "description": "Contains chocolate" },
|
||||
{ "id": "cookies", "name": "Cookies", "description": "Cookie recipes" },
|
||||
{ "id": "pasta", "name": "Pasta", "description": "Pasta dishes" },
|
||||
{ "id": "soup", "name": "Soup", "description": "Soups and stews" },
|
||||
{ "id": "salad", "name": "Salad", "description": "Salads and fresh dishes" },
|
||||
{ "id": "sandwich", "name": "Sandwich", "description": "Sandwiches and wraps" },
|
||||
{ "id": "pizza", "name": "Pizza", "description": "Pizza recipes" },
|
||||
{ "id": "bbq", "name": "BBQ", "description": "Barbecue style" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"difficulty": [
|
||||
{
|
||||
"id": "easy",
|
||||
"name": "Easy",
|
||||
"description": "Beginner-friendly, simple techniques, common ingredients",
|
||||
"icon": "⭐"
|
||||
},
|
||||
{
|
||||
"id": "medium",
|
||||
"name": "Medium",
|
||||
"description": "Some cooking experience needed, multiple steps",
|
||||
"icon": "⭐⭐"
|
||||
},
|
||||
{
|
||||
"id": "hard",
|
||||
"name": "Hard",
|
||||
"description": "Advanced techniques, precise timing, specialty equipment",
|
||||
"icon": "⭐⭐⭐"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -2,7 +2,7 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import matter from 'gray-matter';
|
||||
|
||||
const recipesDirectory = path.join(process.cwd(), 'data/recipes');
|
||||
const recipesDirectory = path.join(process.cwd(), 'public/recipes');
|
||||
|
||||
// Cache to avoid repeated file system walks
|
||||
let recipesCache: Recipe[] | null = null;
|
||||
|
||||
@ -4,20 +4,6 @@ import createMDX from '@next/mdx';
|
||||
const nextConfig: NextConfig = {
|
||||
reactStrictMode: true,
|
||||
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: 'localhost',
|
||||
pathname: '/api/recipe-image**',
|
||||
},
|
||||
],
|
||||
localPatterns: [
|
||||
{
|
||||
pathname: '/api/recipe-image**',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const withMDX = createMDX({
|
||||
|
||||
334
package-lock.json
generated
334
package-lock.json
generated
@ -15,7 +15,9 @@
|
||||
"gray-matter": "^4.0.3",
|
||||
"next": "^15.1.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
@ -4033,6 +4035,16 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/html-url-attributes": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
|
||||
"integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@ -4844,6 +4856,16 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-table": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
|
||||
"integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@ -4854,6 +4876,34 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-find-and-replace": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
|
||||
"integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"escape-string-regexp": "^5.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/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
|
||||
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-from-markdown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
|
||||
@ -4878,6 +4928,107 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
|
||||
"integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-gfm-autolink-literal": "^2.0.0",
|
||||
"mdast-util-gfm-footnote": "^2.0.0",
|
||||
"mdast-util-gfm-strikethrough": "^2.0.0",
|
||||
"mdast-util-gfm-table": "^2.0.0",
|
||||
"mdast-util-gfm-task-list-item": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm-autolink-literal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
|
||||
"integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"ccount": "^2.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"mdast-util-find-and-replace": "^3.0.0",
|
||||
"micromark-util-character": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm-footnote": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
|
||||
"integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.1.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0",
|
||||
"micromark-util-normalize-identifier": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm-strikethrough": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
|
||||
"integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm-table": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
|
||||
"integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"markdown-table": "^3.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm-task-list-item": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
|
||||
"integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-mdx": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz",
|
||||
@ -5103,6 +5254,127 @@
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
|
||||
"integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-extension-gfm-autolink-literal": "^2.0.0",
|
||||
"micromark-extension-gfm-footnote": "^2.0.0",
|
||||
"micromark-extension-gfm-strikethrough": "^2.0.0",
|
||||
"micromark-extension-gfm-table": "^2.0.0",
|
||||
"micromark-extension-gfm-tagfilter": "^2.0.0",
|
||||
"micromark-extension-gfm-task-list-item": "^2.0.0",
|
||||
"micromark-util-combine-extensions": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-autolink-literal": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
|
||||
"integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-sanitize-uri": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-footnote": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
|
||||
"integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-core-commonmark": "^2.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-normalize-identifier": "^2.0.0",
|
||||
"micromark-util-sanitize-uri": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-strikethrough": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
|
||||
"integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-util-chunked": "^2.0.0",
|
||||
"micromark-util-classify-character": "^2.0.0",
|
||||
"micromark-util-resolve-all": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-table": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
|
||||
"integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-tagfilter": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
|
||||
"integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-task-list-item": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
|
||||
"integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-mdx-expression": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz",
|
||||
@ -6415,6 +6687,33 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-markdown": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
|
||||
"integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"hast-util-to-jsx-runtime": "^2.0.0",
|
||||
"html-url-attributes": "^3.0.0",
|
||||
"mdast-util-to-hast": "^13.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.0.0",
|
||||
"unified": "^11.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18",
|
||||
"react": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@ -6564,6 +6863,24 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-gfm": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
|
||||
"integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"mdast-util-gfm": "^3.0.0",
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-stringify": "^11.0.0",
|
||||
"unified": "^11.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-mdx": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz",
|
||||
@ -6611,6 +6928,21 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-stringify": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
|
||||
"integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0",
|
||||
"unified": "^11.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.11",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||
|
||||
@ -16,7 +16,9 @@
|
||||
"gray-matter": "^4.0.3",
|
||||
"next": "^15.1.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user