Jura Skrlec
10 min read

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: Runs next 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?

  1. No Egress Fees: R2 eliminates the unpredictable costs of serving files to your users, making it incredibly budget-friendly.

  2. Ease of Integration: R2 works seamlessly with Workers and Pages, so you can quickly integrate storage for your static or dynamic content.

  3. 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.

  4. 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: 4.50per1millionclassAoperations(PUT,COPY,POST,LIST)and4.50 per 1 million class A operations (PUT, COPY, POST, LIST) and 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:

  1. First, install the required dependencies:
npm install @aws-sdk/client-s3
  1. 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!,
  },
});
  1. 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
  1. Add .env.local to your .gitignore file to prevent committing sensitive data:
# .gitignore 
.env.local
  1. 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;
  }
}
  1. 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>
  );
}
  1. 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.