← Back to blog

From Notion to Edge: Deploying a High-Performance Blog on Cloudflare

Feb 25, 2026

Created: Feb 25, 2026 · Updated: Mar 5, 2026

Building a Personal Website with Astro, Notion CMS, and Cloudflare

In this post, I’ll walk you through how I built my personal website using Astro as the frontend framework, Notion as a headless CMS, and Cloudflare’s ecosystem for deployment and asset storage. This setup gives me the best of both worlds: the familiar writing experience of Notion and the performance benefits of a static site.

Architecture Overview

Architecture Diagram

Here’s how the pieces fit together:

  1. Content Creation: I write blog posts in Notion using a structured database

  2. Sync Pipeline: A Node.js script fetches content from Notion API and converts it to Markdown

  3. Asset Storage: Images are automatically uploaded to Cloudflare R2 for reliable hosting

  4. Build & Deploy: Cloudflare Pages builds and deploys the Astro site

  5. One-Click Deploy: A protected API endpoint triggers rebuilds when I’m ready to publish

Why This Stack?

Notion as CMS

  • Familiar interface: No need to learn a new editor

  • Rich formatting: Tables, code blocks, callouts, embeds - all supported

  • Version history: Built-in revision tracking

  • Mobile editing: Write from anywhere

Astro for the Frontend

  • Performance: Ships zero JavaScript by default

  • Content Collections: Type-safe markdown handling

  • Flexible: Use any UI framework (React, Vue, Svelte) when needed

  • SEO-friendly: Static HTML output

Cloudflare for Infrastructure

  • Pages: Free static hosting with automatic deployments

  • R2: S3-compatible object storage (no egress fees!)

  • Zero Trust: Secure access to admin endpoints

  • Global CDN: Fast loading worldwide

Setting Up Notion

1. Create a Notion Integration

First, create a Notion integration to allow API access:

  1. Go to Notion Developers

  2. Click “New integration”

  3. Give it a name and select your workspace

  4. Copy the “Internal Integration Token”

2. Set Up Your Blog Database

Create a database with these properties:

3. Connect the Integration

Share your database with the integration:

  1. Open your database
  2. Click the ... menu → “Connections”
  3. Add your integration

The Sync Pipeline

The heart of this setup is the notion-sync.mjs script. Here’s what it does:

Fetching Content

// Fetch published pages from Notion
const pages = await queryDatabase({
  filter: {
    property: 'Published',
    checkbox: { equals: true }
  }
});

Converting Blocks to Markdown

The script converts Notion blocks to standard Markdown:

  • Paragraphs → Plain text

  • Headings#, ##, ###

  • Lists → or 1.

  • Code blocks → Fenced code with language

  • Images![caption](url)

  • Callouts → Blockquotes

  • Tables → Markdown tables

Image Handling with R2

Notion uses temporary signed URLs for images. The script:

  1. Downloads images from Notion

  2. Uploads them to Cloudflare R2

  3. Replaces URLs in the markdown with permanent R2 links

// Upload to R2
const command = new PutObjectCommand({
  Bucket: config.r2Bucket,
  Key: objectKey,
  Body: imageBuffer,
  ContentType: contentType,
});
await s3Client.send(command);

Generated Markdown Structure

Each synced post becomes a markdown file:

---
title: "Your Post Title"
slug: "your-post-title"
excerpt: "A brief summary..."
tags:
  - "astro"
  - "notion"
date: "2024-02-25"
createTimestamp: "2024-02-20T10:00:00.000Z"
lastUpdateTimestamp: "2024-02-25T14:30:00.000Z"
notionPageId: "abc123..."
showTableOfContents: true
draft: false
---

Your content here...

Cloudflare Pages Setup

1. Connect Your Repository

  1. Go to Cloudflare Dashboard → Pages

  2. Create a new project

  3. Connect your GitHub repository

  4. Configure build settings:

  • Build command: npm run build
  • Output directory: dist

2. Environment Variables

Add these variables in Pages settings:

# Notion
NOTION_TOKEN=secret_xxx
NOTION_DATABASE_ID=xxx

# R2 Storage
R2_ACCOUNT_ID=xxx
R2_ACCESS_KEY_ID=xxx
R2_SECRET_ACCESS_KEY=xxx
R2_BUCKET=blog-assets
R2_PUBLIC_BASE_URL=https://assets.yourdomain.com
R2_ENDPOINT=https://xxx.r2.cloudflarestorage.com

3. Deploy Hook

Create a deploy hook for triggering builds:

  1. Go to your Pages project settings
  2. Under “Builds & deployments”, find “Deploy hooks”
  3. Create a new hook and save the URL

One-Click Deploy Button

I created a simple API endpoint that triggers the deploy hook:

// functions/api/deploy.ts
export const onRequestGet: PagesFunction<Env> = async ({ env }) => {
  const response = await fetch(env.DEPLOY_HOOK_URL, { method: 'POST' });

  if (response.ok) {
    return new Response('Deploy triggered!');
  }
  return new Response('Deploy failed', { status: 500 });
};

Protected by Cloudflare Zero Trust, only I can access /api/deploy.

The Publishing Workflow

Here’s my actual workflow:

  1. Write a new post in Notion

  2. Toggle the “Published” checkbox

  3. Visit /api/deploy (or click my bookmarklet)

  4. Wait ~2 minutes for the build

  5. Done! The post is live

Tips & Lessons Learned

Use Incremental Sync

The script maintains a manifest to track what’s been synced:

{
  "pages": {
    "post-slug": {
      "documentHash": "abc123...",
      "lastUpdateTimestamp": "2024-02-25T14:30:00.000Z"
    }
  }
}

This avoids re-uploading unchanged content.

Handle Image Cleanup

When you unpublish a post, the script also deletes its R2 images:

if (shouldDelete) {
  await s3Client.send(new DeleteObjectCommand({
    Bucket: config.r2Bucket,
    Key: imageAsset.objectKey
  }));
}

Separate Dev and Prod Buckets

Use different R2 buckets for development and production to avoid polluting your production assets during testing.

Cost Breakdown

This entire setup is essentially free for personal use:

  • Cloudflare Pages: Free tier (500 builds/month)

  • Cloudflare R2: Free tier (10GB storage, 10M requests/month)

  • Notion: Free personal plan

  • GitHub: Free

Conclusion

This architecture gives me:

  • A delightful writing experience in Notion
  • Blazing-fast static site performance
  • Complete control over my content and hosting
  • Zero ongoing costs

The initial setup takes a few hours, but the ongoing maintenance is minimal. I just write in Notion and click deploy!


Check out the full source code on GitHub.

Comments