When it comes to hosting modern web applications, developers are spoiled for choice. Platforms like Vercel, Netlify, and Cloudflare Pages offer a wealth of features, making it easier than ever to deploy, scale, and manage web projects. But how do you choose the right one? In this post, I’ll break down the strengths of each platform and explain why I ultimately decided to host this website with Cloudflare Pages.
Vercel
Vercel is a favorite among developers, especially those working with Next.js (a framework they created). Known for its seamless integration with Git-based workflows and features like automatic deployments, preview URLs, and built-in serverless functions, Vercel is a powerhouse for front-end hosting. Its pros include:
- Optimized for Next.js: Unparalleled support for SSR and static site generation
- Ease of Use: Automatic builds and previews with minimal configuration.
- Performance: Built-in CDN ensures fast page loads globally.
However, Vercel’s pricing can climb as your app scales, especially if you need advanced features like custom edge functions.
Netlify
Netlify has become synonymous with modern web development. Its strong emphasis on JAMstack architecture has made it a go-to for developers working with static sites and single-page applications (SPAs). Netlify shines in areas like:
- All-in-One Solution: Hosting, serverless functions, forms, and identity management under one roof.
- Developer Experience: Git-based workflows, preview URLs, and plugins for CI/CD pipelines.
- Generous Free Tier: Perfect for small projects and proofs of concept.
But Netlify can sometimes feel opinionated, and complex projects with non-standard workflows may require workarounds.
Cloudflare Pages
Cloudflare Pages might seem like the new kid on the block compared to Vercel and Netlify, but it’s backed by Cloudflare’s extensive edge network, known for speed and reliability. Its strengths include:
- Global Edge Network: Blazing-fast static file delivery, optimized for low latency.
- Built-In Cloudflare Features: Easily integrate with Workers, KV storage, and DDoS protection.
- Scalability: Handle sudden traffic spikes without breaking a sweat.
- Developer-Friendly Pricing: A very generous free tier with features like unlimited bandwidth.
That said, Cloudflare Pages is still maturing in terms of developer experience and integrations compared to its competitors.
Why I Chose Cloudflare Pages
After weighing the pros and cons of each platform, I decided to host my project with Cloudflare Pages for several reasons:
1. Performance at the Edge
Cloudflare Pages leverages Cloudflare’s massive global edge network, ensuring my site loads quickly no matter where users are located. This is crucial for delivering a great user experience in regions where latency is often an issue.
2. Cost-Effectiveness
As a developer running a small project, I need a hosting solution that scales without breaking the bank. Cloudflare Pages offers unlimited bandwidth on its free tier, which is a game-changer for projects with unpredictable traffic patterns.
3. Seamless Integration with Workers
Cloudflare Workers are a powerful tool for adding custom logic at the edge, and integrating them with Pages is straightforward. This allows me to experiment with features like real-time transformations, APIs, and dynamic routing.
4. Future-Proofing
Cloudflare is investing heavily in its developer ecosystem. With tools like D1 (Cloudflare’s SQLite database) and Workers, it’s clear that Pages is part of a larger vision. This gives me confidence that my choice will serve me well as my project grows.
5. Simple Setup
The setup process for Cloudflare Pages was intuitive and Git-centric, aligning perfectly with my workflow. Automatic deployments and preview URLs for every pull request streamline collaboration and testing.
How to Deploy a Next.js App on Cloudflare Pages
It’s important to note that Cloudflare Pages currently supports Next.js versions 13 and 14. If you’re using an older or newer version, you’ll need to upgrade or ensure compatibility. This website is using Next.js v14.2.21
1. Install next-on-pages
First, install @cloudflare/next-on-pages
npm install --save-dev @cloudflare/next-on-pages
2. Add wrangler.toml
file
Then, add a wrangler.toml
file to the root directory of your Next.js app:
name = "name-of-your-project" compatibility_date = "2024-12-27" compatibility_flags = ["nodejs_compat"] pages_build_output_dir = " .vercel/output/static"
3. Update next.config.mjs
import { setupDevPlatform } from '@cloudflare/next-on-pages/next-dev'; /** @type {import('next').NextConfig} */ const nextConfig = {}; if (process.env.NODE_ENV === 'development') { await setupDevPlatform(); } export default nextConfig;
4. Ensure all server-rendered routes use the Edge Runtime
Next.js has two "runtimes" — "Edge" and "Node.js". When you run your Next.js app on Cloudflare, you can use available Node.js APIs — but you currently can only use Next.js' "Edge" runtime.
This means that for each server-rendered route — ex: an API route or one that uses getServerSideProps
— you must configure it to use the "Edge" runtime:
export const runtime = "edge";
Put that in all appropriate page.tsx
files.
5. Update package.json
Add the following to the scripts field of your package.json
file:
"scripts": { "pages:build": "npx @cloudflare/next-on-pages", "preview": "npm run pages:build && wrangler pages dev", "deploy": "npm run pages:build && wrangler pages deploy" }
npm run pages:build
: Runsnext build
, and then transforms its output to be compatible with Cloudflare Pages.npm run preview
: Builds your app, and runs it locally in workerd, the open-source Workers Runtime. (next dev
will only run your app in Node.js)npm run deploy
: Builds your app, and then deploys it to Cloudflare
6. Cloudflare Dashboard
Go to Cloudflare Dashboard and from the menu on the left choose Workers & Pages. Choose Create Cloudflare Page and link your Git repository (Github, Bitbucket, Gitlab...). The deploy will start. It will deploy your site to: web_name.pages.dev. Add your custom domain. You can choose the production branch, by default is main.
Congratulations! You've deployed your site with Cloudflare Pages!
Storage
f you want to deploy a blog like this one, you’ll need to do a little extra work. Since you can’t use fs
from Node.js—the file system is not available on Cloudflare Workers or Pages—you’ll need a solution to store and retrieve your blog content. A database or object storage is essential. I chose Cloudflare R2, a powerful, cost-effective, and highly scalable object storage solution.
Cloudflare R2 is designed to store unstructured data, such as media files, documents, or blog content, and makes it accessible via its simple API. Unlike traditional file storage, R2 doesn’t charge for egress bandwidth, which is a huge advantage if your blog attracts significant traffic or serves large files.
Why Choose Cloudflare R2?
-
No Egress Fees: R2 eliminates the unpredictable costs of serving files to your users, making it incredibly budget-friendly.
-
Ease of Integration: R2 works seamlessly with Workers and Pages, so you can quickly integrate storage for your static or dynamic content.
-
S3-Compatible API: R2 supports the widely used AWS S3 API, which means you can use existing S3-compatible tools and libraries without additional setup.
-
Cost-Effectiveness: R2 offers competitive pricing that suits both small-scale blogs and enterprise-level projects.
How R2 Works
Cloudflare R2 stores your files (objects) in buckets, similar to AWS S3. You can read, write, and manage these objects using RESTful API calls. For a blog, you could store markdown files, images, or JSON metadata as objects in an R2 bucket. The blog application then fetches these files dynamically via API calls, ensuring fast delivery with Cloudflare’s global network.
R2 Pricing
Cloudflare R2 has a straightforward pricing model:
-
Storage: 10GB free then $0.015 per GB per month.
-
Operations: 0.36 per 1 million class B operations (GET, HEAD).
-
No Egress Fees: Unlike most object storage solutions, R2 does not charge for data transfer (egress) from your storage to users.
For example, hosting a blog with 10 GB of content and moderate traffic might cost less than $1 per month—significantly cheaper than traditional cloud storage providers.
Using Cloudflare R2 to host blog content is a smart choice for developers who want fast, reliable, and cost-effective storage. Its no-egress-fees model and seamless integration with Cloudflare Pages and Workers make it ideal for dynamic applications like blogs. Whether you’re serving markdown files, media assets, or JSON APIs, R2 provides the flexibility and scalability you need without the high costs.
Connect R2
Go to Cloudflare Dashboard and choose R2 Object Storage from the menu. Choose Create bucket. Name your bucker. Drag and drop markdown files to your bucket.
In your terminal:
- First, install the required dependencies:
npm install @aws-sdk/client-s3
- Create an R2 client configuration. Create a new file, let's say
lib/r2.ts
:
import { S3Client } from '@aws-sdk/client-s3'; export const r2Client = new S3Client({ region: 'auto', endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`, credentials: { accessKeyId: process.env.R2_ACCESS_KEY_ID!, secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!, }, });
- During Local Development: Create a
.env.local
file in your project root (same level as package.json):
R2_ACCOUNT_ID=your_account_id R2_ACCESS_KEY_ID=your_access_key_id R2_SECRET_ACCESS_KEY=your_secret_access_key R2_BUCKET_NAME=your_bucket_name
- Add
.env.local
to your.gitignore
file to prevent committing sensitive data:
# .gitignore .env.local
- Create a server action or API route to fetch posts. Here's an example using a server action in
app/actions/getPosts.ts
:
'use server' import { ListObjectsV2Command, GetObjectCommand } from '@aws-sdk/client-s3'; import { r2Client } from '@/lib/r2'; export async function getPosts() { try { const listCommand = new ListObjectsV2Command({ Bucket: process.env.R2_BUCKET_NAME, Prefix: 'posts/', }); const { Contents } = await r2Client.send(listCommand); if (!Contents) { return []; } const posts = await Promise.all( Contents.map(async (object) => { if (!object.Key) return null; const getCommand = new GetObjectCommand({ Bucket: process.env.R2_BUCKET_NAME, Key: object.Key, }); const response = await r2Client.send(getCommand); const content = await response.Body?.transformToString(); return { key: object.Key, content, lastModified: object.LastModified, }; }) ); return posts.filter(Boolean); } catch (error) { console.error('Error fetching posts:', error); throw error; } }
- Use the server action in your page component:
// app/blog/page.tsx import { getPosts } from '@/app/actions/getPosts'; export default async function BlogPage() { const posts = await getPosts(); return ( <div> {posts.map((post) => ( <article key={post.key}> <h2>{post.key.replace('posts/', '').replace('.md', '')}</h2> <div>{post.content}</div> </article> ))} </div> ); }
-
For Cloudflare Pages deployment, add your R2 environment variables in the Cloudflare Pages dashboard:
- Go to your Pages project settings
- Go to Variables and Secrets
- For each env (Production and Preview), add your values, as same as in
.env.local
- Your environment variables structure should look like this:
Production environment: - R2_ACCOUNT_ID: [encrypted] - R2_ACCESS_KEY_ID: [encrypted] - R2_SECRET_ACCESS_KEY: [encrypted] - R2_BUCKET_NAME: your-bucket-name Preview environment: - R2_ACCOUNT_ID: [encrypted] - R2_ACCESS_KEY_ID: [encrypted] - R2_SECRET_ACCESS_KEY: [encrypted] - R2_BUCKET_NAME: your-bucket-name-preview
That's it! You can store and retrieve whatever you want now! :)
Using Cloudflare R2 to host blog content is a smart choice for developers who want fast, reliable, and cost-effective storage. Its no-egress-fees model and seamless integration with Cloudflare Pages and Workers make it ideal for dynamic applications like blogs. Whether you’re serving markdown files, media assets, or JSON APIs, R2 provides the flexibility and scalability you need without the high costs.