TutorialsDec 3, 20258 min read

CI/CD for Cloudflare Pages using CircleCI and Wrangler

Olususi Oluyemi

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

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.

View books page

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:

  1. Go to API TokensCreate Token
  2. Select “Edit Cloudflare Workers” template (this includes Pages permissions)
  3. Configure Token Scope:
    • Zone Resources: Select “All zones”
    • Account resources: Include your specific account
  4. 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:

  1. Go to the Cloudflare Dashboard
  2. The Account ID will be in the right sidebar
  3. 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.

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

CircleCI project setup

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.

CircleCI deploy job failure

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_TOKEN variable is the API token value.
  • The CLOUDFLARE_ACCOUNT_ID variable is the account ID value.

Enter each variable name and its value and click Add Environment Variable to save it.

CircleCI Add environment variables

Successful deployment

Go back to the dashboard. Click Rerun Workflow from Failed. Expect a successful build this time.

CircleCI successful build

Visit your Cloudflare Pages URL to see it live!

Cloudflare Pages live URL

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.


Oluyemi is a tech enthusiast with a background in Telecommunication Engineering. With a keen interest in solving day-to-day problems encountered by users, he ventured into programming and has since directed his problem solving skills at building software for both web and mobile. A full stack software engineer with a passion for sharing knowledge, Oluyemi has published a good number of technical articles and blog posts on several blogs around the world. Being tech savvy, his hobbies include trying out new programming languages and frameworks.