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:
2026-04-21 15:21:21 -05:00
parent e830e5da50
commit f77da87eaa
21 changed files with 1533 additions and 7 deletions

View File

@@ -8,13 +8,26 @@ via ``app.main:app``.
from __future__ import annotations
from pathlib import Path
import structlog
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from app import __version__
from app.config import get_settings
from app.logging_config import configure_logging
from app.routes.health import router as health_router
from app.routes.public import router as public_router
# Resolve the package root once so template / static paths stay correct
# regardless of the current working directory at startup (running under
# uvicorn from the repo root vs. pytest from anywhere vs. inside Docker).
_PACKAGE_ROOT: Path = Path(__file__).resolve().parent
_TEMPLATES_DIR: Path = _PACKAGE_ROOT / "templates"
_STATIC_DIR: Path = _PACKAGE_ROOT / "static"
def create_app() -> FastAPI:
@@ -24,7 +37,11 @@ def create_app() -> FastAPI:
- Load validated configuration via :func:`get_settings`.
- Initialize structured logging *before* any logger is used.
- Instantiate FastAPI with canonical title + version.
- Register routers (Phase 0: only ``/healthz``).
- Mount the ``/static`` directory for CSS, JS, and image assets.
- Attach the shared :class:`Jinja2Templates` to ``app.state`` so route
dependencies can retrieve it without a circular import on this
module.
- Register routers (Phase 1: health + public).
- Emit a single ``app_started`` structured log event.
"""
# Parse + validate configuration first so a bad environment fails fast
@@ -44,9 +61,27 @@ def create_app() -> FastAPI:
redoc_url="/redoc",
)
# Serve CSS, images, and other static assets from app/static. The
# `check_dir=False` would let Starlette skip the existence check; we
# leave it at its default so a missing directory surfaces loudly in
# dev. In prod the directory is baked into the container image.
application.mount(
"/static",
StaticFiles(directory=_STATIC_DIR),
name="static",
)
# Single shared Jinja2 environment. Storing it on ``app.state`` keeps
# route modules free of an import dependency on this module (which
# would be circular once admin/auth routers are added in later
# phases). Route handlers pull it via a ``Depends(get_templates)``
# function defined next to the routes.
application.state.templates = Jinja2Templates(directory=_TEMPLATES_DIR)
# Register routers. Kept explicit (no dynamic discovery) so the set of
# mounted endpoints is trivially auditable.
application.include_router(health_router)
application.include_router(public_router)
# Single structured startup event. Do NOT include secret material.
logger = structlog.get_logger(__name__)