|
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,
|
|
)
|