Files
Julien Herr 44fcbfc4f6 fix(favicon): fall back to apex domain when subdomain hosts no icon
Senders on a subdomain that hosts no favicon (e.g. mail.example.com) left
feeds blank because both the direct /favicon.ico and the DuckDuckGo lookup
were tried only against the full subdomain. Resolution now walks up to the
apex via Domain.parents() and caches the result under the original sender
domain.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 23:49:43 +02:00

182 lines
8.8 KiB
Markdown

# Changelog
All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Keep the `## [Unreleased]` section up to date **as part of every change** (the
same rule as the rest of the docs). At release time `npm run release X.Y.Z`
promotes this section to `## [X.Y.Z]` and the Release workflow publishes it
verbatim as the GitHub Release notes — so what you write here is what ships.
## [Unreleased]
### Added
- The admin dashboard now shows each feed's email count on its **Emails** button
and a **"Last email …"** freshness line under the feed title, in both the list
and table views. Both values are projected into `feeds:list`, so the dashboard
stays a single KV read; they backfill on a feed's next email or save.
### Fixed
- Per-feed favicons now resolve for senders on a subdomain that hosts no icon of
its own (e.g. `mail.example.com`): the lookup walks up to the apex domain
(`example.com`) and uses its favicon, caching it under the original sender
domain. Previously both the direct `/favicon.ico` and the DuckDuckGo lookup
were tried only against the full subdomain, leaving such feeds blank.
- Subscription-confirmation detection now flags code-based signup verifications
(OTP) that have no link to click — e.g. "Your verification code is 371404",
whose only link is a `mailto:` support address. These cleared the keyword
threshold but were dropped because the detector required an http(s) candidate
link. A code path now raises the flag/badge/banner when a verification keyword
sits next to an OTP-style code; the code itself is never extracted or surfaced.
- Subscription-confirmation detection now recognizes localized "subscribe" CTAs.
The weak link-signal vocabulary was English-only (`subscrib`),
so a genuine double opt-in whose confirm button reads "Je m'inscris…" over an
opaque tracking redirect scored 0 on every link and was missed. The weak vocab
is now multilingual (FR/DE/ES) to match the confirmation keywords.
- Per-feed favicons no longer fail for senders whose DuckDuckGo icon is a
hi-res PNG: the maximum accepted favicon size is raised from 100 KB to 256 KB,
so legitimate large icons (~107 KB and up) are cached instead of rejected.
A domain that was already negatively cached only re-fetches once that entry's
TTL expires (and something — a new email or a favicon request — retriggers
the fetch); delete its `icon:<domain>` KV key to force an immediate refresh.
- Admin dashboard table view: long feed titles no longer overflow into the Feed
ID column — the title/description cell now shrinks so its text ellipsises.
- RSS and Atom feeds now advertise the WebSub hub inside the feed body
(`<atom:link rel="hub">`), not just in the HTTP `Link` header. Readers like
FreshRSS discover the hub from the XML, so they can now subscribe and receive
an instant push when a new email arrives instead of waiting up to the cache
`max-age` (30 min) to refresh.
- Subscription-confirmation detection now recognises a confirm email whose CTA
button carries the subscribe/subscription hint only in its visible text (e.g.
"Yes, subscribe me to this mailing list.") over an opaque tracking-redirect
href — previously the link scored zero and the email was missed.
- Sender favicons now recover from a transient miss: a failed favicon lookup is
cached negatively for 6 hours instead of a full week, so a domain whose icon
was momentarily unavailable (e.g. not yet indexed upstream) is retried on the
next email instead of staying blank for days.
- Feed entry HTML now escapes bare ampersands in attribute URLs (e.g. query
strings like `?a=1&b=2`), clearing the W3C feed validator's "Named entity
expected. Got none." warning and improving interoperability with stricter
feed readers.
## [0.3.1] - 2026-05-25
### Fixed
- Feed self link (RSS/Atom/JSON) is derived from the configured domain instead
of the request host — it no longer leaks the `workers.dev` host when a feed is
reached directly, and now matches the alternate link.
## [0.3.0] - 2026-05-25
### Added
- **Native feed detection** — incoming newsletters are inspected for a
self-advertised Atom/RSS/JSON feed (`rel=alternate` links in the email HTML);
discovered feeds are stored per sender on the Feed aggregate and surfaced as
chips on the feed detail page, a dashboard pill, and (read-only) on the REST
`Feed` schema, with a dismissable notice.
- **Subscription confirmation surfacing** — confirmation emails ("click to
confirm your subscription") are detected at ingestion and flagged on the feed;
the admin UI surfaces the confirmation link, a badge, a dashboard pill, and an
inline banner (all dismissable), tightened against false positives via a
weak-signal heuristic.
- **JSON Feed 1.1** output (`/json/:feedId`).
- **OPML export** of all feeds (`/admin/opml`).
- **Conditional GET** (ETag / Last-Modified / 304) on the feed routes.
- Per-feed **Subscribe chips** for RSS/Atom/JSON with copy / open / validate
actions, reused across dashboard and feed detail page.
- Email detail page links to its public entry page; land on the feed's emails
page right after creation.
- Optional **per-feed "sender in title"** toggle.
- Running **version** shown in the admin/status footer, `/health`, and
`/api/v1/stats`.
### Changed
- **Read/write identity decoupling (privacy)** — the public read id (`FeedId`,
used in `/rss/:feedId`) is fully decoupled from the inbound email address
(`MailboxId`, `noun.noun.NN`); a feed's read URL never reveals its inbound
alias and vice-versa (reading `/rss/<noun.noun.NN>` 404s).
- Sender display name, site URL and parsing now owned by the `EmailAddress`
value object (DDD cleanup).
- Release version is derived from the git tag; CI guards against tagging the
wrong commit.
## [0.2.1] - 2026-05-24
### Added
- Optional `FALLBACK_FORWARD_ADDRESS`: forward non-feed mail to a verified
address so a domain catch-all can point at kill-the-news without swallowing
personal mail (forwarded mail is counted in the stats dashboard).
### Changed
- Feed, entry, and attachment responses send `X-Robots-Tag: noindex`; a new
`/robots.txt` disallows `/rss`, `/atom`, `/entries`, `/files`, and `/admin`
private feeds and emails stay out of search engines.
- Relative links/images in email bodies are absolutized against the sender's
site; lazy-loaded images are promoted so they don't render blank.
- Feed `<title>` is plain text (HTML stripped, entities decoded).
- Sender-site derivation moved onto the `EmailAddress` value object
(`siteBaseUrl`).
### Fixed
- XML-illegal control characters are stripped from generated feeds (valid astral
characters such as emoji preserved).
## [0.2.0] - 2026-05-24
### Added
- Versioned REST API (`/api/v1/feeds*`) with an OpenAPI 3.1 spec
(`/api/openapi.json`) and rendered reference docs via Scalar (`/api/docs`).
- `/api/v1/stats` as the canonical public stats endpoint (JSON + CORS).
- Optional R2 attachment storage with a config toggle, storage metrics, download
links on the email/admin views, and inline `cid:` image rendering.
- Project favicon (`/favicon.svg`, `/favicon.ico`) and per-feed favicon derived
from the last sender's domain (`/favicon/:feedId`).
- RFC 8058 one-click unsubscribe dispatched when a feed is deleted.
### Changed
- Large internal refactor toward a clean domain-driven architecture; redesigned
landing/status page.
### Removed
- The deprecated `/api/stats` endpoint (use `/api/v1/stats`).
## [0.1.0] - 2026-05-22
### Added
- **Atom feed format** (`/atom/:feedId`) alongside RSS 2.0.
- **WebSub push notifications** advertised via `Link` header for real-time
delivery instead of polling.
- **HTML email processing** — bodies sanitized via `linkedom` + `escape-html`
(XSS prevention, MSO style stripping, plain-text fallback).
- **Email attachments as RSS enclosures**, stored in R2 and served at
`/files/:attachmentId/:filename`.
- **Sender blocklist** with 4-level priority matching and a quick-add dropdown.
- **`EMAIL_DOMAIN`** env var to separate web domain and email domain.
- **Authelia / reverse-proxy auth** via trusted headers (`Remote-User`,
`X-Forwarded-User`).
- Demo environment auto-deployed to `demo.kill-the.news` with a nightly KV
reset.
- Admin UI redesign (Inter font, orange theme), client scripts compiled via
esbuild, templates on `hono/jsx`.
[Unreleased]: https://github.com/juherr/kill-the-news/compare/v0.3.1...HEAD
[0.3.1]: https://github.com/juherr/kill-the-news/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/juherr/kill-the-news/compare/v0.2.1...v0.3.0
[0.2.1]: https://github.com/juherr/kill-the-news/compare/v0.2.0...v0.2.1
[0.2.0]: https://github.com/juherr/kill-the-news/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/juherr/kill-the-news/releases/tag/v0.1.0