From Notion to Edge: Deploying a High-Performance Blog on Cloudflare
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:
-
Content Creation: I write blog posts in Notion using a structured database
-
Sync Pipeline: A Node.js script fetches content from Notion API and converts it to Markdown
-
Asset Storage: Images are automatically uploaded to Cloudflare R2 for reliable hosting
-
Build & Deploy: Cloudflare Pages builds and deploys the Astro site
-
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:
-
Go to Notion Developers
-
Click “New integration”
-
Give it a name and select your workspace
-
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:
- Open your database
- Click the
...menu → “Connections” - 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 →
 -
Callouts → Blockquotes
-
Tables → Markdown tables
Image Handling with R2
Notion uses temporary signed URLs for images. The script:
-
Downloads images from Notion
-
Uploads them to Cloudflare R2
-
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
-
Go to Cloudflare Dashboard → Pages
-
Create a new project
-
Connect your GitHub repository
-
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:
- Go to your Pages project settings
- Under “Builds & deployments”, find “Deploy hooks”
- 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:
-
Write a new post in Notion
-
Toggle the “Published” checkbox
-
Visit
/api/deploy(or click my bookmarklet) -
Wait ~2 minutes for the build
-
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.