Weak subscribe/subscription signals are now matched on the link href OR its
visible text (matched once, not additively), so a double opt-in email whose
button reads "Yes, subscribe me…" over an opaque tracking-redirect href is no
longer missed. Adds a regression test with anonymized fixture data.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
--notes-file (unlike --generate-notes) leaves the release name blank; pass
--title so releases keep a heading.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Only a bug fix (feed self link) landed since 0.3.0, so the next release is a
patch, not a minor. Correct the prematurely-opened 0.4.0 cycle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduce CHANGELOG.md (Keep a Changelog) as the single source of release
notes, and scripts/release.sh (npm run release X.Y.Z) which promotes the
Unreleased section, commits the bare version as a real release commit, tags
it, and reopens the next -develop cycle. The Release workflow now verifies the
tagged commit's version equals the tag and publishes the CHANGELOG section as
the release notes instead of auto-generated commit lists.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Surface the xExtension-KillTheNews integration as a differentiator on
the marketing landing page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The RSS/Atom/JSON self link was derived from the request origin, leaking
the workers.dev host when reached directly instead of via the custom
domain. Use the configured-domain URL builders so self matches alternate.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
email-processor parsed input.from twice — once via EmailAddress for the
native-feed base, once via the favicon infra helper extractEmailDomain
just to get the domain. CLAUDE.md forbids reaching across a layer to
parse a domain: parse once and derive both siteBaseUrl() and domain.value
from the EmailAddress VO, removing the infrastructure/favicon-fetcher import.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
NativeFeedChip duplicated ~all of FormatChip's accessible copy-script
markup. Extract one FeedChip (copy + open + optional validate); both the
Subscribe formats and native feeds now render through it, keeping the
copyable-value/data-copy markup identical in one place.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wire the NativeFeeds chip group into the per-feed emails page, add a
dismissable banner that nudges users to subscribe directly, the dismiss
POST route mirroring the confirmation-dismiss idiom, and the client-side
handler in emails-page.ts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add NativeFeeds/NativeFeedChip components to admin/ui.tsx and a
NativeFeedPill rendered in both list and table dashboard views when
feed.hasNativeFeed is set.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GET /v1/feeds/{id} and PATCH /v1/feeds/{id} now include a required
nativeFeeds array (possibly empty) derived from the feed metadata via
unionNativeFeeds. POST /v1/feeds always returns [].
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Delete the `iconBase` local helper (which mishandled display-name form
like `Name <a@b.com>`) and replace it with `EmailAddress.parse(input.from)
?.siteBaseUrl()` — the domain-layer VO that already handles bare and
display-name addresses correctly. Adds TEST C to lock the
display-name + relative-href absolutization fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire extractFeedLinks + detectNativeFeeds into storeEmail so that RSS/Atom/JSON
feed <link> tags in the newsletter HTML are detected and stored per-sender on the
feed metadata.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add nativeFeeds/nativeFeedDismissed to FeedMetadata and hasNativeFeed to
FeedListItem; extend IngestOptions with nativeFeeds; add nativeFeeds(),
hasNativeFeed(), and dismissNativeFeed() to the Feed aggregate mirroring
the existing pendingConfirmation/dismissConfirmation pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per-sender storage (latest-wins, like unsubscribe), pure domain detector
(strict 3 MIME types), admin surfaces (detail/badge/pill/dismiss) and a
read-only REST API field, mirroring the confirmation-detection feature.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Document the -develop/tag-driven release contract in CLAUDE.md so a
release is cut correctly: main stays *-develop, the tag is the source of
truth, never commit a bare version, and re-bump the next cycle after.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The release job built whatever version package.json held at the tagged
commit — but main always carries a -develop suffix, so a vX.Y.Z bundle
would have reported X.Y.Z-develop. Make the tag the source of truth:
strip the suffix in the ephemeral CI checkout before building (never
committed), and fail fast when the tag base doesn't match package.json's
base (wrong-commit guard). Update CONTRIBUTING with the tag-driven flow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
package.json now holds a -develop pre-release suffix so the version
reported in the footer/health/stats distinguishes a dev build from a
shipped one (0.3.0-develop sorts below 0.3.0 per SemVer). Document the
release flow in CONTRIBUTING.md: strip the suffix at tag time, re-bump
to the next -develop afterward.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Push the "Name <addr>" display-name parsing onto the EmailAddress VO
(displayName field + label() = displayName ?? normalized) and delete the
ad-hoc parseFromAddress helper in feed-generator. The feed builder now
parses the from-address once via EmailAddress and reuses it for the site
base URL, the [Sender] title prefix, and the entry author — one parser,
on the type that owns the data.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The sender-in-title rendering reads the FeedConfig DTO via the read
model, so the Feed.senderInTitle getter had no consumer — remove it.
In buildFeed, parse the from-address once and reuse it for both the
title prefix and the author; drop the dead `?? email.from` fallback
(parseFromAddress already returns the address as the name).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add version to StatsResponse and getStats so the canonical public
monitoring endpoint reports the running build alongside the footer
and /health.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a per-feed senderInTitle flag (domain FeedState.senderInTitle ↔
FeedConfig.sender_in_title). When set, the feed generator prefixes each
entry title with [Sender] (display name, falling back to the address).
Exposed as an admin edit-form checkbox and across the REST API
create/update/response schemas.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Inline package.json version at bundle time via src/config/version.ts
(resolveJsonModule), surface it in the shared admin/status footer and
add it to the /health JSON so self-hosters can tell which build runs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Hoist the shared format chips, expiry pill, and copy icons into admin/ui.tsx
so the feed detail (emails) page renders the same Email + Subscribe block as
the dashboard list, dropping the old per-format rows, W3C validator images,
and the now-dead .feed-validate CSS.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A "manage subscription" / "subscribe" footer link is now a weak (+1) URL
signal instead of strong (+2), so an ordinary newsletter with a stray body
keyword (active/valid) no longer crosses the detection threshold. A genuine
"confirm your subscription" subject + a bare /subscribe link still passes.
Also dedupe surfaced links. Adds false-positive + recall + dedupe tests.
Return the ranked links directly (string[] | null) instead of an unused
{score, links} wrapper, and drop the redundant hasKeyword helper in favor
of matchesAny(_, KEYWORDS). No behavior change.
saveMetadata now also upserts the list entry so the pendingConfirmation
flag is reflected in the dashboard without an extra per-feed KV read.
toListItemDTO gains an optional third parameter for the flag.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>