Source feeds: click a source to see its publication feed
Click a source name on any card → a feed of just that source's articles, newest-first, still accepted / non-duplicate / boundary-filtered (the calm promise isn't bypassed). A natural way to follow a publication's feel. * queries.feed + /api/feed: source_id filter; Article output gains source_id. * Frontend: source label is a button → transient 'source:<id>' view (like 'tag:<slug>'), rendered in the feed grid with Load more, header = source name. * Ad-hoc, not a pinned lane. Foundation for a future source page (metadata) + Follow; shareable /source/<slug> route and source_view analytics come then. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+5
-2
@@ -241,6 +241,7 @@ class Article(BaseModel):
|
||||
image_url: str | None = None
|
||||
published_at: str | None = None
|
||||
source: str
|
||||
source_id: int | None = None
|
||||
topic: str | None = None
|
||||
flavor: str | None = None
|
||||
accepted: bool
|
||||
@@ -265,6 +266,7 @@ class Article(BaseModel):
|
||||
image_url=row.get("image_url"),
|
||||
published_at=row.get("published_at"),
|
||||
source=row["source_name"],
|
||||
source_id=row.get("source_id"),
|
||||
topic=row.get("topic"),
|
||||
flavor=row.get("flavor"),
|
||||
accepted=bool(row.get("accepted")),
|
||||
@@ -921,6 +923,7 @@ def create_app() -> FastAPI:
|
||||
prefs: str | None = Query(None),
|
||||
exclude: str = Query("", description="comma-separated article ids the reader has dismissed"),
|
||||
tag: str | None = Query(None, description="grouping tag to browse"),
|
||||
source_id: int | None = Query(None, ge=1, description="show only this source's articles"),
|
||||
sort: str = Query("ranked", pattern="^(ranked|latest)$", description="ranked (best-first) or latest (newest-first)"),
|
||||
) -> FeedResponse:
|
||||
if topic and topic.lower() not in TOPICS:
|
||||
@@ -940,14 +943,14 @@ def create_app() -> FastAPI:
|
||||
fetch_n = min(2000, (offset + limit) * 4 + 50 + len(excl))
|
||||
raw = queries.feed(
|
||||
conn, topic=topic, flavor=flavor, accepted_only=accepted_only,
|
||||
limit=fetch_n, offset=0, tag=tag, sort=sort, **kw,
|
||||
limit=fetch_n, offset=0, tag=tag, source_id=source_id, sort=sort, **kw,
|
||||
)
|
||||
kept = [a for a in filter_articles(raw, fp, now) if a["id"] not in excl]
|
||||
rows = kept[offset : offset + limit]
|
||||
else:
|
||||
rows = queries.feed(
|
||||
conn, topic=topic, flavor=flavor, accepted_only=accepted_only,
|
||||
limit=limit, offset=offset, tag=tag, sort=sort, **kw,
|
||||
limit=limit, offset=offset, tag=tag, source_id=source_id, sort=sort, **kw,
|
||||
)
|
||||
# Keep the top of a browse view readable: stable-sort paywalled items
|
||||
# below readable ones (composite order preserved within each group).
|
||||
|
||||
@@ -25,6 +25,7 @@ _ARTICLE_COLUMNS = f"""
|
||||
a.canonical_url,
|
||||
a.published_at,
|
||||
a.image_url,
|
||||
a.source_id,
|
||||
src.name AS source_name,
|
||||
s.topic,
|
||||
s.flavor,
|
||||
@@ -57,6 +58,7 @@ def feed(
|
||||
max_cortisol: int | None = None,
|
||||
max_ragebait: int | None = None,
|
||||
tag: str | None = None,
|
||||
source_id: int | None = None,
|
||||
sort: str = "ranked",
|
||||
) -> list[dict]:
|
||||
"""Return articles with categorical filters applied in SQL.
|
||||
@@ -106,6 +108,9 @@ def feed(
|
||||
if tag:
|
||||
clauses.append("EXISTS (SELECT 1 FROM article_tags at WHERE at.article_id = a.id AND at.tag = ?)")
|
||||
params.append(tag.lower())
|
||||
if source_id:
|
||||
clauses.append("a.source_id = ?")
|
||||
params.append(source_id)
|
||||
|
||||
where = "WHERE " + " AND ".join(clauses)
|
||||
params.extend([limit, offset])
|
||||
|
||||
Reference in New Issue
Block a user