Adapted from the repo's docs/master-blueprint.md — Part of Internals: the platform's design set, published as content on the platform it describes.
Status: Ratified (2026-07-05). This design of record spans every tier. ADRs 0005–0008 are Accepted, and the read boundary is published as contract v0.2.0 (
contract/openapi.yaml+contract/asyncapi.yaml; see../contract/CHANGELOG.md). Tier owners implement against the published contract version, never against this document (Rule 1,rules-of-engagement.md).Delivery (2026-07-07): Phases 0–4 are complete and verified — the full roadmap (§11) is delivered at contract v0.8.1 (v0.8.0 + a description-only patch settling the curated-nav semantics), ADRs 0005–0017. See
how-it-works.mdfor the end-to-end technical tour.Reading order: this sits after
architecture.md. It applies the template's fixed tier model to one concrete product; it does not restate or override it.
1. Vision & scope
A personal publishing platform — a blog — where a single author publishes articles by writing files, not by operating a UI. There is no administrative panel: content is authored out-of-band (files edited in an editor, synced via git or a mounted volume) and the system's job is to read those files, understand their structure and schedule, and serve them over a clean REST API to a Next.js reader front-end.
The defining architectural choice: the filesystem is the content source of truth. The platform reads a content directory recursively, treats each subdirectory as a section and each Markdown file as an article, honours a publication schedule declared in each file's front-matter, and projects all of it into fast, cacheable read models. PostgreSQL and Redis remain present and authoritative for everything they store — they just hold a derived index and cache of content, not the content's origin (see §10, ADR-0005).
1.1 Goals
- Author writes a Markdown file → it appears on the site, on schedule, with zero UI steps.
- Arbitrarily nested content tree (sections within sections) mirrored 1:1 in the URL space.
- Scheduled publishing: an article declares when it goes live; it stays invisible until then.
- Fast, statically-cacheable reads; a small, honest, read-only public API.
- Fits the template's tiers and rules unchanged:
frontend → contract ← backend, and backendHttp → Domain → Data.
1.2 Non-goals (this line)
| Non-goal | Why / where it goes instead |
|---|---|
| Admin panel / CMS UI | Explicit product decision. Authoring is file-based, out-of-band. |
| Public write/comment/auth surface | The API is read-only and unauthenticated (public blog). Any future write surface is a new contract major + auth ADR. |
| Content stored in a database as SoR | Filesystem is SoR; Postgres is a derived index (ADR-0005). |
| Multi-tenant / multi-author accounts | Single author (front-matter author is a label, not an identity). |
| WYSIWYG, media library, plugin system | Out of scope; revisit per-project via ADR. |
2. How it maps onto the template
Nothing about the tier model changes. This blueprint only fills the tiers with content.
┌─────────────────────────────────────────────────────────────┐
│ Content root (filesystem) — the CONTENT source of truth │
│ storage/app/content/** (recursive: metadata.json + *.md) │
└───────────────────────────┬───────────────────────────────────┘
│ read + parse (Data layer, one-way)
┌────────────────────────────▼──────────────────────────────────┐
│ backend/ (Laravel API) │
│ Http listArticles / getArticle / getSection(Tree) … │
│ Domain content model, publication policy, ports │
│ Data Filesystem ContentSource → Postgres index + Redis │
└───────────────┬───────────────────────────────────────────────┘
│ conforms-to
┌──────▼───────┐ consumes (generated client) ┌───────────────┐
│ contract/ │◄──────────────────────────────────── │ frontend/ │
│ OpenAPI 3.1 │ │ Next.js │
│ AsyncAPI 3.1 │ │ reader UI │
└──────────────┘ └───────────────┘
frontend/andbackend/couple only throughcontract/. The front-end never reads the content directory or the database — it consumes the published REST API.infra/provides the runtime unchanged (DDEV: PHP 8.5, PostgreSQL 16, Redis, Node 22), plus one wiring addition: theCONTENT_ROOTpath/volume (§7).
3. Content model — the storage layout
The content root is a directory tree scanned recursively. Two file kinds carry meaning:
| On disk | Means | Companion of |
|---|---|---|
| a directory | a Section — a node in the content tree | its metadata.json |
metadata.json in a directory |
that section's metadata (title, order, …) | the directory |
a *.md file |
an Article — a leaf; front-matter + Markdown body | itself |
any other file (*.png, *.jpg, …) |
a content asset referenced by nearby articles | — |
3.1 Example tree
storage/app/content/ ← CONTENT_ROOT (configurable)
├── metadata.json ← root section metadata
├── hello-world.md ← article at path: hello-world
├── engineering/ ← section: engineering
│ ├── metadata.json
│ ├── my-first-post.md ← article: engineering/my-first-post
│ ├── assets/
│ │ └── diagram.png ← asset: engineering/assets/diagram.png
│ └── deep-dives/ ← nested section: engineering/deep-dives
│ ├── metadata.json
│ └── concurrency.md ← article: engineering/deep-dives/concurrency
└── travel/
├── metadata.json
└── japan-2025.md ← article: travel/japan-2025
- Path = identity. An article's canonical id is its slug path relative to the content
root, without the
.mdextension:engineering/deep-dives/concurrency. A section's id is its directory path:engineering/deep-dives. Both are the natural, human-readable keys and map 1:1 onto front-end URLs. - Slugs are kebab-case. Filenames and directory names are the slug segments; the indexer validates the charset and rejects/reports anything that would not round-trip a URL.
metadata.jsonis optional but recommended. When absent, the section defaults to a humanised directory name and alphabetical ordering — a missing companion is a soft warning, not a hard failure.
3.2 Section companion — metadata.json
{
"title": "Engineering",
"description": "Notes on building software.",
"order": 10,
"visibility": "public",
"cover": "assets/engineering-cover.jpg"
}
| Field | Type | Meaning | Default |
|---|---|---|---|
title |
string | Display name of the section | humanised directory name |
description |
string | Short blurb for section index pages / feeds | null |
order |
integer | Sort weight among siblings (ascending) | 1000, then title A→Z |
visibility |
public | hidden |
hidden prunes the whole subtree from the API |
public |
cover |
string | Path (relative to this directory) to a cover asset | null |
3.3 Article — Markdown file with front-matter
Each article is a UTF-8 Markdown file whose header is a YAML front-matter block delimited
by ---, followed by the Markdown body:
---
title: "Understanding Concurrency"
description: "Threads, async, and the models in between."
author: "Ada Lovelace"
published_at: 2026-06-01T09:00:00Z
scheduled_at: 2026-07-10T09:00:00Z # optional — see §3.4
updated_at: 2026-06-15T12:00:00Z
tags: [concurrency, systems]
draft: false
cover: "assets/diagram.png"
slug: "concurrency" # optional — defaults to the filename
---
Concurrency is not parallelism. …the Markdown body renders from here.
| Field | Type | Required | Meaning |
|---|---|---|---|
title |
string | ✔ | Article title. Absent → article is reported invalid and skipped. |
description |
string | — | Summary for lists, <meta> tags, and feeds. |
author |
string | string[] | — | Display author(s). A label, not an account. |
published_at |
date-time (ISO-8601, UTC) | — | Canonical publication instant / displayed date. |
scheduled_at |
date-time (ISO-8601, UTC) | — | Optional go-live gate; overrides published_at for visibility only. |
updated_at |
date-time | — | Last meaningful edit; shown as "updated". |
tags |
string[] | — | Free-form tags; normalised to kebab-case slugs for browsing. |
draft |
boolean | — | true ⇒ never public, regardless of dates. Default false. |
cover |
string | — | Path (relative to the file's directory) to a cover asset. |
slug |
string | — | Override the URL segment; defaults to the filename stem. |
Unknown front-matter keys are preserved as opaque extra metadata (never trusted for behaviour) so authors can annotate freely without breaking the parser.
3.4 Publication state machine (the scheduling rule)
Visibility is a pure function of front-matter and the current time — the single most
important invariant in the Domain layer. Precedence is explicit so published_at and
scheduled_at never contradict:
go-live instant = scheduled_at ?? published_at (when the article becomes public)
display date = published_at ?? scheduled_at (the date shown to readers)
state(article, now):
draft == true → DRAFT (never public)
go-live instant is null → DRAFT (no publish instant declared)
go-live instant > now → SCHEDULED (hidden until the instant arrives)
go-live instant <= now → PUBLISHED (public)
| State | Public API surfaces it? | Notes |
|---|---|---|
DRAFT |
✗ | Missing/null go-live instant, or draft: true. |
SCHEDULED |
✗ | Go-live instant is in the future; becomes PUBLISHED automatically. |
PUBLISHED |
✔ | Only these appear in any public list or fetch. |
Read-time gating is the source of truth. Because visibility is time-derived, the API is
correct even with no background job: every query filters go-live <= now(). The scheduler
(§5.4) is an optimisation and notifier, not a
correctness dependency — it re-warms caches and emits content.article-published the moment a
SCHEDULED article crosses into PUBLISHED.
4. The two content stores — SoR vs. index
Filesystem (SoR) Postgres (derived index) Redis (cache)
───────────────── ───────────────────────── ────────────────────
metadata.json + *.md ──► articles, sections tables ──► rendered HTML by content-hash
(author writes here) (query, filter, paginate, section tree snapshot
full-text search) tag/author aggregates
▲ ▲ ▲
└─ reindex reads ────────────┴── reindex writes ──────────────┘
(idempotent, hash-guarded, one direction)
- Filesystem — the only place a human edits. Owns truth for content.
- PostgreSQL 16 — a rebuildable projection: one row per article / section, with the
parsed metadata, the resolved state,
go_live_at, tags, and acontent_hash. Powers listing, filtering, pagination, and (phase 2) full-text search. Hierarchy is stored as a materialised path (candidate: theltreeextension) so subtree queries are index-friendly. - Redis — caches the expensive, stable outputs: rendered+sanitised article HTML keyed by
content_hash, the section-tree snapshot, and tag/author aggregates. Invalidated by content hash, so a re-render only happens when a file actually changed.
This refines — does not violate — ADR-0003: Postgres stays the system of record for everything it holds; content simply originates on disk and is projected in. Recorded as ADR-0005 (§10).
5. Backend design (backend/)
Layered Http → Domain → Data, exactly as architecture.md mandates.
Ownership below follows the roster in team.md.
5.1 Domain (app/Domain/** — domain-engineer)
The heart of the system; framework- and storage-agnostic.
- Value objects / entities:
ContentPath(validated slug path, traversal-safe),Article,ArticleSummary,Section,SectionTree,Tag,Author,PublicationState. - Publication policy:
PublicationPolicy::state(frontMatter, Clock): PublicationStateimplementing §3.4 against an injectedClock(so scheduling is unit-testable without real time). - Use-case actions (one per API operation):
ListPublishedArticles,GetArticleByPath,GetSection,GetSectionTree,ListTags,SearchArticles(phase 2). Each returns Domain results; none knows about HTTP or Eloquent. - Ports (interfaces Domain defines, Data implements):
ContentSource— enumerate the tree, read a raw article/section, resolve an asset path.ContentIndex— the query side: paginated published lists, fetch-by-path, subtree, tag catalogue, search.RenderedContentCache— get/put rendered HTML by content hash.MarkdownRenderer— Markdown → sanitised HTML + table of contents.Clock— current instant.
- Domain events:
ArticlePublished,ContentReindexed— shapes mirrorcontract/asyncapi.yaml(§6.2).
5.2 Data (app/Models/**, app/Infrastructure/**, database/** — data-engineer)
Implements every Domain port; owns all storage concerns.
FilesystemContentSource— implementsContentSourceover Laravel Storage / Flysystem rooted atCONTENT_ROOT. Walks recursively, parses YAML front-matter, computes a per-filecontent_hash, and refuses to resolve any path that escapes the root (traversal guard).- Index schema (Postgres):
sections(path, parent_path, title, order, visibility, cover) andarticles(path, section_path, slug, title, description, authors,go_live_at,display_date,updated_at, tags, state,content_hash,body_excerpt, search vector). Indexes:go_live_at,section_path(subtree via materialised path /ltree), GIN on tags and on the FTStsvector. PostgresContentIndex— implementsContentIndex; all list queries filterstate = PUBLISHED AND go_live_at <= now()and page by opaque cursor (PageMeta).RedisRenderedContentCache— implementsRenderedContentCache, keyed by content hash; documented TTL + hash-keyed invalidation.MarkdownRenderer— CommonMark pipeline (e.g.league/commonmark) + an HTML sanitiser allowlist; output is safe to inject client-side (§8).- Migrations/factories/seeders stay in lock-step with the schema; a seeder can point at a fixture content tree for tests.
5.3 Http (app/Http/**, routes/** — backend-api)
- Thin controllers: validate query params (form requests), invoke one Domain action, map the result to an API resource matching the published contract exactly.
- Route note: article/section paths contain
/, so the catch-all route parameter uses an unencoded-slash constraint, e.g.Route::get('/articles/{path}')->where('path', '.+'). - One exception renderer → RFC 9457
application/problem+jsonfor every 4xx/5xx (404 for unknown or not-yet-public path — the API never reveals that aSCHEDULED/DRAFTitem exists). No ad-hoc error JSON. - Public endpoints are unauthenticated; per-IP rate limiting and CORS restricted to the front-end origin are applied at this layer.
5.4 Indexing pipeline & scheduler
trigger ─────────────► reindex(ContentSource → ContentIndex)
├─ artisan content:reindex (manual / CI / post-deploy) idempotent,
├─ Laravel scheduler tick (~1 min) (picks up file edits + SCHEDULED hash-guarded:
│ → PUBLISHED transitions) unchanged files
└─ (optional) filesystem watcher (near-instant local authoring) are skipped
│
├─ upsert changed rows, prune deleted
├─ warm Redis for changed content hashes
└─ emit content.article-published / content.reindexed
content:reindexis idempotent and hash-guarded: it diffscontent_hashper file and only re-parses/re-renders what changed — cheap to run every minute.- The scheduler run is what turns a
SCHEDULEDarticlePUBLISHEDin the index and emits the event; read-time gating already made it correct, so the event is for cache-warming and downstream notification (e.g. front-end on-demand revalidation, §6.2). - Malformed front-matter / duplicate slugs / bad charset ⇒ the offending item is skipped,
counted, and logged;
content.reindexedcarries the failure count so problems are observable without an admin UI.
6. The contract (contract/) — published as v0.2.0
The API is read-only and public. The shapes below were published as contract v0.2.0
by contract-owner (minor/additive over the 0.1.0 skeleton) — reviewed, versioned, linted,
and released ahead of implementation. Phase-2 /search and /authors/{author} are deferred
to later minor bumps. Conventions from
contract/README.md are honoured throughout: operationId +
description per operation, kebab-case paths, camelCase JSON, cursor pagination via PageMeta,
RFC 9457 errors, EventEnvelope for events.
6.1 REST surface (OpenAPI 3.1)
| Method & path | operationId | Purpose | Key params |
|---|---|---|---|
GET /health |
getHealth |
Liveness + deps (adds a content check) |
— |
GET /articles |
listArticles |
Paginated published articles, newest first | cursor, pageSize, section, tag, author |
GET /articles/{path} |
getArticle |
One published article, full rendered body | path (slug path) |
GET /sections |
getSectionTree |
The nested public section tree | depth? |
GET /sections/{path} |
getSection |
One section + its subsections & article summaries | path, cursor, pageSize |
GET /tags |
listTags |
Tag catalogue with counts | — |
GET /tags/{tag} |
listArticlesByTag |
Published articles for a tag | tag, cursor, pageSize |
GET /search (phase 2) |
searchArticles |
Full-text search over published articles | q, cursor, pageSize |
GET /authors/{author} (phase 2) |
listArticlesByAuthor |
Published articles by author label | author, cursor, pageSize |
Core schemas (boundary shapes — never the persistence rows):
ArticleSummary—path,slug,title,description,authors[],sectionPath,publishedAt,updatedAt,tags[],readingMinutes,coverImageUrl.Article—ArticleSummary+body(sanitised HTML),tableOfContents[],prev/next(adjacent-article refs for navigation).SectionNode(tree) —path,title,order,children[](recursive).Section(detail) —path,title,description,order,coverImageUrl,subsections[](SectionNode),articles[](ArticleSummary) +meta(PageMeta).Tag—slug,label,count.Author—slug,name,count.- Reuse the skeleton's
Problem,PageMeta,cursor,pageSizeverbatim.
Rendering decision (recommended): the backend returns server-rendered, sanitised HTML
in Article.body, plus a structured tableOfContents. Rationale: one trusted rendering +
sanitising pipeline (not shipped to every client), consistent output for pages and feeds,
and safe SSR injection on the front-end. bodyFormat: "html" is declared in the schema so a
future raw-Markdown variant is an additive change. (Ratify as ADR-0007.)
Assets: images referenced by articles live in the content tree. Options for
contract-owner to decide: (a) a backend GET /assets/{path} that streams content-root files
with correct content-types, or (b) rewrite asset URLs at render time to a static/CDN origin.
Option (a) keeps the boundary self-contained and is the recommended default.
6.2 Events (AsyncAPI 3.1)
Two channels, both wrapping the mandatory EventEnvelope:
| Channel address | When | data payload |
|---|---|---|
content.article-published |
a SCHEDULED article crosses to PUBLISHED (or a new published article is indexed) |
path, slug, title, sectionPath, publishedAt |
content.reindexed |
a reindex run completes | articlesIndexed, sectionsIndexed, skipped, durationMs, contentVersion |
Primary consumer: the front-end's on-demand revalidation (turn a publish into an immediate
page refresh). content.reindexed's skipped count is the "content health" signal in the
absence of an admin panel.
7. Infra / platform (infra/ — platform-engineer)
Baseline is unchanged (ADR-0003/0004). Additions this product needs:
CONTENT_ROOTenv var (defaultstorage/app/content), wired inbackend/.env.example; in DDEV it may be a bind-mounted host directory or a git-synced path so authoring happens on the host with a normal editor. Document the sync mechanism as a known manual step per theinfra/convention ("an undocumented manual step is a platform bug").- Scheduler:
php artisan schedule:work(or cron) running the ~1-minute reindex tick — add as adocker-compose.<service>.yamlworker following the frontend-service pattern, or via DDEV post-start, owned byplatform-engineer. - Health:
GET /healthgains acontentcheck (content root readable + last reindex fresh) so the existing infra verify (curl …/health) covers content wiring too. - CI unchanged in shape: contract-lint → backend verify → frontend verify.
8. Cross-cutting concerns
| Concern | Approach |
|---|---|
| XSS / sanitisation | Markdown → HTML through an allowlist sanitiser server-side; even owner-authored content is sanitised. Front-end injects trusted HTML only. |
| Path traversal | ContentPath value object + FilesystemContentSource reject any resolved path escaping CONTENT_ROOT; /assets/{path} shares the guard. |
| Leakage of unpublished | SCHEDULED/DRAFT items return 404 (never 403) — their existence is never disclosed. |
| Caching / invalidation | Redis keyed by content_hash (documented TTLs); Postgres index is the query cache; front-end uses ISR + event-driven on-demand revalidation. Invalidation rules are documented decisions, not folklore. |
| Performance | Cursor pagination; go_live_at, section-path, tag GIN, and FTS indexes; hash-guarded reindex avoids needless re-render. |
| Observability | Structured logs for skipped/malformed content; content.reindexed metrics; health content check. |
| Accessibility | Semantic prose rendering, heading hierarchy from the ToC, alt text from asset metadata; a11y is in-scope per component (frontend/README.md). |
| SEO / feeds | Front-end emits sitemap.xml and an RSS/Atom feed from listArticles; per-article <meta>/OpenGraph from summary fields. |
9. Frontend design (frontend/ — frontend-engineer)
Consumes the API only through the generated client (openapi-typescript from
contract/openapi.yaml), one fetch wrapper handling the Problem shape uniformly — never
hand-written shapes, never backend/content access.
- Routing (App Router): a catch-all
app/[...slug]/page.tsxmirrors the content tree — resolveslugas an article (getArticle) and fall back to a section (getSection); plus/,/tags/[tag],/search,/authors/[author]. - Rendering strategy: SSG + ISR for published pages (paths enumerated from
listArticles/getSectionTreeat build, revalidated on an interval), with on-demand revalidation driven bycontent.article-publishedso a scheduled post appears promptly. SSR fallback for not-yet-generated paths. - Article rendering:
Article.bodyarrives as sanitised HTML → render into a styledprosecontainer; ToC and prev/next from the payload. No client-side Markdown parser. - Explicit loading and error states on every remote call; regenerate the client on every contract bump (typecheck failing until call sites adopt is intentional).
10. Decisions to ratify (ADRs)
This blueprint implied decisions that are "expensive to reverse or that another member could
unknowingly violate" — i.e. ADR-worthy. They are now Accepted and stewarded by
contract-owner (adr/README.md); each row links to its record:
| ADR | Decision |
|---|---|
| ADR-0005 | Filesystem is the content source of truth; PostgreSQL is a derived, rebuildable index; Redis caches rendered output. Refines ADR-0003's "system of record" for content. |
| ADR-0006 | The public API is read-only and unauthenticated; content is authored out-of-band; no admin panel. Any write/auth surface is a future major + auth ADR. |
| ADR-0007 | Markdown is rendered to sanitised HTML server-side and delivered as Article.body; single rendering/sanitising pipeline. |
| ADR-0008 | Scheduled publishing is enforced by read-time gating (correctness) plus a scheduler that emits content.article-published (cache-warming/notification). |
11. Delivery roadmap (contract-first sequencing)
Each phase ends at its tier's Definition of Done (CLAUDE.md → verify commands). Cross-tier
work is routed through contract-owner; parallel same-tier edits use worktree isolation.
Each phase also begins at a gate: its task list is presented for explicit user
confirmation before execution (from Phase 2 onward) — see
orchestration.md → Phase-start confirmation gate.
Status (2026-07-07): Phases 0–3 COMPLETE and verified; Phase 4 (Enrichment) COMPLETE — all five tracks (4a search, 4c assets & covers, 4b authors, 4d on-demand revalidation, 4e full-content feed) delivered and lead-verified. Contract v0.8.1 published (v0.8.0 plus the description-only curated-nav clarification); ADRs 0005–0017 Accepted. Notable since the original design: the client and API were split across hosts (ADR-0010 — the Next client on the bare host, the API on the api. host under a /v1 base); the reader UI was redesigned to the "Opened File" system (two review rounds); Mermaid diagrams render client-side (ADR-0011 assets, ADR-0012 Mermaid); author identity is settled as a free label with server-published slugs (ADR-0013); site identity lives in the root content metadata, served by GET /site (ADR-0014); unlisted pages + curated site.nav + the one-h1 render invariant landed as ADR-0015 (the About page is the first unlisted nav page); and the AsyncAPI events gained their first consumer — the relay → signed webhook → revalidateTag loop (ADR-0016), making publishing hands-off (save the file; fresh in well under 90 s — measured 21 s to appear, 51 s to prune). The live content root holds the published article set; the former example tree is now the test-fixture set (backend/tests/fixtures/content).
| Phase | Owner(s) | Deliverable | Done when |
|---|---|---|---|
| 0 · Bootstrap ✅ | platform-engineer |
DDEV up (Laravel + Next + Postgres + Redis), CONTENT_ROOT wired, scheduler service, example content tree |
ddev restart green; /health content check passes |
| 1 · Contract ✅ | contract-owner |
Publish v0.2.0: articles + sections + tags endpoints, core schemas, both events; ratify ADR-0005..0008 | ddev contract-lint clean; CHANGELOG entry; adoption tasks routed |
| 2 · Backend ✅ | domain-engineer → data-engineer → backend-api |
Domain model + policy + ports; filesystem source + Postgres index + reindex/scheduler + Redis cache; controllers conforming to v0.2.0 | ddev composer verify green; endpoints match contract; no forbidden edges |
| 3 · Frontend ✅ | frontend-engineer |
Generated client, catch-all routing, ISR + article rendering, sitemap/RSS | ddev frontend npm run verify green; reads live API |
| 4 · Enrichment ✅ | contract-owner + owners | Search (FTS) ✅, assets & covers ✅, authors browse ✅, on-demand revalidation ✅, full-content feed ✅ (ADR-0017, no contract change) | Each rode its own contract bump + tier verify |
12. Open questions
- Content sync — how does the content root reach the running backend in each environment: git pull on deploy, bind-mounted volume, or object-storage sync? (Infra ADR.)
- Draft preview — with no admin panel, is a signed preview-token route wanted for
SCHEDULED/DRAFTarticles, or is git-branch preview enough? (Would add an authenticated read path — contract + auth ADR.) - Assets —
ratifyRESOLVED (ADR-0011): backendGET /assets/{path}vs. render-time URL rewriting to a static originGET /v1/assets/{path}streaming content-root files, originals as-is, behind a fail-closed media-type allowlist; cover/in-body URLs rewritten to it. - Author identity —
keepRESOLVED (ADR-0013): authors are free-label strings (no accounts/bios); the server publishes a canonical slug per byline author, andauthoras a free label, or introduce anauthors.json/authors,/authors/{author},?author=operate on it. - i18n — single-locale now; if needed later, is locale a top-level section, a path convention, or a front-matter field? (Decide before it calcifies.)
Cross-references
- Constitution & rules:
../CLAUDE.md,rules-of-engagement.md - Tier model this builds on:
architecture.md - Who builds what:
team.md,orchestration.md - Interface home (proposals land here once published):
../contract/README.md