Hacker News Daily digest ๐Ÿ“ฐ

August 28, 2026 ยท View on GitHub

A completely free, self-hosted email newsletter that sends you the top Hacker News stories every morning, each with a short AI summary (article + community reaction) and links to both the article and its HN discussion.

No servers, no paid services, no n8n. Just a GitHub Action on a cron schedule.

Top-N stories ยท AI summaries by Google Gemini ยท delivered over Gmail SMTP

๐Ÿ“„ See a sample (generated from the .html file).

Example Hacker News Daily digest email

Project structure

.
โ”œโ”€โ”€ .github/workflows/          # GitHub Actions (daily cron + manual dispatch)
โ”‚   โ””โ”€โ”€ daily-digest.yml
โ”œโ”€โ”€ docs/
โ”‚   โ”œโ”€โ”€ example-email.png       # Screenshot shown in this README
โ”‚   โ””โ”€โ”€ example-digest.html     # Sample rendered email (open in a browser)
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ config.py               # Env-based configuration
โ”‚   โ”œโ”€โ”€ hn_client.py            # Hacker News API client (stories + comments)
โ”‚   โ”œโ”€โ”€ article_fetcher.py      # Best-effort article text extraction
โ”‚   โ”œโ”€โ”€ summarizer.py           # Gemini summaries (batched, paced + retried)
โ”‚   โ”œโ”€โ”€ schedule.py             # Reads the workflow cron -> "next run" time
โ”‚   โ”œโ”€โ”€ email_renderer.py       # HTML email template
โ”‚   โ”œโ”€โ”€ mailer.py               # Gmail SMTP sender
โ”‚   โ””โ”€โ”€ list_models.py          # Helper: list models your API key supports
โ”œโ”€โ”€ main.py                     # Orchestrator (fetch -> summarize -> send)
โ”œโ”€โ”€ pyproject.toml              # Project metadata & dependencies (uv)
โ”œโ”€โ”€ uv.lock                     # Locked, reproducible dependency versions
โ”œโ”€โ”€ .env.example                # Template for local environment variables
โ””โ”€โ”€ README.md                   # This file

Why it's free

PieceServiceCost
Scheduling / computeGitHub ActionsFree, unlimited minutes for public repos
Story dataHacker News Firebase APIFree, no auth, no rate limits
AI summariesGoogle Gemini API (free tier)Free; stories are batched into ~4 requests/day
Email deliveryGmail SMTP (app password)Free

The consumer Google AI Pro subscription is not required; the Gemini API has its own free tier (the pricing page lists the Free, Paid, and Enterprise tiers) available to any Google account.

How it works

GitHub Actions (cron, ~05:17 Poland time)
        โ”‚
        โ–ผ
  main.py  โ”€โ”€โ–ถ  Hacker News API      (top 30 stories + top comments)
        โ”‚  โ”€โ”€โ–ถ  fetch article pages   (best-effort text extraction)
        โ”‚  โ”€โ”€โ–ถ  Google Gemini         (batched: ~8 stories per request)
        โ”‚  โ”€โ”€โ–ถ  render HTML email
        โ–ผ
   Gmail SMTP  โ”€โ”€โ–ถ  your inbox ๐Ÿ“ฌ

Each story in the email shows its rank (#1, #2, โ€ฆ), score, title (โ†’ article), source domain, N replies (โ†’ HN discussion), author, age, and a concise 1-2 sentence summary.


Setup (โ‰ˆ 10 minutes)

1. Fork / use this repo

Fork it (or click Use this template) into your own account so the Action runs under your quota. Keep it public for free unlimited Actions minutes.

2. Get a free Gemini API key

Go to aistudio.google.com/apikey โ†’ Create API key. Copy it.

3. Create a Gmail App Password

  1. Enable 2-Step Verification on your Google account.
  2. Go to myaccount.google.com/apppasswords.
  3. Create a password (name it e.g. "HN digest"). Copy the 16-character value.

Tip: consider using a dedicated Gmail account as the sender (e.g. you.news@gmail.com) instead of your personal one. It keeps the daily digest out of your personal Sent folder, gives the newsletter its own avatar (set a profile picture on that account), and isolates it. Point RECIPIENTS at your real inbox.

4. Add repository secrets

In your repo: Settings โ†’ Secrets and variables โ†’ Actions โ†’ New repository secret.

SecretValue
GEMINI_API_KEYyour Gemini key
GMAIL_USERNAMEyou@gmail.com
GMAIL_APP_PASSWORDthe 16-char app password
RECIPIENTS(optional) comma-separated recipients; defaults to GMAIL_USERNAME

Optional Variables (same page, Variables tab) to tweak without editing code: NUM_STORIES (default 30), GEMINI_MODEL (default gemini-3.7-flash).

Model not available? Gemini model IDs change over time. If a run fails with a 404 ... model is no longer available error, list what your key supports and set GEMINI_MODEL to one of them:

GEMINI_API_KEY=your-key uv run python -m src.list_models

5. Test it

Go to Actions โ†’ Daily HN Digest โ†’ Run workflow.

  • Tick Dry run to build the email as a downloadable artifact without sending.
  • Leave it unticked to send a real email.

6. Done

It now runs automatically every day at โ‰ˆ 05:17 Poland time, year-round.

How the timing survives daylight saving: GitHub cron is UTC-only and ignores DST, so the workflow schedules two UTC times (03:17 and 04:17) and a guard (RUN_ONLY_AT_LOCAL_HOUR=5) lets only the run that actually lands on 05:xx in DISPLAY_TIMEZONE proceed; the other exits in seconds. To change the time, edit the two cron lines and the guard hour in .github/workflows/daily-digest.yml (times there are UTC; crontab.guru helps).


Run locally (without GitHub Actions)

The whole digest runs as a plain Python script, so you can generate or send it from your own machine with no GitHub Actions involved. You only need a Gemini API key; Gmail credentials are needed solely for the real send.

Dependencies are managed with uv (install it first, then run):

uv sync                   # creates a virtualenv from uv.lock
cp .env.example .env      # then fill in your keys

# Preview only: writes output/digest.html and sends nothing.
# Needs just GEMINI_API_KEY. Open the file in a browser to see the result.
DRY_RUN=true uv run python main.py

# Send for real (needs GMAIL_USERNAME + GMAIL_APP_PASSWORD too).
uv run python main.py

This is exactly what the GitHub Action does; it just runs the same python main.py on a schedule. Running locally is handy for previewing layout changes or sending an ad-hoc digest.

Trigger a run manually

You don't have to wait for the daily schedule; you can run it any time:

From GitHub (no setup needed): open Actions โ†’ Daily HN Digest โ†’ Run workflow. Tick Dry run to build a downloadable HTML artifact without sending, or leave it unticked to send a real email.

From your terminal (needs the gh CLI):

gh workflow run "Daily HN Digest" -f dry_run=true    # preview (no email)
gh workflow run "Daily HN Digest"                    # send for real

Manual runs always execute immediately and bypass the daylight-saving guard (that guard only applies to the scheduled cron runs).

Configuration

All settings are environment variables (see .env.example):

VariableDefaultDescription
GEMINI_API_KEYโ€”Gemini API key (required)
GEMINI_MODELgemini-3.7-flashModel used for summaries
GEMINI_FALLBACK_MODELSgemini-3.6-flashComma-separated models tried if the above is unavailable
GMAIL_USERNAMEโ€”Sender Gmail address
GMAIL_APP_PASSWORDโ€”Gmail app password
RECIPIENTSsenderComma-separated recipients
NUM_STORIES30Stories per email
MIN_SCORE0Skip stories below this score
MAX_COMMENTS6Top comments fed to the summarizer
BATCH_SIZE8Stories summarized per Gemini request
DISPLAY_TIMEZONEEurope/WarsawTimezone for timestamps shown in the email
DISPLAY_TZ_LABEL(empty)Force a fixed tz label; empty = DST-aware (CET/CEST)
RUN_ONLY_AT_LOCAL_HOUR(empty)DST guard: only run at this local hour on schedule
FETCH_ARTICLEStrueAlso fetch article bodies for context
REQUEST_DELAY_SECONDS6Pause between Gemini batches (free-tier pacing)
GEMINI_TIMEOUT_SECONDS90Hard cap on a single Gemini request
SUMMARY_DEADLINE_SECONDS600Total budget for summarizing; then fall back
DRY_RUNfalseWrite HTML file instead of emailing

Notes & troubleshooting

  • Gemini free-tier rate limits are per-minute. Stories are summarized in batches (BATCH_SIZE, default 8), so 30 stories cost only ~4 requests. If you still see 429 retries, lower BATCH_SIZE or raise REQUEST_DELAY_SECONDS.
  • 404 model not available? Model IDs get deprecated. Run uv run python -m src.list_models and set the GEMINI_MODEL variable to a listed one.
  • Every summary says (summary unavailable)? The model was unreachable for the whole run, usually a sustained 503 while Google rebalances capacity for a popular ID. GEMINI_FALLBACK_MODELS exists for exactly this: the run switches to the next model listed and stays there. If even the fallbacks are down, the digest still sends with placeholders rather than failing.
  • Some articles won't be fetched (paywalls, JS-only, PDFs, videos). The summary then falls back to the title + HN comments, which is usually enough.
  • Email in spam? Mark it "not spam" once; sending to yourself is very reliable.
  • Scheduled runs can be delayed by GitHub during peak load, and occasionally skipped altogether. Roughly 40 minutes late is typical here; we have also seen 11 hours, and days with no run at all. schedule is best-effort and GitHub offers no delivery guarantee, so this is normal Actions behaviour rather than a bug in the digest. Run the workflow manually from the Actions tab to catch up.
  • A run that seems stuck is bounded from two directions: every Gemini request gives up after GEMINI_TIMEOUT_SECONDS, and the whole summarization phase after SUMMARY_DEADLINE_SECONDS, after which remaining stories get placeholder summaries so the email still goes out. The job's own timeout-minutes: 30 is only an outer backstop; reaching it means something is genuinely wrong.