|
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, paginate
|
|
from devplacepy.templating import templates
|
|
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
|
|
|
|
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 templates.TemplateResponse(request, "news.html", {
|
|
**seo_ctx,
|
|
"request": request,
|
|
"user": user,
|
|
"articles": enriched,
|
|
"next_cursor": next_cursor,
|
|
})
|
|
|
|
|
|
@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)
|
|
|
|
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 templates.TemplateResponse(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,
|
|
})
|