Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
253 changes: 91 additions & 162 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,192 +1,121 @@
# Atlas of the Known World

An interactive atlas of George R. R. Martin's world of Ice and Fire, covering the maps, timeline, and the rolls of the great houses. Statically generated from a corpus of markdown files with Zod-validated frontmatter, rendered through a parchment-styled UI.

![The Known World main menu](docs/screenshots/home.png)
An interactive, static atlas and encyclopedia for the world of _A Song of Ice
and Fire_. It combines maps, a historical timeline, house and character
genealogies, and reference pages for battles, dragons, and weapons.

## Stack

- **Next.js 16** (App Router) with `output: 'export'`, so every route is pre-rendered to static HTML.
- **React 19**.
- **TypeScript 5**.
- **Bun** for install, scripts, and the Netlify build.
- **Zod 4** for content schemas.
- **gray-matter** + **remark** / **remark-html** for markdown.
- **react-svg-pan-zoom** for the regional map view.
- **Vitest 4** + **jsdom** + **@testing-library/react** for unit and component tests.
- **Netlify** for hosting (build = `bun run build`, publish = `out/`).

## Getting started
- Next.js App Router with static export
- React and TypeScript
- SCSS modules
- Markdown content with YAML frontmatter, validated by Zod
- Vitest and Testing Library
- Bun for dependency management and scripts
- Netlify for deployment and image transformation

```bash
bun install
bun dev # http://localhost:3000
bun test # vitest run
bun test:watch
bun run build # static export → out/
bun run lint
bun run typecheck # tsc --noEmit
bun run system-check # lint + typecheck + test, in sequence
bun run prettier # format every file
bun run prettier:check # fail if any file is unformatted
bun run prettier:quick # format only staged files
```
## Requirements

### Git hooks
- Node.js 24.16 or newer
- Bun 1.3.14 or newer

Husky installs hooks on `bun install` (via the `prepare` script):

- **`pre-commit`** runs `bun system-check` (lint + typecheck + test).
- **`pre-push`** runs `bun run build` to confirm the static export still succeeds.
## Getting started

## Project layout
Install dependencies and start the development server:

```
app/ Next.js App Router routes
page.tsx Home, main menu of atlas sections
maps/ Interactive world map (pan/zoom, keyboard shortcuts)
timeline/ Coming-soon stub
houses/ Index + per-house pages
[slug]/ Per-house page with family tree
characters/ Index + per-character pages
[slug]/ Per-character page (stub)
castles/[slug]/ Per-castle page
layout.tsx Root layout (Cinzel / EB Garamond / Inter fonts)
not-found.tsx

components/ React components, each paired with a co-located CSS module
ParchmentLayout, MainMenu, MainMenuTile, ComingSoonPage,
WorldMap, MapStage, MapMarker, MapLayerToggle,
FamilyTree, DropCap, Sources, Sigil, SiteHeader, SiteMenu, ViewToggle,
FilteredHouseList, FilteredCharacterList, HouseInfobox

lib/ Domain logic (loaders, schemas, helpers)
schemas.ts Zod schemas for Castle / House / Person / Event
content.ts Markdown loaders + renderer
family-tree.ts buildFamilyTree
map.ts Map coord helpers
relations.ts House / person relation helpers
*.test.ts Co-located vitest specs

content/ Markdown source of truth
castles/ 9 entries (Winterfell, Casterly Rock, Highgarden, …)
houses/ 4 entries (Stark, Lannister, Targaryen, Tyrell)
characters/ characters with parents/spouses/children

styles/ Single global stylesheet
globals.css Resets, CSS custom properties (--ink-*, --parchment-*,
--region-color-*, --font-*, --bp-*), `html`/`body`/`h1-h3`
rules, and the `.subtitle` typographic primitive.

# All other styles are CSS modules co-located with the React component or
# page route that owns them — see "Styling" below.

public/map/ westeros.svg basemap

docs/superpowers/ Design specs + implementation plans
netlify.toml Build config + 404 redirect
next.config.ts output: 'export', trailingSlash: true
```sh
bun install
bun dev
```

## Styling
The site is available at <http://localhost:46642>.

Styles are hand-written SCSS in two layers:
## Commands

- **`styles/globals.scss`** is the only global stylesheet. It holds resets, CSS custom properties (colour, font, and breakpoint tokens, plus the `--region-color-*` heraldic palette shared by both list views), `html`/`body`/`h1-h3` rules, and the `.subtitle` typographic primitive used by every `ParchmentLayout` page.
- **SCSS modules** carry everything else. Each component owns a sibling `<Component>.module.scss` (e.g. `components/SiteHeader.tsx` ↔ `components/SiteHeader.module.scss`); each route owns a sibling `page.module.scss` (e.g. `app/houses/[slug]/page.module.scss`). Two filtered list components share `components/listSearch.module.scss` for the search input + pagination apparatus they both render.

Class names inside modules are `camelCase`, dropping BEM noise — the file scope already isolates them. Multiple classes compose through `lib/cx.ts`, a six-line helper that joins truthy class strings:

```tsx
import styles from "@/components/Foo.module.scss";
import { cx } from "@/lib/cx";

<div className={cx(styles.row, isActive && styles.rowActive)} />;
```sh
bun run dev # start the development server
bun run test # run the test suite once
bun run test:watch # run tests in watch mode
bun run coverage # generate test coverage
bun run typecheck # check TypeScript
bun run lint # run ESLint
bun run prettier:check # check formatting
bun run build # create the static export in out/
bun run system-check # run every repository quality gate
```

Dynamic variant lookups use the indexed form: `styles[`card${capitalize(region)}`]` or a small per-component map. Cross-module styling — when a parent module needs to tweak a child component's element — is done by passing a className prop (`<Sigil className={styles.sigilFill} />`), not by reaching into another module's class names.

Tests assert class strings directly because `vitest.config.ts` sets `test.css.modules.classNameStrategy: 'non-scoped'`, so `styles.foo` resolves to the literal `'foo'` in jsdom. Production builds use Vite's default scoping with hashes.
Repository scripts must be run through Bun. Dependencies are pinned to exact
versions in `package.json`.

## Routes

| Route | Description |
| --------------------- | ----------------------------------- |
| `/` | Main atlas menu |
| `/maps/` | Interactive world map |
| `/timeline/` | Chronological battles and events |
| `/houses/` | Searchable and grouped house index |
| `/houses/[slug]/` | House details and family tree |
| `/characters/` | Searchable character index |
| `/characters/[slug]/` | Character details and relationships |
| `/battles/` | Battle index |
| `/battles/[slug]/` | Battle details |
| `/weapons/` | Weapon index |
| `/weapons/[slug]/` | Weapon details |
| `/dragons/` | Dragon index |
| `/dragons/[slug]/` | Dragon details |
| `/castles/[slug]/` | Castle details |
| `/events/[slug]/` | Timeline event details |

## Repository layout

```text
app/ Next.js routes and route-level styles
components/ Reusable UI components and co-located tests
content/ Markdown collections for atlas entities
docs/ Feature specifications, plans, and screenshots
lib/ Content loading, validation, relationships, and utilities
netlify/ Local Netlify build plugins
public/ Images, sigils, maps, and other static assets
styles/ Global design tokens and typographic primitives
```

## Content model

All content is markdown with frontmatter validated by Zod (`lib/schemas.ts`). Cross-references between entries are by slug.

- **`House`**: `slug`, `name`, `seat` (castle slug), `liege`, `words`, `sigil.description`, `founded` (date), `status` (`extant` / `extinct` / `exiled` / `hidden`), `sworn-from`, `cadet-houses`, `sources`.
- **`Castle`**: `slug`, `name`, `type` (`castle` / `town` / `ruin` / `watchtower` / `holdfast`), `sub-region`, `liege-house`, `founded`, `sworn-houses`, `features`, `coords` (`{x, y}` on the basemap), `sources`.
- **`Character`**: `slug`, `name`, `born` / `died` (date or `null`), `primary-house`, `also-of-houses`, `parents`, `spouses`, `children`, `titles`, `placeholder` (+ reason), `sources`. Placeholder characters fill unnamed slots in family trees.
- **`Event`**: `slug`, `name`, `type` (`battle` / `siege` / `treaty` / `wedding` / `death` / `betrayal` / `other`), `date`, `location` (castle slug or coords), `participants` (sides + houses), `outcome`, `casualties`, `sources`. Schema is in place; no event entries yet.

Dates use `{year, era, precision}` where `era` is one of `dawn-age`, `age-of-heroes`, `long-night`, `andal-invasion`, `targaryen-conquest`, `roberts-reign`, `game-of-thrones`, `AC`, `BC`, and `precision` is `exact` / `year` / `decade` / `era` / `legendary`.

Sources point back to AWOIAF (CC-BY-SA-3.0) or to a book / show / other reference.

## Routes today
Each entity is stored as `content/<collection>/<slug>.md`. Its filename must
match the `slug` in its YAML frontmatter. Frontmatter is parsed and validated
against the corresponding schema in `lib/schemas.ts`.

| Route | Status | Notes |
| --------------------- | ------ | ------------------------------------------------------------------------------------- |
| `/` | live | Atlas main menu (Maps · Timeline · Houses) |
| `/houses/` | live | A to Z list of houses, alphabetized by short name |
| `/houses/[slug]/` | live | Per-house page: words, seat link, sigil, founded, status, body, family tree |
| `/characters/` | live | A to Z list of characters (sigil + name) with debounced filter |
| `/characters/[slug]/` | live | Per-character page: sigil, born/died, primary house link, titles, body, linked family |
| `/castles/[slug]/` | live | Per-castle page |
| `/maps/` | live | Interactive world map: drag/pinch/wheel pan & zoom, on-screen controls, keyboard keys |
| `/timeline/` | stub | Coming soon |
Cross-references use slugs. The content-integrity test validates unique slugs,
filename/frontmatter agreement, and references between modeled entities. Run it
directly with:

Per-house and per-castle pages are pre-rendered via `generateStaticParams` from the content directory.

### Houses

The A to Z roll of the great and minor houses, each tile showing the family sigil. Click through to a per-house page with its words, seat, founding date, status, body text, and full family tree.

![Houses index](docs/screenshots/houses-index.png)

![House Stark page](docs/screenshots/house-stark.png)

![House Targaryen page](docs/screenshots/house-targaryen.png)

### Characters

A debounced filter over every named character, each row showing the character portrait and primary-house sigil. Per-character pages link back into the family graph and primary house.

![Characters index](docs/screenshots/characters-index.png)

![Jon Snow page](docs/screenshots/character-jon-snow.png)

### Castles

Per-castle pages cover the seat's type, sub-region, liege house, founding date, sworn houses, and notable features.

![Winterfell page](docs/screenshots/castle-winterfell.png)
```sh
bun run test lib/content-integrity.test.ts
```

## Family tree
Named people that are needed for a relationship but do not yet have a complete
article can use the character schema's `placeholder` fields. Draft entries are
excluded from generated route parameters.

`lib/family-tree.ts` builds a hierarchical tree for a house from the `parents` / `children` graph in `content/characters/`. Placeholder ancestors fill in unnamed slots (e.g. unknown mothers). Rendered by `components/FamilyTree.tsx` on each house page.
## Development workflow

## Testing
Read `CLAUDE.md` for repository conventions before making changes. In
particular:

Vitest runs in jsdom with the React plugin and native tsconfig path resolution. Unit tests live next to the modules they cover (`lib/*.test.ts`, `components/*.test.tsx`).
1. Create a branch for each feature or bug fix.
2. Keep tests co-located with their implementation.
3. Run `bun run system-check` before handing off a change.
4. Prefix commit subjects with `TKW:`.

```bash
bun test # one shot
bun test:watch # watch mode
```
The pre-commit hook runs formatting, type checking, linting, and tests. The
pre-push hook runs the production build.

## Deployment

Netlify builds with `bun run build` and publishes `out/`. The Next config sets `output: 'export'` and `trailingSlash: true`, so every page ships as an `index.html` under a directory. `netlify.toml` includes a catch-all 404 redirect.

## Design + planning docs

`docs/superpowers/` holds the specs and implementation plans the work has followed:

- `specs/2026-05-19-game-of-thrones-atlas-design.md`: overall atlas design
- `specs/2026-05-26-main-menu-design.md`: three-tile main menu
- `plans/2026-05-19-foundation-and-first-castle.md`
- `plans/2026-05-19-map-view.md`
- `plans/2026-05-26-main-menu.md`

## Convention notes
`next.config.ts` configures `output: "export"`, so `bun run build` writes the
deployable site to `out/`. Netlify runs the same build, publishes `out/`, and
uses the custom image loader in `lib/netlify-image-loader.ts` for production
image transformations.

This repo uses a newer Next.js than most training data, so read `node_modules/next/dist/docs/` for current APIs before writing route handlers, params, or metadata. See `AGENTS.md`.
Content source attribution is rendered from each entry's `sources` frontmatter.
Loading
Loading