November 28, 2023
NeetoCourse allows you to build and sell courses online. BigBinary Academy runs on NeetoCourse. NeetoCourse uses Next.js to build its pages.
If one uses Vercel to deploy a Next.js application, Vercel automatically configures a global CDN. However, NeetoCourse is hosted at NeetoDeploy, our own app deployment platform. This meant we had to figure out how to serve assets and images from a CDN. We like using Cloudflare for various things and we decided to use Cloudflare as the CDN for this case.
There are two kinds of files in a server-side rendered Next.js app that can be served from a CDN.
When Next.js is deployed in production then during the next build
step all
server rendered assets are put in .next/static/
folder. Next.js provides an
option to set a CDN to serve these assets. This can be done by setting the
assetPrefix
property in next.config.js
, where ASSET_HOST
is an environment
variable that contains the CDN url.
/* next.config.js */
...
const assetHost = process.env.ASSET_HOST;
const nextConfig = {
...
assetPrefix: isPresent(assetHost) ? assetHost : undefined,
...
};
module.exports = nextConfig;
The official doc has more details about it.
As we can see in the below pic we had a cache hit. It means the assets are being served from the configured CDN.
Next.js has
<Image>
component that comes with out-of-the-box necessary optimizations. If we could
use <Image>
tag then our CDN problem would also be solved. However the
<Image>
component
does not work well with Tailwind CSS,
and we rely heavily on Tailwind CSS.
Therefore, we loaded images from the public folder using the traditional <img>
tag.
import React from "react";
import dottedBackground from "public/index-dotted-bg";
const HeaderImage = () => (
<img
alt="dotted background"
className="m-0 w-20 md:w-40 lg:w-auto"
src={dottedBackground}
/>
);
However now images are not optimized by Next.js and are served from the server directly. This is because Next.js does not consider media in public folder as static assets to be sent to CDN.
Since we had already set up and configured a CDN using cloudflare, we set up a util to prefix the source with our asset host if it is present in the app environment.
import { isNotNil } from "ramda";
export const prefixed = src =>
src.startsWith("/") && isNotNil(process.env.ASSET_HOST)
? `${process.env.ASSET_HOST}${src}`
: src;
Now we can use prefixed
in the <img>
tag.
import React from "react";
import { prefixed } from "utils/media";
const HeaderImage = () => (
<img
alt="dotted background"
className="m-0 w-20 md:w-40 lg:w-auto"
src={prefixed("/index-dotted-bg.svg")}
/>
);
We expected that with this configuration if ASSET_HOST
value was set up then
the images would be served from the CDN. Otherwise they would be served from the
server.
The images did load correctly, but the CDN was not able to cache them. All of the requests resulted in cache misses.
The CDN was not able to cache the images because the Cache-Control
header was
not set on the images. This header is set by Next.js when the assets are served
from the .next/static/
folder, but not when they are served from the public
folder. In such cases, we have to explicitly configure the header to be sent
when a request is made from the CDN.
So, we ended up adding a custom headers
block to the next.config.js
.
/* next.config.js */
...
const nextConfig = {
...
headers: async () => [
{
source: "/:all*(.png|.jpg|.jpeg|.gif|.svg)",
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, must-revalidate",
},
],
},
],
...
};
module.exports = nextConfig;
This worked. All subsequent requests from the CDN for public images turned into cache hits.
assetPrefix
option in next.config.js
to serve server rendered assets
from a CDN.Cache-Control
header in next.config.js
and ensure image src
URLs contain the CDN asset host to serve public images from a CDN.If this blog was helpful, check out our full blog archive.