mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
feat: reader-compat batch — JSON Feed, OPML export, conditional GET, dedup
Batch of four reader-facing improvements (TODO "Compat lecteurs + dedup"): - JSON Feed at /json/:feedId (feed lib .json1()); all formats cross-link - OPML export at /admin/opml (admin-protected; the registry lists every feed URL, so it must not be public) - Conditional GET on /rss + /atom: strong ETag + Last-Modified, 304 on If-None-Match/If-Modified-Since, validators shared via http-cache.ts - Duplicate-send dedup in ingestion: match by Message-ID, fall back to a SHA-256 of normalized subject+content; a duplicate is a no-op and bumps the new emails_deduplicated counter (status page + /api/v1/stats) 429 tests green, tsc clean, build dry-run OK. Docs (README/CLAUDE/TODO + landing cards) updated. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { Context } from "hono";
|
||||
import { Env } from "../types";
|
||||
import { generateJsonFeed } from "../infrastructure/feed-generator";
|
||||
import { fetchFeedData } from "../application/feed-fetcher";
|
||||
import { baseUrl } from "../infrastructure/urls";
|
||||
import { isExpired } from "../domain/feed";
|
||||
import { FeedId } from "../domain/value-objects/feed-id";
|
||||
|
||||
export async function handle(c: Context<{ Bindings: Env }>): Promise<Response> {
|
||||
try {
|
||||
const feedId = c.req.param("feedId");
|
||||
if (!feedId) {
|
||||
return new Response("Feed ID is required", { status: 400 });
|
||||
}
|
||||
|
||||
const feedData = await fetchFeedData(FeedId.unchecked(feedId), c.env);
|
||||
if (!feedData) {
|
||||
return new Response("Feed not found", { status: 404 });
|
||||
}
|
||||
if (isExpired(feedData.feedConfig)) {
|
||||
return new Response("Feed has expired", { status: 410 });
|
||||
}
|
||||
|
||||
const base = baseUrl(c.env);
|
||||
const selfUrl = new URL(c.req.url).origin + `/json/${feedId}`;
|
||||
const jsonFeed = generateJsonFeed(
|
||||
feedData.feedConfig,
|
||||
feedData.emails,
|
||||
base,
|
||||
feedId,
|
||||
selfUrl,
|
||||
);
|
||||
const linkHeader = [
|
||||
`<${base}/hub>; rel="hub"`,
|
||||
`<${selfUrl}>; rel="self"`,
|
||||
].join(", ");
|
||||
|
||||
return new Response(jsonFeed, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/feed+json",
|
||||
"Cache-Control": "max-age=1800",
|
||||
"X-Robots-Tag": "noindex",
|
||||
Link: linkHeader,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error generating JSON feed:", error);
|
||||
return new Response("Internal Server Error", { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user