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.
- 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/).
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 filesHusky installs hooks on bun install (via the prepare script):
pre-commitrunsbun system-check(lint + typecheck + test).pre-pushrunsbun run buildto confirm the static export still succeeds.
app/ Next.js App Router routes
page.tsx Home, main menu of atlas sections
maps/ Coming-soon stub
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,
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
Styles are hand-written SCSS in two layers:
styles/globals.scssis 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-h3rules, and the.subtitletypographic primitive used by everyParchmentLayoutpage.- 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 siblingpage.module.scss(e.g.app/houses/[slug]/page.module.scss). Two filtered list components sharecomponents/listSearch.module.scssfor 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:
import styles from "@/components/Foo.module.scss";
import { cx } from "@/lib/cx";
<div className={cx(styles.row, isActive && styles.rowActive)} />;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.
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 ornull),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.
| 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/ |
stub | Coming soon |
/timeline/ |
stub | Coming soon |
Per-house and per-castle pages are pre-rendered via generateStaticParams from the content directory.
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.
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.
Per-castle pages cover the seat's type, sub-region, liege house, founding date, sworn houses, and notable features.
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.
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).
bun test # one shot
bun test:watch # watch modeNetlify 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.
docs/superpowers/ holds the specs and implementation plans the work has followed:
specs/2026-05-19-game-of-thrones-atlas-design.md: overall atlas designspecs/2026-05-26-main-menu-design.md: three-tile main menuplans/2026-05-19-foundation-and-first-castle.mdplans/2026-05-19-map-view.mdplans/2026-05-26-main-menu.md
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.






