Note: this tutorial content is taken from the Hugo tutorial of the same title
For many the most powerful part of Hugo is its amazing support for structured data files. This feature allows Hugo to truly replace sites dependent on low-volume databases and gain all the advantages of hosting your site on a full CDN hosting provider.
Many beginners, however, do not realize that to use data files
effectively still requires the minimal use of
content files.
This tutorial suggests one way to minimize the use of content
files by linking your data files to Hugo content
types. We use
substr on a lesser known—but
very useful—.File.LogicalName template variable in the
layouts/TYPE/single.html template to link to a matching .Site.Data
object that has the same base name as the nearly-empty content file
(ex: content/student/steve.md <-> data/student/steve.toml).
This way you don't have to put anything in the front-matter of the
content file. Yeah, I thought I saw you smile.
Often the easiest answer is just to not use the data directory
and put whatever was in your TOML/JSON/YAML files into the front
matter of your content documents. (If all of that sounds confusing
you might want to go back and read about
it.)
Just using the content directory has the advantage of keeping all
the content in one place but starts to fall down as the complexity
of the data on which your site is built increases—particularly if
you are sharing your site data externally or deriving it from other
data sources before Hugo even touches it.
This data-centric (over content-centric) approach is becoming more common as static sites continue to efficiently and effectively replace the overly complex and expensive dynamic database + caching server architectures out there. Indeed, this is the same reasoning behind the increase in JSON Web and REST APIs in general.
As we've read on the
forums,
some developers of complex Hugo sites, (which we likely will never
see because of their enterprise-ness), are actually forced to design
their Hugo site structure entirely from the data directory because
the precious, primary source of the data is behind an API or secure
data store that requires it to be "dumped" and validated before
Hugo builds it.
This not only requires the data directory be used but also a
structure to the data that can be easily validated. In a sense,
this data dump-and-validate step covers more robust data validation
constraints that are normally implemented in the database itself or
application business logic.
You can probably see where this is going. Most data is read and
viewed way more often than it is written. Hugo workflows are being
combined with watchdogs and cron jobs that auto dump to data
triggering a live data validation process followed by a Hugo compile
and push deployment.
In this sense Hugo sites are just what traditional database people would see as "views" of the data. This concept of "views" to organize and simplify large amounts of data is a long-standing best-practice and architecture. Hugo just happens to be perfect for the job.
You should have a firm grasp on Hugo source organization and how data files work. This tutorial uses the beautiful TOML, which was designed for structured data files that are maintained by humans. If you are tying into other data sources you might want to use JSON, but maybe not.
Ok, let's get on with it. If you prefer you can fork the tutorial site we are making. You can also view the working demo.
In this example, we'll create a few logical persons in different
roles: student, admin, creator, and user. Start by making some
nearly empty content/person/ files:
data/person/spf13.mddata/person/bep.mddata/person/robmuh.mddata/person/betropper.md
All you need in each one is the empty front-matter section:
+++
+++You can obviously make these with a simple script:
for i in spf13 bep robmuh betropper;do printf "+++\n+++\n" > $i.md; doneAlthough content files can also end in .html we stick with .md
so we have a consistent suffix we can chop off later with
substr.
That's it for the content for the individual person types. The rest
will come from the data/person files.
Now we need the data/person TOML files to match:
data/person/spf13.tomldata/person/bep.tomldata/person/robmuh.tomldata/person/betropper.toml
We'll have to do these by hand, as we would if building up a real site. But thankfully this is the only place we need to remember to make any changes. Here's one file as an example:
name = "Rob Muhlestein"
github = "robmuh"
roles = ["student","user"]This is enough of a data file to illustrate the point. Make sure you understand the data file concept fully to get the most out of it. It is really amazing.
We use partials instead
of content views, which are
older than partials and require
.Render.
These days partials are almost always preferred for their flexibility
by allowing the passing of context, (which .Render infers instead
causing it to fail when using .Site.Data and not .Data.Pages).
First create the partials directory to keep things organized:
mkdir layouts/partials/personNow create a partial that just contains the markup for the default
person view. We will later pull this in from the content layout
single.html. We'll name our partial "block" to imply the use of
BEM methodology So
layouts/partials/person/block.html would contain:
<ul>
<li>Name: {{ .name }}</li>
<li>GitHub: <a href="https://github.com/{{ .github }}">{{ .github }}</a>
<li>Roles: {{ delimit .roles ", " }}</a>
</ul>Nothing fancy really just to make the point. Notice how clean all the references are.
Here's where the magic happens and we link it all together. Now is a good time to make sure you understand what a Hugo Content Type is.
First, get into the layouts/person directory. If you haven't made
one yet, make it. Now add something like the following template
HTML to the single.html file:
{{ with (index .Site.Data.person (substr $.File.LogicalName 0 -3)) }}
<!doctype html>
<html>
<head>
<title>{{ .name }}</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
{{ partial "person/block" . }}
</body>
</html>
{{ end }}There are obviously lots of variations of this but you get the idea.
The first line is the key, literally. This index Go text template
function looks up
the item in the .Site.Data.person map that has the index key
matching the base file name (aka substr .File.LogicalName 0 -3) of the content
file that triggers the generation of the HTML that will be served
up from http://localhost:1313/person/robmuh for example.
Notice substr chops the last three characters off of .File.LogicalName,
which is why we name all our content/person files with .md at
the end.
If everything is set you should be able to run hugo server and
pull it up at http://localhost:1313/person/robmuh. Keep in mind
that http://localhost:1313/person will not yet work. We'll do
that next.
Hugo calls collections lists and there are many places Hugo will
look for these special collection
view templates, which originally were only known as lists but
have come to be called sections as well. Sections seems to be the
preferred name now since themes only support a
themes/THEME/layout/section directory. (While we are not using
themes in this tutorial you really should start with a theme because
it doesn't hurt.)
Before we create the layout/section page let's make a
layouts/partials/person/li.html that we can easily use in it:
<li><a href="/person/{{ .github }}/">{{ .name }}</a></li>That's it. Short and sweet. Notice we linked to a relative link
matching the .github user name. We'll use github as our unique
identifier for "persons", but that sounds weird so let's fix it with
a permalink and configuration addition.
This is where we run into a limitation in the theme system somewhat.
There is something of a debate about whether permalinks should be able
to be dictated by a theme or not. Currently the theme does not have
this ability so you need to add them to your config.toml file. While
we are at it let's add a .Site.Title:
title = "Testing Hugo Data Linked To Content Type"
[permalinks]
person = "/:filename"Since a person is a fundamental data type we don't have a problem
with permalinking its unique identifier.
Now you can remove the person portion of the link in the li.html
partial view:
<li><a href="/{{ .github }}/">{{ .name }}</a></li>Throw the following into your /static/styles.css if you like:
body {
margin: 5rem;
font-family: "Helvetica", "Arial", sans-serif;
background: lightgrey;
}
li {
padding .2rem;
font-weight: bold;
list-style-type: none;
}
a {
text-decoration: none;
color: grey;
}
a:hover {
color: goldenrod;
}Ah, much better, well at least a little.
The logical idea of a person looks good in UML and data design
diagrams but no one really uses it. What they really want is to see
what role that person has in the organization. In our tutorial there
are admins, creators, users, and students, each potentially
with extra data related to that role inside their individual
data/person files. But how do we create collection views for these
different roles? This is where content list
templates, which some prefer to call
section templates, come in.
Like everything else in Hugo, nothing gets built without something
being in the content directory. In the case of our collection
section view all we need is a directory that matches the name of
the section we want containing a single empty file that we'll call
on.md:
mkdir content/students
touch content/students/on.mdNotice we named our section students instead of student. Try
http://localhost:1313/person in comparison. Notice the title is
"persons". This is a case for setting pluralizelisttitles to
false in your config.toml. If this isn't enough you can add
another http://localhost:1313/people view using the same steps as
those we are doing for students now.
This is the same as earlier just with a logical filter to only include
those person data objects that have student in their roles. To
save time just copy the layout/sections/person.html file to
layout/sections/students.html, which will look like this when you
are done with it:
<!doctype html>
<html>
<head>
<title>Students</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<h1>Students</h1>
<ul>
{{ range .Site.Data.person }}
{{ if in .roles "student" }}{{ partial "person/li" . }}{{ end }}
{{ end }}
</ul>
</body>
</html>Obviously a lot of this should be factored into partials such as a
top and bottom so that other views can be quickly added but
this conveys the idea.
Trade Offs: Unfortunately the grouping
functions you
may have become accustomed to when dealing with .Data.Pages simply
don't work with .Site.Data and probably never will. But we really
don't need them because we can use the standard range sort, which is
probably a better habit and dependency anyway.
Beware: Where will not work here because, and let's emphasize
this, "where
only works on lists, taxonomies, terms, and groups". .Data.Pages
is a list so it works. The way we have organized our data/person
files means .Site.Data.person is a map, not a list.
Now that we've created the students data view (or section) you can
experiment with tweaking everything to add views for users,
creators, admins, and people.
At this point you should have all the boilerplate you need to get a solid, data-driven Hugo site up and running. The data model is up to you. Perhaps you have it already polished in UML, perhaps not. But this approach affords the most flexibility and efficiency when implementing changes to the most important part of many complex sites, the data.