feat: phase 1 public site skeleton — layout, routes, CSS, logo pipeline
Ship base Jinja layout (header/nav/main/footer with skip link and aria-current), mobile-first single-file CSS using the ROADMAP palette tokens, and four public routes: /, /about, /contact, /shop. Blog index renders via a stable PostService.list_published() stub returning [] — Phase 2 only swaps the body. About is static placeholder copy, /contact ships an inert form plus a mailto: link driven by ADMIN_CONTACT_EMAIL, /shop shows a "Coming soon" card. Adds a Pillow-based scripts/generate_static_assets.py producing resized logo PNG + WebP, multi-size favicon.ico, and a 180x180 apple-touch-icon on a cream background. Outputs committed for a reproducible build. Also ship docs/MANUAL_TESTING.md with per-route / responsive / a11y / static- asset checklists, and mark Phase 1 complete in docs/ROADMAP.md.
This commit is contained in:
61
app/services/posts.py
Normal file
61
app/services/posts.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Blog post service layer.
|
||||
|
||||
Phase 1 ships a stub: :meth:`PostService.list_published` returns an empty
|
||||
list so the home page renders cleanly without a database. Phase 2 will
|
||||
replace the stub with a real SQLite-backed implementation. The public
|
||||
method signature and return type (`list[PostSummary]`) are frozen now so
|
||||
route and template code written in Phase 1 won't need to change when the
|
||||
DB arrives.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.models.posts import PostSummary
|
||||
|
||||
|
||||
class PostService:
|
||||
"""Read-side service for published blog posts.
|
||||
|
||||
The service is intentionally stateless in Phase 1. Phase 2 will give
|
||||
it a SQLite connection (or connection factory) via constructor
|
||||
injection; callers obtain an instance through :func:`get_post_service`
|
||||
so the swap is transparent to the routes that depend on it.
|
||||
"""
|
||||
|
||||
def list_published(self, limit: int = 20) -> list[PostSummary]:
|
||||
"""Return up to ``limit`` published posts, most recent first.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
limit:
|
||||
Maximum number of summaries to return. Kept in the signature
|
||||
now (even though the stub ignores it) so Phase 2's real
|
||||
implementation is a drop-in replacement.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[PostSummary]
|
||||
Currently always an empty list. The template treats an empty
|
||||
list as the "no posts yet" state.
|
||||
"""
|
||||
# Phase 1 stub: no DB, no posts. Phase 2 will issue a parameterized
|
||||
# SELECT against the `posts` table filtered by status='published'
|
||||
# and ordered by published_at DESC.
|
||||
return []
|
||||
|
||||
|
||||
# Module-level singleton. The service is stateless in Phase 1, so one
|
||||
# instance is safe to share across requests. Phase 2 may relocate this
|
||||
# behind a factory if per-request scoping becomes useful.
|
||||
_post_service: PostService = PostService()
|
||||
|
||||
|
||||
def get_post_service() -> PostService:
|
||||
"""Return the shared :class:`PostService` for FastAPI dependency injection.
|
||||
|
||||
Keeping this as a module-level function (rather than instantiating a
|
||||
fresh service on every request) means FastAPI's ``Depends`` wiring
|
||||
pays no construction cost on the hot path, and tests can override the
|
||||
dependency via ``app.dependency_overrides[get_post_service]``.
|
||||
"""
|
||||
return _post_service
|
||||
Reference in New Issue
Block a user