Frontend Development

How we built our company blog with Vue and Nuxt

Nick Ciolpan

Nick Ciolpan

We're an all design and code craftsmen agency, an experienced team of software engineers and designers that generate unique user experiences every day.

We decided that sharing from our experience with others, should be something we should do more often. In order to do this, we would have to switch our blog from Medium to and integrate it with our site. This would allow us to give our readers a more polished experience, and at the same time to make our posts more versatile and unique.

We have a long tradition of designing and developing WordPress sites. This time, as our website is built in Nuxt, we decided not to go with WordPress and build something from the ground up to suit our needs. We've been experimenting with various possible alternatives for WordPress. Among the shortlisted, we had Craft, Gatsby, Hugo as well as Strapi and Grav. After a while we decided none of them was suited for what we were trying to achieve.

After years of rolling out content management systems for our clients, we knew exactly what we didn't want from our blog:

  • To be concerned with issues like whether the project we're building on is maintained, extensibility or plugin availability.
  • A database, we prefered instead a hassle-free approach to persistence and forget about database backups, fragile schema updates or rollbacks.
  • A fancy admin interface, as we're all more comfortable writing plain code. This would also allow for more flexibility and custom unique posts.
  • More server processes. Bonus: There's not much you can hack on a static site.

What we wanted instead:

  • A review and publishing process that feels closer to our way of work.
  • Blazing fast page navigation and content load times. This was a critical requirement.
  • One command, no hassle deploys.
  • A modern tech-stack our team can enjoy.

To sum it up, we wanted a truly static site generator with the minimum viable blog functionality that adds no extra complexity layer for our development team.

Cloud is just another people's computer and serverless is just other peoples' servers. Don't get us wrong, we love the scalability and pricing tiers that come with cloud computing. We just wanted to keep it simple this time.

Since we already had Vue and Nuxt in our tech stack, as the website was built on it, we decided to use it as a base for upcoming integrations. After "extensive research" we concluded VuePress is the closest thing to what I need. But not quite. So, we went on to devise our own custom solution.

Our custom solution needed to feature the following concepts:

  • Blog categories
  • Blog articles
  • Related articles
  • Search functionality
  • Intuitive content navigation

Other aspects we were concerned about:

  • Good discoverability and SEO
  • Server-side rendering
  • Reusable Vue components

As most of the content on our blog will include work-related experiences, to a certain degree, the categories mirror Graffino's services. For example, as Frontend is a core service at Graffino, it will most certainly have its own category.

If you're not familiar with how Nuxt works, I recommend going through the official docs at nuxtjs.org to learn more about its intended use and structure. We'll try to provide you with the short version here:

The unique selling point of Nuxt is an architecture that promises to make server-side rendering a breeze. It is pure Vue with a dash of pre-configuration and a simple build system. The directory structure represents a core concept of its architecture and it will drive the aspect of the router.

Folder hierarchy

    
      
              pages/
              --| web-development/
              -----| index.vue
              -----| how-we-built-our-company-blog-with-vue-and-nuxt.vue
              --| index.vue
              
    
  

In this scenario, every file under pages will be collected and turned into a page. Every folder becomes a parent route. The immediately nested index will become that parent route's template. Then, every other named file, such as my-page.vue will turn into a route of itself with a parent route corresponding to the parent folder name. Does it make sense?

With this in mind, now index.vue makes now perfect sense to be promoted to be the category template of our future blog.

All other sibling files to index.vue will be considered articles of the containing category and will be listed under graffino.com/frontend/my-super-duper-frontend-article.

Alright! What do we have so far? We have an article and a category along with a logical route to navigate their URL locations. But we still lack some basic blog functionality, such as related content and content search, which are critical for a satisfying blog reading experience.

Let's start with the first issue, the related content display. In our case, content is related by membership to the same category. If only we had a way to make our pages aware of their siblings without a database... There is!

To achieve this, we will need to make use of Vue and Nuxt internals and take advantage of the router's omniscience about URL paths and file hierarchy.

Related articles

    
      
                ...
                created() {
                  const currentRouteName = this.$router.history.current.path.match(/\/([a-zA-Z0-9'-]+$)/)[1]
                  const parentRouteName = this.$router.history.current.name.replace(`-${currentRouteName}`, '')
                  const articles = []
                  this.$router.options.routes
                    .filter(
                      (route) =>
                        route.name.includes(parentRouteName) &&
                        route.name !== this.$router.history.current.name &&
                        route.name !== parentRouteName
                      )
                      .map((article) => {
                        article.component().then((component) => {
                        articles.push({
                        path: article.path,
                        component,
                      })
                    })
                  })
                  this.$set(this, 'articles', articles)
                },
                ...
              
    
  

Here's what's happening in the snippet above:

  • 1. Get the current route name so we can exclude it at a later time.
  • 2. Get the parent route's name which will be used to represent the category
  • 3. Get all the routes and filter out those whose parent does not match our subject.
  • 4. We'll then create an array with all these and have them ready in order to render the article list at a later time.

Here's the catch: we load the entire component before we hurl it in the array. It might seem we create the whole jungle in our pursuit of capturing the gorilla, but let's not optimize things too early. We have a couple of indisputable advantages by doing so: all the components' data is now available for use (thumbnails, alternative titles, content excerpts, authors).

We can now loop through our article collection and display them however we please.

Loop through related articles

    
      
              <nuxt-link :to="article.path" class="category__article">
                {\{ article.component.data().title }\}
              </nuxt-link>
              
    
  

If we want to bring all the articles within a specific category, the code will be very similar to our previous snippet. Let's take this chance and add a small but useful feature.

We might find it handy to have some kind of flag that allows us to keep articles as drafts before publishing or quickly disable a piece of content for maintenance without having to delete the whole thing.

Get all articles from a category

    
      
              ...
              created() {
                const currentRouteName = this.$router.history.current.name
                const articles = []
                this.$router.options.routes
                  .filter((route) => route.name.includes(currentRouteName) && route.name !== currentRouteName)
                  .map((article) => {
                    article.component().then((component) => {
                      if (component.meta && component.meta.indexed === true) {
                        articles.push({
                        path: article.path,
                        component,
                      })
                    }
                  })
                })
                this.$set(this, 'articles', articles)
              },
              ...
              
    
  

Now that we have a way to list our articles and a way to keep our user reading by further suggesting them interesting, related content, let's proceed to another important piece of functionality: Search.

For the site navigation, we stuck to a service list that is embedded both in the footer of the site as well as the header, and a search widget that displays both articles and matching categories. It does not do full body searches but does a good job matching against titles and keywords.

Search

    
      
              ...
              search() {
                const routes = this.$router.options.routes
                const query = this.searchQuery
                const results = []
                routes.map((result) =>
                  result.component().then((data) => {
                  if (
                    data.meta &&
                    data.meta.title &&
                    data.meta.indexed &&
                    data.meta.keywords &&
                      (result.path.split('-').join(' ').toLowerCase().includes(query.toLowerCase()) ||
                        data.meta.title.toLowerCase().includes(query.toLowerCase()) ||
                        data.meta.keywords.toLowerCase().includes(query.toLowerCase()))
                      ) {
                        results.push({
                        path: result.path,
                        component: data,
                      })
                    }
                  })
                )
                this.$set(this, 'results', results)
              }
              ...
              
    
  

That's about it!

Version control and publishing

We will use GitHub to review, contribute, edit, and publish all new articles. Each article becomes a pull request in which all team colleagues are invited to contribute with what they know best: content editing, visual enhancements, or if necessary, code review.

As soon as the pull request gets tagged as ready to merge, the article is ready to be merged and deployed at the assigned go-live date.

This flow fits our way of work nicely. We're not adding any new levels of complexity as the tools we're employing are already known to our team, because we're using them on a daily basis. No admin panels, no advanced settings, or obscure text revision ui controls.

Results & Impressions

Overall, the blog is blazing fast. You cannot believe how smooth it feels. There's zero loading time between page transitions and while we lazy-load most content, the load times are, again, unnoticeable. We were expecting this kind of experience, since there are no requests made to the server or database.

The full transparency of the code puts zero limits on the creative potential of the contributors. Anything goes: partial or full customization of design and layout, math functions in the subject of an article, or custom interactivity done in JavaScript. All this to create a unique reading experience.

The development of template pages with the Vue stack didn't take any more time that would have took to do it in WordPress for example.

There are a couple of improvements that we would like to implement in the future:

  • Avoid loading the same components multiple times. For example, a section of the results returned by the search widget might already be present in the article list or related article list.
  • Introduce pagination. This will improve the site performance as well as the user experience.
  • We still have a small node process that takes care of email sending. We're planning on moving this to AWS Lambda functions or switch to plain old mailto:.
  • Add an archive-like functionality to allow for time-based navigation.
  • List all articles for a specific author.

Other concerns:

  • We're loading a bunch of components in memory. This can backfire at some point in the future.
  • Migrating away to another platform in the future won't be a straightforward task.

All being said, taking this approach instead of settling for a more classic solution has been a very rewarding experience. It was fun, It was challenging, and the results are fantastic.

Building it in Vue and Nuxt is a central selling point.

Amidst the rise of infinitely new contenders to front-end SPA development hegemony, not to mention some of the more classic competition like Angular and React, for us, Vue remains the more elegant and more productive framework.

Thanks for reading this. Let us know your thoughts on Twitter.

You deserve a better brand.

We’ll build something beautiful that converts!