CI/CD for Cloudflare Pages using CircleCI and Wrangler
Fullstack Developer and Tech Author
When building static websites with tools like Next.js, getting your content live should be just as seamless as writing it. But in practice, deployment can quickly become a manual chore, especially when testing, caching, and previews are involved. That’s why this guide shows you how to set up a CI/CD pipeline with CircleCI, Cloudflare Pages, and Wrangler. You will use the pipeline to deploy a static Next.js site only when your tests pass.
You will develop the project from a simple Next.js app to a fully automated deployment pipeline that runs tests, builds, and deploys your site to Cloudflare Pages whenever you push changes. You will build a simple book discovery app called CircleReads, showcasing books loved by engineers; a simple, static 2-page site that can be easily extended.
Prerequisites
- Cloudflare account. A free account is sufficient.
- CircleCI account
- Node.js version 20 or higher installed on your computer
- A GitHub account
- Basic knowledge of building applications with Next.js and TypeScript
Scaffolding the Next.js project
To begin, you will scaffold a new Next.js project using the create-next-app command. This will set up a basic Next.js application with TypeScript support. Issue the following command in your terminal to create a new Next.js app named circleci-books-pages:
npx create-next-app@latest circleci-books-pages --typescript
You will be prompted with several options during the setup process, respond as follows:
✔ Would you like to use ESLint? … No
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like your code inside a `src/` directory? … No
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to use Turbopack for `next dev`? … Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No
Creating a new Next.js app in /Users/yemiwebby/tutorial/circleci/circleci-books-pages.
...
Once the setup is complete, go to the newly created project directory:
cd circleci-books-pages
Start setting up your Next.js application with Tailwind CSS, static export configuration, and basic pages in the next steps.
Static export configuration
Next.js needs to be configured for static site generation to work seamlessly with Cloudflare Pages. This involves enabling the export mode and adjusting image handling. To do this, you will modify the next.config.js file to enable static export and configure image optimization settings.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export",
trailingSlash: true,
images: {
unoptimized: true,
},
};
export default nextConfig;
This configuration tells Next.js to generate static HTML files instead of server-side rendering. The output: "export" enables static export mode, trailingSlash: true ensures URLs end with a slash for proper routing, and images: { unoptimized: true } disables Next.js image optimization since static sites can’t use the built-in image optimization server.
Building the application pages
Now we’ll create the main pages for your app. We’ll start with a simple home page that welcomes users and provides navigation to your book catalog.
Modify the home page at app/page.tsx to look like this:
import Image from "next/image";
import Link from "next/link";
export default function Home() {
return (
<main className="flex flex-col items-center justify-center min-h-screen p-8">
<h1 className="text-4xl font-bold mb-4">Welcome to CircleReads </h1>
<p className="text-lg mb-6 text-center">
Discover books loved by developers and engineering leaders at CircleCI
</p>
<Image
src="/bookshelf.png"
alt="Bookshelf"
width={400}
height={300}
className="rounded shadow-md mb-6"
/>
<Link
href="/books"
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
>
View Books
</Link>
</main>
);
}
This component creates a centered layout with a welcome message, an image of a bookshelf, and a navigation button to the books page.
Next, you will create the books page that lists some featured books. Create a new file at app/books/page.tsx:
import Link from "next/link";
import Image from "next/image";
const books = [
{
title: "Accelerate",
author: "Nicole Forsgren",
image: "/accelerate.png",
},
{
title: "The Phoenix Project",
author: "Gene Kim",
image: "/phoenix.png",
},
{
title: "Continuous Delivery",
author: "Jez Humble",
image: "/cd.png",
},
];
export default function BooksPage() {
return (
<main className="p-8">
<h1 className="text-3xl font-semibold mb-6"> Featured Books</h1>
<div className="grid md:grid-cols-3 gap-6">
{books.map((book, idx) => (
<div key={idx} className="border p-4 rounded shadow">
<Image src={book.image} alt={book.title} width={200} height={250} />
<h2 className="text-xl font-bold mt-2">{book.title}</h2>
<p className="text-gray-600">by {book.author}</p>
</div>
))}
</div>
<Link href="/" className="block mt-8 text-blue-600 hover:underline">
← Back to Home
</Link>
</main>
);
}
This page component defines a static array of books and renders them in a responsive grid layout. Each book is displayed in a card format with its cover image, title, and author.
Adding static assets
Our app needs some images to display properly. We’ll add book cover images and a hero image to the public directory.
public/
├── accelerate.jpg
├── cd.jpg
├── phoenix.jpg
└── bookshelf.jpg
You can find these images online or create your own. Make sure to use appropriate image sizes for better performance. The images should be in the public/ directory so they can be served statically by Next.js.
You can use placeholder images (e.g., from Unsplash) and rename them accordingly. These files will be accessible at the root URL path (e.g., /bookshelf.jpg) when the app is deployed.
Run the app locally
npm run dev
Visit http://localhost:3000 to see the home page with a welcome message and a button to view books.
Add unit tests with Jest + Testing Library
We’ll add unit tests to ensure your app works correctly before deployment. Jest and React Testing Library provide a robust testing foundation for Next.js applications and their page components.
Installing test dependencies
First, install the necessary testing packages:
npm install --save-dev jest @testing-library/react @testing-library/jest-dom @testing-library/user-event identity-obj-proxy babel-jest @types/jest jest-environment-jsdom
This installs Jest as the test runner, React Testing Library for component testing, and supporting utilities for handling CSS imports and TypeScript.
Configuring package scripts
Update your package.json to include the test script:
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "serve out",
"test": "jest"
}
Jest configuration
Create jest.config.js in your project root:
module.exports = {
testEnvironment: "jsdom",
moduleNameMapper: {
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
},
setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
transform: {
"^.+\\.(js|jsx|ts|tsx)$": [
"babel-jest",
{
presets: [
[
"next/babel",
{
"preset-react": {
runtime: "automatic",
},
},
],
],
},
],
},
moduleFileExtensions: ["ts", "tsx", "js", "jsx"],
testMatch: ["**/__tests__/**/*.(ts|tsx|js)", "**/*.(test|spec).(ts|tsx|js)"],
};
This configuration sets up jsdom for browser-like testing, handles CSS imports with mocks, and configures Babel to transform TypeScript and JSX files using Next.js presets.
Test set-up file
Create jest.setup.js in your project root:
require("@testing-library/jest-dom");
This imports custom Jest matchers like toBeInTheDocument() for more expressive assertions.
Writing component tests
Create your first test file at app/page.test.tsx:
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import Home from "./page";
describe("Home Page", () => {
it("renders welcome text", () => {
render(<Home />);
expect(screen.getByText(/Welcome to CircleReads/i)).toBeInTheDocument();
});
it("renders the main heading", () => {
render(<Home />);
const heading = screen.getByRole("heading", { level: 1 });
expect(heading).toBeInTheDocument();
});
it("has proper page structure", () => {
render(<Home />);
const main = screen.getByRole("main");
expect(main).toBeInTheDocument();
});
it("renders navigation elements", () => {
render(<Home />);
const nav = screen.queryByRole("navigation");
if (nav) {
expect(nav).toBeInTheDocument();
}
});
it("has accessible content", () => {
render(<Home />);
expect(screen.getByRole("main")).toBeInTheDocument();
});
});
These tests verify that your Home component renders correctly, has proper structure, and maintains accessibility standards. The tests use semantic queries (like getByRole) to ensure components are accessible to screen readers.
Running the tests
Run the tests using the following command:
npm test
You should see output confirming all tests pass, validating that your components render and function as expected.
> circleci-books-pages@0.1.0 test
> jest
PASS app/page.test.tsx
Home Page
✓ renders welcome text (21 ms)
✓ renders the main heading (24 ms)
✓ has proper page structure (4 ms)
✓ renders navigation elements (2 ms)
✓ has accessible content (4 ms)
Test Suites: 1 passed, 1 total
Tests: 5 passed, 5 total
Snapshots: 0 total
Time: 0.87 s, estimated 1 s
Ran all test suites.
Setting up Cloudflare deployment tools
Now we’ll configure the tools needed to deploy your Next.js app to Cloudflare Pages.
Installing the Wrangler CLI
Wrangler is Cloudflare’s command-line tool for managing Workers and Pages deployments.
Install Wrangler as a development dependency:
npm i -D wrangler@latest
Authenticating with Cloudflare
Log into your Cloudflare account through Wrangler:
npx wrangler@latest login
You will be prompted to log in via OAuth in your browser. Give permission to access your account.
There will be a message in the terminal: “Successfully logged in.”
Project configuration
Create wrangler.toml in your project root:
name = "circleci-books-pages"
compatibility_date = "2024-07-01"
pages_build_output_dir = "out"
[env.production]
compatibility_date = "2024-07-01"
This configuration file tells Wrangler about your project name and sets the compatibility date for Cloudflare’s runtime features.
Creating Cloudflare API token
To allow CircleCI to deploy on your behalf, you’ll need an API token with the right permissions.
Visit https://dash.cloudflare.com/profile/api-tokens and follow these steps:
- Go to API Tokens → Create Token
- Select “Edit Cloudflare Workers” template (this includes Pages permissions)
- Configure Token Scope:
- Zone Resources: Select “All zones”
- Account resources: Include your specific account
- Copy and store the token safely (you’ll add it to CircleCI later)
Finding your Cloudflare account ID
You’ll also need your Account ID for the CircleCI configuration:
- Go to the Cloudflare Dashboard
- The Account ID will be in the right sidebar
- Copy that value for so you can use it later
The Account ID is also visible in the URL when you go to the “Workers & Pages” section of your dashboard.
Creating the pipeline configuration
Now you’ll create the CI/CD pipeline that will automatically test, build, and deploy your app whenever you push changes to the main branch. To begin, create .circleci/config.yml in your project root and add the following configuration:
version: 2.1
executors:
node-executor:
docker:
- image: cimg/node:20.10.0
jobs:
build:
executor: node-executor
steps:
- checkout
- restore_cache:
keys:
- npm-deps-{{ checksum "package-lock.json" }}
- run: npm ci
- save_cache:
paths:
- ~/.npm
key: npm-deps-{{ checksum "package-lock.json" }}
- run: npm test
- run: npm run build
- persist_to_workspace:
root: .
paths:
- out
deploy:
executor: node-executor
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Create and Deploy to Cloudflare Pages
command: |
# Try to create project (will fail if it exists)
npx wrangler pages project create circleci-books-pages || true
# Deploy to the project
npx wrangler pages deploy out --project-name=circleci-books-pages
workflows:
build-and-deploy:
jobs:
- build
- deploy:
requires:
- build
filters:
branches:
only: main
This configuration defines two jobs: build and deploy. The build job installs dependencies, runs tests, creates the static export, and saves the out directory to the workspace. The deploy job then takes that build output and deploys it to Cloudflare Pages using Wrangler. The workflow ensures deployment only happens after a successful build and only on the main branch.
Connecting the application to CircleCI
The next step is to set up a repository on GitHub and link the project to CircleCI. Review the pushing a project to GitHub tutorial for instructions.
Log in to your CircleCI account and select the appropriate organization. Your repository should be listed on the Projects dashboard.
Click Set Up next to your circleci-books-pages project.
The existing configuration file in your project is recognized. Update the name of the branch (if necessary) then click Set Up Project.
Your first workflow will start running.
The deploy job will fail because you have not yet specified the Cloudflare credentials. Click the job to see the details.
Configuring environment variables
To fix the failed deployment, you will need to add the cloudflare API Token and account ID. Click Project Settings.
Click Environment Variables on the left sidebar and then on Add Environment Variable to add the variables.
The values to be used here are the CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID as obtained from your Cloudflare dashboard.
- The
CLOUDFLARE_API_TOKENvariable is the API token value. - The
CLOUDFLARE_ACCOUNT_IDvariable is the account ID value.
Enter each variable name and its value and click Add Environment Variable to save it.
Successful deployment
Go back to the dashboard. Click Rerun Workflow from Failed. Expect a successful build this time.
Visit your Cloudflare Pages URL to see it live!
Testing the CI/CD pipeline
Let’s verify that your automated deployment pipeline works by making a change and pushing it.
For example, change title in page.tsx to:
<p className="text-lg mb-6 text-center">
Discover books loved by developers and engineering leaders at CircleCI - Dev
Picks
</p>
Then commit and push the changes:
npm run build
git commit -am "Update homepage title"
git push
That will trigger a new build in CircleCI, which will run the tests, build the app, and deploy it to Cloudflare Pages. Check the Cloudflare Pages URL to see the change reflected.
Conclusion
You’ve successfully set up a complete CI/CD pipeline that automatically deploys your Next.js application to Cloudflare Pages using CircleCI. This setup provides several key benefits:
- Automated Testing: Every code change runs through your test suite before deployment
- Static Site Generation: Next.js exports optimized static files perfect for Cloudflare Pages
- Fast Global Distribution: Cloudflare’s edge network ensures fast loading times worldwide
- Zero-Downtime Deployments: Changes are deployed seamlessly without service interruption
The pipeline you’ve built is production-ready and can easily be extended. With this foundation, you can focus on building great user experiences while your CI/CD pipeline handles the deployment complexity automatically.
The complete source code for this project is available on GitHub.