AISafety.com is a Next.js 16 web application using the App Router pattern. It serves as a community hub for AI safety resources, pulling content from Airtable.
┌─────────────────────────────────────────────────────────────┐
│ BROWSER │
├─────────────────────────────────────────────────────────────┤
│ Pages (SSR) │ Client Components │
│ - Homepage │ - D3Map (interactive map) │
│ - Events │ - LastUpdated (dynamic dates) │
│ - Map page │ - UpButton (scroll behavior) │
└────────────┬───────────┴──────────────┬─────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ NEXT.JS SERVER │
├─────────────────────────────────────────────────────────────┤
│ API Routes (src/app/api/) │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ /api/map │ │ /api/last-updated│ │
│ │ GET: all orgs │ │ GET: timestamps │ │
│ │ 5-min cache │ │ 5-min cache │ │
│ └────────┬────────┘ └────────┬─────────┘ │
└───────────┼────────────────────┼────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ AIRTABLE │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Map Table │ │ Metadata Table │ │
│ │ 323+ orgs │ │ Last updated │ │
│ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
| Component | Rendering | Why |
|---|---|---|
| Navigation | Server | Static content, no interactivity |
| Footer | Server | Static content |
| D3Map | Client | D3 requires DOM access, zoom/pan interactions |
| LastUpdated | Client | Fetches data after hydration |
| Pages | Server | SEO, faster initial load |
- API Routes as proxy: Don't expose Airtable credentials to client
- 5-minute cache:
{ next: { revalidate: 300 } }balances freshness with performance - Pagination handling: Airtable returns max 100 records, API loops to get all
Instead of Tailwind's unlimited utilities, this project enforces a design system:
/* Allowed spacing values (globals.css) */
.padding-4px, .padding-8px, .padding-12px, .padding-16px,
.padding-24px, .padding-32px, .padding-40px, .padding-56px,
.padding-80px, .padding-104px
/* Color palette */
--teal-100 through --teal-900 (grays with teal tint)
--bright-teal-300, --bright-teal-500 (accent colors)Rationale: Prevents arbitrary values that break visual consistency. If you need padding-bottom: 14px, the answer is "use 12px or 16px instead."
Current state management uses only React's built-in hooks:
useStatefor local component stateuseEffectfor side effects- No Redux, Zustand, or Context API needed yet
This is appropriate given the app's current complexity. Consider adding global state if:
- Multiple unrelated components need the same data
- You're prop-drilling more than 2-3 levels deep
Returns all organizations for the field map.
Response:
{
"records": [
{
"id": "rec123",
"title": "Anthropic",
"shortName": null,
"description": "AI safety company...",
"category": "Research Lab",
"status": "Active",
"logo": "https://...",
"mapLogo": "https://...",
"link": "https://anthropic.com",
"x": 45.3,
"y": 15.9,
"scale": "Large",
"isMagic": false
}
],
"lastUpdated": "2026-01-15",
"count": 323
}Returns timestamp for when data was last updated.
Response:
{
"lastUpdated": "2026-01-15T00:00:00.000Z",
"formattedDate": "15 January 2026"
}src/app/
├── layout.tsx # Root layout - wraps all pages
├── globals.css # ALL styles live here
├── page.tsx # Homepage
├── not-found.tsx # 404 page
├── map/
│ ├── page.tsx # Map page (fetches data, renders D3Map)
│ ├── D3Map.tsx # Client component with D3 visualization
│ ├── layout.tsx # Map-specific layout
│ └── page.module.css # Map-specific styles
├── events-and-training/
│ └── page.tsx
└── api/
├── map/
│ └── route.ts # Main data endpoint
└── last-updated/
├── map/route.ts
└── events/route.ts
- Platform: Vercel (recommended for Next.js)
- Environment Variables: Set in Vercel dashboard
- Build:
npm run build(automatic on push)
As more pages are built, consider extracting:
- Card components (currently inline in homepage)
- Page header/title pattern
- Airtable embed wrapper
- Filter/search UI (for jobs, communities pages)
Currently no tests. When adding:
- Use Vitest or Jest for unit tests
- Playwright for E2E (especially the D3 map)
- Images use
next/imagefor optimization - Consider adding
loading="lazy"to below-fold images - D3 map could benefit from virtualization for very large datasets