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 person
s in different
roles: student
, admin
, creator
, and user
. Start by making some
nearly empty content/person/
files:
data/person/spf13.md
data/person/bep.md
data/person/robmuh.md
data/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; done
Although 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.toml
data/person/bep.toml
data/person/robmuh.toml
data/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/person
Now 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.md
Notice 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.