Categories
JavaScript Vue

Generate Static Websites with Nuxt

Spread the love

Static websites are getting popular again nowadays. Informational and brochure sites no longer need to use content management systems like WordPress to be updated dynamically.

With static site generators, you can get your content from dynamic sources like headless CMS’s, APIs, and also from files like Markdown files.

Nuxt is a great static site generator based on Vue.js that is easy to use to build static websites. With Nuxt, all we have to do to build static websites from dynamic content is that we create the templates for showing the content dynamically from the dynamic sources like APIs and Markdown files. Then in the Nuxt configuration file, we define the routes statically so that it can go through the same routes to generate the content into static files.

In this article, we will build a news website using Nuxt and will use the News API, located at https://newsapi.org/, for the content. You have to know Vue.js before you can build a website using Nuxt since Nuxt is a framework based on Vue.js.

To start, first we register for an API key at the News API website. It is free if we only want the headlines. We start building the website by using the Nuxt CLI. We run the create-nuxt-app command by typing in:

npx create-nuxt-app news-website

This will create the initial project files in the news-website folder. When the wizard is run, we select none for server side frameworks, none for UI framework, none for test framework, Universal for the Nuxt mode, and choose to include Axios, linting and prettifying choices are up to you.

Next we need to install some packages. We need the @nuxtjs/dotenv package for reading the environment variables locally and the country-list library for getting a list of countries in our website. To install them we run:

npm i @nuxtjs/dotenv country-list

Now we can start building our website. In the default.vue file, we replace the existing code with:

<template>  
  <div>  
    <nav class="navbar navbar-expand-lg navbar-light bg-light">  
      <nuxt-link class="navbar-brand" to="/">News Website</nuxt-link>  
      <button  
        class="navbar-toggler"  
        type="button"  
        data-toggle="collapse"  
        data-target="#navbarSupportedContent"  
        aria-controls="navbarSupportedContent"  
        aria-expanded="false"  
        aria-label="Toggle navigation"  
      >  
        <span class="navbar-toggler-icon"></span>  
      </button> <div class="collapse navbar-collapse" id="navbarSupportedContent">  
        <ul class="navbar-nav mr-auto">  
          <li class="nav-item active">  
            <nuxt-link class="nav-link" to="/">Home</nuxt-link>  
          </li>  
          <li class="nav-item dropdown">  
            <a  
              class="nav-link dropdown-toggle"  
              href="#"  
              id="navbarDropdown"  
              role="button"  
              data-toggle="dropdown"  
              aria-haspopup="true"  
              aria-expanded="false"  
            >Headliny by Country</a>  
            <div class="dropdown-menu" aria-labelledby="navbarDropdown">  
              <nuxt-link  
                class="dropdown-item"  
                :to="`/headlines/${c.code}`"  
                v-for="(c, i) of countries"  
                :key="i"  
              >{{c.name}}</nuxt-link>  
            </div>  
          </li>  
        </ul>  
      </div>  
    </nav>  
    <nuxt />  
  </div>  
</template>

<script>  
import { requestsMixin } from "~/mixins/requestsMixin";  
const { getData } = require("country-list");

export default {  
  mixins: [requestsMixin],  
  data() {  
    return {  
      countries: getData()  
    };  
  }  
};  
</script>

<style>  
.bg-light {  
  background-color: lightcoral !important;  
}  
</style>

This is the file for defining the layout of our website. We added the Bootstrap navigation bar here. The bar has links for the home page and a drop-down for the list of countries. The nuxt-link components are all links to pages for getting the headlines for the country when the static files are generated. The countries are obtained from the country-list package in the script section by calling the getData function. In the style section, we changed the background color of our navigation bar by overriding the default color of the .bg-light class. The nuxt component in the bottom of the template section is where our content will be displayed.

Next we create amixins folder and create a file called requestsMixin.js file. In there, we add:

const APIURL = "https://newsapi.org/v2";  
const axios = require("axios");

export const requestsMixin = {  
  methods: {  
    getHeadlines(country) {  
      return axios.get(  
        `${APIURL}/top-headlines?country=${country}&apiKey=${process.env.VUE_APP_APIKEY}`  
      );  
    }, 

    getEverything(keyword) {  
      return axios.get(  
        `${APIURL}/everything?q=${keyword}&apiKey=${process.env.VUE_APP_APIKEY}`  
      );  
    }  
  }  
};

This file has the code to get the headlines by country and keyword from the News API.

Then in the pages folder, we create the headlines folder and in the headlines folder, create a _countryCode.vue file. In the file, we add:

<template>  
  <div class="container">  
    <h1 class="text-center">Headlines in {{getCountryName()}}</h1>  
    <div v-if="headlines.length > 0">  
      <div class="card" v-for="(h, i) of headlines" :key="i">  
        <div class="card-body">  
          <h5 class="card-title">{{h.title}}</h5>  
          <p class="card-text">{{h.content}}</p>  
          <button class="btn btn-primary" :href="h.url" target="_blank" variant="primary">Read</button>  
        </div>  
        <img :src="h.urlToImage" class="card-img-bottom" />  
      </div>  
    </div>  
    <div v-else>  
      <h2 class="text-center">No headlines found.</h2>  
    </div>  
  </div>  
</template><script>  
import { requestsMixin } from "~/mixins/requestsMixin";  
const { getData } = require("country-list");

export default {  
  mixins: [requestsMixin],  
  data() {  
    return {  
      headlines: [],  
      countries: getData()  
    };  
  },  
  beforeMount() {  
    this.getHeadlinesByCountry();  
  },  
  methods: {  
    async getHeadlinesByCountry() {  
      this.country = this.$route.params.countryCode;  
      const { data } = await this.getHeadlines(this.country);  
      this.headlines = data.articles;  
    }, 

    getCountryName() {  
      const country = this.countries.find(  
        c => c.code == this.$route.params.countryCode  
      );  
      return country ? country.name : "";  
    }  
  }  
};  
</script>

In the file, we accept the route parameter countryCode and from there, we call the this.getHeadlines function from the requestsMixin that we made earlier and included in this component to get the headlines from the News API. Then the results are displayed in Bootstrap cards in the template section. In the template, we get the country name by finding it from the country-list data. We display a message if there are no headlines found. In general, if we want to make a page that accepts URL parameters, we have to make a file with an underscore as the first character and the variable name of the URL parameter that we want. So _countryCode.vue will let us get the countryCode parameter by using this.$route.params.countryCode in this example.

Next in index.vue in the pages folder, we replace the exist code with:

<template>  
  <div class="container">  
    <h1 class="text-center">Home</h1>  
    <div class="card" v-for="(h, i) of headlines" :key="i">  
      <div class="card-body">  
        <h5 class="card-title">{{h.title}}</h5>  
        <p class="card-text">{{h.content}}</p>  
        <button class="btn btn-primary" :href="h.url" target="_blank" variant="primary">Read</button>  
      </div>  
      <img :src="h.urlToImage" class="card-img-bottom" />  
    </div>  
  </div>  
</template>

<script>  
import { requestsMixin } from "~/mixins/requestsMixin";  
const { getData } = require("country-list");

export default {  
  mixins: [requestsMixin],  
  data() {  
    return {  
      headlines: []  
    };  
  },  
  beforeMount() {  
    this.getHeadlinesByCountry();  
  },  
  methods: {  
    async getHeadlinesByCountry() {  
      const { data } = await this.getHeadlines("us");  
      this.headlines = data.articles;  
    }  
  }  
};  
</script>

<style>  
</style>

This lets us display the headlines for the US in the home page. It works similarly to the _countryCode.vue page except that we only get the US headlines instead of accepting a URL parameter to get headlines from different countries depending on the URL.

Next we create an create-env.js in the project’s root folder and add the following:

const fs = require('fs')  
fs.writeFileSync('./.env', `API_KEY=${process.env.API_KEY}`)

This allows us to deploy to Netlify because we need to create the .env file on the fly there from the entered environment variables. Also, we create the .env file manually and put the API_KEY as the key and the News API API key as the value.

Next in nuxt.config.js , we replace the existing code with:

require("dotenv").config();  
const { getData } = require("country-list");

export default {  
  mode: "universal",  
  /*  
   ** Headers of the page  
   */  
  head: {  
    title: "News Website",  
    meta: [  
      { charset: "utf-8" },  
      { name: "viewport", content: "width=device-width, initial-scale=1" },  
      {  
        hid: "description",  
        name: "description",  
        content: process.env.npm_package_description || ""  
      }  
    ],  
    link: [  
      { rel: "icon", type: "image/x-icon", href: "/favicon.ico" },  
      {  
        rel: "stylesheet",  
        href:  
         "https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"  
      }  
    ],  
    script: [  
      { src: "https://code.jquery.com/jquery-3.3.1.slim.min.js" },  
      {  
        src:  
          "https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"  
      },  
      {  
        src:  
          "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"  
      }  
    ]  
  },  
  /*  
   ** Customize the progress-bar color  
   */  
  loading: { color: "#fff" },  
  /*  
   ** Global CSS  
   */  
  css: [],  
  /*  
   ** Plugins to load before mounting the App  
   */  
  plugins: [],  
  /*  
   ** Nuxt.js dev-modules  
   */  
  buildModules: [],  
  /*  
   ** Nuxt.js modules  
   */  
  modules: [  
    // Doc: https://axios.nuxtjs.org/usage    
    "@nuxtjs/axios",  
    "@nuxtjs/dotenv"  
  ],  
  /*  
   ** Axios module configuration  
   ** See https://axios.nuxtjs.org/options
   */  
  axios: {},  
  /*  
   ** Build configuration  
   */  
  build: {  
    /*  
     ** You can extend webpack config here  
     */  
    extend(config, ctx) {}  
  },  
  env: {  
    apiKey: process.env.API_KEY || ""  
  },  
  router: {  
    routes: [  
      {  
        name: "index",  
        path: "/",  
        component: "pages/index.vue"  
      },  
      {  
        name: "headlines-id",  
        path: "/headlines/:countryCode?",  
        component: "pages/headlines/_countryCode.vue"  
      }  
    ]  
  },  
  generate: {  
    routes() {  
      return getData().map(d => `headlines/${d.code}`);  
    }  
  }  
};

In the head object, we changed the title so that we display the title we want instead of the default title. In the link section, we add the Bootstrap CSS, and in the script section, we add the Bootstrap JavaScript files and jQuery, which is a dependency of Bootstrap. Since we want to build a static site, we cannot use BootstrapVue because it is dynamic. We do not want any dynamic JavaScript in the generated output, so we have to use plain Bootstrap. In the modules section, we added ”@nuxtjs/dotenv” to read the environment variables from the .env file that we created into our Nuxt app. We also added require(“dotenv”).config(); so that we get the process.env.API_KEY which can be added to this configuration file. We have to do this so that we don’t have to check in our .env file. In the env section, we have the apiKey: process.env.API_KEY || “”, which is what we get by reading the API KEY in the .env file with dotenv.

In the router section, we define the dynamic routes so that they can be viewed when users click on links with the given URLs or click on a link with such URLs. Nuxt also uses these routes to generate static files. In the generate section, we define the routes that Nuxt will traverse to generate the static files for the static website. In this case, the array of routes consist of routes for the headlines page that we created earlier. It will loop through them to get the data for them, then render them and generate the file from the rendered results. The folder structure will correspond to the routes. So since our path is /headlines/:countryCode , the generated artifact will have the headlines folder along withe all the country code as names of subfolders, and inside each folder there will be a index.html with the rendered content.

Now we are ready to deploy our website to Netlify. Create a Netlify account by going to https://www.netlify.com/. The free plan will work for our needs. Then commit your code to a Git repository hosted on GitHub, Gitlab or Bitbucket. Then when you log in to Netlify, click on New site from Git. From there, you can add your Git repository that’s hosted in one of those services. Then when you’re asked to enter the Build Command, enter node ./create-env.js && npm run generate, and the Publish directory would be dist .

After that, enter the API Key in the .env file into the Environment variables section of the website settings, which you can go to by clicking on the Environment link on the Build & deploy menu. Enter API_KEY as the key and your News API API key as the value. Then click the save button.

Once you commit and push everything in a Git repository hosted by GitHub, Gitlab or Bitbucket, Netlify will build and deploy automatically.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

One reply on “Generate Static Websites with Nuxt”

Leave a Reply

Your email address will not be published. Required fields are marked *