Published on October 21, 2023

Deploying Next.js 13 (app dir) to Cloudflare Pages

Deploying Next.js 13 (app dir) to Cloudflare Pages

In this blog post, I’m going to walk you through deploying Next.js 13 (app dir) to Cloudflare Pages. And don't worry, I won't skip any details, but will do my best to explain everything as clearly as possible.

So, let's roll up our sleeves and get started.

Step 1: Creating a Next.js application

So, let's start by creating a new Next.js application. You're smart, you know the drill - just give this command a run.

npx create-next-app@latest

You can follow my lead and say a big, fat 'YES' to the following options. (just kidding, you can choose whatever you want, just make sure to select the app dir option when asked as that is what we're going to use in this blog post)

npx create-next-app@latest
Need to install the following packages:
create-next-app@13.5.6
Ok to proceed? (y) y
What is your project named? … my-website
Would you like to use TypeScript? … No / Yes
Would you like to use ESLint? … No / Yes
Would you like to use Tailwind CSS? … No / Yes
Would you like to use `src/` directory? … No / Yes
Would you like to use App Router? (recommended) … No / Yes
Would you like to customize the default import alias (@/*)? … No / Yes
What import alias would you like configured? … @/*

Now, change the directory to your website's directory using cd my-website and run npm run dev to start the development server. It's live on localhost:3000!

Step 2: Setting up a GitHub repository

Now that we have our Next.js app up and running, let's set up a Github repository for it. I have created a new repository on Github called nextjs-cloudflare-example and will then set it as my remote origin and push the code to it.

git add .
git commit -m "first commit"
git branch -M main
git remote add origin git@github.com:<username>/nextjs-cloudflare-example.git
git push -u origin main

That's it, now you should be able to see your code on Github.

Step 3: The Cloudflare Pages Configuration

To deploy to Cloudflare Pages, we need to use the @cloudflare/next-on-pages package to ensure the app is compatible with Cloudflare Pages. You need to add a new build step to package.json and add the package to dependencies. I will call this step pages:build and will use npx to run the build command.

Your package.json should look similar to this:

{
"name": "my-website",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
// our custom build step for cloudflare pages
"pages:build": "npx @cloudflare/next-on-pages"
},
"dependencies": {
"react": "^18",
"react-dom": "^18",
"next": "13.5.6"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10",
"postcss": "^8",
"tailwindcss": "^3",
"eslint": "^8",
"eslint-config-next": "13.5.6"
}
}

p.s. make sure to remove the comments from the code below, as JSON doesn't support comments and it will break your build.

Step 4: Configuring the Runtime

You see, by default, Next.js runs on two different runtimes: Node.js and edge (V8 Isolates). When you deploy on Vercel, they use a combination of AWS Lambdas for the Node.js runtime and Cloudflare Workers for the edge runtime. However, Cloudflare Pages only supports the edge runtime. So, we need to configure our app to run on the edge runtime.

To do that, we need to add the runtime route segment configuration to our root layout.tsx file so that it gets applied to all pages. This is what my layout.tsx file looks like:

// src/app/layout.tsx
export const runtime = "edge";

Once you've done that, you can commit and push your code to Github. Now, we're ready to deploy to Cloudflare Pages.

Step 5: Deploying to Cloudflare Pages

Head to Cloudflare pages, log in and click on 'Create application'. Switch the tab to pages and click on Connect to Git. Select your repository and branch and click on Begin setup.

Now, select Next.js as the framework and change the build command to npm run pages:build. Keep the output directory as .vercel/output/static and click on Save and Deploy.

Now, sit back, grab some popcorn, and watch your app get deployed. Don’t freak out when the first deployment fails; it’s just all part of the process™. We will fix it in the next step.

Step 6: Configuring the Compatibility Flags

Head to Settings > Functions > Compatibility Flags and add the nodejs_compat flag to both the production and preview environments. Make sure the compatibility date for both environments is set to at least '2022-10-30'.

Now go back to the Deployments tab and click on Redeploy to deploy your app again. This time, it should deploy successfully, and you will find your app running on the provided URL - a big hurray on successful deployment to Cloudflare pages!

Step 7: Making it Truly Static

If you refresh your app a few times and then head to your function usage on Cloudflare Pages, you will be greeted with a surprise. The function usage is increasing with every refresh. This is because Next.js defaults to SSR and Streaming mode, even for fully static pages unless you go out of your way to opt-out of SSR and Streaming.

Route (app) Size First Load JS
┌ ℇ / 5.25 kB 85.6 kB

See that little next to the route? That means it's using SSR and Streaming mode. Why? I have no idea. 🤷🏻‍♂️

Anyway, to fix this, head back to layout.tsx and modify it to:

// src/app/layout.tsx
export const dynamic = "force-static";

This will force Next.js to use the static mode for all pages. Now, do the boring git commit and push dance and redeploy your app.

So, we should be good now, right? Well, not quite. If you check out the build logs, you will still see that snarky little next to the route. Why? Because of this:

Page "/" is using runtime = 'edge' which is currently incompatible with dynamic = 'force-static'. Please remove either "runtime" or "force-static" for correct behavior

Don't yell at me, I didn't make the rules. Apparently, we cannot use the force-static mode with the edge runtime. So, we need to remove the runtime config to make this work. So, you can either remove the runtime export from layout.tsx or set it to nodejs like so:

// src/app/layout.tsx
export const runtime = "nodejs";
export const dynamic = "force-static";

Now, let's redeploy and pray to the CI/CD gods that it works this time.

Route (app) Size First Load JS
┌ ○ / 5.25 kB 85.6 kB

Step 8: Dealing with Dynamic Routes

Alright, so another issue that I ran into was that dynamically created static pages were still being served using SSR and Streaming mode. Let me show you:

Let's say we have a dynamic route called blog/[slug]/page.tsx and we want to statically generate the pages for all the blog posts. So, we would do something like this:

// src/app/blog/[slug]/page.tsx
export default function Blog({ params }: { params: { slug: string } }) {
return (
<div className="space-y-10">
<h1>My Blog Post</h1>
<div>Slug: {params.slug}</div>
</div>
)
}

I will not go into details of how this page should be implemented, but for the sake of this example, imagine we get the slug for each blog post, and in the component we load the actual blog post from the CMS or whatever and then render it. But for now, I am just going to render the slug.

Now let's define the generateStaticParams so we can statically generate the pages for all the blog posts during build time.

// src/app/blog/[slug]/page.tsx
export const generateStaticParams = async () => {
const slugs = ["blog-post-1", "blog-post-2", "blog-post-3"]
return slugs.map((slug) => ({ params: { slug } }))
}

Here, we are just returning an array of slugs and then mapping over them to return an array of objects with the params key. This is telling Next.js to statically generate the pages for all the slugs during build time.

We have given Next.js all the slugs that we want to statically generate during build time, we have defined the generateStaticParams function and we have defined the dynamic export to force static mode. So, we should be good to go, right? Let's deploy and see what happens.

⚡️ The following routes were not configured to run with the Edge Runtime:
⚡️ - /blog/[slug]

Surprise, surprise! We got an error and the build failed. Why? Because even though we are asking Next.js to statically generate the pages, it is defaulting to SSR and Streaming mode for the fallback route. So, we need to explicitly tell Next.js that we don't have a fallback route for our STATICALLY GENERATED pages. So, let's do that.

// src/app/blog/[slug]/page.tsx
export const dynamicParams = false;

Now, let's redeploy our changes and once again pray to the CI/CD lords to save us from further embarrassment.

Route (app) Size First Load JS
┌ ○ / 5.25 kB .6 kB
└ ● /blog/[slug] 137 B 80.5 kB

As you can see, we got rid of the error and that little next to the route. Now, if you head to the blog post page, you will see that it is being served statically.

Step 9: Adding a Custom Domain

Finally, now that we have our app deployed to Cloudflare Pages, let's add a custom domain to it. Head to the Custom domains tab and click on Set up a custom domain.

Here you can either add a subdomain or a root domain, it will then give you a list of DNS records that you need to add to your DNS provider. So, head to your DNS provider and add the records.

If you are using Cloudflare as your DNS provider, you can just click on Verify DNS configuration and it will automatically add the records for you.

Wrapping Up

And that's it! We have successfully deployed our Next.js app to Cloudflare Pages. From here on, Cloudflare will automatically deploy your app to production anytime you push to the main branch, and it will also create a preview deployment for the rest of the branches.

In addition, Cloudflare Pages gives you a lot of flexibility and control over your deployments, in addition to a ton of features, and benefits such as zero bandwidth costs, free SSL, and more. So, I highly recommend you check it out.

Interested in LogSnag?

Get started right now for free!