A mobile-first German news app that adapts today's headlines to your CEFR level (A1–C1) using OpenAI.
- Fetches 5 German RSS feeds daily (Tagesschau, ZEIT, Spiegel, FAZ, Süddeutsche)
- Deduplicates and normalizes articles
- Generates CEFR-leveled German summaries via OpenAI
- Stores everything in Supabase
- Single-page Next.js frontend with expandable card UI
git clone <repo-url>
cd german-news-app
npm installcp .env.local.example .env.localEdit .env.local and fill in all values:
| Variable | Where to find it |
|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Supabase → Project Settings → API → Project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Supabase → Project Settings → API → anon key |
SUPABASE_SERVICE_ROLE_KEY |
Supabase → Project Settings → API → service_role key |
OPENAI_API_KEY |
platform.openai.com → API Keys |
CRON_SECRET |
Any random string (e.g. openssl rand -hex 32) |
OPENAI_MODEL |
Optional. OpenAI model ID (default: gpt-5-nano-2025-08-07). For GPT-5 models, sampling params like temperature are not sent (model requires defaults). |
MAX_HEADLINES_PER_DAY |
Optional. Max headlines to generate OpenAI summaries for per run (default: 20). Set to 2 or 3 locally to limit API cost. |
Open the Supabase SQL Editor for your project and run the contents of:
supabase/migrations/001_initial.sql
This creates the four required tables: daily_issue, headline, headline_summary, pipeline_run.
npm run devVisit http://localhost:3000.
The page will show an empty state until the pipeline has been run.
The pipeline fetches today's RSS feeds, deduplicates headlines, and generates OpenAI summaries for all CEFR levels.
curl -X POST http://localhost:3000/api/internal/run-daily-pipeline \
-H "x-cron-secret: your-cron-secret-here"Replace your-cron-secret-here with the value of CRON_SECRET in your .env.local.
A successful response looks like:
{
"ok": true,
"issueDate": "2026-02-19",
"headlinesProcessed": 18,
"summariesGenerated": 15,
"errors": [],
"durationMs": 42000
}Note: The pipeline calls OpenAI once per new headline (generating all 5 levels in a single request). With 20 headlines it typically takes 1–3 minutes.
The pipeline is fully idempotent. Re-running on the same day:
- Skips headlines already in the database (dedupe by SHA-256 key)
- Skips headlines that already have all 5 summaries
- Only generates missing summaries
Set up a daily cron job to call the endpoint at a fixed time (e.g. 06:00 Europe/Berlin). Services that can trigger HTTP endpoints on a schedule:
- Vercel Cron Jobs (add to
vercel.json) - GitHub Actions scheduled workflow
- Upstash QStash
- Any cron service that can POST to a URL
Example vercel.json for Vercel Cron:
{
"crons": [
{
"path": "/api/internal/run-daily-pipeline",
"schedule": "0 5 * * *"
}
]
}Add CRON_SECRET as an environment variable in Vercel dashboard.
Returns today's headlines at the specified CEFR level.
Query params:
level— one ofA1,A2,B1,B2,C1(default:B1)
Response:
{
"issueDate": "2026-02-19",
"level": "B1",
"lastUpdated": "2026-02-19T06:42:00Z",
"items": [
{
"id": "uuid",
"titleDe": "...",
"summaryDe": "...",
"bulletsDe": ["...", "...", "..."],
"sourceName": "Tagesschau",
"canonicalUrl": "https://...",
"publishedAt": "2026-02-19T05:00:00Z"
}
]
}Triggers the daily pipeline. Requires x-cron-secret header.
src/
app/
page.tsx — Main SPA page
layout.tsx
globals.css
api/
news/today/route.ts — GET headlines by level
internal/run-daily-pipeline/route.ts — POST pipeline trigger
components/
LevelSelector.tsx
HeadlineCard.tsx
ExpandedCard.tsx
SkeletonCard.tsx
hooks/
useNews.ts
lib/
types.ts
supabase-browser.ts — Client-side (anon key)
supabase-server.ts — Server-side (service role)
rss.ts — RSS fetch + dedupe
openai.ts — CEFR summary generation
pipeline.ts — Full pipeline orchestration
supabase/
migrations/
001_initial.sql