A recipe extraction tool that uses local AI (Ollama) to pull recipes from web pages and turn them into structured, clean Markdown.
Add a URL → fetch the page HTML → process it → extract the recipe → save it.
| Area | Tool |
|---|---|
| Framework | Next.js 15 (App Router, Turbopack) |
| Auth | NextAuth.js v5 beta — magic-link email via Nodemailer |
| Database | SQLite via Prisma |
| AI / LLM | Ollama (local inference) |
| UI | daisyUI 5 + Tailwind CSS v4 + @heroicons/react |
| Forms / Actions | next-safe-action + zod + zod-form-data |
| Data Fetching | SWR |
| Styling Utilities | clsx + tailwind-merge |
| HTML Processing | sanitize-html + isomorphic-dompurify |
| Markdown | marked |
| Package Manager | pnpm |
| CI | GitHub Actions (format check, peer-dep check, build) |
- Node.js 24.x (version specified in
.tool-versions; if you use asdf, runasdf install) - pnpm (install guide)
- Ollama (install guide) — used for local LLM inference
- Docker (install guide) — used only for the local dev mail server
pnpm i
ollama pull mistralCopy the template and fill in your local values:
cp .env .env.localThen edit .env.local. The template .env contains sensible dev defaults for the mail server (smtp4dev), so the only value you must change is AUTH_SECRET:
# Generate a proper secret:
pnpm dlx auth secret| Variable | Purpose | Dev default |
|---|---|---|
DATABASE_URL |
SQLite database file path | file:./dev.db |
OLLAMA_MODEL |
Ollama model name | mistral |
AUTH_SECRET |
NextAuth encryption key | — must generate — |
EMAIL_SERVER_HOST |
SMTP host | localhost |
EMAIL_SERVER_PORT |
SMTP port | 1025 |
EMAIL_SERVER_USER |
SMTP username | username |
EMAIL_SERVER_PASSWORD |
SMTP password | password |
EMAIL_FROM |
Sender address | noreply@example.com |
Note
The EMAIL_SERVER_* values match the smtp4dev container defined in docker-compose.yml. Don't change them unless you also update the container config.
pnpm prisma:generate
pnpm prisma:migrateOptional — seed with sample data:
pnpm db:seedTo wipe and re-seed:
pnpm db:drop
pnpm db:seeddocker compose up -dIf you get a "permission denied" error connecting to the Docker API, make sure your user is in the docker group (see Docker post-installation steps).
This launches smtp4dev — a local SMTP server with a web UI for viewing emails sent during development.
pnpm devOpens the Next.js dev server with Turbopack on http://localhost:3000.
Cookbook uses NextAuth.js v5 with the Nodemailer provider (magic-link email sign-in). There are no passwords — users enter their email, receive a sign-in link, and click it.
How it works locally:
- On any page, click Sign In (or navigate to
/api/auth/signin). - Enter any email address.
- Open the smtp4dev web UI at http://localhost:8025.
- Find the sign-in email and click the magic link.
Middleware protection: All routes except /, /api/auth/signin, and /api/auth/callback/nodemailer require authentication. Unauthenticated visitors are redirected to the sign-in page.
Session strategy: JWT-based (not database sessions), with the user's database ID attached to the session via a callback in src/lib/auth.ts.
User creation: Users are automatically created on first sign-in via findOrCreateUserByEmail in the auth session callback.
Each "Source" goes through a 5-step pipeline:
- Add URL — provide a web page URL
- Fetch HTML — the server fetches the full HTML from that URL
- Process HTML — strips navigation, footers, scripts, etc. and isolates recipe-like content
- Extract Recipe — sends processed HTML to Ollama, which returns structured Markdown
- Create Recipe — Ollama parses the extracted Markdown into a named recipe object and saves it
Steps 4 and 5 call Ollama's local generate endpoint with custom system prompts (see src/lib/helpers/prompts.ts). Each step can be re-run independently.
src/
├── app/
│ ├── api/
│ │ ├── auth/ # NextAuth routes
│ │ └── sources/[sourceId]/
│ │ ├── html/ # Fetch raw HTML from source URL
│ │ ├── html/extract-recipe/ # Extract recipe via Ollama
│ │ └── create-recipe/ # Create recipe via Ollama
│ ├── recipes/ # Recipe pages & forms
│ ├── sources/ # Source pages, forms, modals
│ ├── Navbar.tsx
│ ├── Footer.tsx
│ ├── Hero.tsx
│ └── ...
├── lib/
│ ├── actions/ # next-safe-action server actions
│ │ ├── auth.ts # signIn / signOut actions
│ │ ├── sources.ts # CRUD actions for sources
│ │ └── recipes.ts # CRUD actions for recipes
│ ├── db/ # Prisma query helpers
│ │ ├── users.ts
│ │ ├── sources.ts
│ │ ├── recipes.ts
│ │ └── seed/ # Seed script & fixtures
│ ├── helpers/
│ │ ├── html.ts # HTML processing (sanitize, condense)
│ │ ├── markdown.ts # Markdown → HTML rendering
│ │ ├── prompts.ts # Ollama system prompts
│ │ ├── source.ts # Source progress calculation
│ │ └── index.ts # cn() utility
│ ├── hooks/ # SWR hooks for API calls
│ ├── auth.ts # NextAuth config
│ ├── prisma.ts # Prisma client singleton
│ ├── safe-action.ts # Safe-action client (with auth middleware)
│ ├── fetcher.ts # SWR fetcher
│ └── ...
├── auth.config.ts # Edge-compatible auth config
├── middleware.ts # Route protection
└── types.ts # Shared types
| Script | What it does |
|---|---|
pnpm dev |
Start Next.js dev server (Turbopack) |
pnpm build |
Generate Prisma client + production build |
pnpm start |
Start production server |
pnpm tsc |
Type-check (includes Prisma generation) |
pnpm lint:check |
ESLint check |
pnpm lint:fix |
ESLint auto-fix |
pnpm format:check |
Prettier check |
pnpm format:fix |
Prettier auto-fix |
pnpm prisma:generate |
Generate Prisma client |
pnpm prisma:migrate |
Run database migrations |
pnpm prisma:studio |
Open Prisma Studio GUI |
pnpm db:seed |
Seed the database with sample data |
pnpm db:drop |
Reset the database |
pnpm check:peer-deps |
Check for peer dependency warnings |
SQLite, managed by Prisma. The schema lives in prisma/schema.prisma and defines four main models:
- User — identity, linked to accounts, sessions, sources, and recipes
- Source — a URL with progressively filled fields:
fullHtml→processedHtml→extractedRecipe - Recipe — a name + Markdown content, always linked to a source
- Account / Session / VerificationToken — NextAuth internal models
Run pnpm prisma:studio to browse data in a GUI.
We're using daisyUI with the dracula theme (configured in src/app/globals.css).
For iconography, we're using @heroicons/react. See the full list of icons here and their exported components here. You should import from the @heroicons/react/24/solid package:
import { GlobeAltIcon } from "@heroicons/react/24/solid";
...
<GlobeAltIcon className="size-4" />Example prompt: give me a light daisyUI 5 theme with tropical color palette. use context7 (the suffix is required)