Comprehensive guidance for AI/code assistants working on Vanilla Cookbook, a self-hosted SvelteKit recipe manager with advanced parsing, unit conversion, and PWA capabilities.
Vanilla Cookbook prioritizes simplicity in user experience while handling complexity under the hood. The core principle: let users work with unstructured text (recipes as they exist on the web) while automatically providing structure, conversion, and scaling capabilities. Avoid over-engineering the UI or forcing users into rigid data entry patterns.
- Framework: SvelteKit (Svelte 5) with Vite
- Database: Prisma ORM with SQLite (default), supports migrations
- Styling: Tailwind CSS + DaisyUI component library
- Authentication: Lucia v2 with Prisma adapter
- Testing: Vitest with jsdom, @testing-library/svelte
- PWA: Workbox for service worker generation
- AI Integration: Optional OpenAI/Anthropic API for parsing assistance
- Recipe Parsing: Custom ingredient parser (git submodule at
src/lib/submodules/recipe-ingredient-parser) - Package Manager: pnpm (engine-strict, required)
vanilla-cookbook/
├── src/
│ ├── lib/
│ │ ├── components/ # Svelte UI components (39 files)
│ │ ├── utils/ # Core business logic
│ │ │ ├── parse/ # Recipe scraping & parsing (HTML, JSON-LD, microdata)
│ │ │ ├── import/ # Import formats (Paprika, JSON, CSV)
│ │ │ ├── image/ # Image processing with Sharp
│ │ │ ├── pwa/ # Service worker generation
│ │ │ ├── seed/ # Database seeding utilities
│ │ │ ├── prisma/ # Prisma helpers
│ │ │ ├── ai.js # LLM integration (OpenAI/Anthropic)
│ │ │ ├── converter.js # Unit conversion (US/metric/imperial)
│ │ │ ├── crud.js # Database operations
│ │ │ ├── filters.js # Search & filtering logic
│ │ │ └── ... # Other utilities
│ │ ├── submodules/
│ │ │ └── recipe-ingredient-parser/ # Git submodule for parsing
│ │ ├── server/ # Server-side utilities (auth, hooks)
│ │ ├── stores/ # Svelte stores
│ │ ├── data/ # Static data (ingredient densities, etc.)
│ │ ├── css/ # Global styles
│ │ └── typeDefinitions.js # JSDoc type definitions
│ └── routes/ # SvelteKit routes
│ ├── api/ # API endpoints
│ │ ├── recipe/ # Recipe CRUD, scraping, image ops
│ │ ├── user/ # User management
│ │ ├── ingredients/ # Ingredient data
│ │ ├── log/ # Cooking logs
│ │ ├── site/ # Site settings
│ │ ├── oauth/ # OAuth providers
│ │ └── health/ # Health check
│ ├── user/[id]/ # User-specific pages
│ ├── recipe/ # Recipe pages
│ ├── login/ # Auth pages
│ ├── register/
│ └── ...
├── prisma/
│ ├── schema.prisma # Database schema
│ ├── migrations/ # Migration history
│ └── db/ # SQLite database file
├── static/ # PWA assets, icons, manifest
├── docs/ # Documentation
│ ├── manual/ # User manual
│ ├── technical/ # Technical docs (auto-generated JSDoc)
│ ├── scripts/ # Python scripts for docs/screenshots
│ └── images/ # Screenshots
├── build/ # Production build output (gitignored)
├── site/ # Generated docs site (gitignored)
└── uploads/ # User-uploaded images (persistent)
Location: src/lib/utils/parse/, src/routes/api/recipe/scrape/
- JSON-LD parsing: Primary method, extracts schema.org Recipe data from
<script type="application/ld+json"> - Microdata fallback: Extracts schema.org microdata from HTML attributes (
itemprop,itemscope) - Site-specific configs: Custom CSS selectors for sites without structured data
- AI assist: Falls back to OpenAI/Anthropic when structured parsing fails
- Bookmarklet: Browser bookmarklet for easy recipe capture from any site
Key files:
recipeParse.js: Main parsing orchestrator (parseURL,parseHTML)parseHelpers.js: Extraction utilities (JSON-LD, microdata, nutrition, images)parseHelpersClient.js: Client-side parsing (scrapeRecipeFromURL,handleParse,handleImage)
Location: src/lib/submodules/recipe-ingredient-parser/ (submodule), src/lib/utils/converter.js
- Multi-language parsing: Supports 10+ languages (English, German, Italian, Spanish, French, Portuguese, Indonesian, Hindi, Russian, Arabic)
- Smart unit conversion: US volumetric → metric/imperial weight using ingredient-specific densities
- Ingredient database: Thousands of ingredients with gram-per-cup densities (
src/lib/data/) - Inline conversion: Temperature, fractions, ranges handled automatically
- Recipe scaling: Multiplies all parsed quantities while preserving units
Important: The ingredient parser is a git submodule. Changes to parsing logic belong in the submodule repo, not here.
Location: src/lib/server/, src/routes/api/user/, Prisma schema
- Lucia v2: Session-based auth with Prisma adapter
- Multi-user support: User creation, admin/root roles, registration toggle
- Public/private recipes: Per-user and per-recipe privacy settings
- OAuth support: Google, GitHub providers (optional)
- Rate limiting: Login attempts limited by IP and user ID
Prisma models: AuthUser, AuthSession, AuthKey, AuthAccount, SiteSettings
Location: src/lib/utils/pwa/, static/
- Service worker: Auto-generated with Workbox (
generate-sw.js) - Installable: Web app manifest for mobile/desktop installation
- Share target: Receive URLs and text from other apps (mobile)
- Offline caching: Recipes and assets cached for offline access
- Domain switching:
./scripts/pwa/sw-domain.shupdates service worker scope whenORIGINchanges
Key commands: pnpm generate-sw, ./scripts/pwa/sw-domain.sh
Location: src/routes/api/log/, Prisma models
- Shopping list: Add ingredients from recipes, persist checked items, show/hide purchased
- Cooking logs: Track when recipes are cooked, calendar view, recipe-specific history
- Database:
ShoppingListItem,RecipeLogmodels
Location: src/lib/utils/categories.js, Prisma Category model
- Hierarchical categories: Parent-child relationships, unlimited nesting
- User-specific: Each user has their own category tree
- Optional feature: Power user feature (hidden by default,
useCatsflag)
Location: src/lib/utils/image/, src/routes/api/recipe/image/
- Sharp processing: Resize, compress, convert images
- Multiple photos:
RecipePhotomodel supports multiple images per recipe - Upload persistence: Images stored in
uploads/directory - Remote scraping: Download images from recipe URLs
# Clone with submodule
git clone --recursive https://github.com/jt196/vanilla-cookbook.git
# If already cloned, pull submodules
git submodule update --init --recursive
# Copy environment template
cp .env.template .env
# Edit .env - CRITICAL: Set ORIGIN=http://localhost:5173 for dev
# Optional: Add OPENAI_API_KEY or ANTHROPIC_API_KEY for AI assist
# Install dependencies and generate Prisma client
pnpm dev:install
# Run dev server (generates client, migrates DB, seeds data, starts Vite)
pnpm devDriver Adapter Required: Prisma 7 requires a database driver adapter. For SQLite, we use @prisma/adapter-libsql.
PrismaClient Instantiation Pattern:
import PrismaClientPkg from '@prisma/client'
import { PrismaLibSql } from '@prisma/adapter-libsql'
const { PrismaClient } = PrismaClientPkg
// Create LibSQL adapter
const adapter = new PrismaLibSql({
url: process.env.DATABASE_URL || 'file:./prisma/db/dev.sqlite'
})
// Initialize with adapter
const prisma = new PrismaClient({ adapter })Configuration File: prisma.config.ts at project root (NOT in prisma/ folder) defines database URL, schema location, and seed script.
Manual Generation: prisma generate must be called explicitly - it no longer runs automatically with migrate or db push.
# Development
pnpm dev # Migrate, seed, start dev server
pnpm dev:setup # Pre-run: generate SW, migrate, update SW domain
pnpm dev:install # Install deps + Prisma generate
# Testing
pnpm test # Run Vitest tests
pnpm test:watch # Watch mode
pnpm coverage # Coverage report
# Linting & Formatting
pnpm lint # Prettier check + ESLint
pnpm format # Format all files
# Database
pnpm prisma generate # Regenerate Prisma client after schema changes
pnpm prisma migrate dev # Create new migration (dev only)
pnpm seed # Seed sample data
# PWA
pnpm generate-sw # Rebuild service worker
./scripts/pwa/sw-domain.sh # Update SW scope (run after ORIGIN change)
# Documentation
pnpm docs:build # Generate JSDoc + screenshots (requires Python setup)
pnpm docs:jsdocs # Generate JSDoc only
pnpm docs:screen # Generate screenshots only
# Production
pnpm build # Prisma generate + SW + Vite build
pnpm serve # Migrate deploy + seed + start Node server
pnpm start # Build then serveRequired:
ORIGIN: Full URL where app is hosted (critical for CORS, auth cookies)- Dev:
http://localhost:5173 - Docker:
http://localhost:3000 - Production: Your actual domain
- Dev:
Optional (all have sensible defaults):
VITE_SITE_NAME: Site name in browser tab (default: "Vanilla Cookbook")BODY_SIZE_LIMIT: Max upload size in bytes (default: 512kb, recommend 5MB)OPENAI_API_KEY,ANTHROPIC_API_KEY,GOOGLE_API_KEY: LLM providersOLLAMA_BASE_URL: For local AI models (default: http://localhost:11434)GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET: Google OAuthGITHUB_CLIENT_ID,GITHUB_CLIENT_SECRET: GitHub OAuthOIDC_ISSUER_URL,OIDC_CLIENT_ID,OIDC_CLIENT_SECRET: Generic OIDC providerRATE_LIMIT_*: Rate limiting config (has defaults)PASSWORD_*: Password requirements (has defaults)
Note: LLM provider/model selection and registration settings are configured in the Site Settings admin panel, not .env.
-
Recipe: Main recipe data (name, ingredients, directions, times, etc.)- Relations:
AuthUser,RecipeCategory[],RecipePhoto[],RecipeLog[],ShoppingListItem[] - Key fields:
uid(PK),userId(FK),is_public,is_pinned,in_trash,on_favorites
- Relations:
-
RecipePhoto: Multiple photos per recipe- Relations:
Recipe - Fields:
id(PK),recipeUid(FK),url,fileType,isMain,notes
- Relations:
-
RecipeLog: Cooking history- Relations:
Recipe,AuthUser - Fields:
id(PK),recipeUid(FK),userId(FK),cooked,cookedEnd
- Relations:
-
Category: Hierarchical categories- Self-relation:
parent(one),children(many) - Relations:
AuthUser,RecipeCategory[] - Fields:
uid(PK),userId(FK),parent_uid(FK nullable),name,order_flag
- Self-relation:
-
RecipeCategory: Many-to-many join table- Composite PK:
[recipeUid, categoryUid]
- Composite PK:
-
ShoppingListItem: Shopping list entries- Relations:
AuthUser,Recipe(nullable) - Fields:
uid(PK),userId(FK),recipeUid(FK nullable),name,quantity,unit,purchased
- Relations:
-
AuthUser: User accounts & preferences- Relations: All recipe/log/category/shopping data
- Key fields:
id(PK),username(unique),email(unique),units(metric/us),language,theme,isAdmin,isRoot,publicProfile,publicRecipes - Preference flags:
skipSmallUnits,ingSymbol,ingMatch,ingOriginal,ingExtra,useCats
-
Ingredient: Ingredient density database- Fields:
id(PK),name(unique),gramsPerCup
- Fields:
-
SiteSettings: Global settings- Fields:
version,registrationAllowed
- Fields:
AuthSession: User sessionsAuthKey: Password hashesAuthAccount: OAuth accounts
- Location:
src/lib/utils/parse/recipeParse.test.js(main parsing tests) - Framework: Vitest + jsdom + @testing-library/svelte
- Mocking:
parseTesting.jsprovidesmockFetchForURLto test parsing without network requests - Test data: Saved HTML files in
src/lib/data/recipe_html/
pnpm test # Run all tests
pnpm test:watch # Watch mode
pnpm coverage # Coverage report- Add test URL to
downloadRecipes.jssites array - Run script to download HTML:
node src/lib/utils/parse/downloadRecipes.js - Add test case to
recipeParse.test.js - Verify parsing extracts all expected fields
- No TypeScript: Project uses JavaScript with JSDoc types (see
typeDefinitions.js) - Formatting: Prettier enforced, 2-space indents, trailing commas
- Linting: ESLint with svelte, jsdoc plugins
- Markdown docs: Keep edited Markdown files compliant with
.markdownlint.json(runpnpm -s dlx markdownlint-cli <file>.mdon changed docs) - JSDoc: Document all utility functions (see
docs/technical/*.mdfor examples)
- Svelte 5 syntax: Use runes (
$state,$derived,$effect) for reactivity - Component naming: PascalCase for components, kebab-case for files (e.g.,
RecipeCard.svelte) - Props: Destructure props in component
<script>tag - Stores: Use Svelte stores for global state (see
src/lib/stores/) - Styling: Prefer Tailwind utility classes and DaisyUI components; keep custom CSS minimal
- Default approach: Reuse existing components before adding new markup or one-off Tailwind class blocks
- Primary UI primitives: Prefer
src/lib/components/ui/(Button,Card,Dialog,ConfirmationDialog,Spinner,Sidebar,Badge,Container) - Form controls: Prefer
src/lib/components/ui/Form/(Input,Textarea,Dropdown,Checkbox,Toggle,Radio,FileInput,ValidationMessage) - Tabular layouts: Prefer
src/lib/components/ui/Table/(Table,TableHead,TableBody,TableRow,TableCell) - Icons: Reuse
src/lib/components/svg/icons instead of adding inline SVGs - Domain components: Reuse feature components under
recipe/,shopping/,category/,user/,auth/when they match behavior - New components: Only create a new component when no existing component fits; place reusable primitives in
ui/and feature-specific components in their domain folder
- Structure:
src/routes/api/[resource]/[action]/+server.js - Methods: Export
GET,POST,PUT,DELETE,PATCHas needed - Responses: Return
json()from@sveltejs/kit, use HTTP status codes - Error handling: Try-catch, return
{ error: 'message' }with 400/500 status
- CRUD utilities: Use functions from
src/lib/utils/crud.jswhere possible - Transactions: Use Prisma transactions for multi-step operations
- Cascades: Set up in schema (e.g., deleting user cascades to recipes/logs)
- Migrations: Never edit existing migrations, create new ones with
pnpm prisma migrate dev - Prisma Client: Always use the adapter pattern (see Prisma 7 Specifics above)
- Import syntax: Use
import PrismaClientPkg from '@prisma/client'then destructure (CommonJS compatibility)
- Ingredient parser is a submodule: Changes to parsing logic belong in the submodule repo
- Sync carefully:
git submodule update --init --recursiveafter pulling - Commit separately: Submodule commits are independent
- Regenerate after changes: Run
pnpm generate-swif static assets change - Domain changes: Run
./scripts/pwa/sw-domain.shafter changingORIGINin.env - Cache invalidation: SW version bumps automatically on rebuild
- ORIGIN is critical: If
ORIGINdoesn't match actual URL, auth cookies will fail - Docker vs local: Different default ports (3000 vs 5173)
- HTTPS in production: Required for PWA features and secure cookies
- SQLite by default: Single file at
prisma/db/dev.sqlite - Prisma 7 driver: Uses
@prisma/adapter-libsqlwithbetter-sqlite3 - Configuration: Database URL in
prisma.config.ts, not schema - Migrations in version control: All migrations committed to repo
- Seed data: Sample recipes/users created on first run, idempotent
- File uploads:
uploads/directory must persist (not gitignored)
- Sharp dependency: Native module, may need rebuild on platform changes
- Upload size limit: Default 5MB (
BODY_SIZE_LIMIT) - Multiple formats: Supports JPEG, PNG, WebP, AVIF
- Optional, not required: All features work without API keys
- Fallback gracefully: If AI parsing fails, return error to user
- Cost awareness: Image parsing uses GPT-4o (more expensive)
# Create Python virtual environment
virtualenv .venv
source .venv/bin/activate # Or .venv\Scripts\activate on Windows
# Install dependencies
pip install -r docs/scripts/docs-requirements.txt# Ensure dev server is running
pnpm dev
# In separate terminal
pnpm docs:build # Generates JSDoc + screenshots- JSDoc:
docs/technical/*.md(auto-generated from code comments) - Screenshots:
docs/images/*.png(captured from running dev server) - Configuration:
docs/scripts/generate_jsdocs.py,docs/scripts/screenshots.py
- URL state: Search params, filters, pagination
- Svelte stores: Global UI state (theme, user preferences)
- Component state: Local UI state (
$staterunes) - Server data: Loaded via
+page.server.js, passed as props
- SvelteKit load functions: Fetch data server-side, return to page
- API routes: Handle mutations, called from client via
fetch - Prisma: Single source of truth, migrations versioned
- CRUD utilities: Reusable functions for common operations
- Scrape: Download HTML from URL
- Extract: JSON-LD → Microdata → Site config → AI fallback
- Parse ingredients: Use submodule parser
- Convert units: Apply user's unit preference
- Save: Store in database with user ID
- Upload/scrape: Receive file or download from URL
- Validate: Check file type, size
- Process: Resize, compress with Sharp
- Store: Save to
uploads/with unique filename - Reference: Store URL in
RecipePhotomodel
- Ingredient lookup: Match ingredient text to density database
- Volume to weight: Multiply volume (cups) by grams-per-cup
- Temperature: Celsius ↔ Fahrenheit inline conversion
- Fractions: Parse "1/2", "1 1/2" to decimals, convert, re-format
- Ranges: Handle "2-3 cups" (convert both values)
- Parse quantities: Extract numbers from ingredient lines
- Multiply: Apply scale factor (1.5x, 2x, etc.)
- Preserve formatting: Maintain fractions, ranges after scaling
- Fuse.js: Fuzzy search on recipe names, ingredients, descriptions
- Category filter: Hierarchical category matching
- Time filter: Parse ISO durations, filter by prep/cook time
- Favorites/pinned: Boolean flags, sort to top
- Tokenization: Split ingredient line into quantity, unit, ingredient, preparation
- Unit normalization: "cup" = "cups" = "c"
- Language detection: Auto-detect or user-specified
- Multi-language support: Different tokenization rules per language
- Create
src/routes/api/[resource]/[action]/+server.js - Export HTTP method functions (
GET,POST, etc.) - Import Prisma client, CRUD utilities
- Validate input, handle errors, return JSON
- Add JSDoc comments for documentation
- Test with Vitest or manual API calls
- Check
src/lib/components/for an existing component to reuse (especiallyui/,ui/Form/,ui/Table/,svg/) - If no existing component fits, create
src/lib/components/[domain]/[ComponentName].svelte(orui/if broadly reusable) - Use Svelte 5 runes for reactivity (
$state,$derived) - Follow existing Tailwind + DaisyUI patterns (minimal custom CSS)
- Document props with JSDoc comments
- Import and use in relevant pages
- Edit
prisma/schema.prisma - Run
pnpm prisma migrate dev --name [description] - Commit migration files in
prisma/migrations/ - Update seed data if needed (
src/lib/utils/seed/seed.js) - Regenerate Prisma client:
pnpm prisma generate
- Test URL against current parser:
parseURL(url) - If fails, check for JSON-LD, microdata, or add site config
- Add test case to
recipeParse.test.js - Download HTML for offline testing:
downloadRecipes.js - Iterate on
parseHelpers.jsor site configs
- Add ingredients to density database (
src/lib/data/) - Update
converter.jsmatching logic - Test with sample recipes containing new ingredients
- Consider edge cases (fractions, ranges, multiple units)
- Release notes are drafted automatically via Release Drafter (
.github/release-drafter.yml). - Contributors should fill the
## Release Notessection in.github/pull_request_template.mdwith concise user-facing bullets. - PR labels drive release categorization and version bump behavior; apply at least one changelog label on every PR.
- Required label set (create in GitHub UI under repository Issues > Labels if missing):
feature,enhancementfix,bug,bugfixdocs,documentationchore,refactor,test,dependenciesbreaking,majorskip-changelog(exclude from release notes)
- PR title should be clear and user-facing; Release Drafter includes title + body in draft notes.
- Documentation: https://vanilla-cookbook.readthedocs.io/
- GitHub: https://github.com/jt196/vanilla-cookbook
- Issues: https://github.com/jt196/vanilla-cookbook/issues
- Docker Hub: jt196/vanilla-cookbook (
:latest,:stabletags)
- Respect the aesthetic: Keep UI simple, don't over-engineer
- Test parsing changes: Recipe parsing is complex, test thoroughly
- Preserve backwards compatibility: Many users have existing databases
- Document changes: JSDoc comments + manual docs where appropriate
- Consider mobile: PWA features, responsive design critical
- Security: Validate user input, sanitize HTML, prevent injection
- Performance: Optimize queries, lazy load images, paginate lists
- Accessibility: Semantic HTML, keyboard navigation, screen reader support
When in doubt, check existing patterns in the codebase. The project favors consistency over novelty.