Building Portfolio Site Using Gridsome

17 Jul 2020

My last portfolio site was designed in 2017 using plain HTML and CSS, I made it quickly just to make sure I have a portfolio site as a freelancer, straight out of college. Since then I was learning more front end technologies such as ReactJS on my full-time work experience, and VueJS for my side projects.

I was learning React out of neccessity—because work requirements, industry standards, while learning Vue accidentally. Turns out developing websites and web apps with Vue was much more fun and easier. For comparison, I learned React basics for 6 months, while learning Vue only costs me 2 weeks or so (I guess the experience learning React helps). It was that much easier, and by no means any lesser in features.

So in the end of 2019 I'm getting out of my full-time job and back with my side projects and business. I needed a new personal website. I knew I wanted to use Vue for front-end, but what about the back-end? The only server tech I'm familiar with and suits the job would be Laravel.

So I began looking for how to built CMS in Laravel. I found twill.io which is used by Pentagram, but I didn't think I need that much capability and complexity. I need something simple and fast. It also means I need to maintain a Laravel server and its database.

I knew about this static website builder called NextJS—built on React, and NuxtJS—its Vue equivalent. There's also this newer thing called Gatsby (React), and again its Vue equivalent: Gridsome.

I also looked into Hugo (golang), but I don't bother to learn whole new thing for this, and I wanted something quick and familiar. I also consider using Webflow, but as a developer I want the whole code flexibility for my portfolio site.

Making static site using Gridsome also means lower hosting cost for me as I don't need full server access capabilities. I can use shared hosting with cPanel and Filezilla access, which only cost me IDR 15000 (USD 1~) per month.

Why and What is Gridsome?

First, it's quick, easy and creates blazing fast static and dynamic website, no database needed with markdown abilities. It takes what I already knew about Vue and making it powerful with data.

This is the definition of Gridsome:

Gridsome is a Vue-powered static site generator for building CDN-ready websites for any headless CMS, local files or APIs

Compared to Nuxt, Gridsome is simpler in a way. Nuxt has more option how to render site: Server Site Rendering, Static Site Generation, and Single Page Application. While Gridsome belongs to Static Site Generation, in a category called JAMstack—JavaScript, APIs, and Markup. Nuxt is more suitable for large scale web application, while Gridsome is for something quick like a personal website.

It's still in early state of development (version 0.7.19 as of writing) but already has all the features you need for a portfolio site. What sold me on Gridsome is the GraphQL, markdown, array of plugins for connecting many data sources, available templates, and the obvious Vue power!

Read more on Why Gridsome?

Ready Made Templates

The rest of this article will be a long tutorial with codes. If you just want to get quickly up and running, don't want to make everything from scratch, you can check Gridsome starters and get an almost instant site!

Few templates worth checking: Gridsome Blog Starter, Forestry Starter, Gridsome Minimal Markdown Starter, and Darkfolio.

Building the Portfolio Site

I'll show you the important building parts so you can build a portfolio site using Gridsome from scratch. These are the important building blocks I wished I've known when learning Gridsome:

  1. Creating Content
  2. Connecting Content Source
  3. Creating Pages
  4. Linking
  5. Making Page Layouts
  6. Listing Content in a Page
  7. Viewing Single Content
  8. Working with Tags

Gridsome Installation

Here's the quick start guide:

  1. npm install --global @gridsome/cli to install Gridsome CLI tool
  2. gridsome create my-portfolio-site to create a new project
  3. cd my-portfolio-site to open the folder
  4. gridsome develop to start a local dev server at http://localhost:8080
  5. Create .vue components in the ./src/pages directory to create pages
  6. Use gridsome build to generate static files in a ./dist folder

Learn more...

After initiating Gridsome project, I also install these dependencies:

  1. @gridsome/vue-remark : The power of Vue component inside Markdown file.
  2. gridsome-plugin-tailwindcss : Utility-first CSS, use ready made CSS classes instead of inventing your own names
  3. @tailwindcss/typography : Instant great looking typography for blog post (with customization capability)
  4. gsap : Animation power! (Optional, I'll use it on later tutorial)

Directory Structure

Each .vue file and folder inside /pages will get their own route. For each new content, you have to create their own markdown .md file under designated folder, in this case /content.

📦project-name
 ├─📂content // markdown content files
 │ └─📂posts
 │ │ └─📜building-portfolio-gridsome.md
 │ └─📂projects
 │ │ └─📜first-project.md
 ├─📂src
 │ └─📂assets // css files, images
 │ └─📂components // Vue components
 │ └─📂layouts // page layout
 │ └─📂pages // top level pages
 │ │ └─📜Index.vue // home page
 │ │ └─📜About.vue
 │ │ └─📜Blog.vue
 │ └─📂templates // single content page template
 │ │ └─📜Post.vue
 │ │ └─📜Project.vue
 │ └─📜App.vue
 │ └─📜main.js
 ├─📜tailwind.config.js
 ├─📜gridsome.config.js
 ├─📜gridsome.server.js
 ├─📜package.json
 ├─ ...

Creating Content

First, we'll be making the markdown content we needed under /content folder. For blog posts, each post will be a markdown file like this: /content/posts/blog-post-title.md.

Each markdown file consist of 2 parts: front-matter and the content itself. These part marked by three em-dashes (---). Front-matter consist of attributes that can be collected and represented as data using GraphQL. For a blog post, the front-matter will be: title, summary, thumbnail, slug, year, month, date, tags.

For this post, this is the markdown file:

title: "Building Portfolio Site Using Gridsome"
summary: "..."
thumbnail: "..."
slug: "building-portfolio-gridsome"
year: "2020"
month: "7"
date: "17"
tags: ['development', 'gridsome', 'tutorial']
---

My last portfolio site was ...
(markdown content continues)

Connecting Content Source

Here in Gridsome Config we can add all the plugins we installed earlier. This includes the vue-remark and gridsome-plugin-tailwindcss.

For each markdown content type (say we have Post and Project), we need to register them here. baseDir indicates where to read the markdown files from. pathPrefix will be added to the website URL (domain.com/prefix/content-post-here).

After making changes on Gridsome Config always restart your dev server for it to apply the changes. You can kill the process (Ctrl+C or cmd+C) then restart gridsome develop.

Go ahead and open gridsome.config.js and put this code:

module.exports = {
  siteName: 'Portfolio Site',
  plugins: [
    {
      use: '@gridsome/vue-remark',
      options: {
        typeName: 'Post',
        baseDir: './content/posts',
        pathPrefix: '/blog',
        template: './src/templates/Post.vue'
      }
    },
    {
      use: '@gridsome/vue-remark',
      options: {
        typeName: 'Project',
        baseDir: './content/projects',
        pathPrefix: '/project',
        template: './src/templates/Project.vue'
      }
    },
    {
      use: 'gridsome-plugin-tailwindcss',
    },
  ]
}

Creating Pages

For each pages created inside /pages, Gridsome will detect them and make the routes. So if you create AboutMe.vue, you can access them on URL domain.com/about-me. Index.vue will be homepage, or default for that folder. If you create a file under folder: pages/lettering/Index.vue, it can be accessed from domain.com/lettering.

Here's my Index.vue

<template>
  <Layout>
    <div class="flex flex-col lg:flex-row">
      <div class="mt-16 lg:w-1/2 text-xl sm:text-2xl md:text-3xl lg:text-4xl">
        <p class="max-w-md lg:max-w-3xl lg:pr-16 lg:mt-8">Hello! I'm Laurensius, a lettering artist, type designer & web developer.</p>
        <p class="max-w-md lg:max-w-3xl lg:pr-16 mt-4">I’m open to projects and collaboration, feel free to contact me.</p>
      </div>
      <div class="mt-16 lg:w-1/2 text-5xl lg:text-6xl xl:text-8xl uppercase font-bold mb-40">
        <div class="block">
          <g-link class="link text-current leading-none" to="/studio">Studio</g-link>
        </div>
        <div class="block">
          <g-link class="link text-current leading-none" to="/lettering">Lettering</g-link>
        </div>
        <div class="block">
          <g-link class="link text-current leading-none" to="/blog">Journal</g-link>
        </div>
        <div class="block">
          <g-link class="link text-current leading-none" to="/about">About</g-link>
        </div>
      </div>
    </div>
  </Layout>
</template>

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

Linking

Notice those <g-link> components on the page code above. It's a component provided by Gridsome. From the docs: <g-link> uses IntersectionObserver to prefetch linked pages when link is in view. This makes browsing around in a Gridsome site very fast because the clicked page is already downloaded.

Making Page Layouts

Notices the use of <Layout> component, it's from /layouts/Default.vue. You can create new layout by making a file inside /layouts then register them on /main.js.

Here's StudioLayout.vue a custom layout I made for /studio pages.

<template>
  <main class="max-w-full min-h-screen relative mx-5" role="main">
    <StudioHeader/>
    <slot/>
  </main>
</template>

<script>
import StudioHeader from "@/components/StudioHeader";
export default {
  components: {
    StudioHeader
  }
}
</script>

Then I register them on main.js

import DefaultLayout from '~/layouts/Default.vue'
import StudioLayout from '~/layouts/StudioLayout.vue'
import "./assets/css/main.css";

export default function (Vue, { router, head, isClient }) {
  Vue.component('Layout', DefaultLayout)
  Vue.component('StudioLayout', StudioLayout)
}

Then I can use it on pages I want to be different than the default layout, in this case /pages/studio/RetailFonts.vue

<template>
  <StudioLayout>
    <h1 class="text-4xl lg:text-6xl uppercase leading-none mb-4">Retail Fonts</h1>
    ...
  </StudioLayout>
</template>

<script>
export default {
  metaInfo: {
    title: 'Retail Fonts'
  }
}
</script>

Listing Content in a Page

Now, we can start getting and presenting data from /content folder. We'll be displaying blog posts using GraphQL to get the data with <page-query> inside the page /pages/Blog.vue.

<template>
  <Layout>
    <h1 class="text-4xl lg:text-6xl uppercase leading-none mb-4">Journal</h1>
    <div class="mb-4" v-for="post in $page.posts.edges"
      :key="post.node.id">
      <span class="md:w-24 lg:w-32 block md:inline-block text-sm sm:text-lg lg:text-2xl text-secondary">
        {{ `${post.node.year} ${post.node.month}` }}
      </span>
      <g-link class="text-3xl sm:text-5xl lg:text-6xl link text-current"
        :to="post.node.path">{{ post.node.title }}</g-link>
    </div>
  </Layout>
</template>

<page-query>
query {
    posts: allPost {
    edges {
      node {
        id
        path
        title
        summary
        year
        month
      }
    }
  }
}
</page-query>

Here's what you need to know about the GraphQL query above.

posts is the variable we'll be using in the page itself. allPost is like a template function given by Gridsome, if you want to get other type of content change it into content names prefixed with all such as allProject. Inside node are attributes from the Frontmatter inside markdown files. Only listed attributes will be available in the pages, so you need to list each one of them.

After getting the data, we loop through the posts to show the title with links using v-for="post in $page.posts.edges".

Viewing Single Content

Now that the index page is done, we need to view the individual blog post. Let's create Post.vue files under /templates folder. Gridsome will detect this template belongs to content type Post.

<template>
  <Layout>
    <article class="prose md:prose-md lg:prose-lg mx-auto pt-20 pb-40 font-body font-light">
      <h1 class="font-sans font-medium">{{ $page.post.title }}</h1>
      <small class="text-secondary tracking-wider">{{ $page.post.date }} {{ monthToWord }} {{ $page.post.year }}</small>
      <VueRemarkContent />
    </article>
  </Layout>
</template>

<page-query>
metaInfo() {
  return {
    title: this.$page.post.title
  }
},
query Post ($id: ID!) {
  post(id: $id) {
    title
    summary
    year
    month
    date
  }
}
</page-query>

<script>
export default {
  computed: {
    monthToWord() {
      const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
      return months[this.$page.post.month-1]
    }
  }
}
</script>

Like a normal Vue component, each .vue files have access to <script> and <style>. In this example I'm using a computed to get month data and show them as word instead of numerical. You can access the page data using this.$page.

Notice in the page-query we're using a variable ID on query Post ($id: ID!) to get the page we needed. There's also the <VueRemarkContent /> component that will render the markdown content there, and Vue component inside markdown because we're using the vue-remark plugin. I'm also using tailwind typography plugin here with the CSS class prose to get instant beautiful blog typography.

Working with Tags

To make the tags working, we need to add the reference in gridsome.config.js. Under plugins, for the Post you can change it into this.

{
  use: '@gridsome/vue-remark',
  options: {
    typeName: 'Post',
    baseDir: './content/posts',
    pathPrefix: '/blog',
    template: './src/templates/Post.vue',
    refs: {
      tags: {
        typeName: 'Tag',
        create: true
      }
    }
  }
}

Now restart Gridsome CLI and the data will be available for you. We can check them in http://localhost:8080/___explore.

You can get Post with certain tags using this query:

query {
  allPost(filter: { tags: { contains: ["tutorial"] }}) {
    edges {
      node {
        title
      }
    }
  }
}

Or you can list all the tags:

query {
  allTag {
    edges {
      node {
        id
      }
    }
  }
}

Then making a page that displays all post with a certain tag:

query ($id: ID!) {
  tag(id: $id) {
    title
    belongsTo {
      edges {
        node {
          ... on Post {
            id
            title
            path
          }
        }
      }
    }
  }
}

Note: on bottom left page on GraphQL explore add this to QUERY VARIABLE to try the query above:

{
  "id": "tutorial"
}

Wrapping Up

By this stage you should be able to creating your own portfolio site. I hope this post is enough to getting you off the ground using Gridsome.

Thanks for reading.

Built with 💖 using Gridsome