feat: initialize blog app with blueprints and templates
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Extract blog-specific code from the coop monolith into a standalone repository. Includes auth, blog, post, admin, menu_items, snippets blueprints, associated templates, Dockerfile (APP_MODULE=app:app), entrypoint, and Gitea CI workflow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
83
bp/coop_api.py
Normal file
83
bp/coop_api.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
Internal JSON API for the coop app.
|
||||
|
||||
These endpoints are called by other apps (market, cart) over HTTP
|
||||
to fetch Ghost CMS content and menu items without importing blog services.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from quart import Blueprint, g, jsonify
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from models.menu_item import MenuItem
|
||||
from suma_browser.app.csrf import csrf_exempt
|
||||
|
||||
|
||||
def register() -> Blueprint:
|
||||
bp = Blueprint("coop_api", __name__, url_prefix="/internal")
|
||||
|
||||
@bp.get("/menu-items")
|
||||
@csrf_exempt
|
||||
async def menu_items():
|
||||
"""
|
||||
Return all active menu items as lightweight JSON.
|
||||
Called by market and cart apps to render the nav.
|
||||
"""
|
||||
result = await g.s.execute(
|
||||
select(MenuItem)
|
||||
.where(MenuItem.deleted_at.is_(None))
|
||||
.options(selectinload(MenuItem.post))
|
||||
.order_by(MenuItem.sort_order.asc(), MenuItem.id.asc())
|
||||
)
|
||||
items = result.scalars().all()
|
||||
|
||||
return jsonify(
|
||||
[
|
||||
{
|
||||
"id": mi.id,
|
||||
"post": {
|
||||
"title": mi.post.title if mi.post else None,
|
||||
"slug": mi.post.slug if mi.post else None,
|
||||
"feature_image": mi.post.feature_image if mi.post else None,
|
||||
},
|
||||
}
|
||||
for mi in items
|
||||
]
|
||||
)
|
||||
|
||||
@bp.get("/post/<slug>")
|
||||
@csrf_exempt
|
||||
async def post_by_slug(slug: str):
|
||||
"""
|
||||
Return a Ghost post's key fields by slug.
|
||||
Called by market app for the landing page.
|
||||
"""
|
||||
from suma_browser.app.bp.blog.ghost_db import DBClient
|
||||
|
||||
client = DBClient(g.s)
|
||||
posts = await client.posts_by_slug(slug, include_drafts=False)
|
||||
|
||||
if not posts:
|
||||
return jsonify(None), 404
|
||||
|
||||
post, original_post = posts[0]
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
"post": {
|
||||
"id": post.get("id"),
|
||||
"title": post.get("title"),
|
||||
"html": post.get("html"),
|
||||
"custom_excerpt": post.get("custom_excerpt"),
|
||||
"feature_image": post.get("feature_image"),
|
||||
"slug": post.get("slug"),
|
||||
},
|
||||
"original_post": {
|
||||
"id": getattr(original_post, "id", None),
|
||||
"title": getattr(original_post, "title", None),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return bp
|
||||
Reference in New Issue
Block a user