Multi-page articles
Posted on August 15, 2023 (Last modified on August 30, 2023) • 12 min read • 2,360 wordsAdding the option to create multi-page articles with pagination
For the projects articles I am planning to write, I need the option to split an article over multiple pages. These pages belong together and it needs to be possible to navigate from one page to the other. On top of that it needs to have as little configuration as possible.
While searching for potential ways to solve this, I found this solution by jmooring , which I felt was a nice solution. It is a generic solution for Hugo and I only had to make it work for the Hinode theme, I am using. The following describes the steps I took to accomplish this.
Each multi-page article is to be located in its own folder. In that folder the bundle page file _index.md
is created and it needs to have the following in the frontmatter:
layout: multipage
Then the pages that are part of this multi-page article can be added to the same folder. As many pages as needed can be added. Each of these pages has the following in the frontmatter:
title: Title of the page
pagenumber: 1
It should be noted that the actual page number needs to be different for each page. These numbers will indicate the order of the pages. The first page has pagenumber: 1
, the second pagenumber: 2
, etc.
This would result in something like the following:
This is used as an example throughout this blog.
When Hugo processes the files, the _index.md
file in the projects
folder will be responsible for searching for all the page files. It will however not find the _index.md
file in the mpfolder
. instead, it will show the 3 pages in that folder as separate projects. As a result there will be four projects shown. Whereas the multi-page folder, should be shown as 1 project.
A change to the Hinode theme is needed to correct this.
To have the _index.md
file in mpfolder
act as an entry point to the multi-page article consisting of the 3 pages in that folder, the file layouts/partials/assets/section-list.html
needs to be changed. This file is called by layouts/_default/list.html
to get the pages that belong to the _index.md
page bundle file, located in projects
.
34{{- $width := 100 -}}
35{{- $multipage := false -}}
36
37{{- with (index site.Params.sections $section) -}}
38 {{- with index . "title" }}{{ $title = or $.title . }}{{ end -}}
39 {{- with index . "sectionHeader" }}{{ $sectionHeader = . }}{{ end -}}
40 {{- with index . "sort" }}{{ $sort = . }}{{ end -}}
41 {{- if (index . "reverse") }}{{ $order = "desc" }}{{ else }}{{ $order = "asc" }}{{ end -}}
42 {{- if $home }}{{- if (isset . "nested") }}{{ $nested = (index . "nested") }}{{ end -}}{{ end -}}
43 {{- if (index . "separator") }}{{ $separator = true }}{{ else }}{{ $separator = false }}{{ end -}}
44 {{- with index . "orientation" }}{{ $orientation = . }}{{ end -}}
45 {{- with index . "cols" }}{{ $cols = . }}{{ end -}}
46 {{- with index . "color" }}{{ $color = . }}{{ end -}}
47 {{- with index . "padding" }}{{ $padding = . }}{{ end -}}
48 {{- with index . "header" }}{{ $header = . }}{{ end -}}
49 {{- with index . "footer" }}{{ $footer = . }}{{ end -}}
50 {{- with index . "style" }}{{ $style = . }}{{ end -}}
51 {{- with index . "homepage" }}{{ $homepage = . }}{{ end -}}
52 {{- with index . "background" }}{{ $background = . }}{{ end -}}
53 {{- with index . "layout" }}{{ $layout = . }}{{ end -}}
54 {{- with index . "pane" }}{{ $pane = . }}{{ end -}}
55 {{- with index . "type" }}{{ $type = . }}{{ end -}}
56 {{- with index . "vertical" }}{{ $vertical = . }}{{ end -}}
57 {{- with index . "width" }}{{ $width = . }}{{ end -}}
58 {{- if index . "multipage" }}{{ $multipage = true }}{{ $nested = false }}{{ end -}}
59{{- end -}}
60{{ if ne (printf "%T" $nested) "bool" }}
61 {{ errorf "partial [assets/section-list.html] - Invalid value for param 'nested'"}}
62{{ end }}
63
64{{ $list := "" }}
65{{ if $nested }}
66 {{ $list = where site.RegularPages "Type" "in" $section }}
67{{ else if $home }}
68 {{ $sectionPage := site.GetPage "section" $section }}
69 {{ if $multipage }}
70 {{ $list = $sectionPage.Pages }}
71 {{ else }}
72 {{ $list = $sectionPage.RegularPages }}
73 {{ end }}
74{{ else }}
75 {{ if eq $page.Params.layout "docs" }}
76 {{ $list = where $page.RegularPages "Params.landing" true }}
77 {{ else }}
78 {{ if $multipage }}
79 {{ $list = $page.Pages }}
80 {{ else }}
81 {{ $list = $page.RegularPages }}
82 {{ end }}
83 {{ end }}
84{{ end }}
The highlighted lines need to be added to the file. Note that the assumption is that the documentation changes on line 75-77 have already been applied.
The first two highlighted lines are for checking if the section supports multi-page articles. If so, it sets the $multipage
variable, but also forces nested to be false
, because that conflicts with multi-page articles.
The rest of the highlighted lines are responsible for locating the files that belong to the page bundle. RegularPages
will find all page files in the projects
folder and all sub-folders, excluding any _index.md
file.
By changing RegularPages
to Pages
, it will find all page files in the projects
folder and all sub-folders, including any _index.md
file. If it finds that file, it will discard any other page files in that sub-folder. This is the desired behavior, hence the added extra lines.
In order for this to work properly the multipage
parameter needs to be added to the section in config/_default/params.toml
. For the projects
section this would look like the following:
[sections.projects]
title = "Projects"
layout = "card"
sort = "title"
reverse = false
nested = true
cols = 1
background = "body-tertiary"
color = "body"
padding = "3"
header = "none"
footer = "tags"
orientation = "horizontal"
style = "border-1 card-emphasize"
homepage = 3
separator = false
multiplayer = true
The highlighted line is the line to add.
After the previous changes the mpfolder/_index.md
file is used as the entry point to the multi-page article. The Hinode theme will use the file’s frontmatter contents to display it as a card on the home page and the section page. Because of that the following should be available in the front matter of mpfolder/_index.md
:
author: The author
title: The title of this project
description: A short description of the project
date: 2023-08-15
tags: ["tag1", "tag2"]
icon: fa clock
thumbnail:
url: /img/pages.webp
author: Olga Tutunaru
authorURL: https://unsplash.com/@otutunaru
origin: Unsplash
originURL: https://unsplash.com/photos/JMATuFkXeHU
layout: multipage
The highlighted lines are required, the first 3 because the card would not look good, if they weren’t there. The last one because it indicates a multi-page article.
What is also good to add, is either an icon or a thumbnail. One of the two should be available. If both are available, the thumbnail will be displayed in the card.
As it is now, the actual pages in mpfolder
are not yet found. This is because the multipage
layout setting is not recognized yet.
The default behavior of Hugo to display a page is to use layouts/_default/single.html
, unless the layout
parameter is set in the frontmatter of the page bundle. In that case it will try to find the file with the same name as what layout
has been set to, with the extension .html
in the layouts/_default
folder. If it cannot find that file, it will use layouts/_default/single.html
instead. In this case that would mean the file layouts/_default/multipage.html
.
This file is to be added, but as it resembles layouts/_default/single.html
a lot, it will use parts of that file.
The following shows the full contents of the new file layouts/_default/multipage.html
.
1{{ define "partials/multi-footer.html" -}}
2 <!-- Show comments when enabled -->
3 {{- if and .Site.Params.comments.enabled .Params.showComments | default true -}}
4 <br/><hr>
5 {{ partial "assets/comments.html" . }}
6 {{ end -}}
7{{ end -}}
8
9{{ define "main" }}
10 {{ $page := . }}
11
12 {{/* Create paginator from the regular pages */}}
13 {{ $paginator := .Paginate (.RegularPages.ByParam "pagenumber") 1 }}
14
15 {{/* Calculate the number of pages in the paginator */}}
16 {{ $totalPages := $paginator.TotalPages }}
17
18 {{ range $paginator.Pages }}
19 {{- $menu := .Scratch.Get "sidebar" -}}
20 {{- $version := .Scratch.Get "version" -}}
21 {{- $sidebar := .Site.Params.navigation.sidebar | default true -}}
22 {{ if and $menu $sidebar -}}
23 <div class="offcanvas offcanvas-start" tabindex="-1" id="offcanvass-sidebar" aria-inledby="offcanvas-label">
24 <div class="offcanvas-header">
25 <h5 class="offcanvas-title" id="offcanvas-label">{{ strings.FirstUpper .Section }}</h5>
26 <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
27 </div>
28 <div class="offcanvas-body">
29 {{ partial "assets/sidebar" (dict "page" . "menu" $menu "version" $version) }}
30 </div>
31 </div>
32
33 <div class="container-xxl px-3 px-xxl-0">
34 <div class="row row-cols-md-2 row-cols-lg-3">
35 <div class="col col-md-3 col-lg-2 d-none pt-5 d-md-block sidebar-overflow sticky-top">
36 {{ partial "assets/sidebar" (dict "page" . "menu" $menu "version" $version) }}
37 </div>
38 <div class="col col-md-9 col-lg-8 mb-5 p-4">
39 {{ partial "partials/header.html" . }}
40 {{ partial "partials/body.html" . }}
41 {{ if gt $totalPages 1 }}
42 {{ partial "assets/mpagination.html" (dict "page" $page
43 "mode" .Site.Params.multipage.paginator
44 "tooltips" .Site.Params.multipage.tooltips
45 "positions" .Site.Params.multipage.positions
46 ) }}
47 {{ end }}
48 {{ partial "partials/multi-footer.html" . }}
49 </div>
50 <div class="col col-lg-2 d-none d-lg-block pt-5">
51 {{- if and .Site.Params.navigation.toc .Params.includeToc | default true -}}
52 {{ partial "assets/toc.html" . -}}
53 {{ end -}}
54 </div>
55 </div>
56 </div>
57 {{ else }}
58 <div class="container-xxl px-3 px-xxl-0">
59 <div class="row row-cols-1 row-cols-sm-3">
60 <div class="col col-md-2 d-none d-md-block"></div>
61 <div class="col col-sm-12 col-md-8">
62 {{ partial "partials/header.html" . }}
63 {{ partial "partials/body.html" . }}
64 {{ if gt $totalPages 1 }}
65 {{ partial "assets/mpagination.html" (dict "page" $page
66 "mode" .Site.Params.multipage.paginator
67 "tooltips" .Site.Params.multipage.tooltips
68 "positions" .Site.Params.multipage.positions
69 ) }}
70 {{ end }}
71 {{ partial "partials/multi-footer.html" . }}
72 </div>
73 <div class="col col-md-2 d-none d-md-block">
74 {{- if and .Site.Params.navigation.toc .Params.includeToc | default true -}}
75 {{ partial "assets/toc.html" . -}}
76 {{ end -}}
77 </div>
78 </div>
79 </div>
80 {{ end }}
81 {{ end }}
82{{ end }}
Lines 1-7 will create the footer that is needed. This is a simplified version of the footer as it is used in layouts/_default/single.html
, as the pagination in that file will not be used here. At least not in that way.
Line 13 is where the paginator is created. It will search for all regular pages in the current folder and will sort them by the pagenumber
that is specified in the frontmatter of the page, as described at the start of this blog.
From line 18 onwards, the pages in the paginator will be processed. This resembles the contents of layouts/_default/single.html
quite a bit, with the exception of the highlighted parts, which will take care of showing a pagination and will call the partial/multi-footer.html
that has been defined at the start of the file.
The assets/mpagination.html
takes care of showing a configurable way to navigate between the pages and the partials/header.html
and partials/body.html
partials are defined in layouts/_default/single.html
.
The call to the assets/mpagination.html
partial has a maximum of four parameters, which are defined in config/_default/params.toml
as follows:
[multipage]
paginator = "buttons" # The paginator to show at the bottom of the page. Valid options are:
# "arrows" Shows arrows to the left/right if there is a next/previous page
# "buttons" Shows navigation buttons (centered)
# "list" Shows a list of available pages (centered)
# "dropdown" Shows a drop-down box and the available pages (centered)
# "dropup" Shows a drop-up box and the available pages (centered)
tooltips = "on" # When paginator="buttons", enables ("on") or disables ("off") the display of tooltips
# when hovering over the buttons. Not used in the other paginator options.
positions = 4 # When paginator="buttons", Sets the maximum number of button positions.
# Not used in the other paginator options.
The accompanying comments should provide sufficient explanation. The following sections show screenshots of the different paginator
options.
Using 6 pages, with page 4 being the current page and the mouse hovering over page 2.
Using 6 pages, with the first page being the current page.
Using 6 pages, with the last page being the current page.
Page 4 is the current page.
Page 1 is the current page and the mouse hovers over page 4.
When the dropdown button is not clicked.
When the dropdown button is clicked, the list shows below the button. Page 1 is the current page and the mouse hovers over page 4.
For the dropup button, the list shows above the button and the triangle in that button, points up.
By default each page in a multi-page article will have its own comments. It might make sense to have all pages in a multi-page article use the same set of comments. If that is required add the following to the frontmatter of each of the pages in the multi-page article:
commentsterm : .
This will only work when this change has been applied.
The provided solutions works as long as there is no need for another layout to be defined. On my site there is a different layout for documentation and Gallery, so I can’t use it for those sections. But, I can use it for blogs and projects, which is sufficient for my needs.