Skip to main content
The build process transforms Notion content and Astro components into a fully static website. This page explains each stage of the pipeline.

Build Pipeline Overview

Build Stages

1

Install Dependencies

Package manager installs all dependencies:
pnpm install
Duration: 30-60 seconds (cached after first build)Key dependencies:
  • astro - Static site generator
  • @notionhq/client - Notion API client
  • solid-js, react - UI frameworks
  • tailwindcss - Styling
  • sharp - Image optimization
2

Prebuild Hook

The prebuild script runs automatically before the build:
package.json
{
  "scripts": {
    "prebuild": "jiti scripts/index.ts",
    "build": "astro build"
  }
}
This executes the Notion sync script using jiti (TypeScript runner).
3

Notion Content Sync

The sync script (scripts/index.ts) fetches content from Notion:
scripts/index.ts
import "dotenv/config";
import { downloadPostsAsMdx } from "../src/lib/notion-download";

downloadPostsAsMdx("blog");
downloadPostsAsMdx("projects");

console.log("Finished downloading content.");
Duration: 10-60 seconds (depending on content changes)For each collection, the sync process:
  1. Queries Notion database for published posts
  2. Checks which posts need updating (incremental sync)
  3. Fetches blocks for changed posts
  4. Converts blocks to Markdown
  5. Downloads images/assets
  6. Writes MDX files with frontmatter
See Content Sync for detailed explanation.
4

Astro Build

Astro builds the static site:
astro build
Duration: 1-2 minutesBuild process:
  1. Content collections: Load and validate MDX files
  2. Static pages: Render all routes at build time
  3. Component compilation: Astro, React, and Solid.js components
  4. Asset optimization: Images, CSS, fonts
  5. Bundle generation: Create optimized JavaScript bundles
  6. HTML generation: Render final HTML for each page
5

Output Generation

Astro generates the dist/ directory:
dist/
├── index.html              # Home page
├── blog/
│   ├── index.html         # Blog listing
│   └── [slug].html        # Individual posts
├── projects/
│   ├── index.html         # Projects listing
│   └── [slug].html        # Individual projects
├── _astro/                # Bundled assets
│   ├── *.css             # Compiled Tailwind CSS
│   └── *.js              # JavaScript bundles
├── assets/                # Static images
└── fonts/                 # Variable fonts
All pages are pre-rendered as static HTML.

Optimization Strategies

Incremental Content Sync

The sync process only updates files that changed in Notion:
src/lib/notion-download.ts
async function shouldUpdateLocalFile(
  serverLastEditedTime: string,
  srcContentPath: string,
  postId: string
): Promise<boolean> {
  try {
    // Read lastEditedTime from existing MDX file
    const dest = path.join(process.cwd(), "src", "content", srcContentPath, postId).concat(".mdx");
    const readStream = fs.createReadStream(dest);
    // ... read lastEditedTime from frontmatter
    
    // Only update if server time is newer
    return `'${serverLastEditedTime}'` > lastEditedTime;
  } catch (err) {
    // File doesn't exist, fetch it
    return true;
  }
}
Benefits:
  • Faster builds (only fetch changed content)
  • Reduced Notion API usage
  • Lower bandwidth consumption

Asset Optimization

Images are optimized using Astro’s built-in Image component:
---
import { Image } from 'astro:assets';
---

<Image src={imagePath} alt="Description" />
Optimizations applied:
  • Automatic WebP/AVIF conversion
  • Responsive image generation
  • Lazy loading
  • Width/height attributes (no CLS)
Original images from Notion are downloaded to public/ during sync.

Build Performance

Build Time Breakdown

Cold Build

Total: 3-5 minutes
  • Dependencies: 1-2 min
  • Notion sync: 30-60 sec
  • Astro build: 1-2 min
  • Deploy: 30 sec

Incremental Build

Total: 2-3 minutes
  • Dependencies: 30 sec (cached)
  • Notion sync: 10-20 sec
  • Astro build: 1-2 min
  • Deploy: 30 sec

Output Size

Total: ~2-5 MB (uncompressed)
├── HTML: ~500 KB
├── CSS: ~20 KB
├── JavaScript: ~50 KB
├── Images: ~1-3 MB
└── Fonts: ~400 KB

Compressed (Brotli): ~500 KB - 1 MB

Environment Variables

Environment variables are used during the build:
These are embedded into the static output:
const siteUrl = import.meta.env.SITE_URL;
Used for:
  • Site URL in meta tags
  • Canonical URLs
  • Open Graph images
These are only available during build (not in browser):
const notionToken = import.meta.env.NOTION_TOKEN;
Used for:
  • Notion API authentication
  • Database queries
  • Asset downloads
Never expose secret variables in client-side code. They’re only available in .astro files and server-side scripts.

Build Hooks

You can customize the build process:

Adding Custom Steps

Modify scripts/index.ts to add custom build steps:
scripts/index.ts
import "dotenv/config";
import { downloadPostsAsMdx } from "../src/lib/notion-download";

// Existing steps
await downloadPostsAsMdx("blog");
await downloadPostsAsMdx("projects");

// Add custom steps
import { generateSitemap } from "./generate-sitemap";
await generateSitemap();

import { optimizeImages } from "./optimize-images";
await optimizeImages();

console.log("Finished downloading content.");

Skipping Notion Sync

For faster local builds, skip Notion sync:
# Skip prebuild hook
SKIP_NOTION_SYNC=true pnpm build
Modify scripts/index.ts:
if (process.env.SKIP_NOTION_SYNC !== 'true') {
  await downloadPostsAsMdx("blog");
  await downloadPostsAsMdx("projects");
} else {
  console.log("Skipping Notion sync (SKIP_NOTION_SYNC=true)");
}

Debugging Build Issues

1

Enable Verbose Logging

Add debug output to the sync script:
console.log(`Fetching ${posts.length} posts from Notion...`);
console.log(`Updated ${updatedCount} posts`);
2

Check Build Logs

On Vercel, view detailed logs:
  1. Go to Deployments
  2. Click the deployment
  3. View Build Logs tab
Look for:
  • Notion API errors
  • MDX parsing errors
  • Asset download failures
3

Local Build Testing

Test the full build locally:
pnpm build
pnpm preview
This runs the exact same process as production.

Caching Strategy

Build Cache

Vercel caches:
  • node_modules/ (dependencies)
  • .astro/ (Astro build cache)
  • pnpm-lock.yaml (lockfile)
Invalidated by:
  • Changes to package.json
  • Manual cache clear

Content Cache

Notion content is cached:
  • In src/content/ as MDX files
  • Incremental sync checks timestamps
  • Revalidated on every build
Invalidated by:
  • Changes in Notion (lastEditedTime)
  • Manual file deletion

Next Steps

Vercel Deployment

Configure Vercel deployment

Content Sync

Deep dive into Notion sync process

Build docs developers (and LLMs) love