Geocoding using Mapbox, Rails 5 and NuxtJS/VueJS

<strong>Geocoding is the process of taking latitude and longitude to determine an address, or taking an address and producing latitude and longitude coordinates.</strong>

Geocoding is the process of taking latitude and longitude to determine an address, or taking an address and producing latitude and longitude coordinates.

There are numerous reasons an application needs to use a geocoder. For Now Serving, we use it during the sign up process, as well as making it easy to find nearby restaurants with a single click of ‘Find Me’.

Let’s get to coding!

Rails API

We’ll need to grab the mapbox-sdk and add it to the Gemfile.

_gem_ **'mapbox-sdk'**, **'~>2'**

Create a simple initializer to set the access token in your app (e.g config/initializers/mapbox.rb)

**_Mapbox_**.access_token = MAPBOX_ACCESS_TOKEN

Next, let’s add a couple routes:

_namespace_ **:address_search do** _get_ **'expand'**, **to**: **'expand'** _get_ **'parse'**, **to**: **'parse'
end**

And an address_search_controller.rb:

class AddressSearchController < ApplicationController
Take an addresss and return lat/lng

def expand
begin
@addresses = Mapbox::Geocoder.geocode_forward(address_params[:a]) unless address_params[:a].nil?
render template: 'address_search/result'
rescue StandardError
render json: { errors: ['Unable to perform forward geocoding'] }
end
end

Take lat/lng array and return a postal address

def parse
begin
@location = { latitude: address_params[:latitude].to_f, longitude: address_params[:longitude].to_f }
@addresses = Mapbox::Geocoder.geocode_reverse(@location)
render template: 'address_search/result'
rescue StandardError
render json: { errors: ['Unable to perform reverse geocoding'] }
end
end

private

def address_params
params.permit(:a, :latitude, :longitude)
end
end

The expand method takes the a query params and asks the geocoder service to return a latitude/longitude array. For getting an address from lat/lng we are expecting a hash like { latitude: 0, longitude: 0 }.

You may not want to render a template here but in my case I wanted to always return an array, so the best way to ensure that happened was rendering it with jbuilder one-liner:

json.array! @addresses

And a request spec:

RSpec.describe 'Address Search' do

it 'parses an address and returns latitude and longitude' do
get '/api/v1/address_search/expand', params: { a: '401 B St, San Diego CA' }
expect(response).to be_successful
end

it 'parses latitude and longitude and returns an address' do
get '/api/v1/address_search/parse', params: { longitude: 127.0, latitude: -43.64}
expect(response).to be_successful
end
end

Front End

We’re using the awesome NuxtJS framework for our UI. If you haven’t used it before, definitely give it a look. If you can’t use it, don’t worry; this code will work fine without Nuxt.

We use Vuex actions to call our back end, so we have a store for our Mapbox configuration.

export const actions = {
locate({ commit }, { longitude, latitude }) {
return this.$axios.get('/address_search/parse', { params: { longitude: longitude, latitude: latitude } })
},
coordinate({ commit }, params) {
return this.$axios.get('/address_search/expand', { params: { a: params } })
}
}

For presentation, we use vue-i18n, vue-notify, bootstrap-vue and vue-fontawesome.

<template>
<b-btn
v-b-tooltip.hover="true"
:data-state="state"
:variant="btnVariant"
:title="locationLabel"
type="button"
@click="findMe">
<font-awesome-icon v-if="state === 1" :icon="['far', 'spinner']" spin />
<font-awesome-icon v-else :icon="['far', 'location-arrow']" />
</b-btn>
</template>
<script>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'

export default {
components: {
FontAwesomeIcon
},
props: {
locationLabel: {
default: 'Find my current location',
type: String
}
},
data() {
return {
state: 0
}
},
computed: {
btnVariant() {
switch (this.state) {
case 0:
return 'outline-primary'
case 1:
return 'info'
case 2:
return 'success'
default:
return 'outline-primary'
}
}
},
methods: {
findMe() {
const vm = this
this.state = 1
if (!navigator.geolocation) {
vm.$notify({ text: vm.$t('geolocation.not_supported'), group: 'alerts' })
return
}

  function success(position) {
    const accuracy = position.coords.accuracy
    vm.$store.dispatch('mapbox/locate', {
      latitude: position.coords.latitude,
      longitude: position.coords.longitude,
      accuracy: accuracy
    })
      .then((resp) =&gt; {
        vm.state = 2
        vm.$emit('result', { name: resp.data[0].features[0].place_name, center: resp.data[0].features[0].center })
      })
      .catch(() =&gt; {
        vm.state = 0
        vm.$notify({ text: vm.$t('geolocation.not_found'), type: 'warning', group: 'alerts' })
      })
  }

  function error() {
    vm.$notify({ text: vm.$t('geolocation.not_found'), group: 'alerts', type: 'warning' })
  }

  navigator.geolocation.getCurrentPosition(success, error)
}

}
}
</script>

There’s a lot going on here, so lets break it all down.

The location button has three states; default state, active state, and a success state. The computed property handles changing out the css classes for each state.

There is also a tooltip that displays on hover to explain that the browser will ask for permission to send location information to the back end.

The findMe method is called on click. In it we have two callbacks for success and error that the browser’s built in [getCurrentPosition](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/getCurrentPosition)needs to work correctly. When the browser provides the latitude and longitude to the success callback, we can send that to the back end using the Vuex action. Once the back end response comes, the component emits a result event containing the address name and coordinates. If permission is denied, we display an error notification. Also if the browser doesn’t support location services, we notify the user of that case.

Conclusion

Congrats you have a fully implemented API for forward and reverse geocoding solution!

Originally published by Todd Baur at https://itnext.io

Learn more

☞ The Complete Ruby on Rails Developer Course

☞ Testing Ruby with RSpec: The Complete Guide

☞ How to build Instagram using Ruby on Rails

☞ Vue JS 2 - The Complete Guide (incl. Vue Router & Vuex)

☞ Nuxt JS with Laravel - Build API driven SSR Vue JS Apps

How to build a Ruby on Rails Application with Vue.js using JSX

How to build a Ruby on Rails Application with Vue.js using JSX

This is a step-by-step tutorial on how to build a Ruby on Rails application with Vue.js using JSX.

Have you ever wondered how many ways there are to build a Ruby on Rails application with VueJS?

This is the first of three articles which explain step by step how you can build a Rails application with VueJS with some advice on which technique you should use based on your needs.

Why JSX?

JSX is an extension of JavaScript. It can be used with VueJS to build components avoiding to use .vue templates.

With this approach, we can build a large and scalable frontend easily.

JSX syntax is recommended to integrate VueJS to an existing complex project or to start a project which needs a bold framework like Solidus.

Here are some of the advantages of using JSX:

  • The backend and frontend are in the same codebase
  • We can use the Rails routes (It isn’t a SPA)
  • We can share context between application sections (e.g., product page, sliding cart)
  • We can create many Vue instances for each section
  • We don’t necessarily have to build the APIs

And some of the disadvantages:

  • We can’t use .vue templates
  • With JSX Babel preset we can’t use some Vue directives like v-for, v-if, etc.
TL;DR;

You can find the code in this GitHub repository.

Branches:

  • master: Rails products catalog application without Webpack and VueJS
  • vuejs-jsx: integration of Webpack and VueJS on the Ruby on Rails application
Let’s start

We’ll start from an existing Rails application and will move it step by step to VueJS.

Clone the repository and bootstrap the project:

$ git clone https://github.com/vassalloandrea/rails-vuejs-jsx.git
$ cd rails-vuejs-jsx
$ asdf local ruby 2.5.1 # If you use asdf as version manager
$ ./bin/setup
$ bundle exec rails s
Project overview

The application is a products catalog.

The root path shows the list of the products and clicking on one of them reveals the product details. For each product you can read, add or delete the related comments.

Our goal is to move some parts of this app into VueJS components.

Start with the Vuetification

To run VueJS code in the Rails application, we need to install Webpack which is a static module bundler.

A Rails application is usually built with Sprockets to compile and serve web assets. Both libraries can live together.

Install Webpack using webpacker gem
  1. Add the webpacker gem into your Gemfile and install it
gem 'webpacker', '~> 4.x'$ bundle install
$ bundle exec rails webpacker:install

The installation command generates all the files needed to configure Webpack on Rails.

To manage all the JS dependencies we use yarn. Install Node using your favorite version manager, I usually use asdf

$ asdf install nodejs 10.16.0
$ asdf local nodejs 10.16.0

and install yarn

$ npm i -g [email protected]

To check if the project is now working with Webpack, restart the Rails server and the webpack-dev-server

$ yarn install
$ bundle exec rails server
$ ./bin/webpack-dev-server 

2. Now add the pack link in your application.html.erb file

<head>
        <title>RailsVuejsJsx</title>
        <%= csrf_meta_tags %>
        <%= csp_meta_tag %>

        <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
        <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>

        <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>

3. If everything is set in the correct way, you should see the Webpacker message in the browser console

Install VueJS

1. Install VueJS using the Webpacker command:

$ bundle exec rails webpacker:install:vue

2. Remove useless files like: hello_vue.js and app.vue

3. If your project uses Turbolinks, install the vue-turbolinks library

$ yarn add vue-turbolinks

4. Edit the application.js file, pay attention here: we MUST change theapplication.js in the javascript/packs directory of the app

    /* eslint no-console:0 */

    import TurbolinksAdapter from 'vue-turbolinks'
    import Vue from 'vue'

    // Import all the macro components of the application
    import * as instances from '../instances'

    Vue.use(TurbolinksAdapter)

    document.addEventListener('turbolinks:load', () => {
        // Initialize available instances
        Object.keys(instances).forEach((instanceName) => {
            const instance = instances[instanceName]
            const elements = document.querySelectorAll(instance.el)

            elements.forEach((element) => {
              const props = JSON.parse(element.getAttribute('data-props'))

              new Vue({
                el: element,
                render: h => h(instance.component, { props })
              })
            })
        })
    })

5. Create the instances.js file which contains all the Vue instances, the application macro-areas that you want to migrate to Vue

app/javascript/instances.js

   // Import components
    import ProductList from './components/product/index'

    export const ProductListInstance = {
        el: '.vue-products',
        component: ProductList
    }

6. Add your first Vue component app/javascript/components/product/index.js that shows the product list

export default {
    name: 'ProductList',

    render() {
        return(
            <h1>Products catalog</h1>
        )
    }
}

7. Replace the content of app/views/products/index.html.erb:

<div class="vue-products"></div>

If you restart the Rails and Webpack server, you should see an error:

The problem is that Babel doesn’t have the correct preset to understand the JSX syntax with VueJS. To solve the issue, we must add the preset and configure Babel to use it.

$ yarn add @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props

Open the Babel configuration file babel.config.js and add the preset to the presets array. At the end it should look like this:

    presets: [
        isTestEnv && [
            require('@babel/preset-env').default,
            {
              targets: {
                node: 'current'
              }
            }
        ],
          (isProductionEnv || isDevelopmentEnv) && [
            require('@babel/preset-env').default,
            {
              forceAllTransforms: true,
              useBuiltIns: 'entry',
              corejs: 3,
              modules: false,
              exclude: ['transform-typeof-symbol']
            }
        ],
        '@vue/babel-preset-jsx'
    ].filter(Boolean)

If you restart the Webpack server and reload the page, you should see your first component.

Move the product list to VueJS

The fastest way to do this should be to copy the html.erb template in the component file and replace the ERB code with JSX.

In this example, I copied the content of app/views/products/index.html.erb to app/javascript/components/product/index.js and deleted the Rails code.

When you build a VueJS application, it’s very important to create a component for every piece of code that has a different context. For example: product/index.js will show the list of products but each product should be a separated component called product/card.js.

Here are the results:

  • app/javascript/components/product/index.js
    import ProductCard from './card'

    export default {
        name: 'ProductList',

        props: {
        products: Array
        },

        render() {
            return(
              <div>
                <h1 class="my-4">
                  Products catalog
                </h1>

                <div class="row">
                  {this.products.map(product => (
                    <ProductCard product={product} />
                  ))}
                </div>
              </div>
            )
        }
    }
  • app/javascript/components/product/card.js
 export default {
        name: 'ProductCard',

        props: {
            product: Object
        },

        methods: {
            shortDescription() {
              let description = this.product.description
              if (description.length > 50) {
                return `${description.substr(0, 50)}...`
              } else {
                return description
              }
            }
        },

        render() {
            return(
              <div class="col-lg-4 col-sm-6 mb-4">
                <div class="card h-100">
                  <a href={this.product.url}>
                    <img src={this.product.image} class="card-img-top" alt="" />
                  </a>
                  <div class="card-body">
                    <h4 class="card-title">
                      <a href={this.product.url}>
                        { this.product.name }
                      </a>
                    </h4>
                    <p class="card-text">
                      { this.shortDescription() }
                    </p>
                  </div>
                </div>
              </div>
            )
        }
    }
  • The product list component has to render the products which should be passed using props. props is a JS object which contains the params passed by the parent component. In this example, you have to pass the products to the ProductCard component when you render it.
  • app/views/products/index.html.erb
   <% props = { products: serialize('serializers/products', products: @products) }.to_json %>

    <div class="vue-products" data-props="<%= props %>"></div>
  • The serialize method is a helper method which you must add to app/helpers/application_helper.rb
    module ApplicationHelper
        def serialize(template, options = {})
            JbuilderTemplate
              .new(self) { |json| json.partial! template, options }.attributes!
        end
    end
  • Product list serializer: app/views/serializers/_products.jbuilder
   json.array! products do |product|
        json.partial! 'serializers/product', product: product
    end
  • Product detail serializer: app/views/serializers/_product.jbuilder
    json.id product.id
    json.name product.name
    json.description product.description
    json.image url_for(product.image)
    json.url product_path(product)

Now the product list page should work showing the list of the products using VueJS.

Add Vuex to manage the application state

Vuex is a VueJS library that enables us to share the state of the application between all the Vue instances and components that use it.

If you want to pass some data from a parent to a child you could use props and the state is not needed, but what happens if a sibling component changes some data that is showed from another component?

Vuex resolves this problem centralizing the application data.

A Vuex instance, usually called store, could have many modules. Each module is a JS object which has a state, some actions, mutations and getters.

  • State: contains the JS objects initialization that should be shared between Vue instances. It can’t be modified directly from a component.
  • Actions: methods called from a component that act as a middleware between the components and the state of the application. For example, if a component wants to delete a comment, the actions should call the APIs to accomplish the request and commit the changes calling the correct mutation based on the response.
  • Mutations: list of methods that change the state of the store/application.

Install vuex

$ yarn add vuex

Configure the store: a little bit of boilerplate

  • Create the app/javascript/store/index.js file which creates the Vuex instance
    import Vue from 'vue'
    import Vuex from 'vuex'

    import modules from './modules'

    Vue.use(Vuex)

    export default new Vuex.Store({
      modules,
      strict: process.env.NODE_ENV !== 'production'
    })
  • Create the app/javascript/store/modules/index.js file which includes all the store modules
    import product from './product'
  
    export default {
        product
    }
  • Create the modules. In this case, the store should store only the product comments app/javascript/store/modules/product.js
    const defaultState = {
        comments: []
    }

    export const actions = {
        fillComments({ commit }, comments) {
            commit('fillComments', comments)
        }
    }

    export const mutations = {
        fillComments(state, comments) {
            state.comments = comments
        }
    }

    export default {
        state: defaultState,
        actions,
        mutations
    }
  • Add the store instance to the VueJS instances in the app/javascript/packs/application.js file like this
    ...
    ...

    // Import the store
    import store from '../store'

    ...
    ...
    ...

        new Vue({
            el: element,
            store,
            render: h => h(instance.component, { props })
        })

Install and configure i18n-js gem

This is used to share translations between Rails and Javascript.

  • Add the i18n-js gem to the Gemfile
  • Run bundle install
  • Add the //= require i18n/translations into the app/assets/javascripts/application.js file
  • Restart the server
Move the comments list to VueJS

As initially said, we can move the whole application to VueJS or only some of its sections. In this case, we are moving the product list, the comments list and the comment form.

  • Add the comments list component to app/javascript/instances.js
    // Import components
    import ProductList from './components/product/index'
    import CommentList from './components/comment/index'

    export const ProductListInstance = {
        el: '.vue-products',
        component: ProductList
    }

    export const CommentListInstance = {
        el: '.vue-comments',
        component: CommentList
    }
  • Comments list component: app/javascript/components/comment/index.js
    import { mapState, mapActions } from 'vuex'

    import CommentCard from './card'

    export default {
        name: 'CommentList',

        props: {
            product: Object
        },

        computed: {
            ...mapState({
              comments: state => state.product.comments
            })
        },

        methods: {
            ...mapActions({
              fillComments: 'fillComments'
            }),
            thereAreComments() {
              return this.comments.length > 0
            }
        },

        mounted() {
            this.fillComments(this.product.comments)
        },

        render() {
            return(
              <div>
                <h4 class="my-4">Comments</h4>

                <div class="row">
                  {this.thereAreComments() &&
                    this.comments.map(comment => (
                      <CommentCard comment={comment} />
                    ))
                  }

                  {!this.thereAreComments() &&
                    <div class="col-md-12">
                      <p>
                        { I18n.t('comments.empty') }
                      </p>
                    </div>
                  }
                </div>
              </div>
            )
        }
    }
  • Comment card component: app/javascript/components/comment/card.js
    import { mapActions } from 'vuex'

    export default {
        name: 'CommentCard',

        props: {
            comment: Object
        },

        methods: {
            ...mapActions({
              cancelComment: 'cancelComment'
            })
        },

        render() {
            return(
              <div class="col-md-12 my-2">
                <div class="card">
                  <div class="card-body">
                    <h5 class="card-title">{ this.comment.title }</h5>
                    <p class="card-text">{ this.comment.description }</p>

                    <button class="btn btn-sm btn-danger" onClick={event => this.cancelComment(this.comment.id)}>
                      { I18n.t('comments.form.delete') }
                    </button>
                  </div>
                </div>
              </div>
            )
        }
    }
  • Remove the comments partial: app/views/shared/_comments.html.erb
  • Replace the content of app/views/products/show.html.erb with this:
    <h1 class="my-4">
        <%= @product.name %>
    </h1>
    <p>
        <%= link_to t('products.back'), products_path %>
    </p>

    <div class="row">
        <div class="col-md-8">
            <%= image_tag @product.image, class: 'img-fluid' %>
        </div>

        <div class="col-md-4">
            <h3 class="my-3">Project Description</h3>
            <p>
              <%= @product.description %>
            </p>
        </div>
    </div>

    <%
        props = {
            product: serialize('serializers/product', product: @product)
        }.to_json
    %>

    <div class="vue-comments" data-props="<%= props %>"></div>
  • Add the comment serializer at the end of app/views/serializers/_product.jbuilder:
    json.comments product.comments do |comment|
        json.partial! 'serializers/comment', comment: comment
    end
  • Create the comment serializer app/views/serializers/_comment.jbuilder:
    json.id comment.id
    json.title comment.title
    json.description comment.description
  • Create a couple of comments using the console:
$ bundle exec rails console    product = Product.first
    Comment.create!(title: 'This is the first comment', description: 'Comment description', product: product)
    Comment.create!(title: 'This is the second comment', description: 'Comment description', product: product)

At this point, you should see the page like before with the comment list. However, the delete comment button doesn’t work. This happens because the action deleteComment wasn’t implemented into the store.

Install Axios to make HTTP requests

To add or delete a comment without reloading the product page, we must implement the APIs and consume them using the Axios library.

$ yarn add axios

Implement the APIs to create and delete a comment

  1. Change the config/routes.rb file
    Rails.application.routes.draw do
        root 'products#index'

        resources :products, only: %i[index show]

        namespace :api do
            resources :comments, only: :destroy

            resources :products, only: [] do
              resources :comments, only: :create
            end
        end
    end

2. Remove app/controllers/comments_controller.rb

3. Create the API comments controller app/controllers/api/comments_controller.rb

4. Implement the create and destroy methods

    Rails.application.routes.draw do
        root 'products#index'

        resources :products, only: %i[index show]

        namespace :api do
            resources :comments, only: :destroy

            resources :products, only: [] do
              resources :comments, only: :create
            end
        end
    end

Delete the comments

To recap, we added a button to delete a comment to the comment card component.

When the user clicks on the delete button, the component should dispatch the correct action, e.g. deleteComment.

The action calls the correct API method (which doesn’t exist yet) and it will commit the correct mutation based on the response.

If the destroy API call was successful, remove the deleted comment from the comments array.

If the destroy API call was unsuccessful, fill the errors array to show the errors.

Implement the API client with Axios

  1. Create the Axios instance app/javascript/api/instance.js
    import axios from 'axios'

    axios.defaults.headers.common['X-CSRF-Token'] = document.querySelector('meta[name="csrf-token"]').getAttribute('content')

    export default axios.create()

2. Add the index file app/javascript/api/index.js which exports all the API modules. In this case, we use Axios only to manage the comments.

   import comment from './comment'

    export default {
        comment
    }

3. Implement the methods that will make the HTTP request: app/javascript/api/comment.js

    import api from './instance'

    /**
    * Create a comment
    */
    const create = (productId, commentParams) => (
        api.post(Routes.api_product_comments_path(productId), commentParams)
        .then(response => response.data)
    )

    /**
    * Destroy a comment
    */
    const destroy = (commentId) => (
        api.delete(Routes.api_comment_path(commentId))
        .then(response => response.data)
    )

    export default {
        create,
        destroy
    }

Install and configure js-routes gem

This gem is needed to share Rails routes with JavaScript.

  1. Add the gem to the Gemfile and run bundle install:
gem ‘js-routes’

2. Require the gem in app/assets/javascripts/application.js:

//= require js-routes

3. Configure js-routes specifying which routes should be shared by creating the configuration file: config/initializers/js_routes.rb

    # frozen_string_literal: true

    JsRoutes.setup do |config|
        config.include = [
            /^api_comment$/,
            /^api_product_comments$/,
        ]
    end

4. Run this commands and restart the Rails server:

$ bundle exec rails tmp:cache:clear

Implement the action and the mutations

  1. Import the API module in the product store app/javascript/store/modules/product.js
import api from '../../api'

2. Add the method to the actions object:

     cancelComment({ commit }, commentId) {
        api.comment.destroy(commentId)
          .then(() => {
            commit('commentCancelled', commentId)
          })
      }

3. Add the method commentCancelled to the mutation object:

    commentCancelled(state, commentId) {
        state.comments = state.comments.filter(comment => commentId !== comment.id)
    },

The commentCancelled method will filter the comments array removing the canceled comment.

At this point, the delete comment feature should work.

Add a comment

Since we removed the comment form partial from the product show view, the form disappeared from the page. To fix this, we will create the commentForm Vue component.

  1. Create the component: app/javascript/components/comment/form.js
    import { mapActions } from 'vuex'

    export default {
        props: {
            product: Object
        },

        data() {
            return {
              title: '',
              description: ''
            }
        },

        methods: {
            ...mapActions({
              addComment: 'addComment'
            }),
            submitComment() {
              this.addComment({
                productId: this.product.id,
                commentParams: {
                  title: this.title,
                  description: this.description
                }
              })

              this.title = ''
              this.description = ''
            }
        },

        render() {
            return(
              <div class="row my-2">
                <div class="col-md-8">
                  <h4 class="my-4">Add new comment</h4>

                  <div class="form-label-group">
                    <input type="input" class="form-control" name="title"
                      placeholder={I18n.t('comments.form.title')}
                      autofocus="true" vModel_trim={this.title} />
                  </div>

                  <div class="form-label-group my-3">
                    <input type="input" class="form-control" name="description"
                      placeholder={I18n.t('comments.form.description')}
                      vModel_trim={this.description} />
                  </div>

                  <input type="submit" class="btn btn-primary" value={I18n.t('comments.form.submit')}
                    vOn:click_stop_prevent={this.submitComment} />
                </div>
              </div>
            )
        }
    }

2. Add the component to the instances file: app/javascript/instances.js

    // Import components
    import ProductList from './components/product/index'
    import CommentList from './components/comment/index'
    import CommentForm from './components/comment/form'

    export const ProductListInstance = {
        el: '.vue-products',
        component: ProductList
    }

    export const CommentListInstance = {
        el: '.vue-comments',
        component: CommentList
    }

    export const CommentFormInstance = {
        el: '.vue-comment-form',
        component: CommentForm
    }

3. Add the commentForm wrapper at the end of the product show: app/views/products/show.html.erb

    <div class="vue-comment-form" data-props="<%= props %>">
    </div>

Now, the comment form appears at the end of the product detail page again, but it doesn’t work.

  1. Add the addComment action that calls the create method of the APIs in app/javascript/store/modules/product.js
    addComment({ commit }, { productId, commentParams }) {
        api.comment.create(productId, commentParams)
          .then((comment) => {
            commit('commentAdded', comment)
          })
    }

2. Add the commentAdded mutation which updates the comments array in app/javascript/store/modules/product.js

    commentAdded(state, comment) {
        state.comments.push(comment)
    }
Finally we’re done

Now your application uses both Rails and VueJS to render views and components. To learn how to manage errors with the comment form, you can use the repository linked above.

How to build enterprise Vue.js applications with Nuxt?

How to build enterprise Vue.js applications with Nuxt?

Learn How to build enterprise Vue.js applications with Nuxt: Nuxt is a progressive framework based on Vue.js to create modern web applications. It is based on Vue.js official libraries (vue, vue-router and vuex) and powerful development tools (webpack, Babel and PostCSS). Nuxt goal is to make web development powerful and performant with a great developer experience in mind.

Nuxt is a progressive framework based on Vue.js to create modern web applications. It is based on Vue.js official libraries (vue, vue-router and vuex) and powerful development tools (webpack, Babel and PostCSS). Nuxt goal is to make web development powerful and performant with a great developer experience in mind.

  1. Based on Vue-router and Vuex, it also uses Webpack, Babel and PostCSS.

  2. Goal, make web development powerful and performant.

Ok, so a sales pitch, what else?

The WHAT and WHY

Ok, so we love using Vue.js, it works great to use a router, a store, adding UI and so on, so why should we consider Nuxt?

The answer is SSR, Server side rendering

Why is that such a big deal? Well, search engines, they were made to crawl static pages and most SPA frameworks don't produce static pages but dynamic pages. This causes a problem if you want your app's SEO, to be any good, i.e end up high up on a Google search result. If you have a productivity app you might not care about that so much but if you have an e-commerce store you might be out of business if you haven't got this aspect covered.

Is that all Nuxt offer?

There is more. Nuxt makes it easy to bring in things such as code-splitting. Code splitting is an important aspect of the user experience. It allows us to only load as much of our app initially that's needed. It works like this, imagine the user visits your site. You only load a tiny portion of the app so the user perceives you got a fast-loading app - everyone's happy. As the user discovers more and more of your app they start navigating to other parts of it.

At this point, we load in the code needed for that new part the user is navigating to. This approach does a lot for the user experience, especially in places where the network connection is not that good. Having this for free in Nuxt is a big deal.

There is more to Nuxt of course, like hot module replacement, being able to load async data before SSR, a great approach to working with Sass, Less and so on.

Features

Here's a full list of all features Nuxt is offering:

  • Write Vue Files (*.vue)
  • Automatic Code Splitting
  • Server-Side Rendering
  • Powerful Routing System with Asynchronous Data
  • Static File Serving
  • ES2015+ Transpilation
  • Bundling and minifying of your JS & CSS
  • Managing <head> element (<title>, <meta>, etc.)
  • Hot module replacement in Development
  • Pre-processor: Sass, Less, Stylus, etc.
  • HTTP/2 push headers ready
  • Extending with Modular architecture

That's quite the list right?

SSR and code splitting is what really sells it for me though. How about we try to learn it next?

Schema, lifecycle

Let's try to get ourselves a mile-high view of Nuxt. There are some steps from the incoming request to a rendered page.

In Nuxt we use a directive called <nuxt-link> to navigate between pages.

  1. nuxtServerInit, a store is part of your next app. This is an action we can define in the store if we want. It can be used to place data in the store the first thing we do, like a logged-in user. Read more here
  2. middleware, middleware are custom functions that can run before the rendering of a route. You could, for example, ensure that the user is properly authenticated before being able to see a specific route. Read more here
  3. validate, in this step, we validate that we are dealing with a valid request, for example, we could be validating that an incoming request has the correct route parameters
  4. asyncData() & fetch(), here we have the chance of fetching data asynchronously that will be part of the SSR rendering, for example, we could be fetching translations or some other data we need to render our page
  5. Render, here we render the page

SPA or SSR mode

If for any reason, you prefer not to use server-side rendering or need static hosting for your applications, you can simply use SPA mode using nuxt --spa.

In combination with the generate feature, it gives you a powerful SPA deployment mechanism without the need to use a Node.js runtime or any special server handling.

read more about available commands here:

https://nuxtjs.org/guide/commands

SSR, Static site generation

The big innovation of Nuxt.js comes with the nuxt generate command.

When building your application, it will generate the HTML for every one of your routes and store it in a file.

How does that work?

-| pages/
----| about.vue
----| index.vue

turns into

-| dist/
----| about/
------| index.html
----| index.html

Great for crawlers :)

Hosting

Of course, you need to host your app somewhere. Currently, Netlify offers a great and easy way to do so with a simple command:

https://vueschool.io/lessons/how-to-deploy-nuxtjs-to-netlify?friend=nuxt

Creating an App

There are two ways to create apps in Nuxt:

  1. Wizard, Using create-nuxt-app, this will trigger a wizard where you are asked to add/opt in to different things
  2. Manually, A simple NPM install of nuxt and you need to define most things yourself.

Create an app with create-nuxt-app

We can use the command create-nuxt-app to get a full-fledged project. The only thing we need to do is choose among different options.

Let's run the following command:

npx create-nuxt-app <project-name>

or yarn:

yarn create nuxt-app <project-name>

This triggers a dialog, first choose Server-side framework:

-1- server-side framework

This is what will help us to do things like server side rendering that we use for static site generation.

  • None (Nuxt default server)
  • Express
  • Koa
  • Hapi
  • Feathers
  • Micro
  • Fastify
  • Adonis (WIP)

-2- select UI library

Of course you want your app to look good. Below are all the most known UI libraries that will be included in your project.

Then select UI library:

  • None (feel free to add one later)
  • Bootstrap
  • Vuetify
  • Bulma
  • Tailwind
  • Element UI
  • Ant Design Vue
  • Buefy
  • iView
  • Tachyons

-3- choose Testing framework

Do you want testing? No? Well, you need it. Jest and AVA are both good options.

  • None (feel free to add one later)
  • Jest
  • AVA

-4- select mode

This is where we select to have our app server side rendered or not. I'd go with Universal myself.

Select Universal or SPA mode

-5- http

fetch works of course but it's kind of nice when you can use a battle tested library like axios.

Add axios module for HTTP requests

-6- linting

You do lint right? We want our code to look consistent.

Add ESLint

-7- prettier

Add Prettier

-8- launch app

Launch app

cd <project-name>
npm run dev

the app should now be running on http://localhost:3000.

That was simple right? You opted into some tried and testing libraries that are sure to make your app better.

Below is your project overview, the result of running the creation tool.

Get started, from scratch

Let's have a look at the second approach, creating our app from scratch. First, we create a directory for our app to live in:

mkdir <project-name>
cd <project-name>

Update package.json

Next, let's add the necessary command to package.json so we can start up our app from the terminal:

{
  "name": "my-app",
  "scripts": {
    "dev": "nuxt"
  }
}

Now we can type npm run dev, to start the app.

Install nuxt

Next, let's install the nuxt library itself and save a reference to it in package.json:

npm install --save nuxt

Pages directory

Nuxt has some great conventions, so creating directories with certain names has meaning. That is the case with the directory pages. Everything, with file ending .vue, that is placed under the pages directory, will turn into a route.

<!-- index.vue -->
<template>
  <h1>Hello world!</h1>
</template>

Now run

npm run dev

This will start up our app. Let's go to the browser at http://localhost:3000 and have a look.

We have an app :)

Now try changing the template in index.vue, note how the change is reflected in the browser. This is because Nuxt comes with hot module reloading, it responds to changes.

Demo - SSR

Let's demonstrate the server-side rendering and how that works. As we mentioned before placing files under specific directories have meaning. We will continue to work with our pages directory.

This time, let's do the following:

  • Create a file products.vue under pages
  • Generate static site

Create a file

Let's create products.vue, under pages directory, with the following content:

<!-- products.vue -->
<template>
  <h1>Products</h1>
</template>

Our directory pages should now look like this:

-| pages/
---| index.vue
---| products.vue

Generate static site

Next up, let's generate those static files to make search engines everywhere happy.

We do this with the help of nuxt generate. Let's add that command to package.json and its scripts section like so:

"generate": "nuxt generate"

Next we run npm run generate, and we get a dist directory created, looking like so:

We can now easily host the content under dist using a tool, like for example http-server.

It looks like this in the browser:

As you can see http://localhost:5000/products/ and http://localhost:5000/products/index.html lead to the same place.

Working with Assets

Additionally to the pages directory, there's another directory that has special meaning. It's called assets. In there you can put images as well as stylesheets.

Working with image assets

Let's give it a try by doing the following

  • Creating assets directory and place a sample image file in there
  • Refer to sample image file in a Component

Create assets

We create our assets directory and place a file budapest.jpeg in there. Now your directory should look like the following:

-| assets/
---| budapest.jpeg
-| pages
--- | products.vue

Refer to asset

This time we want render our image asset. For this we will update products.vue to:

<!-- products.vue -->
<template>
  <div>
    <h1>Products</h1>
    <img src="~/assets/budapest.jpeg" alt="image" />
  </div>
</template>

What if we want to set this from code though? Then we extend the component to look like this:

<!-- products.vue -->
<template>
  <div>
    <h1>Products</h1>
    <img :src="img" alt="should work" />
  </div>
</template>
<script>
const url = require('~/assets/budapest.jpeg')

export default {
  data() {
    return {
      img: url
    }
  }
}
</script>

as you can see we need to import the resource to ensure we get the correct URL to our resource, like so:

const url = require('~/assets/budapest.jpeg')

Working with Style assets

You can use different approaches to styling in Nuxt like Sass, Scss, Less and CSS. For this example let's use scss.

To compile scss, we need some additional libraries. You need to run the following installation command in the terminal:

npm install node-sass sass-loader --save

This will give us the needed libraries so Nuxt can do the rest.

There are two ways we could be using scss now:

  1. Define styling in the component directly
  2. Create an external file and place in the assets directory

Define scss in the component

Let's start with defining the styles in the component directly, like so:

<!-- sample.vue -->
<template>
  <div class="images">
    add some nice images here
  </div>
</template>
<style lang="scss">
  .images {
    margin: 15px;
    padding: 20px;
    display: flex;
    justify-content: center;
    box-shadow: 0 0 10px grey;
  }
</style>

Nuxt will compile the above and apply it to any matching CSS classes.

Define an external file

The other way we can work with styling assets is by creating separate files.

Let's create the file page.scss under assets/scss with the following content:

.page {
  .title {
    font-size: 20px;
  }
}

NOTE, we can create the styling file directly under assets directory but I just like to separate concerns so I recommend a directory approach like so:

-| assets/
---| images/
---| scss/

We also need to point this out to Webpack so it finds it, so go and create a file called nuxt.config.js in the root and give it the following content:

module.exports = {
  css: [
    '@/assets/scss/page.scss'
  ]
}

Now our directory structure for the whole project should look something like so:

-| assets/
---| scss/
---| images/
-| pages/
-| nuxt.config.js
-| package.json

NOTE, nuxt.config.js is a configuration file that Nuxt will read from if it exists. There are a lot more you can do with nuxt.config.js and we have reason to come back to it.

Working with routing

ok, we understand how we can create different pages by simply creating them in the pages directory. How to move between pages though?

Simple, use <nuxt-link>

We get a lot of routing set up for free. So given the following file structure:

pages/
--| user/
-----| index.vue
-----| one.vue
--| index.vue

we would get routing set up like so:

router: {
  routes: [
    {
      name: 'index',
      path: '/',
      component: 'pages/index.vue'
    },
    {
      name: 'user',
      path: '/user',
      component: 'pages/user/index.vue'
    },
    {
      name: 'user-one',
      path: '/user/one',
      component: 'pages/user/one.vue'
    }
  ]
}

Dynamic routing

We should understand by now how our file structure affects the routing. There are more aspects of routing we need to understand though namely dynamic routing.

Consider the following file structure:

pages/
--| _slug/
-----| comments.vue
-----| index.vue
--| users/
-----| _id.vue
--| index.vue

We see the usage of underscore, _ in both the directory _slug and the Vue file _id.vue. This is how we create routes with parameters.

Let's look at the resulting routes:

router: {
  routes: [
    {
      name: 'index',
      path: '/',
      component: 'pages/index.vue'
    },
    {
      name: 'users-id',
      path: '/users/:id?',
      component: 'pages/users/_id.vue'
    },
    {
      name: 'slug',
      path: '/:slug',
      component: 'pages/_slug/index.vue'
    },
    {
      name: 'slug-comments',
      path: '/:slug/comments',
      component: 'pages/_slug/comments.vue'
    }
  ]
}

our file structure:

--| users/
-----| _id.vue

resulted in a route looking like this /users/:id?.

NOTE, the ? means that's it's optional if you want to make it mandatory you need the file structure to look like this:

--| users/
-----| _id/
---------| index.vue

Also

--| _slug/

resulted in /:slug

Validate route params

Now that we know how to deal with parameters in the routes, how do we validate those parameters?

We can add a validate() method for that. If it fails the check it will automatically route you to the 404 page.

export default {
  validate ({ params }) {
    // Must be a number
    return /^\d+$/.test(params.id)
  }
}

Nested routes

Sometimes you might have routes within your routes. You might have a /products route that in itself might render different pages depending on how the full route looks.

Let's say if we go to /products, it should load a product list but if it goes to /products/1, it should load a product detail. If we have this scenario it means we will have the following behavior:

  • a shell, this page can contain static and dynamic data and will need to contain a <nuxt-child/> component tag
  • a specific file structure

Let's have a look at the file structure:

pages/
--| products/
-----| _id.vue
-----| index.vue
--| products.vue

The above will treat products.vue as the shell page and it will generate the following routes:

router: {
  routes: [
    {
      path: '/products',
      component: 'pages/products.vue',
      children: [
        {
          path: '',
          component: 'pages/products/index.vue',
          name: 'products'
        },
        {
          path: ':id',
          component: 'pages/products/_id.vue',
          name: 'products-id'
        }
      ]
    }
  ]
}

To learn more about routing, check here:

https://nuxtjs.org/guide/routing

Working with async

When a request comes to a specific route we can bring in data asynchronously and use that to render the page. For that, we can use asyncData and fetch our data. Below we have a Vue component where asyncData are asking for data. It will block rendering until the data is fetched. We end by returning the data. The data we return ends up being amended to the data property of the component. As w can see in our <template> we are referring to article that was part of the data we returned from asyncData.

<template>
  <div v-if="article">
    <h3>
    {{article.title}}
    </h3>
    <div>
      {{article.description}}
    </div>
  </div>
  <div v-else>
    <h3>Sorry</h3>
    No article with that name 
    <strong>{{slug}}</strong>
  </div>
</template>
<script>
function getData(slug) {
  const content = {
    docker: {
      title: 'Docker 101',
      description: 'this is about Docker'
    },
    dotnet: {
      title: '.NET news',
      description: 'this is about .NET'
    }
  };
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(content[slug]);
    }, 5000)
  })
}
export default {
  async asyncData ({ params }) {
    console.log('slug', params.slug);
    const res = await getData(params.slug)
    // called every time before loading the component
    return {
      article: res,
      slug: params.slug
    }
  }
}
</script>

Layout

For the layout we want to cover three cases:

  • default page, this is the page that constitutes the default rendering of a page unless you explicitly override it.
  • error page, we can specify a page that will take care of all erroneous responses.
  • custom layout, we will cover how to define a custom layout component and show how to use our custom layout

First off, let's create the directory layouts, like so:

---| layouts
---| pages

Default page

To create a default layout, simply create a component default.vue, like so:

---| layouts
-------| default.vue

Next, give it the following content:

<template>
  <div>
    <div class="header">Hello route demo</div>
    <div class="subheader">
      <div class="item">
        <nuxt-link to="/">Home page</nuxt-link>
      </div>
      <div class="item">
        <nuxt-link to="/products">Products</nuxt-link>
      </div>
    </div>
    <nuxt/>
  </div>
</template>
<style scoped>
</style>

In our template we create a header section with CSS class subheader, in it we have a number of <div> elements, each with a <nuxt-link> element, that's our navigational link.

We also render the <nuxt/> element, now this is important. This is the element that renders our page content, if you forget this page then it won't work properly.

Error page

We can create an error page that will allow us to capture all errors. Let's create the component error.vue, like so:

---| layouts
-------| error.vue

Give it the following content:

<template>
  <div class="container">
    <h1 v-if="error.statusCode === 404">Page not found</h1>
    <h1 v-else>An error occurred</h1>
    <nuxt-link to="/">Home page</nuxt-link>
  </div>
</template>
<script>
export default {
  props: ['error']
  // ,layout: 'blog' // you can set a custom layout for the error page
}
</script>

Note, how we have a input property error, like so props: ['error']. Additionally notehoow we can filter on status code and can render different messages depending on what the error is:

<h1 v-if="error.statusCode === 404">Page not found</h1>

Custom layout

Lastly, let's show how to create and apply a custom layout. You can name your custom layout component anything you want. We elect to name our component blog.vue, like so:

---| layouts
-------| blog.vue

Next let's give it some content, like so:

<template>
  <div class="blog">
    <h1>Here's the template for an article</h1>
    <nuxt class="page" />
  </div>
</template>

This looks just like default.vue in that we have a <nuxt> element somewhere to make sure we render the page content.

How do we use it?

Good question. Let's take a page component and set the layout property, like so:

<template>
  <div>
    <h3>Docker 101</h3>
    <div>
      {{content}}
    </div>
  </div>
</template>
<script>
export default {
  layout: 'blog',
  data() {
    return {
      content: 'something...'
    }
  }
}
</script>

Note, how we point out the layout: 'blog', thereby we refer to our layout by its name, minus the file extension.

Summary

That was quite a lot in one article. Guess what though, there's a lot more to learn.

Let's recap on our learnings, we learned:

  • The WHAT and WHY of Nuxt, it's important to understand the reason for wanting to use something. In the case of Nuxt, we learned that it had an opinionated architecture relying on convention. We also learned that it used a lot of great standard libraries for state management and server-side rendering.
  • Static site generation, we learned how we can address the SEO problem that most SPA apps suffer from.
  • How to create our Nuxt app in two ways, with a wizard and manually
  • Assets, we learned how to deal with assets, images as well as styles
  • Routing, then we covered routing, by convention based on our directory structures but also how to deal with dynamic routing
  • Async, then we learned how to use a method like asyncData() to read asynchronous data before rendering a page.
  • Layout, we learned how to create different layouts like default, error and a custom layout.

That was a lot but hopefully, you are now so comfortable with Nuxt that you see its value and can find out the rest you need in the docs.

Nuxt.js: a Minimalist Framework for Creating Universal Vue.js Apps

Nuxt.js: a Minimalist Framework for Creating Universal Vue.js Apps

In this article, you'll learn how we can take advantage of Nuxt.js to build server-rendered JavaScript applications with Vue.js. Learn how to use its generate command to generate static files for our pages, and deploy them quickly via a service like Firebase Hosting.

In this article, you'll learn how we can take advantage of Nuxt.js to build server-rendered JavaScript applications with Vue.js. Learn how to use its generate command to generate static files for our pages, and deploy them quickly via a service like Firebase Hosting.

Universal (or Isomorphic) JavaScript is a term that has become very common in the JavaScript community. It’s used to describe JavaScript code that can execute both on the client and the server.

Many modern JavaScript frameworks, like Vue.js, are aimed at building single-page applications (SPAs). This is done to improve the user experience and make the app seem faster, since users can see updates to pages instantaneously. While this has a lot of advantages, it also has a couple of disadvantages, such as long “time to content” when initially loading the app as the browser retrieves the JavaScript bundle, and some search engine web crawlers or social network robots won’t see the entire loaded app when they crawl your web pages.

Server-side rendering of JavaScript is about preloading JavaScript applications on a web server and sending rendered HTML as the response to a browser request for a page.

Building server-side rendered JavaScript apps can be a bit tedious, as a lot of configuration needs to be done before you even start coding. This is the problem Nuxt.js aims to solve for Vue.js applications.

What Nuxt.js Is

Simply put, Nuxt.js is a framework that helps you build server-rendered Vue.js applications easily. It abstracts most of the complex configuration involved in managing things like asynchronous data, middleware, and routing. It’s similar to Angular Universal for Angular, and Next.js for React.

According to the Nuxt.js docs, “its main scope is UI rendering while abstracting away the client/server distribution.”

Static Generation

Another great feature of Nuxt.js is its ability to generate static websites with the generate command. It’s pretty cool, and provides features similar to popular static generation tools like Jekyll.

Under the Hood of Nuxt.js

In addition to Vue.js 2.0, Nuxt.js includes the following: Vue-Router, Vuex (only included when using the store option), Vue Server Renderer and vue-meta. This is great, as it takes away the burden of manually including and configuring different libraries needed for developing a server-rendered Vue.js application. Nuxt.js does all this out of the box, while still maintaining a total size of 57kB min+gzip (60KB with vuex).

Nuxt.js also uses webpack with vue-loader and babel-loader to bundle, code-split and minify code.

How it works

This is what happens when a user visits a Nuxt.js app or navigates to one of its pages via <nuxt-link>:

  1. When the user initially visits the app, if the [nuxtServerInit](https://nuxtjs.org/guide/vuex-store/#the-nuxtserverinit-action "nuxtServerInit") action is defined in the store, Nuxt.js will call it and update the store.
  2. Next, it executes any existing middleware for the page being visited. Nuxt checks the nuxt.config.js file first for global middleware, then checks the matching layout file (for the requested page), and finally checks the page and its children for middleware. Middleware are prioritized in that order.
  3. If the route being visited is a dynamic route, and a validate() method exists for it, the route is validated.
  4. Then, Nuxt.js calls the asyncData() and fetch() methods to load data before rendering the page. The [asyncData()](https://nuxtjs.org/guide/async-data/ "asyncData()") method is used for fetching data and rendering it on the server-side, while the [fetch()](https://nuxtjs.org/api/pages-fetch/ "fetch()") method is used to fill the store before rendering the page.
  5. At the final step, the page (containing all the proper data) is rendered.

These actions are portrayed properly in this schema, gotten from the Nuxt docs:

Creating A Serverless Static Site With Nuxt.js

Let’s get our hands dirty with some code and create a simple static generated blog with Nuxt.js. We’ll assume our posts are fetched from an API and will mock the response with a static JSON file.

To follow along properly, a working knowledge of Vue.js is needed. You can check out Jack Franklin’s great getting started guide for Vue.js 2.0 if you’re new to the framework. I’ll also be using ES6 Syntax, and you can get a refresher on that here: sitepoint.com/tag/es6/.

Our final app will look like this:

The entire code for this article can be seen here on GitHub, and you can check out the demo here.

Application Setup and Configuration

The easiest way to get started with Nuxt.js is to use the template created by the Nuxt team. We can install it to our project (ssr-blog) quickly using the vue-cli:

vue init nuxt/starter ssr-blog


Once you’ve run this command, a prompt will open and ask you a couple of questions. You can press Return to accept the default answers, or enter values of your own.

Note: If you don’t have vue-cli installed, you have to run *npm install -g @vue/cli* first, to install it.

Next, we install the project’s dependencies:

cd ssr-blog
npm install


Now we can launch the app:

npm run dev


If all goes well, you should be able to visit http://localhost:3000 to see the Nuxt.js template starter page. You can even view the page’s source, to see that all content generated on the page was rendered on the server and sent as HTML to the browser.

Next, we can make some simple configurations in the nuxt.config.js file. We’ll add a few options:

// ./nuxt.config.js

module.exports = {
  /*
   * Headers of the page
   */
  head: {
    titleTemplate: '%s | Awesome JS SSR Blog',
    // ...
    link: [
      // ...
      {
        rel: 'stylesheet',
        href: 'https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css'
      }
    ]
  },
  // ...
}


In the above config file, we simply specify the title template to be used for the application via the titleTemplate option. Setting the title option in the individual pages or layouts will inject the title value into the %s placeholder in titleTemplate before being rendered.

We also pulled in my current CSS framework of choice, Bulma, to take advantage of some preset styling. This was done via the link option.

Note: Nuxt.js uses vue-meta to update the headers and HTML attributes of our apps. So you can take a look at it for a better understanding of how the headers are being set.

Now we can take the next couple of steps by adding our blog’s pages and functionalities.

Working with Page Layouts

First, we’ll define a custom base layout for all our pages. We can extend the main Nuxt.js layout by updating the layouts/default.vue file:

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

<template>
  <div>
    <!-- navigation -->
    <nav class="navbar has-shadow" role="navigation" aria-label="main navigation">
      <div class="container">
        <div class="navbar-start">
          <nuxt-link to="/" class="navbar-item">
            Awesome JS SSR Blog!
          </nuxt-link>
          <nuxt-link active-class="is-active" to="/" class="navbar-item is-tab" exact>Home</nuxt-link>
          <nuxt-link active-class="is-active" to="/about" class="navbar-item is-tab" exact>About</nuxt-link>
        </div>
      </div>
    </nav>
    <!-- /navigation -->

    <!-- displays the page component -->
    <nuxt/>

  </div>
</template>

<style>
  .main-content {
    margin: 30px 0;
  }
</style>


In our custom base layout, we add the site’s navigation header. We use the <nuxt-link> component to generate links to the routes we want to have on our blog. You can check out the docs on [<nuxt-link>](https://nuxtjs.org/api/components-nuxt-link "<nuxt-link>") to see how it works.

The <nuxt> component is really important when creating a layout, as it displays the page component.

It’s also possible to do a couple of more things — like define custom document templates and error layouts — but we don’t need those for our simple blog. I urge you to check out the Nuxt.js documentation on views to see all the possibilities.

Simple Pages and Routes

Pages in Nuxt.js are created as single file components in the pages directory. Nuxt.js automatically transforms every .vue file in this directory into an application route.

Building the Blog Homepage

We can add our blog homepage by updating the index.vue file generated by the Nuxt.js template in the pages directory:

<!-- ./pages/index.vue -->
<template>
  <div>
    <section class="hero is-medium is-primary is-bold">
      <div class="hero-body">
        <div class="container">
          <h1 class="title">
            Welcome to the JavaScript SSR Blog.
          </h1>
          <h2 class="subtitle">
            Hope you find something you like.
          </h2>
        </div>
      </div>
    </section>
  </div>
</template>

<script>
  export default {
    head: {
      title: 'Home'
    }
  }
</script>

<!-- Remove the CSS styles -->


As stated earlier, specifying the title option here automatically injects its value into the titleTemplate value before rendering the page.

We can now reload our app to see the changes to the homepage.

Building the About page

Another great thing about Nuxt.js is that it will listen to file changes inside the pages directory, so there’s no need to restart the application when adding new pages.

We can test this, by adding a simple about.vue page:

<!-- ./pages/about.vue -->
<template>
  <div class="main-content">
    <div class="container">
      <h2 class="title is-2">About this website.</h2>
      <p>Curabitur accumsan turpis pharetra <strong>augue tincidunt</strong> blandit. Quisque condimentum maximus mi, sit amet commodo arcu rutrum id. Proin pretium urna vel cursus venenatis. Suspendisse potenti. Etiam mattis sem rhoncus lacus dapibus facilisis. Donec at dignissim dui. Ut et neque nisl.</p>
      <br>
      <h4 class="title is-4">What we hope to achieve:</h4>
      <ul>
        <li>In fermentum leo eu lectus mollis, quis dictum mi aliquet.</li>
        <li>Morbi eu nulla lobortis, lobortis est in, fringilla felis.</li>
        <li>Aliquam nec felis in sapien venenatis viverra fermentum nec lectus.</li>
        <li>Ut non enim metus.</li>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  head: {
    title: 'About'
  }
}
</script>


Now, we can visit http://localhost:3000/about to see the about page, without having to restart the app, which is awesome.

Showing Blog Posts on the Homepage

Our current homepage is pretty bare as it is, so we can make it better by showing the recent blog posts from the blog. We’ll do this by creating a <posts> component and displaying it in the index.vue page.

But first, we have to get our saved JSON blog posts and place them in a file in the app root folder. The file can be downloaded from here, or you can just copy the JSON below and save in the root folder as posts.json:

[
    {
        "id": 4,
        "title": "Building universal JS apps with Nuxt.js",
        "summary": "Get introduced to Nuxt.js, and build great SSR Apps with Vue.js.",
        "content": "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>",
        "author": "Jane Doe",
        "published": "08:00 - 07/06/2017"
    },
    {
        "id": 3,
        "title": "Great SSR Use cases",
        "summary": "See simple and rich server-rendered JavaScript apps.",
        "content": "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>",
        "author": "Jane Doe",
        "published": "17:00 - 06/06/2017"
    },
    {
        "id": 2,
        "title": "SSR in Vue.js",
        "summary": "Learn about SSR in Vue.js, and where Nuxt.js can make it all faster.",
        "content": "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>",
        "author": "Jane Doe",
        "published": "13:00 - 06/06/2017"
    },
    {
        "id": 1,
        "title": "Introduction to SSR",
        "summary": "Learn about SSR in JavaScript and how it can be super cool.",
        "content": "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>",
        "author": "John Doe",
        "published": "11:00 - 06/06/2017"
    }
]


Note: Ideally the posts should be retrieved from an API or resource. For example, Contentful is a service that can be used for this.

Components live in the components directory. We’ll create the <posts> single file component in there:

<!-- ./components/Posts.vue -->
<template>
  <section class="main-content">
    <div class="container">
      <h1 class="title has-text-centered">
        Recent Posts.
      </h1>
      <div class="columns is-multiline">
        <div class="column is-half" v-for="post in posts" :key="post.id">
          <div class="card">
           <header class="card-header">
            <p class="card-header-title">
              {{ post.title }}
            </p>
          </header>
          <div class="card-content">
            <div class="content">
              {{ post.summary }}
              <br>
              <small>
                by <strong>{{ post.author}}</strong>
                \\ {{ post.published }}
              </small>
            </div>
          </div>
          <footer class="card-footer">
            <nuxt-link :to="`/post/${post.id}`"
              class="card-footer-item">
              Read More
            </nuxt-link>
          </footer>
        </div>
      </div>
    </div>
  </div>
</section>
</template>

<script>
  import posts from '~/posts.json'

  export default {
    name: 'posts',
    data () {
      return { posts }
    }
  }
</script>


We import the posts data from the saved JSON file and assign it to the posts value in our component. We then loop through all the posts in the component template with the v-for directive and display the post attributes we want.

Note: The *~* symbol is an alias for the */* directory. You can check out the docs here to see the different aliases Nuxt.js provides, and what directories they’re linked to.

Next, we add the <posts> component to the homepage:

<!-- ./pages/index.vue -->
<template>
<div>
    <!-- ... -->
    <posts />
</div>
</template>

<script>
import Posts from '~/components/Posts.vue'

export default {
  components: {
    Posts
  },
  // ...
}
</script>


Adding Dynamic Routes

Now we’ll add dynamic routes for the posts, so we can access a post for example with this URL: /post/1.

To achieve this, we add the post directory to the pages directory and structure it like this:

pages
└── post
    └── _id
        └── index.vue


This generates the corresponding dynamic routes for the application like this:

router: {
  routes: [
    // ...
    {
      name: 'post-id',
      path: '/post/:id',
      component: 'pages/post/_id/index.vue'
    }
  ]
}


Updating the single post file:

<!-- ./pages/post/_id/index.vue -->
<template>
  <div class="main-content">
    <div class="container">
      <h2 class="title is-2">{{ post.title }}</h2>
      <div v-html="post.content"></div>
      <br>
      <h4 class="title is-5 is-marginless">by <strong>{{ post.author }}</strong> at <strong>{{ post.published }}</strong></h4>
    </div>
  </div>
</template>

<script>
  // import posts saved JSON data
  import posts from '~/posts.json'

  export default {
    validate ({ params }) {
      return /^\d+$/.test(params.id)
    },
    asyncData ({ params }, callback) {
      let post = posts.find(post => post.id === parseInt(params.id))
      if (post) {
        callback(null, { post })
      } else {
        callback({ statusCode: 404, message: 'Post not found' })
      }
    },
    head () {
      return {
        title: this.post.title,
        meta: [
          {
            hid: 'description',
            name: 'description',
            content: this.post.summary
          }
        ]
      }
    }
  }
</script>


Nuxt.js adds some custom methods to our page components to help make the development process easier. See how we use some of them on the single post page:

  • Validate the route parameter with the validate method. Our validate method checks if the route parameter passed is a number. If it returns false, Nuxt.js will automatically load the 404 error page. You can read more on it here.
  • The asyncData method is used to fetch data and render it on the server side before sending a response to the browser. It can return data via different methods. In our case, we use a callback function to return the post that has the same id attribute as the route id parameter. You can see the various ways of using this function here.
  • As we’ve seen before, we use the head method to set the page’s headers. In this case, we’re changing the page title to the title of the post, and adding the post summary as a meta description for the page.

Great, now we can visit our blog again to see all routes and pages working properly, and also view the page source to see the HTML being generated. We have a functional server-rendered JavaScript application.

Generating Static Files

Next, we can generate the static HTML files for our pages.

We’ll need to make a minor tweak though, as by default Nuxt.js ignores dynamic routes. To generate the static files for dynamic routes, we need to specify them explicitly in the ./nuxt.config.js file.

We’ll use a callback function to return the list of our dynamic routes:

// ./nuxt.config.js

module.exports = {
  // ...
  generate: {
    routes(callback) {
      const posts = require('./posts.json')
      let routes = posts.map(post => `/post/${post.id}`)
      callback(null, routes)
    }
  }
}


You can check here for the full documentation on using the generate property.

To generate all the routes, we can now run this command:

npm run generate


Nuxt saves all generated static files to a dist folder.

Deployment on Firebase Hosting

As a final step, we can take advantage of hosting by Firebase to make our static website live in a couple of minutes. This step assumes that you have a Google account.

First, install the Firebase CLI, if you don’t already have it:

npm install -g firebase-tools


To connect your local machine to your Firebase account and obtain access to your Firebase projects, run the following command:

firebase login


This should open a browser window and prompt you to sign in. Once you’re signed in, visit https://console.firebase.google.com and click Add project. Make the relevant choices in the wizard that opens.

Once the project is created, go to the project’s hosting page at [https://console.firebase.google.com/project/<project](https://console.firebase.google.com/project/<project "https://console.firebase.google.com/project/<project") name>/hosting and complete the Get started wizard.

Then, on your PC, from the root of your project directory, run the following command:

firebase init


In the wizard that appears, select “Hosting”. Then select your newly created project from the list of options. Next choose the dist directory as the public directory. Select to configure the page as a single-page app and finally select “No” when asked if you want to overwrite dist/index.html.

Firebase will write a couple of configuration files to your project, then put the website live at [https://<project](https://<project "https://<project") name>.firebaseapp.com. The demo app for this article can be seen at nuxt-ssr-blog.firebaseapp.com.

If you run into problems, you can find full instructions on Firebase’s quickstart page.

Conclusion

In this article, we’ve learned how we can take advantage of Nuxt.js to build server-rendered JavaScript applications with Vue.js. We also learned how to use its generate command to generate static files for our pages, and deploy them quickly via a service like Firebase Hosting.

The Nuxt.js framework is really great. It’s even recommended in the official Vue.js SSR GitBook. I really look forward to using it in more SSR projects and exploring all of its capabilities.