Multipart Articles
Hugo is quite flexible when it comes to how website content is structured. To illustrate this point, a new type of content will be defined: articles or series that consist of several parts.
Note that this case differs from that where one page is composed from several markup files. For this Page Bundles could be utilized, with other markup files being imported as resources.
Layouts
Multi-part articles or article series are essentially sections. So, we’ll first have to create a directory that contains the markup files for the individual parts. The overview page provides an introduction to the topic and links to the single pages that represent the parts of the series. The individual pages link to the previous and following parts as well as to the introductory page.
It makes sense to model this structure in the form of specific layouts. To do this, we create a separate subdirectory directly under layouts/
, for example article-series
. We create list.html
for the entry page and single.html
for the parts.
The question now arises under which conditions the defined layouts for our multi-part content take effect. One of the features of sections is that layouts are usually applied automatically. But an example demonstrates why this wouldn’t do in the given case.
Let’s assume we want to write a series of articles about the history of science fiction and create content/science-fiction/history-of-science-fiction/
for this purpose. By default, the layouts from layouts/science-fiction/
would be applied. But what we actually want is a special layout for multi-part articles that can also be used for series that have nothing to do with science fiction.
Layouts are not only used for top-level sections. As long as a directory contains an _index.md
, Hugo checks whether a directory of the same name exists under layouts/
, and uses its layouts if appropriate. So we could create layouts/history-of-science-fiction/
. But we want something less specific—one layout for all article series.
We have two options for using the layouts under layouts/article-series/
:
- Create a separate top-level section: If it fits the structure of the page, we can create a top-level section
content/article-series/
. This would automatically apply the layouts underlayout/article-series/
to all content in this section. - Define the type manually in the front matter: In order to be able to use multi-part articles for any section (regardless of its position in the directory structure), a more manual approach is required. To do this, we define the type in the front matter of the markup files.
type: article-series
This field overwrites the default type derived from the section membership and ensures that the appropriate layout is used.
We wouldn’t want to define the type for each page individually. Instead, we can make use of the relationships between pages to simplify the workflow. The index page of a section is regarded as the parent page of all the individual pages that are part of the same section.
Although individual pages do not automatically inherit the type of the index page, we can use the cascade field to pass on front-matter values to all descendants:
cascade:
type: article-series
To define the order of the pages in the series, a partNumber
value can be added to the front matter of the individual pages:
partNumber: 6
This value has no special meaning for Hugo, but can be used on the index page for sorting or on the individual pages for navigation. The next task is to create partial layouts that make use of these values.
Introduction Page: Ordered Overview of the Parts
On the index page, the parts of the series should be listed in an ordered table, perhaps in a scheme like:
Part <n>: <Title>
We first have to consider how the listing is translated into HTML. As a first approximation, this could look something like the following (with a few placeholders):
<ul>
<li>
Part PART-NUMBER: <a href="LINK-TO-PART">TITLE-of-PART</a>
</li>
</ul>
To collect the required information, we access the front matter of the pages. First, however, we need to retrieve all the individual pages belonging to the section/series. In the template language, the
Context is denoted by .
, and we obtain the collection of all individual pages in the section using the
RegularPages method.
To sort the pages according to our partNumber
field, we use the
ByParam method, passing the parameter to be sorted as a string argument. Now we can sort the pages in the desired order to create the list entries:
<ul>
{{ range .RegularPages.ByParam "partNumber" }}
<li>
Part PART-NUMBER: <a href="LINK-TO-PART">TITLE-of-PART</a>
</li>
{{ end }}
</ul>
Within the loop, .
refers to the objects that represents individual pages. With Title
we get the title and with .Permalink
the URL to the page in question. partNumber
is not a predefined field, but we can access its value under .Params
.
The data is used in our layout for the index page, so that layouts/article-series/list.html
may look like this:
{{ define "main" }}
<main>
<div>
<h1>{{ .Title }}</h1>
{{ .Content }}
<ul>
{{ range .RegularPages.ByParam "partNumber" }}
<li>
Part {{ .Params.partNumber }}: <a href="{{ .Permalink }}">{{ .Title }}</a>
</li>
{{ end }}
</ul>
</div>
</main>
{{ end }}
It is preferable to save the layout components that are used within article series as partial layouts so that they can be reused if required. For example, the directory layouts/partials/articles
can be created for this purpose.
We save the list section of our layout as a partial template, for example under series-overview.html
:
<ul>
{{ range .RegularPages.ByParam "partNumber" }}
<li>
Part {{ .Params.partNumber }}: <a href="{{ .Permalink }}">{{ .Title }}</a>
</li>
{{ end }}
</ul>
Finally, we integrate the partial layout into the main layout and pass it the current context:
{{ define "main" }}
<main>
<div>
<h1>{{ .Title }}</h1>
{{ .Content }}
{{ partial "articles/series-overview.html" . }}
</div>
</main>
{{ end }}
Navigation Between the Parts
It should be possible to navigate easily between the parts—if only to convey the internal logic to visitors of the site. More specifially, it should be possible to move forwards and backwards in the article series and jump to the index page using a special navigation.
For this purpose, we need to determine the previous and next page for each page in the series. As the logic is somewhat more complex, we first define some meaningful variables:
{{ $current_part_number := .Params.partNumber }}
{{ $indexPage := .Parent }}
{{ $pages_sorted_by_part_number := sort .CurrentSection.Pages "Params.partNumber" "asc" }}
Next, we initialize variables that store the data to be determined. We assign a start value for $prevEntry
, which is used for the first part of the series.
{{ $prevEntry := $indexPage }}
{{ $nextEntry := "" }}
Now we can iterate over all pages and check whether it is the page currently being processed:
{{ range $i, $page := $pages_sorted_by_part_number }}
{{ if eq $page.Params.partNumber $current_part_number }}
(...)
{{ end }}
{{ end }}
As soon as the current page has been found, we also know the index of its predecessor and successor pages. Using the index function, we can retrieve the respective pages from the sorted collection:
{{ range $i, $page := $pages_sorted_by_part_number }}
{{ if eq $page.Params.partNumber $current_part_number }}
{{ $prevEntry = (index $pages_sorted_by_part_number (sub $i 1)) }}
{{ $nextEntry = (index $pages_sorted_by_part_number (add $i 1)) }}
{{ end }}
{{ end }}
The first and last page of the series are special cases that need to be handled differently. The first page should have the index page as its predecessor, and the last page has no successor page. We take this into account by first initializing the variables for these cases and then ensure that they are only overwritten if these conditions do not apply:
{{ range $i, $page := $pages_sorted_by_part_number }}
{{ if eq $page.Params.partNumber $current_part_number }}
{{ if gt $i 0 }}
{{ $prevEntry = (index $pages_sorted_by_part_number (sub $i 1)) }}
{{ end }}
{{ if lt $i (sub (len $pages_sorted_by_part_number) 1) }}
{{ $nextEntry = (index $pages_sorted_by_part_number (add $i 1)) }}
{{ end }}
{{ break }}
{{ end }}
{{ end }}
The {{ break }}
ends the loop as soon as the current page has been found, as the condition can no longer be satisfied in the following iterations.
We can save the above code again as a partial layout and then insert it into the layout of the individual article parts.
Search Engine Optimization
From an SEO point of view, there are a few things to consider. Structuring the article series as a separate section already has the advantage of an optimized URL structure: domain.com/.../topic/
for the overview page, and domain.com/.../topic/part-n-subtopic
for the individual parts. This well-connected structure helps search engines to recognize that the pages belong together.
Internal links are important to communicate thematic connections to web crawlers. We achieve this by linking to previous and subsequent sections on the individual pages and through the navigation on the overview page. If the affiliation to the series is to be particularly emphasized, titles such as [Title of the series] - Part n: [Title of part]
could be used to emphasize the connection.
Relevant keywords and categories should be assigned for the overview page so that the series is also categorized under the thematically appropriate keywords. While the overview page should contain a general description of the series, each individual page can offer descriptions tailored to its specific content.