Build a fully static site with Nuxt.js

Generate a fully static site with nuxt. Save request data at build time to avoid repeating requests.

Update: Vue v3 will most likely be shipped with a built-in solution. It's planned to add fully static generation as a configuration option. See the github thread for more info.

The Problem

By default nuxt generate outputs a static build of your site. When you initially open your site it uses prefetched data, but when navigating between pages data is still fetched dynamically in a new request. This results in a mix of prefetched and dynamic content and potentially unnecessary refetching of data. The generate command along with the asyncData hook solves SEO for us but doesn't provide a truly static site.

If you only want to stop repeat requests such as nuxtServerInit running for every pre rendered page checkout axios-cache-adapter for a simple plugin solution.

Doesn't asyncdata handle this?

An asyncData hook will prefetch data on generation. This data is used when you first load the site, however when navigating between pages prefetched data is not used and the asyncData is rerun client-side.

Same goes for fetch hooks (fetch persists data in the vuex store to avoid refetching when returning to a page but still fetches dynamically on a pages first view, excluding the initial page).

Solutions

1. Axios hook (Recommended)

Based on: https://github.com/stursby/nuxt-static/blob/master/src/plugins/axios.js Alternative package using a similar method: nuxt-payload-extractor

Pros:

Cons:

Tip: Theres no point in using vue-axios. It takes the same number of lines to configure with no difference in functionality.

Code:

/*
* Axios instance
*
* Saves response data to local JSON files on generation
* All data files are named after the requests config query parameter
* Generated static builds will return the locally stored JSON data
* Doesn't affect requests made in dev or start mode
*
* Context:
* Nuxt generate allows statically saving data fetched inside asyncdata
* Problem is the static data is only used on the first page viewed
* Subsequent page views fetch fresh data
*
* Usage:
* All requests must provide a unique query parameter
*
* Modification of https://github.com/stursby/nuxt-static/blob/master/src/plugins/axios.js
*
* @author Barry Hood
* @module axios
*/


import axios from "axios";
import config from "../config/index.js";

let axiosConfig = {
baseURL: "https://graphql.datocms.com",
headers: {
Authorization: config.DATO_CMS_API_TOKEN,
},
};

// Config for generated site
if (process.browser && process.static) {
axiosConfig = {
baseURL: "/data",
};
}

const axiosCleanInstance = axios.create(axiosConfig);
const axiosInstance = axios.create(axiosConfig);


// Warn about missing query parameter
axiosInstance.interceptors.request.use(config => {
if (!config.query) {
// eslint-disable-next-line no-console
console.log(
"WARNING: request is missing query parameter - will fail during generation",
);
}

return config;
});


// Handle requests of generated site
if (process.browser && process.static) {
axiosInstance.interceptors.request.use(config => {
config.method = "GET";
config.url = config.query + ".json";
return config;
});
}


// Track query names to ensure all are unique
const queryNames = new Set();

// Handle requests during generation
if (process.server && process.static) {
const { join, dirname } = require("path");
const { mkdir, writeFile } = require("fs").promises;

axiosInstance.interceptors.response.use(
async function(response) {
const { query } = response.config;

// Require for query paramater
if (!query) {
throw new Error(
"ERROR: All requests must have a query parameter defined",
);
}

// Require query name is unique
if (queryNames.has(query)) {
throw new Error(
`ERROR: All requests query parameters must be unique. Duplicate found: "${query}"`,
);
}

queryNames.add(query);
const path = join("./dist/data", query + ".json");

try {
await mkdir(dirname(path), { recursive: true });
} catch (e) {
if (e.code !== "EEXIST") throw e;
}

await writeFile(path, JSON.stringify(response.data));
return response;
},
function(error) {
// Do something with response error
return Promise.reject(error);
},
);
}

export { axiosCleanInstance };
export default axiosInstance;

Example use:

import axios from "~/plugins/axios";

const res = await axios({
// Each request must have a unique query name
query: "HOME",
url: "/",
method: "GET",
});


// If you want to use an unmodified instance of axios
import { axiosCleanInstance } from "~/plugins/axios";

const res = await axiosCleanInstance.get('/api');

2. Vuex store + cache object

Based on:

Alternatively you can also use axios-cache-adaptor to avoid repeat requests instead of the below cache code.

Fetch static data in nuxtServerInit. The nuxtServerInit method runs once for each page but by using a cache we only have to make each request once.

Pros:

Cons:

Custom cache code:

import axios from "axios";

// Temp cache to store pages and promise resolving all requests
// before committing final data to store
const cache = {
};

/**
* Get data from cache by name
* If data doesn't exist call passed function and cache returned data
*
* @param {string} name - name of item in cache
* @param {function} getFunc - async function which returns items data. Called if data's not already cached
* @response {*}
*/

async function cacheGet(name, getFunc) {
if (cache[name]) return cache[name];

// Return proimse, don't await result
// No blocking means function returns before next nuxtServerInit call.
// Each call then awaits the same promise.
const res = getFunc();
cache[name] = res;
return res;
}

async function fetchNuxtServerInit() {
const res = await axios({
url: "/",
method: "POST",
query: "nuxtServerInit",
data: {
query: `{
allApartments {
...
}
allOffers {
...
}
}
`
,
},
});

return res.data.data;
}

export const actions = {
async nuxtServerInit({ commit }) {
const data = await cacheGet("NuxtServerInitQuery", fetchNuxtServerInit);

commit("setApartments", data.allApartments);
commit("setOffers", data.allOffers);
},
};

3. Separate module

Based on: https://joshuastuebner.com/blog/backend/nuxt_static.html#how

Use the nuxt build:before hook to fetch data server-side and save the response to static JSON files. Allows you to load data into components and pages anytime by simply requiring/importing the generated JSON files. You control when to generate static files. As with the vuex cache this requires fetching content outside of components.

Code:

const fs = require('fs').promises;
const axios = require('axios');

// Define where static JSON files are saved
const staticDirectory = 'static/data';

async function emptyDir(directory) {
try {
const files = fs.readdirSync(directory);
const unlinkPromises = files.map(filename => unlink(`${directory}/${filename}`));
return Promise.all(unlinkPromises);
} catch(err) {
console.log(err);
}
}

async function fetchJson(url) {
const res = await axios.get(url);
return res.data;
}

const writeData = (path, data) => {
return new Promise(async (resolve, reject) => {
try {
await fs.writeFile(path, JSON.stringify(data));
resolve(`${path} Write Successful`);
} catch (e) {
console.error(`${path} Write Failed. ${e}`)
reject(`${path} Write Failed. ${e}`)
}
})
}

module.exports = function scraper() {
// Add hook for build
this.nuxt.hook('build:before', async builder => {
try {
await fs.mkdir(staticDirectory);
} catch (e) {
console.error(`Failed to create directory: ${staticDirectory}`)
console.error(e);
}

// Clean data directory
emptyDir(staticDirectory)

// Empty array to fill with promises
const scraper = []

// One of these for every request, a loop for dynamic nested pages
scraper.push(writeData(`${staticDirectory}/index.json`, await fetchJson('https://randomapi.com/api/6de6abfedb24f889e0b5f675edc50deb?fmt=raw&sole')))

// Finish when all requests are done
return Promise.all(scraper).then(() => {
console.log('JSON Build Complete!')
}).catch(err => {
console.error(err)
})
});
};