Architecture Overview
Guiding Principles
Patterns Over Frameworks
Every architectural decision is explained in terms of the underlying pattern first, then how React/Next.js expresses it. “Separation of concerns” is the pattern; “custom hooks for logic, components for UI” is the React expression.
Explicit Over Implicit
React has many “magic” conventions (file-based routing, automatic code splitting, hydration). The documentation makes these explicit - what happens, when, and why it matters.
One Way to Do Things
React offers many paths to the same outcome. This architecture documents ONE chosen pattern for each concern, with reasoning. No guessing.
Teachable and Transferable
Each pattern documented here should be applicable to other projects with minimal translation. The docs serve as a template, not just a reference.
Layer Responsibilities
| Layer | Purpose | Data Flow |
|---|---|---|
pages/ | Routing, SSR decisions | Receives SSR data, passes to modules |
modules/ | Feature implementation | Composes shared components, uses hooks |
shared/ | Reusable code | Imported by modules |
rendering/ | SSR/CSR patterns | Used by pages |
layout/ | App shell | Wraps all pages |
Data flows DOWN through layers. Pages compose modules. Modules compose shared. Never upward.
Dynamic Sections
The site structure is configuration-driven. A Section defines an area of the site with two key properties:
display_type: How content renders —feed(infinite scroll),card-grid(responsive grid),static-page(single block)content_type: What items belong here —story,project,page
This separation enables combinations like blog feed (feed + story), project showcase (card-grid + project), or about page (static-page + page).
Registry Pattern
modules/registry/ maps display types and content types to React components:
- Display registry:
display_type→ display component (FeedDisplay, CardGridDisplay, StaticPageDisplay) - Content registry:
content_type→ list item + detail components (StoryCard/StoryDetail, ProjectCard/ProjectDetail)
Adding a new type means creating components and registering them. No routing or page changes needed.
Dynamic Routing
A single catch-all route (pages/[...slugPath].tsx) resolves all content URLs via a resolve-path backend endpoint that handles any URL depth in one call. Sections support unlimited nesting through parent_id references and a materialized path field for O(1) URL resolution. When content or sections move, automatic 301 redirects preserve old URLs. See ADR-0007 for the full design rationale.
Navigation
TopNav renders from database sections where nav_visibility = "main". A module-level cache in useNavSections prevents redundant API calls, with listener-based invalidation when sections are created, updated, or deleted.
Template System
Templates are CSS-only packages that control the visual identity of the site. Each template lives in templates/<name>/ and exposes a single index.css entry point imported in _app.tsx. No JavaScript, no React components — just CSS custom properties and component classes.
A template must provide:
tokens.css— CSS custom properties for colors, fonts, spacing, radii, and transitions, with dark mode overrides under.darktypography.css— Font assignments and heading styleslayout.css— Layout dimension variables (--layout-nav-height,--layout-container-max-width, etc.)components.css— Component classes (.card,.btn,.badge,.grid,.prose, etc.) consumed directly by React components
The full contract of required properties and classes is documented in templates/CONTRACT.md. Switching templates means copying the default, modifying the CSS, updating the import in _app.tsx, and adjusting site.config.json fonts if needed.
Site Configuration
site.config.json in the frontend root defines build-time site metadata loaded by the config/ module. It contains four sections:
| Key | Purpose |
|---|---|
site | Title, tagline, author, copyright (supports {year} token) |
fonts | Google Fonts for heading and body text |
navigation | Icon map linking section slugs to icon names for mobile bottom nav |
footer | Static footer links (Privacy, Terms) |
The config loader reads this file once at build time and exposes typed accessors to components. Layout, TopNav, Footer, and _document.tsx (Google Fonts injection) all consume it.
How Templates and Config Interact
Templates define what the fonts look like (sizes, weights, line-heights via CSS custom properties). Site config defines which fonts to load (Google Fonts families injected in _document.tsx). The template’s --font-family-sans / --font-family-serif variables reference the families that site.config.json loads. Changing fonts requires updating both: the config to load the new family, and the template tokens to reference it.