Deploy static assets to AWS S3 with NextJS

April 22, 2019 ☼ AWSNextJSReact

Requirements

If you need to speed up your content delivery and improve your site performance the term CDN (Content Delivery Network) will most likely show up in your Google searches.

What is it a CDN ?

A content delivery network or content distribution network (CDN) is a geographically distributed network of proxy servers and their data centers. The goal is to provide high availability and high performance by distributing the service spatially relative to end-users.

Source: Wikipedia

If you want to find out more about how CDN work I’d encourage to read this article: How does a CDN work

According to BuiltWith, over 84% of the top 10K websites are using a CDN.

But, why?

Speed matters these days. I’m sure you’ve come across some statics that go along these lines:

Product X by Company Y found 53% of mobile site visits were abandoned if a page took longer than 3 seconds to load.

I’m not going to spend too much time convincing you why you should focus on performance since you are already reading this article but if you need more proofs read up here: https://developers.google.com/web/fundamentals/performance/why-performance-matters/

If you are developing a React App with NextJS you are already on a good path to achieve fast loading time. Let’s see how you can integrate a CDN inside your NextJS workflow.

Solution

We’re going to upload all our static assets generated by NextJS build script to an Amazon S3 bucket. Our objects inside the bucket will be distributed globally thanks to CloudFront

Luckily for us, with the release of Next 7 the build assets generated in the .next folder will be matching the URL structure of your Next app:

https://cdn.example.com/_next/static/<buildid>/pages/index.js
// mapped to:
.next/static/<buildid>/pages/index.js

We just need to copy cross files as they are :)

There are several ways to accomplish this. If you Google a bit you will find some open issues like this one with some alternative solutions.

The one I’m proposing was specifically for my use case but it might help with yours as well.

The script


// uploadTos3.js

const fs = require('fs');
const readDir = require('recursive-readdir');
const path = require('path');
const AWS = require('aws-sdk');
const mime = require('mime-types');

/*

You will run this script from your CI/Pipeline after build has completed.

It will read the content of the build directory and upload to S3 (live assets bucket)

Every deployment is immutable. Cache will be invalidated every time you deploy.

*/

AWS.config.update({
  region: 'eu-central-1',
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.SECRET_ACCESS_KEY,
  maxRetries: 3
});

const directoryPath = path.resolve(__dirname, './.next');

// Retrive al the files path in the build directory
const getDirectoryFilesRecursive = (dir, ignores = []) => {
  return new Promise((resolve, reject) => {
    readDir(dir, ignores, (err, files) => (err ? reject(err) : resolve(files)));
  });
};

// The Key will look like this: _next/static/<buildid>/pages/index.js
// the <buildid> is exposed by nextJS and it's unique per deployment.
// See: https://nextjs.org/blog/next-7/#static-cdn-support
const generateFileKey = fileName => {
  // I'm interested in only the last part of the file: '/some/path/.next/build-manifest.json',
  const S3objectPath = fileName.split('/.next/')[1];
  return `next-assets/_next/${S3objectPath}`;
};

const s3 = new AWS.S3();

const uploadToS3 = async () => {
  try {
    const fileArray = await getDirectoryFilesRecursive(directoryPath, [
      'BUILD_ID'
    ]);
    fileArray.map(file => {
      // Configuring parameters for S3 Object
      const S3params = {
        Bucket: 's3-service-broker-live-ffc6345a-4627-48d4-8459-c01b75b8279e',
        Body: fs.createReadStream(file),
        Key: generateFileKey(file),
        ACL: 'public-read',
        ContentType: mime.lookup(file),
        ContentEncoding: 'utf-8',
        CacheControl: 'immutable,max-age=31536000,public'
      };
      s3.upload(S3params, function(err, data) {
        if (err) {
          // Set the exit code while letting
          // the process exit gracefully.
          console.error(err);
          process.exitCode = 1;
        } else {
          console.log(`Assets uploaded to S3: `, data);
        }
      });
    });
  } catch (error) {
    console.error(error);
  }
};

uploadToS3();

// next.config.js

const isProd = process.env.NODE_ENV === 'production';
module.exports = {
  // You may only need to add assetPrefix in the production.
  assetPrefix: isProd ? 'https://d3iufi34dfeert.cloudfront.net' : ''
}

Few notes:

My final build script looks something like this:

set -e

npm ci

npm run test
npm run build
npm prune --production

npm run uploadTos3

If you have any suggestions, questions, corrections or if you want to add anything please DM or tweet me: @zanonnicola