Ashley Drake

Software Engineer

[email protected]

Twitter Github

Using Hugo Structured Data to Build a Resume Page

In this post, I’ll demonstrate how I built my Resume page using Hugo’s data directory functionality.

I have enjoyed using Hugo to set up a blog. I don’t particularly enjoy design and layout, so a system that allows me to do that one time, while still outputting an cheap-to-host static site, is nice to work with.

I wanted to set up a resume page, and give myself more control over the design than I would have using Markdown/CSS – treating the resume as a collection of data, instead of a piece of content, provided that flexibility.

What is the data directory?

Hugo is primarily concerned with managing content - blog posts, documentation, etc. - in the context of a static website. Dynamic blogging platforms (Wordpress is a great example) allow you to manage content using an interactive UI, which takes your blog post text as input and saves it to a database. To display it to a user, Wordpress will query the database and dynamically build the page at request time. What gets shipped to the server is a template, and a database of bare, un-templated and unstyled text.

A static website, on the other hand, is shipped to the server with each content page already completely built out into an HTML file. What you might lose in terms of a larger project size, you gain with speed, since there is no round-trip to a database to display a page.

A static site generator like Hugo, then, is primarily interested in providing the same convenience around templates and content - developers can maintain templates separately from their content, which saves time. The main difference is that instead of building the page each time it’s requested, the generator will build your pages once, and ship all of them to the server together.

So … how does this help us understand Hugo’s data directory?

Data in Hugo is supplemental to content. In other words, if each blog post is one piece of content, a unique page will be generated for each blog post. Data, however, is accessible from any page on my site, and therefore can be used anywhere on your site. You could reference the same piece of Data multiple times on your site, but only need to change it in one place to see those changes reflected consistently on each page.

Here is Hugo’s Documentation on the Data directory:

Setting up the data directory

We will be treating the data directory like a dataset, a collection of professional experiences, and using that dataset to build a Resume page. Each work experience has some metadata, like the name of the company you worked for, your title, a photo, and some bullet points for projects you worked on. The experiences can be ordered by time.

For now, let’s format a resume that organizes experiences primarily around “Employers.” (We’ll revisit this structure later.) Here is what you’d create inside your data directory:



A note about sorting: when converting these .yaml files into an object in memory, the files will resolve into an alphasorted order in the object, sorted by filename. We will provide an explicit sort key later in our template, but you could also prefix the filenames if you’d prefer it.

Let’s examine each component:

Root Data

Note the index.yaml file at the root of the data folder. It contains some basic information I want at the top of my resume:

  - "A sentence summarizing how you work."
  - "Another sentence summarizing how you work."
    - Python
    - Golang
    - Chef
    - Terraform
    - Kafka
    - Elasticsearch
    - Redis

You can put whatever you’d like here, regardless of whether you choose to display it. These items should be globally relevant to you as a person, not to any one experience.

Employers / Open Source

Each file inside the OSS and Employers sections represents a unique work or development experience. Here is one example:

Name: "Company ABC"
Website: "<url>"
StartYear: 2014
Image: "<url>"
Blurb: "Company ABC does X awesome thing and Y awesome thing."
  Software Engineer:
    Title: "Software Engineer"
    Start: "2021"
    End: "current"
      - Go
      - Erlang
      - "I do X, Y, and Z awesome things at ABC."

Note that “Roles” is a list of objects, even though there’s currently only one Role. Depending on how you want to set up your resume, you might consider each “title” a separate work experience, which is generally recommended if you get a promotion – that way you can speak to how you stepped into a new level of responsibility, etc. by listing new projects separately under each title.

Here is an example with multiple roles:

Name: "Company"
Website: "<url for company>"
StartYear: 2015
Image: "<url for image>"
Blurb: "Blurb about the company"
  Senior Software Engineer:
    Title: "Senior Software Engineer"
    Start: "2020"
    End: "2021"
      - ...elided
      - ... elided
  Software Engineer:
    Title: "Software Engineer"
    Start: "2017"
    End: "2020"
      - ...elided
      - ..elided

Fantastic. Now we have our data to work with. We only have to worry about writing it once, and we can mentally shift our focus to templating and presenting it.

Using the data

Now that our structured data is set up, let’s use it.

In a Hugo template, the Data object is available at the root of your site, which is accessed in mustache templates at .Site, i.e. {{ range .Site.Data.resume.employers }} would range over the employers collection as we set it up above.

Let’s start with the root item in the folder, data/index.yaml, which contains the “header” material for the Resume.

Setting Up a Single Page in Hugo

First, we need to figure out how to tell Hugo to make our resume a single page. This section presupposes a little bit of Hugo domain knowledge, but not too much more than what can be learned in the Quick Start. This page on Layout lookup order may also be helpful.

In my blog, I have “resume” set up as a content type, so to render it as a static page under http://<>/resume, I did this:

  • gave it a single.html layout template, under layouts/resume/single.html
  • put one content index page directly under the Content folder, i.e. content/ without making a folder for multiple resume pages
  • (optional) placed any partial templates used in layouts/resume/single.html under layouts/partials/resume/

Here is what the index page in content/ looks like:

title = "<Page Title here>"
layout = "resume"
type = "resume"

This setup tells Hugo that I only have one static page that I want to display here, not a collection of content. The single.html template is the default template Hugo uses when looking to display a single piece of content. The front matter in makes sure Hugo uses my single.html template.


Next, let’s set up the base template for the resume, inside layouts/resume/single.html. It is responsible for rendering the root content (the “about me” we wrote earlier), and then calling the two partial templates we will soon set up for work and oss. You can certainly put all of your template in the same file to start with, but we’ll use separate pieces here since they’re easier to read.

{{ define "main" }}
  <section id="resume" class="resume">

    <h2>About Me</h2>
    {{ range .Site.Data.resume.index.Summary }}
      <p>{{ . }}</p>
    {{ end }}

    <h2>Primary Skills/Tools</h2> 
    {{ range $key, $vals := .Site.Data.resume.index.Skills }}
      <ul id="skills"><span>{{ $key }}</span> 
        {{ range $vals }}
          <li>{{ . }}</li>
        {{ end }}
    {{ end }} 
    {{ partial "resume/work.html" . }} 
    {{ partial "resume/oss.html" . }}
{{ end }}

The define statement says that this is the main block for the resume page, which is rendered into layouts/_default.html like this:

So when the URL is, Hugo looks for the resume content section, sees there is a root page, and renders the single page for it by using your resume/single.html template with the main directive you defined for it, which is then templated into layouts/_default.html, the root template for your whole site.

Let’s look more closely at how we use the data in the template we just wrote, starting with our summary:

{{ define "main" }}
  <section id="resume" class="resume">
    <h2>About Me</h2>
    {{ range .Site.Data.resume.index.Summary }}
      <p>{{ . }}</p>
    {{ end }}

The range keyword loops over a list or object, much like for ... in in Python. The {{ . }} directive just uses the item that’s currently in the loop variable directly. You’ll see later how that looks when the item currently in the loop is itself a structured item; here, it’s a string, since each item in the Summary list in our .yaml file was a String. We turn them into a paragraph each.

For Skills, we set it up as a map / dictionary. It has keys and values, which we can also render into our page, like this:

    {{ range $key, $vals := .Site.Data.resume.index.Skills }}
      <ul id="skills"><span>{{ $key }}</span> 
        {{ range $vals }}
          <li>{{ . }}</li>
        {{ end }}
    {{ end }} 

Template Partials

Now, let’s add the work sections. We’ll go ahead and make another partial template for those, to make them easier to manage:


This template will contain our professional work experiences. In this file, we’ll do:

  • range over Site.Data.resume.Employers and print the metadata for each employer
  • range over the Roles inside each Employer object

Here’s how it looks:

<h2>Work Experience</h2>

 {{ range sort .Site.Data.resume.employers "StartYear" "desc" }}
  <h3>{{ .Name }}</h3>
  <a href={{ .Website }}>{{ with .Image }}<img class="thumbnail" src={{ . }}>{{ end }}{{ with .WebsiteText }}{{ . }}{{ end }}</a>
  <p>{{ .Blurb }}</p>
  {{ range .Roles }}
    <h4>{{ .Title }}<span class="u-pull-right">{{ .Start }} {{ with .End }}- {{ . }} {{ end }}</span></h4>
    <span class='technologies'>{{ range .Technologies}} {{ . }} {{ end }}</span>
    <p>{{ .Blurb }}</p>
      {{ range .Bullets }}
        <li>{{ . }}</li>
      {{ end }}
  {{ end }} 
{{ end }}

We sort the experiences by the sort key we provided in our data file. This helps us display items in chronological order. The with keyword allows us to conditionally render those blocks, if that item is present. Note that each is followed by {{ end }} to mark the end of the conditional block.

Note that the term .Blurb appears twice, but references two different values. The top-most .Blurb is the one we wrote for each employer experience. Then, each Role also contains a .Blurb. As we range over .Roles, we can reference a key in the root level for that Role directly, e.g. .Title and .Blurb are available per-Role. That’s because the . now refers to each Role object, instead of one string.

Putting it all Together

Once we have all of our employement data entered, and our templates declare what to display and where, you can style each element as you please. In the templates above, the main <section> tag is identified and tagged with the resume class, which helped isolate any resume-specific styles in my CSS.

Run hugo server, navigate to, and check it out!

Pretty cool, right?

Other Resume Formats

This is by no means the only way to set up a resume. Reverse-Chronological with categories like “Education,” “Professional Experience” is a popular format, but another format might suit your better, like a Story format. If you have internships or work experience interleaved with school, or have a side business, you might prefer a story format, which orients everything on one central timeline. Stack Overflow’s Developer Story is a good example of this format.

Hope this was helpful - there are so many ways to approach building a Resume, and this format supports a lot of them very easily. I’d love to see what you come up with!