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

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 21 2020


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

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

First Thoughts and The Nuxt-Markdown-FrontMatter-Netlify Stack

A couple of months ago, I decided to revamp my old website and I wanted to take the opportunity to learn about static site generators. After doing some research, I decided to go with the Vue-Nuxt-Markdown stack, and host my new site on Netlify

I considered using a headless CMS or even creating my own API (which will be by natural approach due to my strong backend developer background) but the idea of going totally serverless was very attractive to me, so I decided to use Markdown, which I was aware of but had never use it before.

My concern about using Markdown was that I wanted to integrate the content written in Markdown into a Vue component. I knew I would need some Webpack loader able to render .md files as Vue templates. After a little research I decided to use Frontmatter by Kengo Hamasaki .

Another thing to consider was that my website should be in two languages (Spanish and English).

My first though was to use Vue-i18n for the internationalization, but as I was planning to use Nuxt, I did a little more research and found two drawbacks about using Vuei18n:

  • I would have to add it as a plugin, meaning I would have to create the plugin, which is not a big deal but it's something I would have to do, and
  • In order to add the language to the URL, what I wanted to do in my blog posts, I would have to do it manually.

The good news is that I found Nuxt-i18n, which is built on top of Vue-i18n and solves those two problems.

If I didn’t needed to add the languages to the routes, I would probably stick with Vue-i18n, but as I wanted to do it, Nuxt-i18n was a better choice because it provides a function to do this automatically, which is very handy.

For the actual translation on the content, I decided to use a localization file to store the pieces of text for the home page, the buttons, and the menu items and to create two separate pages, one for each language, for the blog posts.

A note about Markdown I'm assuming you have a basic knowledge about Markdown, but if you don't, don't worry. Markdown is just a simple way to style content on the web. You can, for example, format text as bold or italics, create lists and headings, add images... You might be thinking 'like with HTML'... Yes, but keep in mind that Markdown is not a replacement for HTML. It only covers a very small subset of HTML tags. It is mostly plain text with a few non-alphabetic characters thrown in, like # or * to add the formatting.

You can learn more about Markdown and its syntax at the official page of its creator, Daring Fireball or in this guide .

What are static sites and static site generators?

You probably are familiar with the distinction between static sites and dynamic sites, but let me explain the difference in case you need a refresher.

A static website consists of a series of HTML files, one for each page of your website. The content of each page is fixed (static). The content you add when you code your html page is the content that will be displayed.

A dynamic website, however, uses server technologies (such as Java, Nodejs, c#, PHP ) to dynamically build a webpage on the fly when a user visits the corresponding URL, getting the data it needs from a database.

Let's think on a blog, for example. You don't create a HTML page for each post and publish them as independent pages in your website; instead what you would usually do is create a dynamic site that contains the 'template' for building a blog post, and when somebody browses the URL of one specific post, the page shown in the browser is built on the fly by filling the 'template' with data from a database. This data is the title, content, and other info of the post, which was stored into the database when you created the post using your CMS.

This dynamic site approach works perfectly to build a blog, but if you want to use a Javascript framework like Vue, React or Angular in the front, you will face a big issue if you are concern about SEO, and we usually are.

The problem with Javascript frameworks and SEO

If we create a web app using a JavaScript framework, when a page is loaded in the browser, it is basically empty. The content form the database will be loaded later, after the JavaScript code is executed in the browser. This means that the Search Engines will probably miss the content when they visit your page, which will affect your ranking.

Here is where Static Site Generators come into rescue. Nuxt.js is one of them.

With a Static Site Generator, the content is generated upfront. When we build our app, Nuxt will process our Javascript files and generate the corresponding html pages —if you have 50 posts, 50 html pages— and those static HTML pages is what will be served when a visitor browse our blog.

Server Site Rendering in another solution for the same rendering problem. In that case, the static pages will be generated no when we build the app but in the server. The server calls for the content and renders the static pages when it gets the content, before serving them.

For my blog (and for this posts) I'm going to focus in Static Site Generators because I want to take advantage of one characteristic they have: you don't need a server if you store your content in Markdown files instead of in a database. And this is what I'm going to do.

Let's summarize some of the advantages our site will have:

  • We don’t need a server
  • We can host our website free on sites like Netlify or Github Pages
  • Blazing fast site
  • SEO friendly

Brief Intro to Nuxt

Nuxt.js is a framework built on top of Vue. It adds additional features to Vue, like:

  • Static Site Generation
  • Server Site Rendering
  • Adding metatags to our pages
  • VueRouter out of the box
  • Vuex support
  • Automatic Route Handling: Nuxt.js will automatically create the routes based on the files and folders in the pages directory
  • Code-Splitting to make your app render faster by breaking the Javascript containing our app into multiple Javascript files and serving only the code needed for each page
  • Production ready configuration to make your deploy to production easier

Structure of a Nuxt Project

The structure of a Nuxt project will be a little different from that of a Vue project

static site generator with nuxt
  • Assets: Same in both. We store here the uncompiled static assets that will be processed by Webpack, such as css or sass files.
  • Components: To place the components (.vue files). Same in both with a slight difference: in Nuxt there are 3 different types of component (components, pages, and layouts) You place here the components that are meant to be part of other components (pages or layouts), for example, a navigation bar.
  • Layouts: Layouts are a Nuxt special type of components. They are structural components that contain elements and nested components used in all or a set of pages. Let's say all your pages have a navbar and a footer, you create a layout containing those components and create all your pages based on that layout. You can have more than one layout. You can, for example, create an additional one containing a sidebar to use it in your blog.
  • Middleware: You place here your middleware functions. Middleware functions are functions that are executed before rendering all routes or a group or routes. For example, a function to check if a user is signed in before rendering a page.
  • Pages: Pages are another special type of Nuxt component, actually the most important components of a Nuxt application. Nuxt creates routes from all the components in that folder. Every Page component is a Vue component but Nuxt adds special attributes and functions to it.
  • Plugins: The plugins directory contains the JavaScript code that you want to run before instantiating the root Vue.js Application. You can add here vue libraries or packages or create your own plugins.
  • Static: Like in Vue, you place here the static assets (the ones that don't need to be processed by Webpack)
  • Store. Contains the Vuex store. Feel free to delete it if you are not using Vuex. (I'm not going to use it).

As you can see, in a Nuxt project there is no App.vue nor Main.js files. The root module for your Nuxt app (App.vue) will be a layout. There you will find the <nuxt/> element, where the page component will be mounted.

There is no main.js file. All the configuration will be done in the nuxtconfig.json file.

If all this doesn't make a lot of sense to you yet, don't worry, as we build our application all those concepts will become clearer.

A note about images in the Markdown files: I will place the images that I want to add to my Markdown files in the static folder. This way they will not be processed by Webpack and will be available when the post in rendered.

Enough talk. Now you are familiar with Static Site Generators and with Nuxt, so it's time to get our hands dirty.

First Steps to create our Nuxt app

Nuxt provides a scaffolding command to help us create the initial files and folder structure of our app.

I'm going to be using Npm, but feel free to use Yarn if you prefer so.

Make sure you are using Npm 5.2.0 or higher in order to have Npx installed and run:

npx create-nuxt-app <project-name>

Or, if you are using Yarn:

yarn create nuxt-app <project-name>

Those are the options I have chosen:

nuxt markdown blog

Axios is used to make http requests, which you will probably not need, but I like to add it in case I need to fetch data form any external API.

EsLint is to help us write good, consistent code. If you don't like linters, feel free to skip it.

To run the project, go to the directory named as your project: cd sonia-web in my case, and run

npm install to install all the dependencies, and then npm run dev to start the app.

In your browser, go to http://localhost:3000, and voilà…

npm run dev

Back to VS Code, in the terminal, you can see that the scaffolding tool has also created a git repository.

I don't want to work on the master branch, so I'm going to create a new one:

git checkout -b initial

Adding our first components

My site will have one single page with my bio, contact info, and services (the home page) and then a blog.

Let's create the corresponding components —pages, in that case.

In the pages folder, create a subfolder called blog. The index.vue page is the one I will use for the rest of my content, for my home page. If you want to create a site with additional pages, create them here.

nuxt pages

Pages are essentially components with extra features. If we take a look at the index page, we can study the anatomy of a vue component —a template with the html, a script section with the Javascript code, and a style section for the css.

npm run dev

As you might know, in Vue we have a root component within which all additional components are rendered. In Nuxt instead of a root component, we use a Layout. In the layouts folder, there is a default.vue component created out of the box. This is the default layout for your pages. You can see that it has a <nuxt /> element. Here is where the pages components are rendered.

nuxt root component

We can have many different layouts. But for now, we only need this one.

In you have common elements that will be used across all your pages, like a navigation bar, you can add them to the layout. This element will be created as a component, and then added to the layout.

Let's do it.

In the components folder, I'm going to create a NavBar component. By convention, component names are capitalized.

Let's for now create a hardcoded navigation bar. But first, we need to do a little setup because I want to use sass. If you are using plain css, you can skip this step.

Using Sass in a Nuxt app

We need to install the Sass Webpack loader and the Sass Module (style-resources-module)

npm install --save-dev node-sass sass-loader
npm install @nuxtjs/style-resources

In the nuxtconfig.json file, register the modules as a dev module (in buidModules), and add the settings in the styleResources object. We need to tell Nuxt which are the files we want to be processed by the Sass module, and the syntax we are using (sccs or sass)

  buildModules: [
  styleResources: {
    // your settings here
    scss: [
sass in nuxt

This configuration is for global styles. To use Sass in a component, we'll add the lang="scss" attribute to the style tag of the component.


The NavBar Component

Let's create our NavBar component inside the components directory. Create a NavBar.vue file and add a template, script, and style sections with the code for your navigation bar. Remember to add the lang="scss" attribute to the <style> tag. I have also added the scope attribute because I want the scope of this css code to be limited to the NavBar component.

Feel free to add your own html and css, but if you want to follow along with me, here is the code I'm using for the NavBar component:

<nav class="navbar navbar navbar-expand-lg navbar-dark default-color fixed-top">
      <button class="navbar-toggler" type="button"
        aria-expanded="false" aria-label="Toggle navigation"
        v-bind:class=" { 'navbarOpen': show }"
        <span class="navbar-toggler-icon"></span>
      </button >
  <div class="collapse navbar-collapse"
         v-bind:class="{ 'show': show }">
        <ul class="navbar-nav mr-auto">
          <li class="nav-item active">
            <a class="nav-link" href="#">Home
              <span class="sr-only">(current)</span>
          <li class="nav-item">
            <a class="nav-link" href="#">About Me</a>
          <li class="nav-item">
            <a class="nav-link" href="#">Services</a>
          <li class="nav-item">
            <a class="nav-link" href="#">Portfolio</a>
          <li class="nav-item">
            <a class="nav-link" href="#">Contact</a>
        <ul class="navbar-nav ml-auto nav-flex-icons">
          <li class="nav-item">
            <a class="nav-link waves-effect waves-light">
          <li class="nav-item">
            <a class="nav-link waves-effect waves-light">
          <li class="nav-item">
            <a class="nav-link waves-effect waves-light">
           <li class="nav-item">
            <a class="nav-link waves-effect waves-light">

<script>   export default { data() {       return {         show: false       }     },     methods: {       toggleNavbar() { = !;       }     }



<style scoped lang=“scss”>   @import ‘…/assets/styles/main.scss’;   nav {      background-color:$black;   } </style>

I'm using the code from Bootstrap responsive navbar, but in order to make it work the way I wanted, I have tweaked it a little: I have removed the data-toggle="collapse", data-target="XXX", and aria-controls="xxx" attributes because I'm adding the method to open/close the menu manually.

boostrap responsive menu nuxt

You can instead use the component from Vue-boostrap if you prefer so.

We'll come back later and add the links and anything else we need. For now, let's just give some shape to our website.

As you can see in the style section, I'm importing here a main.sccs file, lets add it. Actually we have to add some.

I'm not covering css and html in this tutorial, so I'm not going to get into details. Just the big picture:

I have created some files where I'm starting to define variables, and then importing all them in the _variables.scss file, which is the one I'll then import into main.scss.

You can get the _variables.sccs file from the GitHub repo

In main.scss I'll add the global css that I want to be available across all the components. You can also add it in the default layout, but I prefer to use a css (sccs in that case) file. So, I'm going to remove the css in the layout component and paste it the main.scss file. We'll modify that css later, but for now, let's leave it as it is:

@import '_variables';

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; } *, *:before, *:after {   box-sizing: border-box;   margin: 0; } .button–green {   display: inline-block;   border-radius: 4px;   border: 1px solid #3b8070;   color: #3b8070;   text-decoration: none;   padding: 10px 30px; } .button–green:hover {   color: #fff;   background-color: #3b8070; } .button–grey {   display: inline-block;   border-radius: 4px;   border: 1px solid #35495e;   color: #35495e;   text-decoration: none;   padding: 10px 30px;   margin-left: 15px; } .button–grey:hover {   color: #fff;   background-color: #35495e; }

Using FontAwesome with Nuxt

There is one more thing we need to do. I want to use FontAwesome. For that, we have to install nuxt-fontaswose .

npm i nuxt-fontawesome

Then we need to install the specific icons sets we want to use:

npm install --save @fortawesome/free-solid-svg-icons 
npm install --save @fortawesome/free-brands-svg-icons

And register font-awsome as a module in nuxtconfig.js modules property, specifying the icons sets we wan to use.

  ['nuxt-fontawesome', {
      imports: [
        //import whole set
          set: '@fortawesome/free-solid-svg-icons',
          icons: ['fas']
           icons: ['fab']

Now, we are ready to use FontAwsome in our html.

Let's add some Social Media links t our NavBar:

        <ul class="navbar-nav ml-auto nav-flex-icons">
          <li class="nav-item">
            <a class="nav-link waves-effect waves-light">
          <li class="nav-item">
            <a class="nav-link waves-effect waves-light">
              <font-awesome-icon :icon="['fab', 'linkedin']"/>
          <li class="nav-item">
            <a class="nav-link waves-effect waves-light">
              <font-awesome-icon :icon="['fab', 'youtube']"/>
           <li class="nav-item">
            <a class="nav-link waves-effect waves-light">
             <font-awesome-icon :icon="['fab', 'twitter']"/>

Add the NavBar child components to the Layout

Our NavBar component is ready to be used. We just need to add it to the default layout. To do so, import the NavBar component and add it to the components object. Then, place it the template, just above because we want it to be at the top of each page.

    // add the Navbar component to the layout component
    <nuxt />

<script> // import and register it as a component    import Navbar from ‘@/components/NavBar’

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

<style lang="scss"> </style>

npm run dev

And here is the navigation bar.

boostrap responsive menu nuxt

Let's also create a Footer component.

The Footer Component

You already now how to do it. So, let's go.

Create the Footer.vue file in the components folder and add the code.

This is the html I'm adding to the template. It includes a "go to top" button that calls to the backToTop function, defined in the javascript.

  <footer class="container-fluid">
    <div class="row align-items-center">
      <div class="col-sm text-left ">
        Made with Nuxt
      <div class="col-sm text-center ">
        ©{{ new Date().getFullYear() }} Copyright:
      <div class="col-sm text-right">
            <a id="go-top" v-if="isVisible" @click="backToTop">
              <font-awesome-icon :icon="['fas', 'angle-up']"/>

I'm not going to get into details here. Just notice that there is another function, onScroll, that is called in the mounted life cycle hook. This function checks when the user has scrolled to the bottom of the page, actually to the bottom-200px, in order to show the back to top button.

  export default {
    data: function() {
      return {
        isVisible: true

methods: {       onScroll () {         window.onscroll = () => {           let atBottom = Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop) + window.innerHeight > document.documentElement.offsetHeight -200;           if (atBottom) {             this.isVisible = true;           } else {             this.isVisible = false;           }         }       },       backToTop () {           window.scroll({             top: 0,             left: 0,             behavior: ‘smooth’           });       }     },

mounted () {       this.onScroll()     },   } </script>

And the css. Nothing special here.

<style scoped lang = "scss" >
   @import '../assets/styles/main.scss';
   footer {
     min-height: 50px;

& > div{          height: 50px;      }    }   #go-top {     position: fixed;     width: 45px;     height: 45px;     font-size: 40px;     text-align: center;     line-height: 45px;     display: block;     right: 30px;     bottom: 5px;     border: none;     opacity: 1;     color:$white;      } </style>

Import the Footer component into the layout, register it as a component andd add it to the template, this time below the mounting point.

nested components

Later I will come back the home page and add more sections, but now let's move to creating the blog, which is probably why you are here.

Creating the serverless blog

As I said I'm going to use Markdown to write the content of my blog. I'm going to create the files for each post in Markdown and store them somewhere in my app directory, because I don’t want to use a server. Let's create folder called content for that purpose, and add a subdirectory called blog. As my website will be bilingual, I'm going to create one folder for each language in the blog directory

nuxt markdown blog

Setting up Frontmatter

As I said before, I want to be able to integrate the Markdown content into a Vue component, and to do it I'm going to use Frontmatter

Install it:

npm install frontmatter-markdown-loader

And add the required configuration to the confignuxt.js file. As Fontamatter is a Webpack loader, we need to extend Nuxt's Webpack configuration. This is done in the build property, using the extend option method:

  ** Build configuration
  build: {
    ** You can extend webpack config here
    extend (config, ctx) {
            test: /\.md$/,
            include: path.resolve(__dirname, "content"),
            loader: "frontmatter-markdown-loader",

This configuration tells Webpack to use a custom loader, frontmatter-markdown-loader, to handle the files with the .md extension (markdown) that are in the content folder. This loader will transform our Markdown code to json so that we can process and use it in our Nuxt project.

As we are using path, we need to import the path module at the top of nuxtconfig.js

const path = require('path');

Let's create a couple of blog posts using Markdown.

Here you can get the sample ones I have created.

nuxt frontmatter markdown

FontMatter will then convert the content of the .md file into an object similar to the one below and make it available in our vue component.

    attributes: {
		name: 'first post'
		title:  'My first post in Markdown'
		date: Jan 17 2020
		slug: 'first-post'
		description:  'My first post in Markdown'
    body: 'My Firts….',
    bodyBegin: 6,
    frontmatter: 'title: My first post in Markdown\'n\ndescription: My first post in Markdown'

Dynamic pages and dynamic routes

Now we have to be able to show those posts when the user browses a certain URL. The url structure I want for my blog is http://localhost3000/blog/post-slug.

Nuxt creates automatically the routes for the static pages, but here we want to create dynamic routes. We are not creating a page for each post but a dynamic page that will build its content from the associated Markdown file. We need to tell which route correspond to each Markdown file.

Routes are associated with pages, so we need to create our dynamic page and route inside the pages folder.

Create a new folder called blog inside the pages directory, and a dynamic page inside it.

To make our page dynamic, we need to prefix it with underscore and give it the name of the property we want to use to create the dynamic route, in our case the slug property (it can be also the post id, for example).

dynamic routes nuxt page

Thar _slug.vue page is a vue component, as any other. So we need to create there the template to display our post content, the javascript, and the css

In the javascript, I'm importing the markdown file and returning the content and any other data I want to use. To do so I store the attributes from the markdown in a constant and pass the ones I want to return to a new object, the object I m going to return.

For now lets just hardcode the language folder in the path to the md file.

        const postContent = await import(`'~/content/blog/En/${params.slug}.md`);

Paste the code below in your _slug.vue file and run your application. I'm going to explain what the code does but first I want you to run the code and open Web Dev Tools Console or VS Code Terminal.

    <div class="post-title">
       <h1>TITLE: {{title}}</h1>
    <div class="content" v-html="content"></div>
  export default {
    async asyncData({ params }) {
      try {
        // import the markdown file
        // markdown will convert the file content into an object.
        const postContent = await import(`~/content/blog/EN/${params.slug}.md`);
        // that object has an "attributes" property that stores an object with all the attributes defined in th markdown file
        const attr = postContent.attributes;
        // inspect what we get in the console
        // This is want we want to make available in our template
         return {
            name: params.slug,
            title: attr.title,
            description: attr.description,
            content: postContent.html,
      } catch(err) {
        return false

In the console you will see the object that markdown has passed to our component.

markdown post attributes

As you can see, it has the attributes object, containing the attributes we defined in the .md file, and a html property containing a concatenation of strings containing the html generated form the markdown content.

Let's review our code. It's pretty straight forward, but I have added some comments to explain what it does in case you need a little help.

Notice that we are using AsyncData() to fetch the content of the markdown file. AsyncData() is a method that Nuxt.js provides to fetch the data in the server while Data() fetches the data on the client.

It returns an object, as data() does, that will be merged with the data from data() method.

One more thing to keep in mind when working with AsyncData() is that as it runs before the component is initializes, we can't get access to the component using this inside AsyncData(). This is why Nuxt.js provides a context object where you can access plenty of information about the component, such as the route and its params.

We can access the entire context object by passing it to the asynData(), like this

asyncData (context) { 

Or we can pass only the properties we need:

asyncData ({ params }) {

In the first case, to access the route parameters we'll do something like:

asyncData (context) { 
const parameters= context.params;

In the second:

asyncData ({ params }) {
const parameters= params;
static site with nuxt and markdown

We'll come here later and finish this single post page, but first let's create a page to display the post list.

The Blog's home page

It will also be a page (because we want to associate it to a route) So let's add an additional .vue file to pages/blog directory, index.vue. It will be our blog's home page.

nuxt blog home page

In the blog's home page I want to show a list of all the posts in my blog. We can paginate them later if we have a lot of them. But for now, let's just get all the posts.

To do it, we have to retrieve all the Markdown files in out blog directory. Webpack provides a handy functionality to do so, the require.context() method, which allows you to get all modules that match a condition.

This is the syntax:

require.context(directory, useSubdirectories = false, regExp = /^\.\//)

It takes three arguments: a directory (string) where to search, a flag indicating whether subdirectories should be searched too, and a regular expression to match files against.

 const resolve = require.context("~/content/blog/EN/", true, /\.md$/)

require.context does not returns the content of the files directly. It returns a function to which we can require the content using the keys() method.

As understanding how this works can be a little tricky, let me try to explain it.

This is the piece of code containing some console.logs that I'll use to help me explain the code.

              <li v-for="post in posts" :key="post.attributes.slug">
                   <nuxt-link :to="`/blog/${post.attributes.slug}`">
  export default {
    async asyncData() {
      let postsContent = require.context("~/content/blog/EN/", true, /\.md$/);
      console.log("****postsContent: " + postsContent);
      // require.context does not returns the content of the modules(the md files) directly.
      // It returns a function to which we can require.
      // It provides a keys() method to retrieving the contents of the context.
      const imports = postsContent.keys().map((key) => {
      console.log("****key: " + key); // ./
      console.log("***match: " + key.match(/\/(.+)\.md$/));  // /, first-post
       // with const [, slug] we are taking the second value from the array returned by
       //key.match and assigning it to the variable slug
      const [, slug] = key.match(/\/(.+)\.md$/);
       // now we can retrieve the content from the postsContent by passing the key
       console.log("*****postsContent(key) " + JSON.stringify(postsContent(key)));
       return postsContent(key);
  //sort the posts by date
  const sortedPosts = imports.sort((a, b) =&gt; ( &gt; ? 1 : -1)

console.log( imports);       return {         posts:sortedPosts       }     },   } </script>

Here you can see an screenshot of the code and the logs:

nuxt markdown blog

Now, if we browse to http://localhost:3000/blog, we'll a list a list of our posts. And if we click on a post title, we should go to the post page.

We can of course make this prettier and display more info about each post, such as the description and an image, but for now, let's let is like this and move to making the site multilingual, which will be in Part 2 of this article.

If you don't need a multilingual site, you can go to Part 3 where I'm going to create the site home page or, if you are only interested in how to build the blog, feel free to jump to Part 4