"""Blog post domain models. Phase 1 only needs the *list-view* projection of a post — a minimal immutable record sufficient to render a blog card on the home page. Phase 2 will introduce the richer persisted :class:`Post` dataclass that mirrors the SQLite schema; :class:`PostSummary` intentionally stays as a narrower DTO even after the DB arrives because list endpoints shouldn't pay the cost of loading full post bodies. The dataclass is frozen: summaries flow one-way from the service layer into templates and must never mutate mid-request. """ from __future__ import annotations from dataclasses import dataclass from datetime import datetime @dataclass(frozen=True) class PostSummary: """Immutable summary row for the blog index. Attributes ---------- slug: URL-safe identifier used to build the post's canonical URL. title: Human-readable headline shown on the card. published_at: Timezone-aware UTC publish timestamp. Templates format this for display; storing a real :class:`datetime` (rather than a pre-formatted string) keeps locale/formatting concerns in the view layer. excerpt: Short plaintext teaser. The service layer is responsible for producing a sanitized, already-truncated excerpt so the template can render it without additional escaping beyond Jinja's default HTML autoescape. """ slug: str title: str published_at: datetime excerpt: str