• Follow us

Create a fast NuxtJS website using Prismic

Francesco michelini

Francesco Michelini

on 10 May 2019 in Development, Vue.js, CMS, NuxtJS

28 minutes Read
Create a fast NuxtJS website using Prismic

Like all web developers, I love to experiment with new tools, libraries and frameworks. This time, for one of my side projects (my new portfolio), I decided to use NuxtJS, which is a framework built on top of VueJS that allows to build SPAs and static websites blazingly fast, and Prismic, which is a Cloud headless CMS.

I’ve been using WordPress for years, but recently I’ve started working with VueJS and Nuxt on other projects, and not much time passed before deciding to build my new portfolio with Nuxt, mostly because of VueJS' ease of use.

But having a static website means that whenever I want to update it I have to:

  • Open my code editor
  • Make all the changes needed
  • Commit to my repository
  • Upload the updated/new files to the server

Not the ideal situation to be honest; I wanted to have a static website but manage its content without writing code, this is why I decided to add Prismic to the project.

By using Prismic, I can have a static website that fetches its content from an API; sounds perfect!

But there’s a downside: whenever I add new content (a blog post or a case study), I still have to add the new content’s page to the site’s source code, meaning I’m back at the starting point.

The solution is quite simple: during the generation of the site, I tell Nuxt to make calls to the Prismic API and dynamically create those pages.
Doing so, I don’t have to write a single line of code anymore, bingo!

But I wanted to push it a bit further because it’s true that this mechanism works, but every time a page is visited it makes a call to the Prismic API to fetch the content; this is pretty useless, and on a large scale website it can become costly.

I mean that I wanted to make those API calls only when generating the website, creating a completely static website, and at the same time having a CMS to manage its content.

“But how do I regenerate the website everytime I update the content?”

The website is hosted on Netlify and I’ve configured a webhook that whenever is triggered by Prismic it regenerates the whole website.

It’s really easy to configure:

TL;DR;

You can find the code in this GitHub repository.

There’s also a demo available.

01. Creating the NuxtJS app

Creating a new NuxtJS application is pretty straightforward, all you have to do is open your terminal and launch

yarn create nuxt-app <my-project>

At this point, nuxt-create-app asks a couple of questions like the project’s name, description, server, UI framework to use and so on

"create nuxt app"

The next step is to cd into the project’s folder and run

yarn run dev

and voilà, the new application is running at http://localhost:3000/.

"nuxt app created"

02. Creating the Prismic backend

The website will be quite standard: it will have a homepage, an about page, a list of posts and an X amount of single post pages.

Let’s start by logging in into our Prismic account and create a new repository with three content types:

  • Homepage (single type)
  • About page (single type)
  • Blog post (repeatable type)

Important note

When creating the Blog Post content type, make sure you set up a UID field, it will be the slug of each post.

Noticed that I haven’t set up a “Posts index” custom type? That’s because I’ll setup Nuxt to create that page when generating the website.

With the content types created, the custom types list will look like this

"prismic custom types list"

Now, let’s fill in some content in the Content section

"prismic content created"

03. Set up Nuxt to retrieve content from Prismic

The first thing to do is to add the prismic-javascript and prismic-dom modules, which are the two libraries that allow to query and parse content from Prismic.

Install prismic-javascript and prismic-dom by running

yarn add prismic-javascript prismic-dom

and then create a prismic-config.js file at the root of the project with the following content:

// prismic.config.js

import Prismic from 'prismic-javascript'
import PrismicDOM from 'prismic-dom'

const config = {
  baseUrl: '<API_ENDPOINT>',
  access_token: '<ACCESS_TOKEN>'
}

export const initApi = req => {
  return Prismic.getApi(config.baseUrl, {
    accessToken: config.access_token,
    req: req
  })
}

export const linkResolver = doc => {
  if (doc.type === 'blog_post') return `/blog/${doc.uid}`
  return `/${doc.uid}`
}

export const generatePageData = (documentType, data) => {
  switch (documentType) {
    case 'homepage':
      return {
        title: PrismicDOM.RichText.asText(data.title),
        content: PrismicDOM.RichText.asText(data.content)
      }
    case 'about_page':
      return {
        title: PrismicDOM.RichText.asText(data.title),
        content: PrismicDOM.RichText.asText(data.content)
      }
    case 'blog_page':
      return {
        posts: data
      }
    case 'blog_post':
      return {
        title: PrismicDOM.RichText.asText(data.title),
        content: PrismicDOM.RichText.asText(data.content)
      }
  }
}

This file contains the Prismic’s configuration and a couple of functions that are used in every template to initialize the APIs (initApi) and generate each page’s content (generatePageData)

The Prismic API_ENDPOINT can be found in Settings -> API & Security -> API Endpoint.

An ACCESS_TOKEN is required only if the backend is set as Private in Settings -> API & Security -> Repository security

Now, open the nuxt.config.js file and add the following code:

// nuxt.config.js

const Prismic = require('prismic-javascript')
import { initApi } from './prismic.config'

module.exports = {

  ...

  generate: {
    routes: function() {
      // Fetch content for the homepage and generate it
      const homepage = initApi().then(api => {
        return api
          .query(Prismic.Predicates.at('document.type', 'homepage'))
          .then(response => {
            return response.results.map(payload => {
              return {
                route: '/',
                payload
              }
            })
          })
      })

      // Fetch content for the about page and generate it
      const aboutPage = initApi().then(api => {
        return api
          .query(Prismic.Predicates.at('document.type', 'about_page'))
          .then(response => {
            return response.results.map(payload => {
              return {
                route: '/',
                payload
              }
            })
          })
      })

      // Fetch all the blog posts to generate the Blog page
      const blogPage = initApi().then(api => {
        return api
          .query(Prismic.Predicates.at('document.type', 'blog_post'))
          .then(response => {
            return [{
              route: `/blog`,
              payload: response.results
            }]
          })
      })

      // Fetch again all the blog posts, but this time generating each post's page
      const blogPosts = initApi().then(api => {
        return api
          .query(Prismic.Predicates.at('document.type', 'blog_post'))
          .then(response => {
            return response.results.map(payload => {
              return {
                route: `/blog/${payload.uid}`,
                payload
              }
            })
          })
      })

      // Here I return an array of the results of each promise using the spread operator.
      // It will be passed to each page as the `payload` property of the `context` object,
      // which is used to generate the markup of the page.
      return Promise.all([homepage, aboutPage, blogPage, blogPosts]).then(values => {
        return [...values[0], ...values[1], ...values[2], ...values[3]]
      })
    }
  },

  ...


}

What the code above does is pretty simple: it queries all of our custom types (homepage, about_page and blog_post) from Prismic and passes the data to our templates.

04. Create templates and components

I’ll try to explain all of these templates and components, but you can just copy-paste them in their respective locations.

<!-- components/MainNav.vue -->

<template>
  <nav class="nav">
    <nuxt-link to="/">Logo goes here</nuxt-link>
    <ul class="nav-links">
      <li class="nav-link"><nuxt-link to="/">Homepage</nuxt-link></li>
      <li class="nav-link"><nuxt-link to="/about">About</nuxt-link></li>
      <li class="nav-link"><nuxt-link to="/blog">Blog</nuxt-link></li>
    </ul>
  </nav>
</template>

<style scoped>
.nav {
  align-items: center;
  display: flex;
  justify-content: space-between;
  padding: 20px;
}

.nav-links {
  list-style-type: none;
  margin: 0;
  padding: 0;
}

.nav-link {
  display: inline-block;
  margin-left: 20px;
}
</style>

☝ The navigation top bar

<!-- layouts/default.vue -->

<template>
  <div>
    <main-nav/>
    <nuxt/>
  </div>
</template>

<script>
import MainNav from '@/components/MainNav.vue'

export default {
  components: {
    MainNav
  }
}
</script>


<style>
html {
  font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI',
    Roboto, 'Helvetica Neue', Arial, sans-serif;
  font-size: 16px;
  word-spacing: 1px;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  box-sizing: border-box;
}

body {
  background-color: #fafafa;
  margin: 0;
}

a {
  text-decoration: none;
  color: #5CBEFD;
}

*,
*:before,
*:after {
  box-sizing: border-box;
}
</style>

☝ The default layout with common styles

<!-- pages/index.vue -->

<template>
  <section class="container">
    <h1>{{ title }}</h1>
    <div>{{ content }}</div>
  </section>
</template>

<script>
import Prismic from 'prismic-javascript'
import { initApi, generatePageData } from '@/prismic.config'

export default {
  asyncData(context) {
    if (context.payload) {
      return generatePageData('homepage', context.payload.data)
    } else {
      return initApi().then(api => {
        return api
          .query(Prismic.Predicates.at('document.type', 'homepage'))
          .then(response => {
            return generatePageData('homepage', response.results[0].data)
          })
      })
    }
  }
}
</script>

☝ Our homepage.

As you can see, I’ve set up an asyncData() method. This method is part of NuxtJS and is responsible for fetching the data before rendering the page.

Inside of asyncData() I check if the context object has the payload property (if the yarn generate script is running).

If so, we directly return the data using the generatePageData() function I created in prismic.config.js.

But the payload is only available when I’m generating the site using yarn generate, during development payload doesn’t exist, so we have to fetch our data from Prismic first and then return the data with generatePageData().

<!-- pages/about.vue -->

<template>
  <section class="container">
    <h1>{{ title }}</h1>
    <div>{{ content }}</div>
  </section>
</template>

<script>
import Prismic from 'prismic-javascript'
import { initApi, generatePageData } from '@/prismic.config'

export default {
  asyncData(context) {
    if (context.payload) {
      return generatePageData('about_page', context.payload.data)
    } else {
      return initApi().then(api => {
        return api
          .query(Prismic.Predicates.at('document.type', 'about_page'))
          .then(response => {
            return generatePageData('homepage', response.results[0].data)
          })
      })
    }
  }
}
</script>

☝ The About page, same as the homepage

// pages/blog/index.vue

<template>
  <div class="container">
    <h1>Blog page</h1>
    <hr>
    <article class="post" v-for="(post, index) in posts" :key="index">
      <header>
        <h1>
          <nuxt-link :to="`/blog/${post.uid}`">{{ Dom.RichText.asText(post.data.title) }}</nuxt-link>
        </h1>
      </header>
    </article>
  </div>
</template>

<script>
import Prismic from 'prismic-javascript'
import PrismicDOM from 'prismic-dom'
import { initApi, generatePageData } from '@/prismic.config'

export default {
  data() {
    return {
      Dom: PrismicDOM
    }
  },
  asyncData(context) {
    if (context.payload) {
      return generatePageData('blog_page', context.payload)
    } else {
      return initApi().then(api => {
        return api
          .query(Prismic.Predicates.at('document.type', 'blog_post'))
          .then(response => {
            return generatePageData('blog_page', response.results)
          })
      })
    }
  }
}
</script>

☝ The blog page.

Here, the code inside the asyncData() method is a bit different from the one of Homepage or the About page; that’s because there’s an array of contents.

The difference here is that we do not pass the data property of the payload to generatePageData(), but directly the payload.

Also, we include the PrismicDOM library to the component’s data, so that we can use it to print all our contents into the template by writing

Dom.RichText.asText(post.data.title)

where needed.

(Yes, I could avoid using the PrismicDOM library, but in that case we have to write something like post.data.title[0].text[0])

<!-- pages/blog/_slug/index.vue -->

<template>
  <div class="container">
    <h1>{{ title }}</h1>
    <div>{{ content }}</div>
  </div>
</template>

<script>
import Prismic from 'prismic-javascript'
import { initApi, generatePageData } from '@/prismic.config'

export default {
  asyncData(context) {
    if (context.payload) {
      return generatePageData('blog_post', context.payload.data)
    } else {
      return initApi().then(api => {
        return api
          .query(Prismic.Predicates.at('my.blog_post.uid', context.params.slug))
          .then(response => {
            return generatePageData('blog_post', response.results[0].data)
          })
      })
    }
  }
}
</script>

☝ The single post template

First of all, did you notice the file structure? The folder’s name starts with an underscore, that means that its name will be auto-generated via our configuration in nuxt.config.js.

For our single posts we wrote

route: `/blog/${payload.uid}`

This means that _slug will be replaced with the UID of our post, resulting in a tree that will look like this:

/root
  /blog
    /post-01
      index.html
    /post-02
      index.html
    /post-03
      index-html

Conclusion

You did it, you’ve managed to create a NuxtJS website that fetches its content only during the build process (except for development of course).

This allows you to save a lot of bandwidth (and also money) because it only calls the APIs once per content type instead of every page load. Cool uh?

I hope you found this post useful, if you have questions, found a bug or just want say hi, leave a comment.

Need help with your VueJS project?

Let's talk
Related posts

Join the Conversation