個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證ZapiszPrzepis — archive-first PWA for saving recipes shared from social media
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗An archive-first PWA for saving recipes shared from social media. Every URL arrives via the system Share gesture and is transformed into a permanent, Polish-language copy of the recipe — independent of the original source.
Stack: Next.js 16 (App Router) · TypeScript · Tailwind v4 · Supabase (auth + Postgres + storage) · Inngest (async jobs) · PWA (offline + Web Share Target) · Cloudflare Workers
Live app: https://zapiszprzepis.pl
Registration is gated by an invite code. The code is a runtime secret (INVITE_CODE),
never committed — open an issue
if you would like one to try the app.
Every shared link goes through the same pipeline — fetch the source, extract the recipe with an LLM into a Polish-language copy, archive the image — and only the fetch step differs per source.
| Source | How the recipe text is obtained | Limits |
|---|---|---|
Facebook Reels and videos — facebook.com/reel/…, …/videos/…, app share links facebook.com/share/r/…, fb.watch | The post caption, read from Facebook's embedded-video plugin (plugins/video.php?show_text=true) — no Facebook session needed, and the part hidden behind "See more" is included. The cover frame becomes the recipe image and is archived right away, because Facebook's image links expire within days. | A recipe that exists only in the video, with no caption, cannot be extracted. Text and photo posts are not supported — Facebook's post plugin refuses them. |
YouTube — watch links, youtu.be, Shorts | The video description, scraped with Firecrawl. | The player is embedded on the recipe page. |
| Blogs and recipe sites — any other URL | Firecrawl main-content scrape, retried on the full page for classic blog templates; Blogspot posts come straight from the Blogger JSON feed. | An embedded YouTube player on the post is detected and embedded on the recipe page. |
A share that cannot be turned into a recipe lands in the header bell with the reason, and can be retried from there.
Requirements: Node ≥ 20.6, pnpm (Corepack-managed — version pinned in package.json#packageManager), Supabase account, Cloudflare account.
Clone and install:
git clone https://github.com/spokospace/zapiszprzepis.git
cd zapiszprzepis
corepack enable # one-time — activates the pinned pnpm version
pnpm install
Create a Supabase project at https://supabase.com/dashboard:
zapiszprzepis, Region: Central EU (Frankfurt), Plan: Freehttps://<ref>.supabase.co)anon public JWT)Configure redirect URLs in Supabase Authentication → URL Configuration:
https://zapiszprzepis.plhttp://localhost:3000/auth/callbackhttps://zapiszprzepis.pl/auth/callbackLocal .env.local:
cp .env.local.example .env.local
# fill in NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY
Link Supabase CLI and push migrations:
pnpm exec supabase login
pnpm exec supabase link --project-ref <ref>
pnpm exec supabase db push --linked
Recipe extraction (scraping + LLM) runs asynchronously via Inngest, outside the Cloudflare Worker request path (NFR p95 ≤ 3 min). Locally:
Start the Inngest Dev Server (separate terminal):
npx inngest-cli@latest dev
# → listens on http://localhost:8288
Start Next.js dev:
pnpm dev
Local dashboard: http://localhost:8288 — "Runs" tab shows execution history
Local secrets (.env.local):
INNGEST_EVENT_KEY=... # Inngest Dashboard → Event Keys
INNGEST_SIGNING_KEY=... # Inngest Dashboard → Signing Keys
Deployment:
pnpm run deploy # deploys the Worker to Cloudflare (Inngest connects via /api/inngest)
# `run` is required: bare `pnpm deploy` is pnpm's own workspace-deploy command, not this script
Two cron triggers run in the deployed Worker, declared in wrangler.jsonc and
dispatched by custom-worker.ts.
| Cron (UTC) | Job | Why |
|---|---|---|
30 3 * * * | Dump recipes + recipe_shares to R2 as recipes/<YYYY-MM-DD>.json | The free Supabase plan includes no backups |
0 4 * * * | SELECT one row through PostgREST | The free plan pauses a project after a week without database activity |
One-time setup — the backup silently logs backup: skipped until both exist:
npx wrangler r2 bucket create zapiszprzepis-backups
npx wrangler secret put SUPABASE_SERVICE_ROLE_KEY # paste the key when prompted
The dump needs the service role because RLS would otherwise hand it an empty array. It covers table rows only — archived images live in Supabase Storage and are not copied.
Restore is manual: fetch the object, then feed the arrays back through PostgREST or the SQL editor.
npx wrangler r2 object get zapiszprzepis-backups/recipes/2026-09-05.json --file backup.json
Nothing prunes old objects. At a few hundred kB a day that stays far inside R2's free tier for years, but it does grow without bound.
Installable on Android (Chrome/Edge), Windows, and macOS. Offline-capable with Web Share Target API.
Installation:
Web Share Target (system Share gesture):
text/plain, text/uri-list/share → processes URL → recipe appears in listService Worker & Offline (public/sw.js, tests in src/lib/sw.test.ts):
/_next/static/*, content-hashed): cache-first/api/*, and every non-GET request (Server Actions, Web Share Target): network only, never cached/), then an „Offline” response/sw.js?v=<build>: the new worker gets fresh caches and waits until every app window is closed, so open tabs are never reloadedpnpm dev # http://localhost:3000
pnpm build # production build
pnpm lint # ESLint
pnpm test # Vitest unit tests
pnpm check:auth # smoke test: ping Supabase auth/v1/health
src/middleware.ts — checks the session on every request with supabase.auth.getClaims() (the JWT is verified locally; an expiring session is refreshed); redirects unauthenticated users to /login.src/lib/supabase/{server,client,proxy}.ts — three Supabase client helpers with getAll/setAll cookie adapter.src/app/login/ — Server Component + Server Action (signInWithEmail — magic-link OTP).src/app/rejestracja/ — invite-code gated registration (/signup redirects here).src/app/auth/callback/ — Route Handler (exchangeCodeForSession with error mapping).supabase/migrations/ — baseline: pg_trgm + public.current_user_id() + RLS policies.Full PRD, roadmap, and implementation plans live in context/:
context/foundation/prd.md — Product Requirements Documentcontext/foundation/roadmap.md — feature map F-01 … S-09context/foundation/tech-stack.md — stack rationalecontext/foundation/test-plan.md — risk-based quality contractcontext/changes/<change-id>/ — per-change folder with plan and progress