Static Site Generators: Nuxt.js
A few months ago I wrote a post about trying to improve the performance of a React application by emulating Server Side Rendering. That post included code to build a Webpack plugin which pre-rendered our application using JSDOM, took the HTML that was rendered by React and saved it to HTML files.
The idea was to not render empty HTMLs to improve perceived load speed, and improve SEO. This allowed us to deploy our front-end application in a static file repository, like AWS S3 and/or a CDN, giving us the benefits of only dealing with static files. It’s great for security and it has a low operation cost, while keeping the same benefits of using a JavaScript framework which usually is deployed on costly servers.
Now, it seems that the idea of “Server Side Rendering without a server” mixed SPA’s with Static Site Generators.
Static Site Generators have been around for a few years now. Jekyll is one of the most know SSGs, and it’s seemly used by Github Pages. I tried it a couple of years back to generate an old version of my personal website and, while it wasn’t the most modern and flexible tool, it did its job.
However, the resurge in popularity of Static Site Generators during the last couple months comes along with relatively new tools like Gatsby.js, Next.js or Nuxt.js. These tools are built in top of modern Javascript frameworks (Gatsby and Next are based on React, Nuxt on Vue) and most of them offer the idea of isomorphic/universal apps.
I’ve been very into Vue lately, that’s why I want to talk about Nuxt.
Nuxt
According to Nuxt website: “Nuxt.js is a framework for creating Universal Vue.js Applications. Its main scope is UI rendering while abstracting away the client/server distribution”.
Nuxt is an extra layer of abstraction in top of Vue. It provides a well defined project structure:
If you’re familiar with Ember.js, Nuxt follows a very similar philosophy of convention over configuration: Drop Vue components in their respective folders following the convention, and Nuxt will “magically” bind (almost) everything together; abstracting a lot of infrastructure decisions away from the developers.
These conventions are not just a perk of Nuxt. They are the means in which Nuxt defines the boundaries for your app in a way it can run in multiple contexts, giving it it’s universal app quality:
- SPA. Run as a regular Vue web app.
- Server Side Rendered. For each request, pre-render the application in a server and send that back to the client.
- Static Generated (pre-rendered). Pre-render the application once during build, and serve it as a SPA.
Nuxt adds multiple hooks to your Vue components to make sure you can access the context the application is running on and act accordingly.
An example: User management
Let’s run a simple example, just to understand what are we dealing with. Let’s generate a Nuxt application following the steps from their website.
$ vue init nuxt-community/starter-template <project-name>
$ cd <project-name>
$ npm install
$ npm run dev
This will generate a project similar to the one in the image I pasted before.
Now, let’s create an application what will retrieve a list of users with REST, list them in a page, and allow users to open a user in a new page.
The only dependency we need to install is axios
, and this is because we need an isomorphic tool which can make HTTP requests both in the client and in the server.
yarn add axios
# or
npm install --save axios
State management: Vuex
We will use Vuex to handle the state of our application. Thankfully, Nuxt is already integrated with with Vuex, so we don’t have to install any extra dependencies.
Inside /store
drop the following file:
As you see, we don’t directly declare any Vuex store. This is a sample of the convention-over-configuration concept we discussed earlier. Nuxt knows that named files inside store
are supposed to be Vuex store declarations, so it accepts plan JavaScript objects and functions.
Nuxt uses the name of the file, users.js
in this case, to know that this is the declaration for the users
store.
The store is really simple: It keeps a list of users in memory, which can be pulled from our REST API using the fetchUsers
action.
For this examples, we are using the public REST API https://jsonplaceholder.typicode.com/users
.
The main page
Now, we will create a Nuxt page which will use our store:
Take a look at fetch
. This is a Nuxt-specific hook for retrieving data in a page. This function waits for the data to be retrieved before rendering anything from the server. Also, it’s executed both in the client and the server, so be sure any code you run in there is runnable in both the browser and Node.
This page will render a very simple list of the users we have stored in our Vuex store.
Dyanamic routes
To make this example more complete, let’s add dynamic routes. We want to open the user details when we click on one of the names in the list. We’ll create a new page, which will receive a parameter id
and will display the respective user.
Inside pages
create a folder called users
and inside that new folder add the following _id.vue
component:
See the underscore in the file name? That tells Nuxt that this is a dynamic route, where id
will be the name of the parameter.
In our fetch
function we check if the store is already filled, and if not, it fetches the list of users. This will make sure we don’t make a request every time you visit this page, increasing performance.
Then, on a computed property we use the getter userById
to retrieve the user for the passed route parameter id
, and display it in our template.
Let’s run our application. Execute the following command:
yarn dev
It should start a web server in your computer:
You can click in any of the users and you should be able to see its details.
Generating the static site
Since our application works correctly, it’s time we generate a static version of it.
Run the following command:
npx nuxt generate
You should get some output in your console:
Asset Size Chunks Chunk Namesserver-bundle.json 146 kB [emitted]
nuxt: Call build:done hooks (2) +12s
nuxt: Call generate:distRemoved hooks (1) +17ms
nuxt:generate Destination folder cleaned +12s
nuxt: Call generate:distCopied hooks (1) +17ms
nuxt:generate Static & build files copied +17ms
nuxt:render Rendering url / +0ms
nuxt: Call generate:page hooks (1) +393ms
nuxt:generate Generate file: /index.html +394ms
nuxt:render Rendering url / +394ms
nuxt: Call generate:done hooks (1) +4ms
nuxt:generate HTML Files generated in 12.6s +3ms
nuxt:generate Generate done +0ms
Now navigate to the dist
folder inside the project. You will find index.html
.
For any regular SPA this file is usually empty, with exception of CSS and JS imports, and very basic HTML structure. Your app’s content gets loaded when your JS framework bootstrap it during runtime.
But here Nuxt pre-rendered our pages (look at the console log above) and saved the rendered content in the file:
If you take a closer look, you will see it even stored the response of the REST request, so it doesn’t have to request all the data on load, decreasing page load time:
You can even open the file directly in the browser and you should see content.
And our dynamic routes?
So, index.html
is there, but what about _id
route?
By default, Nuxt won’t generate static files for any dynamic route, as it doesn’t know what id
will be.
However, we can instruct Nuxt to know what data to use to generate those routes. In this case, we want to retrieve the list of available user ID’s and use those to instruct the framework which routes to generate. To do this, add the following snippet to nuxt.config.js
:
generate: {
routes: function () {
return axios.get('https://jsonplaceholder.typicode.com/users')
.then((res) => {
return res.data.map((user) => {
return {
route: '/users/' + user.id,
payload: user
}
})
})
}
},
This block will be executed during the generate
phase of the project. It will make a call to the REST API to retrieve the list of user IDs, then it will return a configuration object with the route and the payload to include for that route.
Generate the static files again:
npx nuxt generate
Now, in your console you should get output like this:
...
nuxt: Call generate:page hooks (1) +459ms
nuxt:generate Generate file: /users/4/index.html +459ms
nuxt: Call generate:page hooks (1) +20ms
nuxt:generate Generate file: /users/3/index.html +20ms
nuxt: Call generate:page hooks (1) +39ms
nuxt:generate Generate file: /index.html +39ms
...
If you navigate to dist
you should see a new folder users
which contains multiple folders, one per each ID. Each of those folders has a index.html
with the contents for that specific page.
When not to use Nuxt
Generating web applications with this approach has its obvious limitations.
Since the data included in static sites is bundled during build time, this approach is not the best choice for highly dynamic apps where data changes very often. For these applications, the version of the page which is first loaded will almost always be replaced by newer content, which is something that users will notice and might cause confusion for search engines.
Applications where data keeps changing will either need a finer tuning (something like building and deploying multiple times every time data changes), or rely on old fashion Server Side Rendering.
Nuxt is a really good tool to allow us to generate static sites using Vue. Static sites generated by tools like Nuxt can increase dramatically the performance of your SPA.
For serverless-based web applications, Static Site Generators are almost a must. We rely on deploying our web resources in services like AWS S3, where Server Side Rendering is not really possible.
If you want to check out more Static Site Generators, check staticgen.com, a website which collects the most popular ones.