War.re is organised as a monorepo containing two fully independent Next.js applications. Each app lives in its own directory, carries its own package.json and yarn.lock, and builds and deploys separately — there is no shared runtime, no shared node_modules, and no cross-app imports. You work inside whichever app directory you need and treat the other as a sibling project.
The Two Apps
| Directory | Domain | Purpose |
|---|
main/ | war.re | Personal homepage |
subdomains/ryan/ | ryan.war.re | Online resume |
Both apps use the Pages Router (pages/), not the App Router. Each has its own _app.tsx and _document.tsx for custom wrappers, its own styles/ directory for CSS modules, and its own public/ directory for static assets such as favicons and OG images.
Directory Tree
The repository root holds the two app directories alongside a small scripts/ folder and repo-level config files. Here is a condensed view of the key paths:
war.re/ ← repository root
├── CLAUDE.md ← project-wide conventions for AI tools
├── README.md
├── scripts/ ← shared utility scripts
│
├── main/ ← Next.js app for war.re
│ ├── package.json
│ ├── yarn.lock
│ ├── next.config.js ← output: 'export', reactStrictMode: true
│ ├── tsconfig.json
│ ├── pages/
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── index.tsx ← meta-refresh redirect → /n
│ │ ├── n/
│ │ │ └── index.tsx ← homepage content (war.re/n)
│ │ └── 404.tsx
│ ├── styles/
│ ├── public/
│ └── e2e/ ← Playwright smoke tests
│
└── subdomains/
└── ryan/ ← Next.js app for ryan.war.re
├── package.json
├── yarn.lock
├── next.config.js ← output: 'export', reactStrictMode: true
├── tsconfig.json
├── pages/
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── index.tsx ← meta-refresh redirect → /n
│ ├── n/
│ │ └── index.tsx ← resume content (ryan.war.re/n)
│ └── 404.tsx
├── components/ ← resume-specific React components
│ ├── Activity.tsx
│ ├── ActivityList.tsx
│ ├── Experience.tsx
│ └── ExperienceItem.tsx
├── styles/
├── public/
├── __tests__/ ← Jest + React Testing Library
└── e2e/ ← Playwright smoke tests
URL Routing and the /n Redirect
Neither app uses server-side rewrites. Both are configured with output: 'export' in next.config.js, which generates a fully static site that Vercel serves directly from the filesystem.
The root path (/) on each domain uses an HTML <meta http-equiv="refresh"> tag to immediately redirect the browser to /n, which is where the actual content lives. This approach works without any server logic:
- Visiting
war.re → browser reads the meta-refresh → navigates to war.re/n
- Visiting
ryan.war.re → browser reads the meta-refresh → navigates to ryan.war.re/n
A <noscript> fallback link is included for browsers with JavaScript disabled so they still get a clickable link to /n.
Pages Directory Structure
Each app mirrors the same pages/ layout:
| File | Route | Role |
|---|
pages/index.tsx | / | Meta-refresh shell; redirects to /n |
pages/n/index.tsx | /n | Main content page |
pages/_app.tsx | — | Custom App wrapper |
pages/_document.tsx | — | Custom Document wrapper |
pages/404.tsx | /404 | Custom not-found page |
The subdomains/ryan/ app also has a components/ directory at the app root, holding the resume-specific React components (Activity, ActivityList, Experience, ExperienceItem) that the /n page composes together.
Both apps configure a TypeScript path alias @/* that maps to the app’s own project root. Use @/components/Foo instead of relative paths like ../../components/Foo to keep imports clean and refactor-safe as the directory tree grows.
Independent Builds
Run all commands from inside the relevant app directory — not from the repository root. Each app is self-contained:
# Work on the main site
cd main/
yarn dev # dev server at localhost:3000
yarn build # static export → out/
yarn lint # ESLint
yarn typecheck # tsc --noEmit
yarn format # Prettier (format:check to verify without writing)
yarn e2e # Playwright smoke tests (run yarn build first)
# Work on the resume
cd subdomains/ryan/
yarn dev # dev server at localhost:3000
yarn build # static export → out/
yarn test # Jest + React Testing Library
yarn lint # ESLint
yarn typecheck # tsc --noEmit
yarn format # Prettier (format:check to verify without writing)
yarn e2e # Playwright smoke tests (run yarn build first)
The main/ app currently has no Jest unit tests (--passWithNoTests is set so the test script exits cleanly). The subdomains/ryan/ app has full Jest coverage for its components.