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

103
tests/test_public_routes.py Normal file
View File

@@ -0,0 +1,103 @@
"""Smoke tests for the public-site skeleton routes.
These tests focus on contract rather than styling:
- every public route returns 200 with an HTML content-type
- each page contains a page-specific substring (proves the template
actually rendered, not just that the route exists)
- the homepage renders the empty-state copy when PostService returns []
- the shared layout emits the logo image path so nav/logo aren't broken
by a future refactor
No mocks of the DB (there is no DB in Phase 1). The PostService stub
already returns an empty list, which is exactly what we want to assert
against.
"""
from __future__ import annotations
import pytest
from fastapi.testclient import TestClient
from app.main import app
@pytest.fixture(scope="module")
def client() -> TestClient:
"""Return a module-scoped FastAPI TestClient.
TestClient uses the module-level `app` built by `create_app()` at
import time, i.e. the exact same app uvicorn runs in production.
"""
return TestClient(app)
@pytest.mark.parametrize(
"path,expected_substring",
[
("/", "Chicken Babies"),
("/about", "About the farm"),
("/contact", "Get in touch"),
("/shop", "Coming soon"),
],
)
def test_public_route_renders_html(
client: TestClient,
path: str,
expected_substring: str,
) -> None:
"""Every public page returns 200 HTML containing a page-specific string.
The substring is intentionally a headline the template owns so the
test fails loudly if the wrong template is accidentally wired up.
"""
response = client.get(path)
assert response.status_code == 200, (
f"{path} returned {response.status_code}: {response.text[:200]}"
)
content_type = response.headers.get("content-type", "")
assert content_type.startswith("text/html"), (
f"{path} returned unexpected content-type: {content_type!r}"
)
assert expected_substring in response.text, (
f"{path} body missing expected substring {expected_substring!r}"
)
def test_home_shows_empty_state_when_no_posts(client: TestClient) -> None:
"""With the Phase 1 stub service, the home page shows 'No posts yet'.
This is the canonical empty-state marker; Phase 2 seeds a welcome
post so this test will need to be updated when the DB lands.
"""
response = client.get("/")
assert response.status_code == 200
assert "No posts yet" in response.text
def test_layout_includes_logo_image(client: TestClient) -> None:
"""Shared layout references the generated logo asset paths.
We check for the stem (``/static/img/logo.``) rather than a specific
extension so both the <source srcset="...webp"> and <img src="...png">
markup are covered by a single assertion.
"""
response = client.get("/")
assert response.status_code == 200
assert "/static/img/logo." in response.text
def test_nav_marks_active_page(client: TestClient) -> None:
"""The About page renders ``aria-current=\"page\"`` on its nav link.
Exercises the shared layout's active-nav logic end-to-end without
inspecting internals.
"""
response = client.get("/about")
assert response.status_code == 200
# The pair should appear exactly once per active link; we only need
# to prove it's present at all.
assert 'aria-current="page"' in response.text