Skip to main content

System Architecture

Andrew Gao’s personal website is a statically generated site built with Astro that uses Notion as a headless CMS. The architecture follows a pre-build synchronization pattern where content is fetched from Notion during the build process and converted to local MDX files.

Tech Stack

Frontend Framework

Astro 5.5.4 - Static site generation with file-based routing

CMS

Notion API - Headless CMS for content management

UI Libraries

  • React 19 (Image component)
  • Solid.js 1.9 (Interactive components)

Styling

Tailwind CSS 4.0 via Vite plugin with Typography plugin

Core Design Decisions

1. Pre-build Content Synchronization

Decision: Fetch and convert Notion content to local MDX files at build time rather than runtime. Rationale:
  • Performance: Static generation eliminates API calls during page loads
  • Reliability: Site remains functional even if Notion API is down
  • Cost: No API rate limit concerns in production
  • SEO: Fully static HTML is optimal for search engines
Implementation: Located in src/lib/notion-download.ts:30
export async function downloadPostsAsMdx(collection: "projects" | "blog") {
  // Queries Notion database for public posts
  // Downloads assets locally
  // Writes MDX files with frontmatter
}

2. Multi-Framework Strategy

The project uses three different frameworks strategically:
Used for layouts, static pages, and server-side data fetching. Provides zero JavaScript by default.
Used specifically for Astro’s <Image /> component which provides automatic image optimization.Configured in astro.config.mjs:18-20 to only include the Satori component.
Used for client-side interactivity (Map, Navigation) with smaller bundle sizes than React.Components use client:only="solid-js" directive for hydration.

3. Incremental Content Updates

The sync process implements smart caching to avoid unnecessary re-downloads:
// src/lib/notion-download.ts:94-130
async function shouldUpdateLocalFile(
  serverLastEditedTime: string,
  srcContentPath: string,
  postId: string
): Promise<boolean> {
  // Reads lastEditedTime from local MDX frontmatter
  // Compares with server timestamp
  // Returns true only if content has changed
}
This optimization significantly reduces build times by only syncing modified content.

4. Type-Safe Notion Integration

All Notion API responses are typed using official SDK types:
import type {
  BlockObjectResponse,
  PageObjectResponse
} from '@notionhq/client/build/src/api-endpoints';
Type guards ensure runtime safety throughout the parsing pipeline (src/lib/notion-cms.ts:23-25).

Project Structure

src/
├── components/     # Astro, React, and Solid.js components
├── content/       # Auto-generated MDX from Notion
├── layouts/       # Page layout templates
├── lib/          # Notion integration & utilities
├── pages/        # File-based routing
└── styles/       # Global CSS and Tailwind config

Build Process

The build follows this sequence:
1

Pre-build Hook

jiti scripts/index.ts runs before astro dev or astro buildConfigured in package.json as predev and prebuild scripts.
2

Content Sync

  • Downloads blog posts from NOTION_BLOG_DB_ID
  • Downloads projects from NOTION_PROJECTS_DB_ID
  • Converts Notion blocks to Markdown
  • Downloads and optimizes assets
  • Writes MDX files to src/content/
3

Astro Build

  • Processes MDX content collections
  • Generates static pages
  • Optimizes images with Sharp
  • Creates sitemap
4

Deployment

  • Outputs to dist/ directory
  • Vercel deploys to CDN

Environment Variables

The system requires these Notion credentials:
NOTION_TOKEN
string
required
Notion API integration token from https://www.notion.so/my-integrations
NOTION_BLOG_DB_ID
string
required
Database ID for blog posts
NOTION_PROJECTS_DB_ID
string
required
Database ID for project entries
NOTION_DB_ID_PAGES
string
Database for dynamic navigation items
NOTION_DB_ID_PLACES
string
Database for places/map visualization
NOTION_DB_ID_KNOWLEDGE_*
string
Databases for yearly knowledge entries (2024, 2025, 2026)

Performance Characteristics

Build Time

  • Initial: ~30-60s (full sync)
  • Incremental: ~10-20s (changed content only)

Page Load

  • Static HTML: Less than 50ms TTFB
  • Zero runtime API calls
  • Optimized images (WebP)

Bundle Size

  • Base: ~50KB (Astro + Tailwind)
  • Interactive pages: +30KB (Solid.js)

Next Steps

Notion CMS Integration

Learn how Notion databases are structured and queried

Content Sync Process

Deep dive into the pre-build synchronization pipeline

Multi-Framework Strategy

Understand when to use Astro, React, or Solid.js

Build docs developers (and LLMs) love