import logging
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from devplacepy.database import (
get_table,
db,
load_comments,
resolve_by_slug,
get_news_images_by_uids,
get_user_bookmarks,
paginate,
)
from devplacepy.utils import get_current_user, time_ago, not_found
from devplacepy.content import canonical_redirect
from devplacepy.seo import (
base_seo_context,
website_schema,
site_url,
news_article_schema,
list_page_seo,
next_page_url,
)
from devplacepy.responses import respond
from devplacepy.schemas import NewsListOut, NewsDetailOut
logger = logging.getLogger(__name__)
router = APIRouter()
NEWS_MAX_AGE_DAYS = 4
@router.get("", response_class=HTMLResponse)
async def news_page(request: Request, before: str = None):
user = get_current_user(request)
cutoff = (
datetime.now(timezone.utc) - timedelta(days=NEWS_MAX_AGE_DAYS)
).isoformat()
news_table = get_table("news")
articles, next_cursor = paginate(
news_table,
before=before,
order=["-grade", "-synced_at"],
cursor_field="synced_at",
status="published",
synced_at={">=": cutoff},
)
article_uids = [a["uid"] for a in articles]
images_by_news = get_news_images_by_uids(article_uids)
enriched = []
for a in articles:
enriched.append(
{
"article": a,
"time_ago": time_ago(a["synced_at"]),
"image_url": images_by_news.get(a["uid"]),
"grade": a.get("grade", 0),
}
)
seo_ctx = list_page_seo(
request,
title="Developer News",
description="Curated developer news and industry signals. Stay ahead with hand-picked articles.",
breadcrumbs=[
{"name": "Home", "url": "/feed"},
{"name": "News", "url": "/news"},
],
next_url=next_page_url(request, next_cursor),
)
return respond(
request,
"news.html",
{
**seo_ctx,
"request": request,
"user": user,
"articles": enriched,
"next_cursor": next_cursor,
},
model=NewsListOut,
)
@router.get("/{news_slug}", response_class=HTMLResponse)
async def news_detail_page(request: Request, news_slug: str):
user = get_current_user(request)
news_table = get_table("news")
article = resolve_by_slug(news_table, news_slug)
if not article:
raise not_found("News article not found")
redirect = canonical_redirect("news", article, news_slug)
if redirect:
return redirect
image_url = ""
if "news_images" in db.tables:
img = get_table("news_images").find_one(news_uid=article["uid"])
if img:
image_url = img["url"]
canonical_slug = article.get("slug", "") or article["uid"]
comments = load_comments("news", article["uid"], user)
bookmarked = bool(user) and article["uid"] in get_user_bookmarks(
user["uid"], "news", [article["uid"]]
)
base = site_url(request)
page_url = f"{base}/news/{canonical_slug}"
seo_ctx = base_seo_context(
request,
title=article.get("title", "News Article"),
description=(article.get("description", "") or "")[:200],
og_type="article",
og_image=image_url or None,
breadcrumbs=[
{"name": "Home", "url": "/feed"},
{"name": "News", "url": "/news"},
{"name": article.get("title", "")[:60], "url": page_url},
],
schemas=[website_schema(base), news_article_schema(article, base, image_url)],
)
return respond(
request,
"news_detail.html",
{
**seo_ctx,
"request": request,
"user": user,
"article": article,
"canonical_slug": canonical_slug,
"image_url": image_url,
"grade": article.get("grade", 0),
"time_ago": time_ago(article["synced_at"]),
"comments": comments,
"bookmarked": bookmarked,
},
model=NewsDetailOut,
)