Multi-page articles

Posted on August 15, 2023 (Last modified on August 30, 2023) • 12 min read • 2,360 words
Share via

Adding the option to create multi-page articles with pagination

Multi-page articles
Olga Tutunaru  on Unsplash 

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.

Basic set-up

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:

projec_mmtiypsnpfdroeolxjd_apt.eeinahmcrnogidtdtes.eh.pmxemad.rdgmpeda.gmed.md


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.

Locating the page bundle file

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.

Modifying the page bundle frontmatter

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.

Finding the pages

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 pagination partial

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.

buttons

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.

arrows

Page 4 is the current page.

list

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.

Comments

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.

Wrap-up

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.



Comments

Site Links
Social Media