How to Build a Blog with SonicJS and Cloudflare Workers
Step-by-step tutorial on building a blazingly fast blog using SonicJS headless CMS deployed on Cloudflare Workers with D1 database and R2 storage.

How to Build a Blog with SonicJS and Cloudflare Workers
TL;DR — Build a complete blog backend with SonicJS in under 30 minutes. Define Authors, Categories, and Posts collections, configure caching for performance, and deploy to Cloudflare Workers for global edge delivery.
Key Stats:
- Sub-50ms response times worldwide
- 3 content collections: Authors, Categories, Posts
- Built-in relationships and media handling
- 1 command to deploy:
npm run deploy
Building a blog has never been faster. In this tutorial, we'll create a complete blog backend using SonicJS, leveraging Cloudflare Workers for global edge deployment. Your blog will have sub-50ms response times worldwide.
What We're Building
By the end of this tutorial, you'll have:
- A complete blog content API
- Author management with relationships
- Category and tag support
- Media uploads for featured images
- Full-text search capability
Prerequisites
- Node.js 20+
- Cloudflare account
- Wrangler CLI installed
Step 1: Project Setup
Create a new SonicJS project:
npx create-sonicjs@latest my-blog
cd my-blog
Step 2: Define Content Collections
Authors Collection
Create src/collections/authors.ts:
import { CollectionConfig } from '@sonicjs-cms/core'
export const authorsCollection: CollectionConfig = {
name: 'authors',
schema: {
properties: {
name: {
type: 'string',
required: true,
maxLength: 100,
},
email: {
type: 'string',
format: 'email',
required: true,
},
bio: {
type: 'string',
maxLength: 500,
},
avatar: {
type: 'string',
},
twitter: {
type: 'string',
maxLength: 50,
},
github: {
type: 'string',
maxLength: 50,
},
},
},
}
Categories Collection
Create src/collections/categories.ts:
import { CollectionConfig } from '@sonicjs-cms/core'
export const categoriesCollection: CollectionConfig = {
name: 'categories',
schema: {
properties: {
name: {
type: 'string',
required: true,
maxLength: 50,
},
slug: {
type: 'string',
required: true,
},
description: {
type: 'string',
maxLength: 200,
},
},
},
}
Posts Collection
Create src/collections/posts.ts:
import { CollectionConfig } from '@sonicjs-cms/core'
export const postsCollection: CollectionConfig = {
name: 'posts',
schema: {
properties: {
title: {
type: 'string',
required: true,
maxLength: 200,
},
slug: {
type: 'string',
required: true,
},
excerpt: {
type: 'string',
maxLength: 300,
},
content: {
type: 'lexical',
required: true,
},
featuredImage: {
type: 'string',
},
author: {
type: 'reference',
collection: 'authors',
required: true,
},
category: {
type: 'reference',
collection: 'categories',
},
tags: {
type: 'array',
of: 'string',
},
status: {
type: 'string',
enum: ['draft', 'published', 'archived'],
default: 'draft',
},
publishedAt: {
type: 'string',
format: 'date-time',
},
seo: {
type: 'object',
properties: {
metaTitle: { type: 'string', maxLength: 60 },
metaDescription: { type: 'string', maxLength: 160 },
ogImage: { type: 'string' },
},
},
},
},
}
Step 3: Configure the CMS
Update src/index.ts:
import { createSonicJSApp, registerCollections } from '@sonicjs-cms/core'
import { authorsCollection } from './collections/authors'
import { categoriesCollection } from './collections/categories'
import { postsCollection } from './collections/posts'
registerCollections([
authorsCollection,
categoriesCollection,
postsCollection,
])
export default createSonicJSApp()
Step 4: Set Up Database
Create and configure D1:
# Create database
wrangler d1 create my-blog-db
# Create KV namespace for caching
wrangler kv:namespace create CACHE
# Create R2 bucket for media
wrangler r2 bucket create my-blog-storage
Update wrangler.toml with your resource IDs:
name = "my-blog"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[[d1_databases]]
binding = "DB"
database_name = "my-blog-db"
database_id = "your-database-id"
[[r2_buckets]]
binding = "MEDIA_BUCKET"
bucket_name = "my-blog-storage"
Run migrations:
npm run db:migrate:local
Step 5: Test Your API
Start the development server:
npm run dev
Create an Author
curl -X POST http://localhost:8787/api/authors \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"name": "Jane Doe",
"email": "jane@example.com",
"bio": "Technical writer and developer advocate.",
"twitter": "janedoe"
}'
Create a Category
curl -X POST http://localhost:8787/api/categories \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"name": "Tutorials",
"slug": "tutorials",
"description": "Step-by-step guides and how-tos"
}'
Create a Post
curl -X POST http://localhost:8787/api/posts \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"title": "Hello World",
"slug": "hello-world",
"excerpt": "My first blog post with SonicJS",
"content": "<p>Welcome to my new blog!</p>",
"author": "author-id-here",
"category": "category-id-here",
"tags": ["introduction", "hello"],
"status": "published",
"publishedAt": "2025-12-12T00:00:00Z"
}'
Query Published Posts
# Get all published posts with author data
curl "http://localhost:8787/api/posts?status=published&include=author,category"
# Search posts
curl "http://localhost:8787/api/posts?search=hello"
# Filter by category
curl "http://localhost:8787/api/posts?category=category-id"
Step 6: Deploy to Production
Deploy your blog to Cloudflare's global network:
# Apply production migrations
npm run db:migrate
# Deploy
npm run deploy
Your blog API is now live at https://my-blog.your-subdomain.workers.dev!
Step 7: Connect Your Frontend
Use any frontend framework to consume your blog API:
React/Next.js Example
// lib/api.ts
const API_URL = process.env.NEXT_PUBLIC_CMS_URL
export async function getPosts() {
const res = await fetch(`${API_URL}/api/posts?status=published&include=author`)
return res.json()
}
export async function getPost(slug: string) {
const res = await fetch(`${API_URL}/api/posts?slug=${slug}&include=author,category`)
const data = await res.json()
return data.data[0]
}
// app/blog/page.tsx
import { getPosts } from '@/lib/api'
export default async function BlogPage() {
const { data: posts } = await getPosts()
return (
<div>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
<span>By {post.author.name}</span>
</article>
))}
</div>
)
}
Performance Tips
Enable Aggressive Caching
cachePlugin({
defaultTTL: 3600,
patterns: {
'/api/posts': { ttl: 60 }, // List updates quickly
'/api/posts/*': { ttl: 3600 }, // Individual posts cache longer
},
})
Use ISR in Next.js
export const revalidate = 60 // Revalidate every 60 seconds
Optimize Images
SonicJS integrates with Cloudflare Images for automatic optimization:
mediaPlugin({
imageOptimization: true,
variants: ['thumbnail', 'medium', 'large'],
})
Key Takeaways
- SonicJS makes building a blog API straightforward
- Collections define your content structure with TypeScript
- Relationships connect authors, categories, and posts
- D1 and KV provide edge-first data storage
- Deploy globally with a single command
Next Steps
- Add comments using a custom collection
- Implement newsletter subscriptions
- Set up webhooks for build triggers
- Add full-text search with Cloudflare Workers AI
Happy blogging with SonicJS!
Related Articles

Deploying SonicJS to Cloudflare Workers: A Step-by-Step Guide
Ship a SonicJS headless CMS to Cloudflare Workers in minutes — wrangler config, D1, KV, R2, secrets, custom domains, preview deploys, and rollback in one guide.

Building Your First SonicJS Plugin: A Walkthrough of the Example Plugin
Learn the SonicJS v3 plugin system by dissecting the example plugin that ships with every new install — routes, collections, settings, hooks, and data seeding explained.

File Uploads with SonicJS and Cloudflare R2
Upload, validate, and serve images, video, and documents with SonicJS and Cloudflare R2 — multipart uploads, MIME checks, signed URLs, and image transforms.