Incremental Writes in Hugo

Hugo’s landing page
Hugo’s homepage.

Lately I’ve been twiddling with a few sufficiently complex Hugo themes and thought I’d share an interesting approach for rendering small changes quickly. In absence of a better term — “incremental writes” will do and this method works best when there’s a lot happening inside a theme that renders in the 1,000 ~ 10,000+ and beyond page range.

So what’s Hugo and why is it great? Well it’s the ultimate static site generator (SSG) that just gets things done. The reason it’s great is due to its Low complexity, easy maintenance, and little to no shenanigans. build and install story along with its In terms of web development. abstraction of things.

The code discussed here isn’t necessarily intended to be used with hugo server because it has its own fast rendering mode that works well enough if a theme is adequately simple and well written. This is more of an approach for rendering and previewing a large, complex, and messy theme quickly with just the plain hugo command. My hugo version is v0.108.0.

shell
$ hugo version
hugo v0.108.0+extended linux/amd64 BuildDate=unknown

Overview of Site Rendering

Before going into the details of making hugo write files incrementally it’s useful to have a simple idea of the minimum requirements of a theme and the way a site is rendered.

shell
|-- config.json
|-- config.toml
|-- config.yaml
|-- content
|   `-- markdown
|       |-- first.md
|       |-- second.md
|       `-- third.md
`-- themes
    `-- base
        `-- layouts
            `-- _default
                `-- baseof.html
                `-- index.html

The directory structure above along with one of the basic configuration formats are all that’s required to render a theme. Inside the content folder there’s the Markdown source and inside the themes folder are the layout files. Below is a minimal config.yaml that disables specific outputs because for this explanation the home and page outputs are the only concern.

yaml
---
baseURL:
theme: base
title: Base
languageCode: en-us

paginate: 10
summaryLength: 1

taxonomies:
  tag: tags

disableKinds:
- sitemap
- term
- section
- taxonomy

outputs:
  home:
  - html
  section:
  - html
  taxonomy:
  - html
  term:
  - html
  page:
  - html
config.yaml

Default formats are output “kinds” that are split across home, section, taxonomy, term, and page. This separation avoids thinking about redundant logic and common repetitive patterns. Home pages are rendered by index.html, sections by section.html, taxonomies by taxonomy.html, terms by term.html and pages by single.html.

Kinds Layouts
home _default/index.html
section _default/section.html
taxonomy _default/taxonomy.html
term _default/term.html
page _default/single.html

Base layouts are the authoritative boilerplate that markup and Generally it’s best to avoid templating data directly — but if you’re careful enough you can get away with it handily. templates of different kinds (home, section, taxonomy, term, page) extend and render. This is a common approach for most frameworks when smashing together HTML (Hypertext Markup Language). The ideal scenario is to only extend base layouts and keep visual interpolation and noise to a minimum.

shell
`-- themes
    `-- base
        `-- layouts
            `-- _default
                `-- baseof.html
                `-- baseof.json
                `-- baseof.xml
                `-- baseof.txt
Base Templates

For HTML and other boilerplate if there’s confusion over what type or kind is in use, then appending that information to the markup or data in a non–obstructive way works wonders. A block opens up a space inside a base template and a define elsewhere fills in or extends that space with the desired content.

html
<!DOCTYPE html>
<html
  lang="en-us"
  data-kind="{{ .Page.Kind }}"
  data-type="{{ .Page.Type }}"
>
  <head>
    <title>
      {{- block "title" . -}}
       {{ .Site.Title }}
      {{- end -}}
    </title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
  </head>

  <body>
    {{- block "main" . -}}
      <p>
        This is what you see if you don't
        extend the base template's main block.
      </p>
    {{- end -}}
  </body>
</html>
themes/base/layouts/_default/baseof.html

Incremental Writes

Writing output incrementally exploits the way hugo evaluates base templates. The whole site still has to be read but like a novel reading is faster than writing — and the computer is not much different. So the speed up here makes hugo mostly read things and not write them. It just so happens that if a base template for a particular kind evaluates to nothing well then — hugo does nothing.

Multiple conditions exist for choosing which files to process and write but the most obvious one compares the modification dates of the source markdown with the resulting index.html in the There’s a bit of string cutting going on inside the $page variable and that’s due to attempts at supporting both domain and sub–directory detection from the base URL (Uniform Resource Locator). folder. A function template below checks the modification time using the page’s context as input and returns its modified state as a true or false output.

go-html-template
{{- $input := . -}}
{{- $pageContext := $input -}}

{{- $markdown := print "content/" $pageContext.File -}}
{{- $markdownModTime := "" -}}
{{- $page := print "public/" (strings.TrimPrefix
      $pageContext.Page.Site.BaseURL
      $pageContext.Page.Permalink
    ) "index.html"
-}}
{{- $pageModTime := "" -}}

{{- if fileExists $markdown -}}
  {{- $markdownModTime = (os.Stat $markdown).ModTime -}}
{{- end -}}

{{- if fileExists $page -}}
  {{- $pageModTime = (os.Stat $page).ModTime -}}
{{- end -}}

{{- $modified := gt $markdownModTime $pageModTime -}}

{{- $output := or $modified (in (slice
    "home"
    "section"
    "taxonomy"
    "term"
  ) $pageContext.Page.Kind)
-}}

{{- return $output -}}
themes/base/layouts/partials/function-page-modified.html

One of the upsides to this particular function is that it also allows turning off writes for particular parts of the site with an in slice check against the current page’s kind.

html
{{- $modified := partial "function-page-modified.html" . -}}

{{- if $modified -}}
<!DOCTYPE html>
<html
  lang="en-us"
  data-kind="{{ .Page.Kind }}"
  data-type="{{ .Page.Type }}"
>
  <head>
    <title>
      {{- block "title" . -}}
       {{ .Site.Title }}
      {{- end -}}
    </title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    {{- $default := resources.Get "css/default.css" -}}
    <link title ="Default" rel="stylesheet" href="{{ $default.Permalink }}" />
  </head>

  <body>
    <main class="container">
      {{- block "main" . -}}
        <p>
          This is what you see if you don't
          extend the base template's main block.
        </p>
      {{- end -}}
    </main>
  </body>
</html>
{{- end -}}
themes/base/layouts/_default/baseof.html

Absolutely no stray characters or Isn’t it sort of neat how much white–space matters in programming? must render in the logic surrounding the base template — when a page is unmodified nothing should evaluate. The dash (-) in the template logic removes leading {{- and trailing -}} white–space. Repeat for every base template type. The clip below shows a demonstration of this function with 20,000 pages.

In this clip the home page is always rebuilt plus any other recently edited and non–existent pages. All other previous index.html outputs are left untouched.

See this repository for trying out different numbers and adjusting the complexity.

Applications and Utility

So how in the world is this useful? Mostly in the case of a theme that executes lots of work. An upper bound of 20,000 pages for the device in the above clip The page and disk cache are also on your side. take around 5 seconds hot plus write time for a single modified page change even in the context of heavy image processing, bundling, and data unmarshalling.

In other words no matter the complexity on previously rendered pages editing, previewing, and building a new page will take at most ~5 seconds because that’s how long it took my device to read all the source markdown.

It’s also important to know that the code paths for hugo and hugo server are different and the server will likely assume that the previously generated pages don’t exist even if they are in the public folder. If this post makes any sense — you’re probably using another web server anyway.

Finally be aware that care must be taken with shortcodes. Even shortcodes that are not in use are evaluated immediately at run time — this is an easy way to make builds slow. Keep them as simple as possible.

Conclusion

More could be said in regards to applying incremental switches especially to pagination logic but my time was too limited to think that one through. The goal here however is to avoid diving too far into incremental shenanigans and instead find a good cut off point for archive rotation while still having fast previews on pages when running the hugo command on a beefy theme. The Either truncation or disjointed archives that are joined back together for a full rebuild. cut off could be 10,000 pages or even 100,000+ pages — more or less depending on a theme’s template efficiency, disk I/O (Input/Output), and device memory.

30 January 2023 — Written
31 January 2023 — Updated
Thedro Neely — Creator
incremental-writes-in-hugo.md — Article

More Content

Openring

Web Ring

Comments

References

  1. https://thedroneely.com/git/
  2. https://thedroneely.com/
  3. https://thedroneely.com/posts/
  4. https://thedroneely.com/projects/
  5. https://thedroneely.com/about/
  6. https://thedroneely.com/contact/
  7. https://thedroneely.com/abstracts/
  8. https://ko-fi.com/thedroneely
  9. https://thedroneely.com/tags/hugo/
  10. https://thedroneely.com/posts/incremental-writes-in-hugo/#isso-thread
  11. https://thedroneely.com/posts/rss.xml
  12. https://thedroneely.com/images/incremental-writes-in-hugo.png
  13. https://gohugo.io/
  14. https://gohugo.io/commands/hugo_server/
  15. https://thedroneely.com/posts/incremental-writes-in-hugo/#code-block-27b0224
  16. https://thedroneely.com/posts/incremental-writes-in-hugo/#overview-of-site-rendering
  17. https://thedroneely.com/posts/incremental-writes-in-hugo/#code-block-bdb1348
  18. https://gohugo.io/getting-started/configuration#configuration-file
  19. https://commonmark.org/help/
  20. https://thedroneely.com/posts/incremental-writes-in-hugo/#code-block-1eebd05
  21. https://gohugo.io/templates/lookup-order#hugo-layouts-lookup-rules
  22. https://gohugo.io/templates/base#define-the-base-template
  23. https://thedroneely.com/posts/incremental-writes-in-hugo/#code-block-b560538
  24. https://thedroneely.com/posts/incremental-writes-in-hugo/#code-block-3b316ac
  25. https://thedroneely.com/posts/incremental-writes-in-hugo/#incremental-writes
  26. https://gohugo.io/templates/partials#returning-a-value-from-a-partial
  27. https://thedroneely.com/posts/incremental-writes-in-hugo/#code-block-a7acf0c
  28. https://thedroneely.com/posts/incremental-writes-in-hugo/#code-block-898ffe1
  29. https://thedroneely.com/videos/incremental-writes-in-hugo.mp4
  30. https://www.thedroneely.com/git/thedroneely/hugo-theme-base/
  31. https://thedroneely.com/posts/incremental-writes-in-hugo/#applications-and-utility
  32. https://en.wikipedia.org/wiki/Page_cache
  33. https://gohugo.io/content-management/shortcodes#what-a-shortcode-is
  34. https://thedroneely.com/posts/incremental-writes-in-hugo/#conclusion
  35. https://www.thedroneely.com/posts/incremental-writes-in-hugo.md
  36. https://thedroneely.com/posts/incremental-writes-in-hugo/#more-content
  37. https://thedroneely.com/projects/news-aggregator/
  38. https://thedroneely.com/abstracts/aria-the-animation/
  39. https://thedroneely.com/posts/hugo-is-good/
  40. https://git.sr.ht/~sircmpwn/openring
  41. https://thedroneely.com/posts/incremental-writes-in-hugo/#web-ring
  42. https://www.taniarascia.com/2022-into-2023/
  43. https://www.taniarascia.com/
  44. https://drewdevault.com/2022/12/01/I-shall-toil-quietly.html
  45. https://drewdevault.com/
  46. https://mxb.dev/blog/the-indieweb-for-everyone/
  47. https://mxb.dev/
  48. https://thedroneely.com/posts/incremental-writes-in-hugo/#comments
  49. https://thedroneely.com/sitemap.xml
  50. https://thedroneely.com/index.json
  51. https://thedroneely.com/resume/
  52. https://gitlab.com/tdro
  53. https://github.com/tdro
  54. https://codeberg.org/tdro
  55. https://thedroneely.com/analytics
  56. https://thedroneely.com/posts/incremental-writes-in-hugo/#
  57. https://creativecommons.org/licenses/by-sa/2.0/
  58. https://thedroneely.com/git/thedroneely/thedroneely.com
  59. https://opensource.org/licenses/GPL-3.0
  60. https://www.thedroneely.com/