DevOps practices that include continuous integration and continuous deployment are an essential part of modern-day software development. Vue.js developers building server-side rendered applications with Nuxt.js should be taking advantage of these modern processes to deploy their applications. This article will show Vue.js developers using Nuxt.js how to build, test, and deploy their applications to GitHub Pages using CircleCI.

Prerequisites

To follow along with this post, you will need a few things:

Creating a Nuxt.js project and connecting it to GitHub

The first task is to create a new Nuxt.js application and connect it to your GitHub account. Go ahead and create a new Nuxt.js app by running the following command:

npx create-nuxt-app my-nuxt-app

Accept the default project name and project description. The interactive project creation process then continues by asking the following questions. Select the corresponding answers to follow along with this article:

  • Use a custom server framework? (Answer: none)
  • Choose features to install? (Answer: click Enter to skip all)
  • Use a custom UI framework? (Answer: none)
  • Use a test framework? (Answer: Jest)
  • Choose rendering mode? (Answer: Universal)
  • Author? (Answer: click Enter to choose the default)
  • Choose a package manager? (Answer: npm)
  • Choose Nuxt.js modules? (Answer: click Enter to skip all)
  • Choose linting tools? (Answer: click Enter to skip all)
  • Choose development tools? (Answer: click Enter to skip all)

Note: Your CLI may display the questions differently but the purpose is the same. You will find it easy to match the questions to the answers above.

After selecting all these preferences, create-nuxt-app begins scaffolding the new Nuxt.js application. Once the scaffolding is done, the next step is to connect the application to a GitHub repository. Go to your GitHub account and create a new repository. For ease in following along with this tutorial, give the repo the same name as the project, i.e. my-nuxt-app. Now, connect the project to the GitHub repo you just created. Go to the root of your Nuxt.js project and run the following commands to make an initial commit:

git add .
git commit -m “First Commit”

Then connect the project to the repo by running the following commands:

git remote add origin https://github.com/coderonfleek/my-nuxt-app.git
git push -u origin master

Note: The GitHub URL for the repository above will not be the same as yours, replace this with yours in order to make your first push and setup tracking between the local and remote master branches.

And that’s it, you now have your project connected to your GitHub repo.

Adding Tests

Based on your selection of Jest as the testing framework, create-nuxt-app set up all that is needed (in terms of the packages required and the default test configuration all found in the jest.config.js file) for testing the application. The tests can be found in the test folder at the root of the application. In the test folder, we see a Logo.spec.js test file which contains a simple test for the Logo component that simply checks if the component is a Vue instance. We will be adding another test that checks if the component renders properly. Go ahead and replace the code in Logo.spec.js with the code below:

import { mount, shallowMount } from "@vue/test-utils";
import Logo from "@/components/Logo.vue";

const factory = () => {
  return shallowMount(Logo, {});
};

describe("Logo", () => {
  test("is a Vue instance", () => {
    const wrapper = mount(Logo);
    expect(wrapper.isVueInstance()).toBeTruthy();
  });

  test("renders properly", () => {
    const wrapper = factory();
    expect(wrapper.html()).toMatchSnapshot();
  });
});

Now, save this file and try out the tests by running the following command:

npm run test

This should display a screen similar to the one below:

nuxt10

Setting up the build script

In order to deploy a server-side rendered Nuxt.js application to a static hosting service like GitHub Pages, we need to run the nuxt generate command which generates a dist folder at the root of our project containing a production version of our application. This generated version contains only static files which makes it suitable for a static host.

Now, one thing to recognize about applications deployed to GitHub Pages is that their base URL is the name of the repository for that application. For example, the resultant homepage for the my-nuxt-app project on GitHub Pages would be https://[YOUR_GITHUB_USERNAME].github.io/my-nuxt-app.

So, we need to find a way to instruct our Nuxt.js application to generate the distribution version with the appropriate router base while deploying to GitHub Pages. If this is not done, all of our routes will fail.

The first thing we need to do is to write a nuxt generate script that specifically targets the deployment to GitHub Pages. Add the following line as an extra script to the scripts section in your package.json file.

  scripts : {
    ...
    "generate:gh-pages": "DEPLOY_ENV=GH_PAGES nuxt generate --fail-on-page-error"
}

The generate:gh-pages script sets the DEPLOY_ENV environment variable (which we will reference later) to GH_PAGES (the identity we will use for GitHub Pages) and also includes the fail-on-page-error flag that ensures that the build fails if there is a page error.

Next, we go into the nuxt.config.js configuration file and set our router base. Just before the module.exports line, add the following:

const routerBase =
  process.env.DEPLOY_ENV === "GH_PAGES"
    ? {
        router: {
          base: "/my-nuxt-app/"
        }
      }
    : {};

Here, we create a routerBase object which, based on the value of the variable DEPLOY_ENV, is set to a router base object targeting GitHub Pages if we are deploying to GitHub Pages, and if not, defaults to an empty object. This will ensure that the router base is appropriately set for our project. Make sure to change the my-nuxt-app value to that of your repo if you are using a different slug.

To finalize our configuration, add the following line inside the exported object in the nuxt.config.js file:

  ...
  module.exports =  {

    ...routerBase,

  /* Other parts of module.exports*/
}

This will spread (pun intended) the routerBase object into the configuration.

Note: Your nuxt.config.js file might contain a different export format in the form:

  export default {
  ...
}

This is totally valid and the routerBase will be included in the exported object since it was included in the module.exports format above.

Writing the CircleCI deployment script

Now to the main action where we get to set up our deployment script to deploy to GitHub Pages.

This is what we want our script to achieve:

  • Install necessary packages
  • Run the tests
  • Build the project
  • Deploy to GitHub Pages

First, let’s create our CircleCI configuration file. Go into the root of your project and create a folder named .circleci and add a file inside this folder named config.yml.

Installing the necessary packages

The node_modules folder is not pushed to the project repo so we need to install the necessary packages for our CircleCI build. Inside the config.yml file, enter the configuration below:

version: 2.1
jobs:
  build:
    working_directory: ~/repo
    docker:
      - image: circleci/node:10.16.3
    steps:
      - checkout
      - run:
          name: update-npm
          command: "sudo npm install -g npm@5"
      - restore_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
      - run:
          name: install-packages
          command: npm install
      - save_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
          paths:
            - ./node_modules

In the configuration above, we create a build job that uses a Node Docker image from the CircleCI registry. We then check out our code and update npm. Next, we install our dependencies by running the npm install command and create a cache of our node_modules folder using the save_cache step. We also add a restore_cache step to restore our cached assets for use after we have saved a cache in a previous run.

Run the tests

The next thing to add to our configuration is a step to run our tests. We do that by adding another run step as shown below:

      ...
      - run:
          name: test
          command: npm run test

This step runs the npm run test command that we ran locally earlier to run our tests using Jest.

Build the project

We now go ahead and build our project by running the nuxt generate command with another step as shown below:

      ...
      - run:
          name: build-project
          command: npm run generate:gh-pages

Note: This step specifically calls the generate:gh-pages script which has been designed for deployment to GitHub Pages.

Deploy to GitHub Pages

Now, to our final task which is to deploy to GitHub Pages. To keep things neat, we would like to keep our deploy branch away from the master branch. This deploy branch will be maintained as an orphan branch solely for deployment to GitHub Pages. That could be a very tedious process, but lucky for us there is a Node.js package called gh-pages that can help us achieve that. This package will help us push our files to a special gh-pages branch on our repo which will then be deployed to GitHub Pages.

Add the following deploy steps to your config.yml file:

      - run:
          name: Install and configure dependencies
          command: |
            npm install gh-pages --save-dev
            git config user.email "fikfikky@gmail.com"
            git config user.name "coderonfleek"
      - run:
          name: Deploy docs to gh-pages branch
          command: './node_modules/.bin/gh-pages --dotfiles --message "[skip ci] Updates" -d dist'
      - store_artifacts:
          path: test-results.xml
          prefix: tests
      - store_test_results:
          path: test-results.xml

In the first step above, we install the gh-pages package and set our git configurations for GitHub in our container. Make sure to change the values of user.email and user.name to your GitHub account details.

In the next step, we use the local installation of our gh-pages package to publish everything in our generated dist folder from the preceding build step to our gh-pages branch.

Notice that we added a few arguments to our gh-pages command. We provided a special commit message (passed with –message) which contains [skip ci]. This instructs CircleCI not to initiate a new build when we push these contents to the gh-pages branch. We also added --dotfiles so that the gh-pages command will ignore all dotfiles.

The complete configuration file is shown below:

version: 2.1
jobs:
  build:
    working_directory: ~/repo
    docker:
      - image: circleci/node:10.16.3
    steps:
      - checkout
      - run:
          name: update-npm
          command: "sudo npm install -g npm@5"
      - restore_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
      - run:
          name: install-packages
          command: npm install
      - save_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
          paths:
            - ./node_modules
      - run:
          name: test
          command: npm run test
      - run:
          name: build-project
          command: npm run generate:gh-pages
      - run:
          name: Install and configure dependencies
          command: |
            npm install gh-pages --save-dev
            git config user.email "fikfikky@gmail.com"
            git config user.name "coderonfleek"
      - run:
          name: Deploy docs to gh-pages branch
          command: './node_modules/.bin/gh-pages --dotfiles --message "[skip ci] Updates" -d dist'
      - store_artifacts:
          path: test-results.xml
          prefix: tests
      - store_test_results:
          path: test-results.xml

Commit your changes and push them to the master branch.

Our first deployment

Now let’s take our deployment for a test drive.

First, we need to connect our project to CircleCI. So head to your CircleCI dashboard and add the project in the Add Project section. Next to your project, click Set Up Project.

This will bring you to a page similar to the one below:

nuxt12

Click Start building to begin building your project.

CircleCI will then run your configuration to build, test, and deploy your project.

Well, you should have notice THE BUILD FAILS!

Why did that happen? If you click on the process tab and inspect every step of the build process, you will notice that the deployment failed at the point where gh-pages was going to push our files to the deploy branch.

Ok, let me confess. I knew it was going to fail. My apologies. :)

The reason why this failed is that our connection to GitHub, needed to push our files, was not authenticated.

To make an authenticated connection to GitHub, we need a deployment key in the form of a private/public SSH key pair that is saved in our GitHub account and given write access.

Setting up authentication to GitHub

To create an SSH key pair on our local machine, simply run the following command:

ssh-keygen -t rsa -b 4096 -C “YOUR_GITHUB_EMAIL”

Make sure to replace YOUR_GITHUB_EMAIL with the email address for your GitHub account.

Chose a destination name like my_deploy_key and accept the default of no password by hitting Enter at the prompt.

This should automatically generate a public/private key pair for you at the chosen destination named my_deploy_key (private key) and my_deploy_key.pub (public key).

The private key is to be saved within CircleCI. Go to your CircleCI Pipelines page and click on the button with the cog icon to access the Settings page.

nuxt4

On the side-menu of the settings page, scroll down and click SSH Permissions:

nuxt5

Then click Add SSH Key to add a new key:

nuxt6

In the hostname field, simply enter “github.com” and paste the contents of your private key into the Private Key field and click Add SSH Key to add the key.

Learn more about adding your private key to CircleCI in the documentation.

Once added, you will see the fingerprint of your newly added key. CircleCI uses fingerprints to identify private keys in its scripts as its safer than exposing the actual private key.

We also need to ensure that the “Pass secrets to builds from forked pull requests” option on our Advanced Settings page is set to Off. The Advanced Settings link can be found on the side menu of our project settings page.

To finalize our security checks, ensure that you delete the private key from your local system.

The next thing we need to do is to submit our public key to our GitHub account. To do this, go to the Settings tab of your GitHub project repo. Then click Deploy Keys on the side menu. On the Deploy Keys page, click Add deploy key and the form below shows up.

nuxt7

In the Title field, give it a convenient name, e.g. “My CircleCI Deployment Key”. In the Key field, paste in the contents of your public key (my_deploy_key.pub) and check the Allow write access checkbox. Then click Add key.

GitHub might ask you to confirm your password, confirm it and you will see your key displayed in the list of deploy keys.

One last thing we need to do is to add the fingerprint of our private key from CircleCI to our configuration file. Our deploy steps now look like this:

      - run:
          name: Install and configure dependencies
          command: |
            npm install gh-pages --save-dev
            git config user.email "fikfikky@gmail.com"
            git config user.name "coderonfleek"
      - add_ssh_keys:
          fingerprints:
            - "3a:9b:c5:67:6f:06:50:55:dd:67:c9:ed:0c:9e:1f:fa"
      - run:
          name: Deploy docs to gh-pages branch
          command: './node_modules/.bin/gh-pages --dotfiles --message "[skip ci] Updates" -d dist'
      - store_artifacts:
          path: test-results.xml
          prefix: tests
      - store_test_results:
          path: test-results.xml

Note: Change the fingerprint used here to your own fingerprint.

The entire configuration file should now look like this:

version: 2.1
jobs:
  build:
    working_directory: ~/repo
    docker:
      - image: circleci/node:10.16.3
    steps:
      - checkout
      - run:
          name: update-npm
          command: "sudo npm install -g npm@5"
      - restore_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
      - run:
          name: install-packages
          command: npm install
      - save_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
          paths:
            - ./node_modules
      - run:
          name: test
          command: npm run test
      - run:
          name: build-project
          command: npm run generate:gh-pages
      - run:
          name: Install and configure dependencies
          command: |
            npm install gh-pages --save-dev
            git config user.email "fikfikky@gmail.com"
            git config user.name "coderonfleek"
      - add_ssh_keys:
          fingerprints:
            - "3a:9b:c5:67:6f:06:50:55:dd:67:c9:ed:0c:9e:1f:fa"
      - run:
          name: Deploy docs to gh-pages branch
          command: './node_modules/.bin/gh-pages --dotfiles --message "[skip ci] Updates" -d dist'
      - store_artifacts:
          path: test-results.xml
          prefix: tests
      - store_test_results:
          path: test-results.xml

Successful deployment of our Nuxt.js application

Now, let’s commit our changes and push to the master branch once again to trigger the deployment.

Once you push, a new build process will be triggered and this time it will be successful.

Click the deployment tab and check the test section. You will notice that all of our tests are also running fine.

nuxt8

To confirm that our application is deployed, visit the URL https://[YOUR_GITHUB_USERNAME].github.io/my-nuxt-app and view the deployed application. You should see the default Nuxt.js index page as displayed below:

nuxt9

Conclusion

In this article, we have seen how continuous integration and continuous delivery can be used to successfully deploy a Nuxt.js application to GitHub Pages. With CircleCI’s exhaustive list of configuration options, we could fine-tune our configuration to suit our specific needs and deploy to other hosting services, too.

I hope you had as much fun as I had on this project. Happy coding :)


Fikayo Adepoju is a LinkedIn Learning (Lynda.com) Author, Full-stack developer, technical writer, and tech content creator proficient in Web and Mobile technologies and DevOps with over 10 years experience developing scalable distributed applications. With over 40 articles written for CircleCI, Twilio, Auth0, and The New Stack blogs, and also on his personal Medium page, he loves to share his knowledge to as many developers as would benefit from it. You can also check out his video courses on Udemy.

Read more posts by Fikayo Adepoju