Vue Static Site Generator with Nuxt and Mardown. Let's Create a Serverless Blog. Part 4


This website you are visiting have been created using Nuxt and Markdown and is serving SEO-friendly blog posts without the need of a server. Want to learn how to do it? Keep reading...


Published date: March 28 2020

In Part 1 of this article we talked about what are static site generators and what are teh advantages of using them when working with Javascrip frameworks, we learned the basics of Nuxt, created our nuxt project and added a navbar and a footer component, learned how to use sass, and started to create our serverless blog using Markdown and FrontMatter.

In Part 2 we saw how to add internationalization to our static site —including the blog— using "Nuxt-i18n".

In Part 3 I show you how to create a home page like the one in this site

In this part, I'm going to ad some style and add some additional elements to the blog.

Feel free to go to Part 5 if what you wanna see is how to generate your static site and deploy it to Netlify.

Index

Complete Project Code: You can get the source code on GitHub

Prerequisite: HTML, CSS, JavaScript, Vue.js, and Markdown.

A secondary layout for the blog

I want a clean minimalists look for my blog. No sidebar, no comments, just a list or grid of posts in the blog homepage, a filter to show only posts of a certain category, and a subscription box to subscribe to my mailing list.

In the single blog page, I would like to show only the post and the subscription box.

I'm going to create a new layout for the blog, which will include a simplified menu and the subscription box.

Let's create the new layout file in the layout folder and call it blog.vue.

This is the code. I'm importing and registering the components I want to add to this new layout (and that we have to create) and adding them to the template.

<template>
<div>
<Navbar/>
<nuxt/>
<Footer/>
</div>
</template>

<script> import Navbar from ‘@/components/NavbarBlog’ import Footer from ‘@/components/FooterBlog’ export default {

components: {
  Navbar,
  Footer
}

} </script>

<style lang="scss">

</style>

For the components, you can get the code from GitHub I will explain how to add the opt-in to the footer but nothing else. The rest of the code is very easy to follow.

But before, let's tell Nuxt that we want to use the new layout with the blog pages. For that just use the layout property set to the name of the blog layout in the component where you want to use it.

nuxt markdown blog

Adding a Sign up form to the Footer - Adding external javaScript code to a Nuxt template

I want to add an Aweber sign up form to my blog. I'm going to do it in the footer.

The code provided by Aweber has html and javascript. The problem is that you cannot create script tags into the template. If you try to do so you will get an error similar to 'Templates should only be responsible for mapping the state to the UI. Avoid placing tags with side-effects in your templates, such as script, as they will not be parsed'. The solution is to add the script tags containing the Aweber's js programmatically.

You can use the same —or a similar— method to add forms from other providers or anytime you need to add external javascript to your templates.

A quick note: If you just have to add external javascript globally to your site, you have the option of adding the script tags to the head of the index.html file by adding it to the head property of the nuxt.config.json file.

Something like this:

head: {
script: [
      { innerHTML: 'the aweber code here', type: 'text/javascript', charset: 'utf-8'}
    ]
}

You can also save the code in an external js file and reference it in the nuxt.config.json file.


head: {
script: [
      { hid: 'the aweber code here', src: 'file-path', defer: true}
    ]
}

Back to our *FooterBlog.vue* component.

Let's start by adding the html provided by AWeber to the template. We'll do it inside a new div I'm going to reference as aWeberScriptHolder (you can call it whatever you like).

Refs are Vue properties used to indicate a reference to HTML elements or child elements in the template so that we can access its DOM node. We need that because we want to manipulate this element in order to append to it the JavaScript code provided by aWeber.

  <div ref="aWeberScriptHolder">
  <form
    method="post"
    class="af-form-wrapper"
    accept-charset="UTF-8"
    action="xxxx"
  >
   <!-- Aweber form html  -->
  </form>
  </div>

Now, to add the js code we'll create a method in the component to append the Aweber code to the *aWeberScriptHlder* element:

This would be the code:


  createAweberScript() {
      // There are two scripts so, create 2 script elements
      let aWeberScript = document.createElement("script");
      let aWeberScript2 = document.createElement("script");
  // Set attributes
  aWeberScript.setAttribute(&quot;type&quot;, &quot;text/javascript&quot;);
  aWeberScript.setAttribute(&quot;language&quot;, &quot;javascript&quot;);

  aWeberScript2.setAttribute(&quot;type&quot;, &quot;text/javascript&quot;);
  aWeberScript2.setAttribute(&quot;language&quot;, &quot;javascript&quot;);

  // Create the inline scripts by creating a text node with the javascript provided by Aweber
  let inlineScript = document.createTextNode(` (function() {
         // aweber's js code
      })();`);

  let inlineScript2 = document.createTextNode(` (function() {
        // the other aweber's js code
    })();`);


  // Append the inline javascript to the script element we created before
  aWeberScript.appendChild(inlineScript);
  aWeberScript2.appendChild(inlineScript2);

  //Finally append the script to the html element 
  this.$refs.aWeberScriptHolder.appendChild(aWeberScript);
  this.$refs.aWeberScriptHolder.appendChild(aWeberScript2);


}


Now, call this method in the mounted() hook, because we need to wait until the DOM is accessible.

mounted() {
  this.createAweberScript();
},

Add any needed css and you are done.

The blog's index page - Adding a filter and a search box

Here I want to show the post's list and a way to filtering them by tag. I'm not going to create a side bar or categories or anything like that: just a post list, a search box(to search by title) and a filter to filter by tag.

To be able to filter the posts I'm going to add tags to them. So, let's add a new attribute to the posts:

static site generator blog

Now, we can show the post's tags.

vue nuxt markdown

Let's create the search by title functionality.

First thing we need is the search box:

    <div>
      <div class="search-box">
          <input type="text" v-model="search" placeholder="Search title.."/>
          <label>Search title:</label>
       </div>
    </div>

Don’t forgut to add the binding to the search property v-model="search" 

Then, we will add a computed property to do the search, which is actually a filtering, and store the filtered results.

  computed: {
        filteredPosts() {
           return this.posts.filter(post => {
              return post.attributes.title.toLowerCase().includes(this.search.toLowerCase())
           })
        }
     }

And one last thing, make sure your are binding the posts list to the filteredPosts computed property.

search posts

Pretty easy, right?

The filter by tag functionality will be very similar but with one addition: I want to allow the user to select from a set of predefined tags, the ones I'm using in the posts.

The first thing we need is a list of all the tags. We can get them from the posts, but as I plan to have an small number of tags, I'm going to hardcode them.

data: function () {
      return {
        search: '',
        tagsES: ['uno', 'dos', 'tres'],
        tagsEN: ['one', 'two', 'three'],
        selectedTags:[],
      }
    },

Let's now create some checkboxes to allow the user to select the tags. Here is the html code, using the checkboxes component form vue-boostrap.


    <b-form-group label="Select by tag:">
              <b-form-checkbox-group
                id="tags-filter"
                v-model="selectedTags"
                :options="tagsEN"
                name="tags"
                v-on:change="clearSearch"
              ></b-form-checkbox-group>
     </b-form-group>

As we are allowing to filter by any of the two criteria (title —from the search box— or tags), we have to modify the filteredPosts cpmputed property and add a new data property to hold the selected tags.

data: function () {
    return {
      search: '',
      tagsES: ['uno', 'dos', 'tres'],
      tagsEN: ['one', 'two', 'three'],
      selectedTags:[]
    }
  },

computed: {     filteredPosts() {        const fromTagFilter = this.selectedTags.length > 0 ? true : false         return this.posts.filter(post => {               return  fromTagFilter ? post.attributes.tags.some(t => this.selectedTags.includes(t))               : post.attributes.title.toLowerCase().includes(this.search.toLowerCase())         })     } },

The code is pretty straight forward, I'm using conditionally filtering by title or tag depending on what triggered the filter, the search box or the tags, what we know by checking if there are tags selected.

Last, I want to make sure that one one filter is used the other one is reset. For that I'm adding a change event to the filters.

Those are the methods triggered by the change events:

 methods: {
      clearTags() {
        this.selectedTags = [];
      },
      clearSearch() {
        this.search ='';
      },
}

search filter nuxt

This is what you should see: markdown nuxt blog

The tag filter and the search box functionality are done. If you want to add the style them as you can see in this blog, feel free to get the code from GitHub

The Blog's Posts- Syntax Highlighting With Nuxt and Markdown

There is nothing special in how I styled the post's pages. As always, you can get the code form the GitHub repo

There is just one thing I want to talk about, and it how to highlight code when using Markdown. It is very difficult to me reading code that has not been syntax-highlighted. This is why finding a way to render the pieces of code in my posts in what for me it a proper way was crucial.

After doing some research I decided to go with Prismjs and Markdown-it

Markdown-it is a Markdown parser which converts markdown text to HTML and allows us to easily adds syntax extensions & sugar (like code highlighing), actually it is the default parser that FrontMatter uses. So we don’t have to add it. (You can read more here)

We have to install Prismjs, though

npm install prismjs

PrismaJs es the actual syntax highlighter.

Let's open _slug.vue and make the required changes to be able to use Markdown-it and Prismjs.

Let's stat by loading Mardown-it using require(), to which we'll pass a configuration object. For more info about the configuration options, you can visit the Markdown-it page


  const md = require('markdown-it')({
    html: true,
    linkify: true,
    typographer: true
  })

Then, in the asyncData() method, we need to replace the content property in the return object with the code below, where we apply the render() method provided by Markdowit to render the markdown string into html.

 content: md.render(postContent.html)
syntax hightlight nuxt markdown

Last thing we have to do is to setup prism.js. To do it, create a prism.js plugin, which only needs to import and export prism to make it available in our app, and import any prism theme you want to use to style the highlighting.

import Prism from 'prismjs'
import 'prismjs/themes/prism-tomorrow.css' // Or whatever theme you like. Find more in prism website

export default Prism

Register it in nuxt.config.js

  plugins: [
    ...
    '~/plugins/prism'
  ],

And we are ready to use it in our _slug.vue file.

Import it.

import Prism from '../../plugins/prism';

And add the Prism.highlightAll() method to the mounted() hook.


  mounted() {
    Prism.highlightAll()
  }

Now to highlight code in your Markdown files, use

code here

or if you want to apply the highlighting specially defined by the theme for a specific language:


code here

markdown code hightlight

Adding pagination to our blog

If your blog has a lot of content, you might want to add pagination to the blog's index page. I don't need it yet because I have very few posts in my blog, but as I plan to keep adding content , I'm going to create a pagination component and have it ready to be used when I need it.

You can add the pagination functionality directly in the blog's home page; however, I prefer to create a component so that I will be able to reuse it if I need to add pagination to other pages in the future.

The goal is to have something like this:

vue nuxt markdown

Let's do it.

The number of buttons will be the total number of posts divided by the posts we want to show per page. Those two variables, along with the current pages are going to be passed from the parent component where we place the Pagination component.

So, lets' create the component and define those props:

<script>
export default {
props: {
    itemsPerPage: {
      type: Number,
      required: false,
      default: 10
    },
    totalItems: {
      type: Number,
      required: true
    },
    currentPage: {
      type: Number,
      required: true
    }
  },

} </script>

And let's also create a computed property to calculate the total number of pages

computed: {
    totalPages(){
      return this.totalItems/this.itemsPerPage
    },
}

This value will be the total number of page buttons that will appear in the pagination component, but we might want to limit the number of button to a smaller number, let's say 3. For that let's create another prop maxButtons.


 maxButtons: {
    type: Number,
    required: false,
    default: 10
}

To render the pagination buttons, we'll use a computed property that returns an array containing all the buttons. To create that array we will use a for loop that will add the buttons to the array. The text for each one of those buttons will be the page number, which will be created with the for loop's counter (i) This counter initial value will depend on the current page. It will be:

  • 1 if the current page is the first one,
  • the value of the total number of pages - the maximum number of buttons we want to show if we are at the last page
  • the current page value - 1 for the rest of pages.

Let's first create a computed property to calculate the value of the first page button:

   firstPageButton() {
      return this.currentPage === 1 ? 1
       : this.currentPage === this.totalPages ?
         this.totalPages - this.maxButtons : this.currentPage - 1;
    }

And then, the *for* loop to create the array of buttons. We want the number of buttons to be not larger than the max number of buttons we want to show and not larger than the total number of pages. Let's keep this is mind when creating the for loop
   pageButtons() {
      const buttons = [];
      for (let i = this.firstPageButton; i <= Math.min(this.firstPageButton + this.maxButtons - 1, this.totalPages); i+= 1 ) {
        buttons.push({
          text: i,
        });
      }
      return buttons;
    }

now, the html to show the buttons:

<template>
  <ul>
    <li>
      <button>
        <font-awesome-icon :icon="['fas', 'angle-double-left']"/>
      </button>
    </li>
    <li>
      <button
      >
         <font-awesome-icon :icon="['fas', 'angle-left']"/>
      </button>
    </li>
    <li v-for="button in pageButtons" :key="button.text">
      <button>
        {{ button.text }}
      </button>
    </li>
    <li>
      <button>
        <font-awesome-icon :icon="['fas', 'angle-right']"/>
      </button>
    </li>
    <li>
      <button>
        <font-awesome-icon :icon="['fas', 'angle-double-right']"/>
      </button>
    </li>
  </ul>
</template>

I want to disable the button that corresponds to the current page, so let's add the disabled attribute to the buttons and bind it to a property that will set it to true or false. We'll add this property to the button object when creating it in the pageButtons() computed prop.

  buttons.push({
    text: i,
    isDisabled: i === this.currentPage
  });

This is for the numbered buttons. Now, for the << and < buttons, we want to disable them if the current page is the fist one. Let's create a computed property to do so, and bind the disabled attribute to it.

   isFirstPage() {
      return this.currentPage === 1;
    }
 <li>
     <button
        @click="onClickFirstPage"
         :disabled="isFirstPage"
      >
        <font-awesome-icon :icon="['fas', 'angle-double-left']"/>
      </button>
    </li>
    <li>
      <button
        @click="onClickPreviousPage"
        :disabled="isFirstPage"
      >
         <font-awesome-icon :icon="['fas', 'angle-left']"/>
      </button>
    </li>

And the same for the last and next buttons.

 isLastPage() {
      return this.currentPage === this.totalPages;
  }

 <li>
      <button
        @click="onClickNextPage"
         :disabled="isLastPage"
      >
        <font-awesome-icon :icon="['fas', 'angle-right']"/>
      </button>
    </li>
&lt;li&gt;
  &lt;button
    @click=&quot;onClickLastPage&quot;
     :disabled=&quot;isLastPage&quot;
  &gt;
    &lt;font-awesome-icon :icon=&quot;['fas', 'angle-double-right']&quot;/&gt;
  &lt;/button&gt;
&lt;/li&gt;


Now we need to add functionality to the buttons. What will happen when we click on them?

We have to emit an event to tell the parent component that the button to show a certain page's items has been clicked. We'll do so by adding a on click directive to the buttons to fire the corresponding methods.

Let's create those methods:

 methods: {
    onClickFirstPage() {
      this.$emit('pagebuttonclicked', 1);
    },
onClickPreviousPage() {
  this.$emit('pagebuttonclicked', this.currentPage - 1);
},

onClickMiddlePage(pageButton) {
  this.$emit('pagebuttonclicked', pageButton);
},

onClickNextPage() {
  this.$emit('pagebuttonclicked', this.currentPage + 1);
},

onClickLastPage() {
  this.$emit('pagebuttonclicked', this.totalPages);
}

}

We are emitting an event (pagebuttonclicked) and passing in the button that has been clicked.

Let's add the event to the buttons:

<ul id="pagination">
    <li>
      <button
        @click="onClickFirstPage"
         :disabled="isFirstPage"
      >
        <font-awesome-icon :icon="['fas', 'angle-double-left']"/>
      </button>
    </li>
&lt;li&gt;
  &lt;button
    @click=&quot;onClickPreviousPage&quot;
    :disabled=&quot;isFirstPage&quot;
  &gt;
     &lt;font-awesome-icon :icon=&quot;['fas', 'angle-left']&quot;/&gt;
  &lt;/button&gt;
&lt;/li&gt;

&lt;li v-for=&quot;button in pageButtons&quot; :key=&quot;button.text&quot;&gt;
  &lt;button
    @click=&quot;onClickMiddlePage(button.text)&quot;
     :disabled=&quot;button.isDisabled&quot;
  &gt;
    {{ button.text }}
  &lt;/button&gt;
&lt;/li&gt;

&lt;li&gt;
  &lt;button
    @click=&quot;onClickNextPage&quot;
     :disabled=&quot;isLastPage&quot;
  &gt;
    &lt;font-awesome-icon :icon=&quot;['fas', 'angle-right']&quot;/&gt;
  &lt;/button&gt;
&lt;/li&gt;

&lt;li&gt;
  &lt;button
    @click=&quot;onClickLastPage&quot;
     :disabled=&quot;isLastPage&quot;
  &gt;
    &lt;font-awesome-icon :icon=&quot;['fas', 'angle-double-right']&quot;/&gt;
  &lt;/button&gt;
&lt;/li&gt;

</ul>


And that's it. We have created our pagination component. You might want to add some style:


<style lang="scss">
@import "../assets/styles/main.scss";
#pagination {
  list-style-type: none;
  margin: 60px 0;
  text-align: center;
  li{
      display: inline-block;
      margin:0;
      button {
        background: $white;
        border: none;
        color: $grey-dark;
        &:disabled {
           opacity: 0.6;
        }
      }
      }
}
</style>

Now, our pagination component is ready to be used in out blog's index page. Let's do it:

 <pagination
  :max-buttons="3"
  :total-items="totalItems"
  :items-per-page="perPage"
  :current-page="currentPage"
  @pagebuttonclicked="onPageButtonClicked"
/>

Remember to import and register the Pagination component:
  import Pagination from "~/components/Pagination.vue";
  export default {
    layout: 'blog',
    components: {
      Blob,
      Pagination,
    },
}


When the user clicks a button in the component, two different things happen:

  • We pass the required values to create the pagination component(max-number of buttons, the total items, and the current page)
  • The pagebuttonclicked event is emitted and, as we are listening to it in the parent component, when it is fired, we are notified telling us which button has been clicked. And we respond to that event triggering the onPageButtonClicked() method, which sets the vale of the current page property in blog's index page. So we are ready to do the corresponding filtering to show only the pots that belong to that page.

Let's define the totalItems, perPage, and currentPage properties, and create the onPageButtonClicked() method.

 data: function () {
      return {
       ...
        currentPage: 1,
        perPage: 2,
  }

the value of the totalItems property will be set after we have filtered the posts, in the filteredPosts() computed property.

   computed: {
        filteredPosts() {
           const fromTagFilter = this.selectedTags.length > 0 ? true : false
           const filtered = this.posts.filter(post => {
              return fromTagFilter ? post.attributes.tags.some(t => this.selectedTags.includes(t))
              : post.attributes.title.toLowerCase().includes(this.search.toLowerCase())
           })
       this.totalItems = filtered.length;  //HERE
    },
},


And one last thing: slicing the array of filtered posts to show only the ones that correspond to the current page. We'll do it in the filteredPosts() computed property:

  computed: {
    filteredPosts() {
      ...
    //paginate
    let from = (this.currentPage * this.perPage) - this.perPage;
    let to = (this.currentPage * this.perPage);
    return  filtered.slice(from, to)
},

},


And here is the final result:

vue nuxt blog pagination

And the only remaining thing is to build the production files and deploy them to Netlify. Let's see how to doit in Part 5