Skip to content

Reimplement and simplify Hugo's template system #13541

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 6, 2025
Merged

Conversation

bep
Copy link
Member

@bep bep commented Mar 30, 2025

Some key differences from v0.145.0:

  • We now consider the complete Page.Path, not just the top level (section/type) when resolving templates.
  • All template lookup follows the same code path. This means that you can use the same set of identifiers in the filenames for page layouts (including base, e.g. baseof.list.html, templates), shortcode templates, and render hook templates.
  • _shortcodes and _markup directories can be added to any level in the file tree.
  • The directories shortcodes and partials are renamed _shortcodes and _partials.
  • The (confusing) concept of _internal/ templates are removed (but will still work, will be deprecated a later Hugo version), so e.g. the Twitter card template needs to be loaded via {{ partial "twitter_cards.html" . }}.
  • Add a new catch-all layout named all, see Add new standard layout 'all' (in addition to single,list) #13545.

Note

I have gone to great lengths to make this as backwards compatible as possible. All existing test passes and I have added lots more. But with the amount of Hugo sites in the wild, I would surprised if we end up with no "my site is broken" reports after releasing this, but we really need to do this.

Identifiers and their weights

Name Description W1 W2 W3
Distance To find the best matching template, we walk from the root down to the page's Path. The distance is measured in directory levels from the page Path up to the template path.
Kind The Page Kind, one of home, page, section, term or taxonomy. 3 1
Layout Standard all, list or single. 2 1
Layout Custom Custom layout set in the layout front matter key, e.g. mylayout. 4 2
Type Set in type front matter key. For template lookups, this effectively changes the root directory of Page.Path.
OutputFormat Output format name, e.g. html. 2 1
MediaType Media type, e.g. text/html. 1 1
Lang Language, e.g. en. 1 1
Variant1 Contextual variant, currently used to identify a render hook, e.g. link. 4
Variant2 Contextual variant, currently used to identify e.g. the code language in codeblock render hooks. 2

Template Resolving

This section covers template resolving for page layouts, shortcodes and render hook templates. Partials has currently a much simpler logic.

To resolve a template, we

  1. Create a Page descriptor with the identifiers listed above.
  2. Walk the template tree down to the Page path and compare the Page descriptor with the current template descriptor.
    1. If the template descriptor is a subset of the Page descriptor, we calculate the weights w1, w2 and w3.
    2. If this is the first match, we store it away.
    3. Else we compare it to the previous best match:
      1. If the new match's path is closer to the Page path than the previous best match's path, we compare their values for w2 and w3, in that order. For example, given a Page path /foo/bar, a layout in /layouts/foo/single.html would win over /layouts/page.html even if the latter has a higher w1 weight.
      2. Else we compare their values for w1.
      3. All things equal, these are the tie breakers:
        1. Shortest distance.
        2. If layout is set, we prefer any value before all.
        3. The lesser layout Path (lexicographically).

There are some special cases to the above:

  • We prefer user provided templates over embedded templates.
  • TODO(bep) complete this.

Example layouts directory

layouts
├── baseof.html
├── home.html
├── list.html
├── single.html
├── taxonomy.html
├── term.html
├── term.mylayout.section.en.rss.xml
├── _markup
│   ├── render-codeblock-go.term.mylayout.no.rss.xml
│   └── render-link.html
├── _partials
│   └── mypartial.html
├── _shortcodes
│   ├── myshortcode.html
│   └── myshortcode.section.mylayout.en.rss.xml
├── docs
│   ├── baseof.html
│   ├── _shortcodes
│   │   └── myshortcode.html
│   └── api
│       ├── mylayout.html
│       ├── single.html
│       └── _markup
│           └── render-link.html
└── tags
    ├── taxonomy.html
    ├── term.html
    └── blue
        └── list.html

Fixes #13515
Updates #7964
Updates #13365
Updates #12988
Updates #4891

Fix/adjusment tasks

@jmooring
Copy link
Member

This is really, really good.

@bep
Copy link
Member Author

bep commented Mar 30, 2025

This is really, really good.

Yea, I'm pretty happy with the end result, but I have changed my mind a few times. Note that this is still a draft; it's mostly working, but there's some TODOs sprinkled around, some commented out tests, and everything I say about shortcodes above isn't yet entirely true, but it will be.

@jmooring
Copy link
Member

still a draft
it's mostly working
isn't yet entirely true

I've gone-to-market with less, but in most cases under protest.

@jmooring
Copy link
Member

jmooring commented Apr 2, 2025

I am not trying to increase the scope of this PR, but am curious if either of these are affected:

@bep bep force-pushed the templatesv2 branch 5 times, most recently from 6a025f2 to 6ac1b67 Compare April 3, 2025 08:55
@bep
Copy link
Member Author

bep commented Apr 3, 2025

@jmooring none of those issues is somehow fixed by this (whatever that would mean). But the order of things is certainly easier to reason about in the new setup:

if err := s.insertTemplates(nil, false); err != nil {
  return nil, err
}
if err := s.insertEmbedded(); err != nil {
  return nil, err
}
if err := s.parseTemplates(); err != nil {
  return nil, err
}
if err := s.extractInlinePartials(); err != nil {
  return nil, err
}
if err := s.transformTemplates(); err != nil {
  return nil, err
}
if err := s.tns.createPrototypes(true); err != nil {
  return nil, err
}
if err := s.prepareTemplates(); err != nil {
  return nil, err
}

So, while we extractInlinePartials later, we don't overwrite partials with the same path1. If we did, that would for one break how we use the file system overlay to override templates (project partial wins over theme partial). That could possibly be handled, but that is certainly not in scope here.

Footnotes

  1. ... but if you should be able to override by using e.g. mypartial.html.html (e.g. both output format and media type)

@bep bep force-pushed the templatesv2 branch 2 times, most recently from 531048b to 1437adf Compare April 3, 2025 13:11
@bep bep marked this pull request as ready for review April 3, 2025 13:40
@bep bep requested a review from Copilot April 3, 2025 13:43
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR reimplements and simplifies Hugo’s template system by unifying template lookup rules across page layouts, shortcodes, and render hook templates. It also refactors internal data structures (e.g. the radix tree and path parsing logic) and renames constants and methods for improved consistency.

  • Unified template lookup using a single code path with a new TemplateStore.
  • Refactored concurrency handling in the radix tree and improved path parsing to incorporate language, output format, and type.
  • Updated tests and configuration to reflect these internal API and naming changes.

Reviewed Changes

Copilot reviewed 133 out of 133 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
hugolib/doctree/simpletree.go Refactored tree interfaces and locking; clarified thread safety.
hugolib/content_render_hooks_test.go Added tests for render hooks and shortcode reuse behavior.
hugolib/content_map_page.go Updated references from old constants to new type names.
hugolib/content_map.go Adjusted switch cases to newer type constants and template store ref.
hugolib/content_factory.go Replaced old template handler calls with TemplateStore methods.
hugolib/alias_test.go Updated test setup to use inline file definitions and new templates.
hugolib/alias.go Switched alias handling to use the new TemplateStore API.
deps/deps.go Removed TemplateProvider in favor of TemplateStore and added counters.
create/content.go Changed key generation for archetypes to exclude language/format.
config/allconfig/allconfig.go Extended ContentPathParser with output format detection.
common/types/types.go Added Locker and RWLocker interfaces for consistency.
common/paths/* Renamed and reworked path parsing and type handling logic.
common/maps/* Minor enhancements to ordered map and cache functionality.
commands/server.go Updated error template lookup to use the TemplateStore.

@bep
Copy link
Member Author

bep commented Apr 3, 2025

@jmooring All tests are green and I have completed my TODO list. I will do some more manual testing of this myself, but I would appreciate if you could take it for a spin, and especially see if you agree with my choices in the "lookup department"; the most opinionated is probably the path/depth logic. I have tried to explain it above, I'm not sure how well I succeeded.

@jmooring
Copy link
Member

jmooring commented Apr 3, 2025

@bep

git clone https://github.com/jmooring/test-site-new-template-system
cd test-site-new-template-system
hugo server

At the top of the page you'll see this:

image

@bep
Copy link
Member Author

bep commented Apr 3, 2025

@jmooring good catch, odd that none of my tests caught this. Probably a confusion between text and html parsing for the partial func. If you do this, it "works":

{{ partialCached "head/css.html" . | safeHTML }}

Which is obviously not what we want. I will add a test for this and fix it.

@bep
Copy link
Member Author

bep commented Apr 3, 2025

Here's what happens:

head/css.html => outputformat = css output format => plain text template.

The isn't entirely incorrect and comes from us now handling all templates more or less the same in this department, but I see that we need to add an additional check to make sure that the suffix matches the output format.

@remkolodder
Copy link

How much modification should end up being required for this? I bisected my Paper-Mod based site breaking to this commit, and I'm not sure whether it's a bug in hugo, or something that just needs to change in the theme.

Total in 38 ms
Error: error building site: html/template:_partials/head.html:156:13: no such template "partials/templates/schema_json.html"

I have this exact error as well. You should update the 'head.html' file to include _partials as well.
See attached on how I did that.

Screenshot 2025-04-11 at 10 13 55

@bep
Copy link
Member Author

bep commented Apr 11, 2025

@jakebailey I don't follow closed PRs (or: almost never). If you think it's a bug, create a new issue, for questions, see https://discourse.gohugo.io/

We have tested many Hugo sites in the wild without any issues and need for any upgrade and the entire Hugo test suite was green without adjustments, but this replaces 10 years of accumulated and confusing logic, so some breakage were expected.

@jakebailey
Copy link

Sorry, I should know better 😄

I filed #13592. adityatelange/hugo-PaperMod#1719 is also talking about this, though I am unsure if the theme is what should change (the partial appears to exist).

I have this exact error as well. You should update the 'head.html' file to include _partials as well.

That's a bit of a challenge given it's not my theme, but something I depend on.

bep added a commit to bep/hugo that referenced this pull request Apr 12, 2025
bep added a commit to bep/hugo that referenced this pull request Apr 12, 2025
bep added a commit to bep/hugo that referenced this pull request Apr 12, 2025
bep added a commit to bep/hugo that referenced this pull request Apr 12, 2025
bep added a commit to bep/hugo that referenced this pull request Apr 12, 2025
bep added a commit to bep/hugo that referenced this pull request Apr 12, 2025
alanorth added a commit to alanorth/hugo-theme-bootstrap4-blog that referenced this pull request Apr 14, 2025
As of Hugo v0.146.0 the template system was overhauled and it seems
we need to use different syntax here. The reason for overriding the
Hugo internal templates in the past (post images) is no longer pre-
sent, so I can revert to the internal ones, but confusingly, we do
not refer to them with "_internal" now.

See: gohugoio/hugo#13541
markdumay added a commit to gethinode/hinode that referenced this pull request May 3, 2025
gyorb added a commit to gyorb/hugo-dusk that referenced this pull request May 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

All values after the first dot in content/layout/etc. base filenames as identifiers (e.g. language, ext)
4 participants