# Running a Next.js Site on Cloudflare Pages

Step by step tutorial on how to deploy your website on Cloudflare Pages.

- **Author:** Patrick Heneise
- **Published:** 2021-07-14
- **Updated:** 2021-07-14
- **Tags:** nextjs, cloudflare, jamstack
- **Source:** https://opstrace.com/articles/nextjs-on-cloudflare

---

import Image from '../../src/components/Image.astro'

## JAMstack Overflow

These days there are so many providers for JAMstack hosting, it's hard to
choose. Netlify, GitHub Pages, Vercel, Heroku, ... the New Dynamic has a
[list of over 30 different hosting/deployment tools](https://www.tnd.dev/tools/hosting-deployment/),
with new ones being added regularly. However, the new kid in town caught our
attention: [Cloudflare Pages](https://pages.cloudflare.com/). They have only
been around for a few months now, but since we already use Cloudflare for DNS
and CDN, consolidating tools could be a nice win.

> Cloudflare Pages radically simplifies the process of developing and deploying
> sites by taking care of all the tedious parts of web development. Now,
> developers can focus on the fun and creative parts instead. –
> https://blog.cloudflare.com/cloudflare-pages-ga/

## Next.js on Cloudflare

Given they're just starting up, the features are still a bit limited. One of the
biggest drawbacks as of this moment is the lack of a server to generate dynamic
content. Currently, Cloudflare uses the
[Next.js static HTML export](https://nextjs.org/docs/advanced-features/static-html-export)
to prerender all pages to plain html. Given we don't use any server-side
rendering capabilities at the moment, this was good enough to move forward now,
but we are also excited to see the further development of Cloudflare Pages.

### Getting started

Let's create a project:

<Image
  src="/images/articles/2021-07/cloudflare-create-project-dialog.png"
  alt="cloudflare create a project first screen"
  width={1000}
  height={312}
/>

Assuming you have a Next.js website and both `next build` and `next export` run
through without any errors (you'll probably see an error when using next/image,
we'll get to that in a moment), make sure everything is committed and pushed to
a repository. In addition, make sure you create alias scripts in package.json
for build and export:

```json
"build": "next build",
"export": "next export",
```

This is necessary to run post-build scripts to generate additional content such
as a `sitemap.xml`, `robots.txt`, RSS/Atom feeds etc.

Log in to your [Cloudflare Dashboard](https://cloudflare.com/) and head to
"Pages" on the right. "Create a project" and connect to your repository.

When you're in the build configuration section, they offer "Next.js (Static
Export)" as a framework preset, but since this preset uses `next` commands by
default, pre/post build hooks from our `package.json` are ignored. Instead, do
not select a preset and configure the build options manually. The Cloudflare
"build command" should be:

```bash
npm run build && npm run export
# or
# yarn build && yarn export
```

Similarly, the static files will be exported to a directory called `out`, so set
the "build output directory" field to this. Your input should look like this:

<Image
  src="/images/articles/2021-07/cloudflare-build-settings.png"
  alt="cloudflare build settings screen"
  width={791}
  height={693}
/>

Once saved, Pages will automatically initiate the first build. Once the build is
successful, you can then head to the `*.pages.dev` URL that will be shown at the
top of the page. In a Pull Request, Cloudflare comments the build status and
preview url.

If you are already using Cloudflare DNS, you can connect your custom domain in
the next step—Cloudflare will automatically generate the `CNAME` for you with a
click on the "activate domain" button:

<Image
  src="/images/articles/2021-07/cloudflare-custom-domain.png"
  alt="cloudflare connect custom domain"
  width={1074}
  height={631}
/>

Congratulations, you're now running your Next.js site on Cloudflare Pages! Now,
the fine print.

### Images

#### The Problem

If you switched to the new
[`next/image` component](https://nextjs.org/docs/api-reference/next/image) in
Next.js 11, you'll see the following warning during `export`:

```txt
Error: Image Optimization using Next.js' default loader is not compatible with `next export`.
  Possible solutions:
    - Use `next start` to run a server, which includes the Image Optimization API.
    - Use any provider which supports Image Optimization (like Vercel).
    - Configure a third-party loader in `next.config.js`.
    - Use the `loader` prop for `next/image`.
  Read more: https://nextjs.org/docs/messages/export-image-api
```

Since we want to do image optimization and we don't want any new tools, we've
decided to look at another Clouflare option—a
[Cloudflare Worker](https://workers.cloudflare.com/).

#### Create a Worker

To create one in your Cloudflare Dashboard, go to "Workers" > "Create a Worker".
Cloudflare Docs has an entire article about
[Resizing Images with Cloudflare Workers](https://developers.cloudflare.com/images/resizing-with-workers),
and prepared the script for you to copy into the `{} Script` box:

```js
// https://developers.cloudflare.com/images/resizing-with-workers
addEventListener('fetch', (event) => {
  event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
  let url = new URL(request.url)
  let options = { cf: { image: {} } }
  if (url.searchParams.has('fit'))
    options.cf.image.fit = url.searchParams.get('fit')
  if (url.searchParams.has('width'))
    options.cf.image.width = url.searchParams.get('width')
  if (url.searchParams.has('height'))
    options.cf.image.height = url.searchParams.get('height')
  if (url.searchParams.has('quality'))
    options.cf.image.quality = url.searchParams.get('quality')
  const imageURL = url.searchParams.get('image')
  const imageRequest = new Request(imageURL, {
    headers: request.headers
  })
  return fetch(imageRequest, options)
}
```

In the top right, you can change the name of the worker. Once saved, note down
the URL, we'll need that.

<Image
  src="/images/articles/2021-07/cloudflare-images-worker-settings.png"
  alt="cloudflare image worker preview"
  width={382}
  height={215}
/>

#### Configure the Loader

As the error gives us possible solutions, unfortunately we can't "Configure a
third-party loader in `next.config.js`." - there is a small list of pre-existing
loaders, but Cloudflare isn't one of them and they're
[no longer adding new default loaders](https://github.com/vercel/next.js/pull/21443#issuecomment-765506401).
So we're using
[the `loader` prop for 'next/image'](https://nextjs.org/docs/api-reference/next/image#loader):

```js
// replace [yourprojectname] and [yourdomain.com] with your actual project name and (custom) domain
const cloudflareImageLoader = ({ src, width, quality }) => {
  if (!quality) {
    quality = 75
  }
  return `https://images.[yourprojectname].workers.dev?width=${width}&quality=${quality}&image=https://[yourdomain.com]${src}`
}
```

And the `<Image>`:

```jsx
const MyImage = (props) => {
  return (
    <Image
      loader={cloudflareImageLoader}
      src="me.png"
      alt="Picture of the author"
      width={500}
      height={500}
    />
  )
}
```

This would be too cumbersome to add this loader to every single `<Image>` tag
you have in your project. So we created a custom Image component
(`/components/Image.jsx`):

```jsx
import Image from 'next/image'

// replace [yourprojectname] and [yourdomain.com] with your actual project name and (custom) domain
const cloudflareImageLoader = ({ src, width, quality }) => {
  if (!quality) {
    quality = 75
  }
  return `https://images.[yourprojectname].workers.dev?width=${width}&quality=${quality}&image=https://[yourdomain.com]${src}`
}

export default function Img(props) {
  if (process.env.NODE_ENV === 'development') {
    return <Image unoptimized={true} {...props} />
  } else {
    return <Image {...props} loader={cloudflareImageLoader} />
  }
}
```

#### Node 16 Hiccup

If you're running on Node 16, you may already have had
[issues with Images on Next.js](https://github.com/vercel/next.js/issues/24421),
and because we don't need image optimziation during development, we've removed
the loader and added `unoptimized={true}` for the development environment.

Now you can search/replace all `import Image from next/image` to
`import Image from components/Image`. To resolve the import path
`components/Image`, you need to add a `jsconfig.json` to your project so that
`components/Image` will resolve to `./src/components/Image`:

```json
{
  "compilerOptions": {
    "baseUrl": "."
  }
}
```

Once you've done all that, you'll probably still see the same error during
`next export`, so let's add a fake third-party loader into `next.config.js`
(this fake loader will not be used—it's just a hack to avoid the build error):

```js
images: {
  loader: 'imgix',
  path: ''
},
```

`npm run export` should now run successfully without any errors!

### Previews

Cloudflare Pages also offer a
[GitHub integration](https://developers.cloudflare.com/pages/platform/github-integration)
that provides Pull Request previews, posting a comment to each Pull Request with
the deployment status, which is extremely useful. (Other services provide this
as well, for example, [Vercel](https://vercel.com/docs/git/vercel-for-github).)

However, one thing that was missing: a direct link out to the preview. In order
to retrieve the preview URL, we built a GitHub Action
[Cloudflare Preview URL](https://github.com/marketplace/actions/cloudflare-preview-url)
that waits for the deployment to be ready and then returns the URL. This is
useful to run E2E tests, URL link checks etc. on a real website before going
live.

There's room for improvement though:

- build times are still very long (2+ minutes to initialize the build
  environment).
- more flexibility with environment and build variables to expose system/build
  related information in user-defined variables.
- use of the GitHub Deployments API, which fires an event when a deployment is
  ready, instead of polling the Cloudflare API.

### Sitemap

You can use [next-sitemap](https://www.npmjs.com/package/next-sitemap) to
generate a sitemap for all your pages automatically after build. Follow the
`README` and add the `next-sitemap.js` as well as the post-build hook. This
works out of the box with static site generation and the `sitemap.xml` and
`robots.txt` will be copied into the export (`/out`) folder during
`npm run export`. You should add both files to `.gitignore` to prevent them
being added to the repository.

### RSS Feeds

If you publish regularly and you want to give your audience a way to subscribe
to your blog, RSS/Atom is the standard format for that. Most tutorials you'll
find about RSS generation require server-side rendering from your dynamic
content, though. But we can also solve this with a post build hook.

First, we need a script to generate the feed from our articles. We put this in
`utils/generate-rss.js`. We use [feed](https://www.npmjs.com/package/feed) to
help generate the XML and JSON files for RSS and Atom.

```js
import fs from 'fs'
import { Feed } from 'feed'
import getPosts from 'utils/getPosts'

// site.js exports the default site variables, such as the link, default image, favicon, etc
import meta from 'content/site.js'

async function generate() {
  const feed = new Feed({
    title: meta.title,
    description: meta.description,
    image: meta.image,
    favicon: meta.favicon,
    copyright: meta.copyright,
    language: meta.language,
    link: meta.link,
    id: meta.link,
    feedLinks: {
      json: `${meta.link}feed.json`,
      rss2: `${meta.link}feed.xml`,
      atom: `${meta.link}atom.xml`
    }
  })

  // we store blog articles in content/articles/article.mdx
  // you can change the path and regex here for your project.
  const posts = ((context) => {
    return getPosts(context)
  })(require.context('content/articles', true, /\.\/.*\.mdx$/))

  posts.forEach((post) => {
    feed.addItem({
      title: post.title,
      id: `${meta.link}${post.slug}`,
      link: `${meta.link}${post.slug}`,
      date: new Date(post.date),
      description: post.description,
      image: `${meta.link}${post.coverImage.src}`
    })
  })

  fs.writeFileSync('./public/feed.xml', feed.rss2())
  fs.writeFileSync('./public/feed.json', feed.json1())
  fs.writeFileSync('./public/atom.xml', feed.atom1())
}

generate()
```

This is a little tricky. As you can see we're using `require.context` which
isn't available outside the Next/Webpack environment. We're using a modified
webpack config in `next.config.js` to compile the script and put it into the
build directory:

```js
webpack: function (config, { dev, isServer }) {
  if (!dev && isServer) {
    const originalEntry = config.entry

    config.entry = async () => {
      const entries = { ...(await originalEntry()) }
      entries['utils/generate-rss.js'] = 'utils/generate-rss.js'
      return entries
    }
  }
  return config
}
```

Finally, we'll need to run the script after build (another post-build hook). In
order to run this in parallel with the sitemap generation and keep everything
neat and tidy, we're using
[npm-run-all](https://www.npmjs.com/package/npm-run-all):

```json
"export": "next export",
"build": "next build",
"postbuild": "run-p generate:sitemap generate:rss",
"generate:rss": "node ./.next/server/utils/generate-rss.js.js",
"generate:sitemap": "next-sitemap",
```

You can now add the feeds into your `_document.js` `<Head>`:

```jsx
<Html lang="en">
  <Head>
    ...
    <link
      rel="alternate"
      type="application/rss+xml"
      title="Opstrace RSS2 Feed"
      href="https://opstrace.com/feed.xml"
    />
    <link
      rel="alternate"
      type="application/atom+xml"
      title="Opstrace Atom Feed"
      href="https://opstrace.com/atom.xml"
    />
    <link
      rel="alternate"
      type="application/json"
      title="Opstrace JSON Feed"
      href="https://opstrace.com/feed.json"
    />
    ...
  </Head>
  <body>
    <Main />
    <NextScript />
  </body>
</Html>
```

## Conclusion

Getting a first build on Cloudflare is incredibly easy, fast and convenient.
Give Cloudflare permission for your repo, build, deploy—done. And it's _free_.
If you use them for DNS, no manual DNS changes need to be made because
Cloudflare does that _for you_. As they're feeding your site directly into their
incredibly powerful content delivery network (CDN), you can expect the best load
performance available.

With Cloudflare you also get some decent
[Account Analytics](https://www.cloudflare.com/analytics/) out of the box
without any tracking snippets. (Also, script blockers cannot side-step this
tracking.) They've also recently added
[Web Analytics](https://www.cloudflare.com/web-analytics/). Overall it's a great
offer for JAMstack sites and reduces the need for _yet another_ tool to log in
to and maintain.

Preparing Next.js was a little bit of work, but there were no serious blockers
that prevented us from deploying with and hosting on Cloudflare Pages. We're now
experimenting with Cloudflare Workers and
[Google Cloud Functions](https://cloud.google.com/functions) to add some
server-side capabilities to our site, for example to collect feedback or handle
our [Stripe](https://stripe.com/) subscriptions. There are a few known
convenience
[features missing](https://developers.cloudflare.com/pages/platform/known-issues)
from the build environment, but Cloudflare will probably add those soon. And
maybe they'll even support Server-Side Rendering (SSR) capabilities for Next.js
sites, too.

If you have any questions or comments, please
[reach out on Bluesky](https://bsky.app/profile/zentered.co) or
[start a discussion on GitHub](https://github.com/zentered/zentered/discussions/new?category=ideas-feedback).

## Acknowledgements & Special Thanks

- to Jean-Philippe Monette for [feed](https://www.npmjs.com/package/feed)
- to Vishnu Sankar for
  [next-sitemap](https://www.npmjs.com/package/next-sitemap)
- to Ian Mitchell for the
  [article on generating rss feeds for static blogs](https://ianmitchell.dev/articles/building-a-nextjs-blog-rss)
