Building and deploying a static blog with Nuxt.js, Sakura, GraphCMS and Vercel
Tags: nuxt, graphcms, vercel, webdev
Published: 2020-11-22 17:40:40

# Introduction

Recently, I've been trying Nuxt.js and it's a really great experience. I decided to write an article on making something cool! The goal of this project is to create a simple static blog.

For this project, I have chosen to use [Nuxt.js](https://nuxtjs.org), [Sakura](https://oxal.org/projects/sakura), [GraphCMS](https://graphcms.com) and [Vercel](https://vercel.co).

**Nuxt.js** is a JavaScript framework built on top of [Vue.js](https://vuejs.org). We'll be using this to build our app, handle the routes, and generate our final website.

Sakura is what we call a **Classless CSS Framework** or a **Drop-in CSS**. It's a CSS framework which can be dropped in an HTML to style it. For this to work, we will be using **HTML semantic tags**.

**GraphCMS** is a **headless CMS**, which means it doesn't serve templates/views created from your content unlike Wordpress, Joomla! or Drupal. GraphCMS will be used to manage and create our articles. Under the hood, GraphCMS uses GraphQL, which we will be using to query our data from Nuxt.js.

**Vercel** is basically a cloud platform for **serverless deployments**. We will be using it to deploy our finished blog on the web.
# Creating our content with GraphCMS

Let's begin by creating our data, in our case: blog posts!

If you haven't done yet, go create yourself an account on [graphcms.com](https://graphcms.com).

Now when that's done, create a new project. Name it however you want, it'll serve for our blog.
![New project](https://media.graphcms.com/output=format:webp/resize=height:657,width:935/a7sgKu3QrOjIRWQAatMg)

After all the basic setup done, you'll be presented with a dashboard. Notice the quick start guide! Click on "Set up your schema".

In GraphCMS a schema represents an entity, which will hold many instances of itself, like a table in SQL. Create a new schema, then fill in and confirm the information in the opened modal.

I created a model named `Post`, and here's the schema I've made:
![Fields definition](https://media.graphcms.com/output=format:webp/resize=height:379,width:737/9cA42muYSpatu8cgfu5p)


Now that we've setup our schema, let's add the first post! Click on the content button on the left, click on create. You'll be prompted with a pretty form, ready to be filled in!

When you are done writing your blog post, press "Save and publish" on the top right, this will make your article ready to be retrieved later on from our code.
![Post details](https://media.graphcms.com/output=format:webp/resize=height:496,width:646/80rsfLVHSQKj2K4BmOcz)

To test our schema, head to what GraphCMS calls "API Playground". Let's click on the "Explorer" tab to see which fields we are able to retrieve. As you check these fields, the related GraphQL query gets generated on the center. This is the query we are going to use in Nuxt.js to get all of our posts:
```graphql
query MyQuery {
  posts {
    title
    slug
  }
}
```
We will use this query to get all the posts of our blog.

Let's continue with Nuxt.js in the next section.
# Building our blog with Nuxt.js

Now the real fun begins, let's open up our terminal and create a new folder for our project. We will be using Git for this project.

In this folder, create a package.json file, and put in this code:
```json
{
  "name": "my-blog",
  "scripts": {
    "dev": "nuxt",
    "generate": "nuxt generate",
    "start": "nuxt start"
  },
}
```
We've already put some scripts in there as you can see ([more info here](https://nuxtjs.org/docs/2.x/get-started/commands)):

* `yarn dev` will be used to launch our development environment
* `yarn generate` will be used by Vercel to build the production files
* `yarn start` can be used to locally test the production files 

After that, go back to your terminal and run `yarn add nuxt` to install Nuxt.js as a dependency.

Here I use yarn, but with npm that would be `npm install nuxt`. If you feel lost, don't hesitate to check out the Nuxt.js [docs](https://nuxtjs.org/docs/2.x/get-started/installation).

As we will be using git, add a `.gitignore` file:
```
node_modules
.nuxt
```
Let's add a bit of configuration, to get setup and running. Add a `nuxt.config.js` in the project root, and write the following:
```js
export default {
  env: {
    graphcmsEndpoint: process.env.GRAPHCMS_ENDPOINT
  },
  target: 'static',
  loading: false, // Disables the page change loader on the top
  head: {
    htmlAttrs: {
      lang: 'en',
    },
    titleTemplate: '%s - My Blog',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
    ],
  },
}
```
As you can see, I use an environment variable to get the GraphCMS endpoint URL, more on this later. Also, notice the target value is static. Doing so, Nuxt will pre-render all your pages. Check [this article](https://nuxtjs.org/blog/going-full-static) for more info.

As we will be using GraphQL, we need to install the `graphql-request` package:
```shell
yarn add graphql graphql-request
```
Now, we need to tell Nuxt.js we're using this module as a plugin, head into your nuxt.config.js and add `plugins: ['~/plugins/graphcms.js'],`.

Create a config file in your project root: `./plugins/graphcms.js`. In this file we'll write this piece of code which will create for us a GraphQL client:
```js
import { GraphQLClient } from 'graphql-request'

export default (_, inject) => {
  inject('graphcms', new GraphQLClient(process.env.graphcmsEndpoint))
}
```
We now need to create the layout of our app. Add a `layouts` folder in the project folder, and a `default.vue` file in it. **Layouts in Nuxt.js are templates which will be extended by our pages**. A layout can contain a header, a footer, etc. Everything that is common to all pages. Note that you can still create specific layouts for different pages if needed.

Here's a basic layout I came up with:
```html
<template>
  <div>
    <header>
      <h1>My blog</h1>
      <nav>
        <NuxtLink to="/">Home</NuxtLink>
      </nav>
    </header>
    <main>
      <Nuxt/>
    </main>
  </div>
</template>
```
In Nuxt.js, `<Nuxt/>` is a special component which indicates that pages will be rendered here.

Just for this sake of this article, let's turn the header into a component. Create a `components` folder at the root of your project and create a `navbar.vue` file. Let's put in our header:
```html
<template>
  <header>
    <h1>My blog</h1>
    <nav>
      <NuxtLink to="/">Home</NuxtLink>
    </nav>
  </header>
</template>
```
Now that we've created a `navbar` component, let's remove the old header and include the new component in our `default.vue` layout:
```html
<template>
  <div>
    <Navbar/>
    <main>
      <Nuxt/>
    </main>
  </div>
</template>
```
Much better!

To use our header component in our layout, we actually don't need to manually import it like in Vue.js. We can put `components: true`, in our `nuxt.config.js` to tell nuxt to [automatically import components](https://nuxtjs.org/docs/2.x/configuration-glossary/configuration-components) based on their file name!

In Nuxt.js the routing is done automatically based on the files and folders, this is commonly called **file-system based routing**. For more information see the [routing docs](https://nuxtjs.org/docs/2.x/features/file-system-routing).

Let's create a `pages` folder inside our project root with an `index.vue` file in it, this will be our main blog page, where all the articles are listed. In the previous section, we created a GraphQL query from the API Playground, let's use it to get all the posts.

This is what I came up with.
```html
<template>
  <article>
    <NuxtLink
      v-for="post in posts"
      :to="{ name: 'slug', params: { slug: post.slug } }"
      :key="post.slug"
    >
      <h3 v-text="post.title"/>
    </NuxtLink>
  </article>
</template>

<script>
import { gql } from 'graphql-request'

export default {
  async asyncData({ $graphcms }) {
    return await $graphcms.request(
      gql`
        {
          posts {
            title
            slug
          }
        }
      `
    )
  },

  head() {
    return { title: 'Blog' }
  }
}
</script>
```
Here I'm using [`asyncData`](https://nuxtjs.org/docs/2.x/features/data-fetching#async-data) to retrieve the post list. The GraphQL request's data is transformed into an object, which is used in the template to display the blog posts. You can see I used a route name called `slug` which in Nuxt.js corresponds to a template located at `project/pages/_slug.vue`.

After creating our post list, let's create a page to show the actual content of the posts. I named it `_slug.vue` in my case, as I want the URL to show the slug of the post.
```html
<template>
  <section v-if="post">
    <header>
      <h2 v-text="post.title"/>
    </header>
    <small>
      <span>Posted {{ formatDate(post.createdAt) }}</span> &mdash;
      <span>Updated {{ formatDate(post.updatedAt) }}</span>
    </small>
    <div v-html="post.content.html"/>
  </section>
</template>

<script>
import { gql } from 'graphql-request'

export default {
  async asyncData({ $graphcms, params }) {
    const { post } = await $graphcms.request(
      gql`
        query GetPost($slug: String) {
          post(where: { slug: $slug }) {
            title
            content {
              html
            }
            slug
            createdAt
            updatedAt
          }
        }
      `,
      { slug: params.slug }
    )

    return { post }
  },

  head() {
    return { title: this.post?.title }
  },

  methods: {
    formatDate(date) {
      const options = { year: "numeric", month: "short", day: "numeric" }

      return new Intl.DateTimeFormat('default', options).format(new Date(date))
    },
  }
}
</script>
```
**Note: I used the `createdAt` field to show the published date. While this is incorrect, I just couldn't get the `publishedAt` to work, as it was changed on every edit -> publish.**

You'll notice I used the [Intl](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) API to format the dates as it doesn't require additional libraries, but you could use something else if needed. For the actual content of the post, I went for HTML. You could retrieve the post content in multiple formats, like HTML, markdown, raw and text. You can experiment with that in the API Playground on GraphCMS.

If you try to launch the project with `yarn dev` right now, you may encounter an "Only absolute URLs are supported" error. This is because the GraphCMS endpoint URL we specified must come from and environment named `GRAPHCMS_ENDPOINT` in our case. You could set that URL now, or wait till the end where we set it on Vercel.

If you wish to set it now, set the env variable in your shell:
```shell
export GRAPHCMS_ENDPOINT=https://api-eu-central-1.graphcms.com/v2/XXXXXXXXXXXXX/master
```
Then you can run the project without issues. Instructions on how to get the URL are in the 'Deploying our blog with Vercel' section down below.

So this is how our blog looks like right now:
![Homepage without Sakura.css](https://media.graphcms.com/output=format:webp/resize=height:137,width:634/UWyvUq0FTrq50kHUxsqA)
# Styling our blog with Sakura

Sakura is one of those drop-in CSS frameworks, just include the CSS file in your HTML, and you're done!

Looking at the [readme](https://github.com/oxalorg/sakura#installation), installing normalize.css is recommended, as Sakura.css is not built with a CSS reset. Let's install the dependencies:
```shell
yarn add sakura.css normalize.css
```
After that is done, add this line to your nuxt.config.js:
```js
css: ['normalize.css', 'sakura.css/css/sakura-vader.css'],
```
Here, we load normalize.css first, then we load Sakura, as explained in their readme. This will ensure the best of both worlds. Don't forget to edit the theme name of Sakura if you've chosen something different.

Now, with this done, let's go back to our browser to see the changes:
![Homepage with Sakura.css](https://media.graphcms.com/output=format:webp/resize=height:605,width:713/qHKYlB7QS7iUbcZmIGec)

You can always add [custom CSS](https://nuxtjs.org/docs/2.x/configuration-glossary/configuration-css) to change the styling according to your needs.

That was easy, it's the spirit of classless CSS frameworks!
# Deploying our blog with Vercel

Importing our blog into Vercel is easy, just click "Import project" on your Vercel dashboard, select "Import Git Repository" and put the link of your repository.

Let's now add the GraphCMS endpoint URL as an env variable so it can be used by our code. From the import project screen, we'll create an `GRAPHCMS_ENDPOINT` env variable and assign our endpoint URL to it. This endpoint URL can be found in GraphCMS by going to Settings -> API Access -> Endpoints (don't forget to edit the 'Public API permissions' settings).
![Environment variables](https://media.graphcms.com/output=format:webp/resize=height:279,width:605/tmt2XynaSsIHKpjVNn0j)

You can also install "Vercel for GitHub" on your repository, this will ensure your deployed app is in sync with a specific branch.

And this concludes our basic Vercel setup!
# Syncing changes with Webhooks

Right now, we need to manually re-deploy our app on Vercel to see the changes we made to our content. What if there is a better way to do this?

*There is!* And it's called **Webhooks**.

> Webhooks are a way to make apps talk to each other, send signals and/or data.

In our case, each time we edit/create content on GraphCMS, we want to trigger a Vercel deployment. Let's do that right now!

First, we need to get a deploy hook from Vercel. This is basically **an URL that can be called and will trigger re-deployments from a specified git branch**. So back to Vercel, in your project settings you should see a "Deploy Hooks" section. Name yours however you like, specify the git branch you want to deploy automatically on each GraphCMS and click "Create Hook". Copy the hook URL and let's head back to GraphCMS!

On your GraphCMS dashboard, click on the webhooks button on the left sidebar. Click on create, and fill in the form with the appropriate details.
![Webhook](https://media.graphcms.com/5V1f4JESKCBVejSFEE4G)

Notice how I specified to execute this webhook only for published posts. Also, you can uncheck 'Include payload', as our webhook doesn't need any extra data to send.

Now go back to your article, edit/save/publish it, and go back to Vercel. You should see a build going on. Wait a bit, and voilà! Your edits are live!

See? That's the power of webhooks, and you could do more than that! Send slack messages, SMS, emails, etc.

Having this done, I guess we can call this little project finished for now. I have a few ideas and improvements for this blog, but this will be subject to another article.
# Closing Words

We're done! Imagine if we were doing this with bare PHP 15 years ago... We've got the chance to make whatever we want with easy tools, so make whatever you dream of!

Classless CSS frameworks were a new discovery for me, but as these frameworks aren't new, they are starting to get popular right now.

> Using this technology stack has proven to be very productive in my opinion. There's little time between idea and prototype. 

You can find the complete blog code over here on my github: [nuxtjs-graphcms-sakura-starter](https://github.com/JulienVanelian/nuxtjs-graphcms-sakura-starter).

That's all for now folks, I hope you liked this post!
# Moving Forward

But wait! At this time of writing, Vue.js 3 is already out! Right now, Nuxt.js still uses Vue.js 2 in the background. I've heard Nuxt.js is going to support Vue.js 3 soon and I think we can expect a lot of improvements from this. Also, GraphCMS are currently working on a Vercel integration, this means we probably wont need to setup the webhook like we did!

Also new stuff coming ➡️ [@Atinux](https://twitter.com/Atinux) (Sébastien Chopin - Co-Creator @ Nuxt.js) said that official [serverless support is coming to Nuxt.js](https://twitter.com/Atinux/status/1324464524402020353)!

If you've found this useful, [follow me on Twitter](https://twitter.com/JulienVanelian) and check out [my blog](https://jvanelian.dev/blog), where this article was first posted!

Thanks for reading,

Julien