Deprecations in v0.156.0

Background

Historically, we used the terms site and project interchangeably. With monolingual projects, that was essentially correct. With multilingual projects, it was not correct, in that every language is internally represented as a separate site. So, we were a bit lazy with our word selection.

With the introduction of the multidimensional content model in v0.153.0, the distinction between site and project becomes much more important. For example, a software documentation project in 4 languages with 3 versions is internally represented by 12 sites. In a single-host setup, all 12 sites will be served from the same URL. With a multihost setup, there would be 4 URLs, one for each language.

So, going forward, we need to use these words carefully.

Definitions

A site is a specific instance of your project representing a unique combination of language, role, and version. While a simple project may consist of only a single site, Hugo’s multidimensional content model allows a single codebase to generate a matrix of sites simultaneously

A project is a collection of components used to generate one or more sites. While a project may consist of only a single site, Hugo allows a single project to generate a matrix of sites based on language, role, and version. The project serves as the parent container for the common assets and logic used across all sites within the build.

Deprecated methods

In several places, our API did not align with this model. For example, accessing files in the data directory via Site.Data is conceptually inaccurate; the data directory is tied to the project, not to an individual site.

To align our API with this content model, we have made the following changes in v0.156.0:

Deprecated method Replacement Explanation
Site.Data hugo.Data Data belongs to the project, not to a site.
Site.Sites hugo.Sites Sites belong to the project, not to a site.
Page.Sites hugo.Sites Sites belong to the project, not to a page.
Site.AllPages See Example A This method does not return all pages for a site. It returns all pages for all languages, which is an odd construct within the new content model.
Site.BuildDrafts Page.Draft This is also an odd construct, reporting whether draft publishing is enabled for the current build, which will return true even when there is no draft content.
Site.Languages See Example B We’ve seen this used in language selectors, which doesn’t make a lot of sense. Language selectors should be based on the current page, not the site or project.

Example A – Range over all pages in a project

If you need to range over all pages in a project, range over the sites, then range over the pages within each site.

{{ range hugo.Sites }}
  {{ range .Pages}}
    ...
  {{ end }}
{{ end }}

Example B – Language selector

If you need to create a language selector, rotate through the translations of the current page.

{{ with .Rotate "language" }}
  <nav class="language-switcher">
    <ul>
      {{ range . }}
        {{ if eq .Site.Language $.Site.Language }}
          <li class="active"><a aria-current="page" href="{{ .Permalink }}">{{ .Site.Language.LanguageName }}</a></li>
        {{ else }}
          <li><a href="{{ .Permalink }}">{{ .Site.Language.LanguageName }}</a></li>
        {{ end }}
      {{ end }}
    </ul>
  </nav>
{{ end }}

Identify usage of deprecated methods

Run hugo build --logLevel info to see if your project uses any of the deprecated methods above. In about 3 months, the info log deprecation messages will be elevated to warnings, and about 12 months after that, they will be elevated to errors. This is in accordance with our published deprecation process.

9 Likes

really appreciate the detailed write-up on this — the site vs project distinction makes a lot more sense now, especially for multilingual setups.

the one that caught my eye is the .Site.Data to hugo.Data change. we use data files quite heavily for client sites (storing things like team members, service lists, pricing tables) and the current pattern of {{ range .Site.Data.team.members }} is scattered across a lot of templates. the migration itself is straightforward enough but I am curious — is hugo.Data functionally identical to .Site.Data for monolingual single-version projects, or are there subtle differences in how data files are scoped now?

asking because we have one project where the same data file is used in both a partial and a shortcode, and those have different context. .Site.Data worked uniformly across both because .Site is always available. with hugo.Data, since hugo is a global function rather than a page context variable, I assume it should work the same way — but wanted to confirm before updating everything.

also good to know about the timeline on Site.AllPages. we have a cross-language sitemap template that uses it and the nested range hugo.Sites pattern is a reasonable replacement — just a bit more verbose.

2 Likes

Yes, it is.

With the new content model, is there any way to mark one of the values to be the “default”?
For example, I have been using (index .Site.Languages 0).Lang to set the default content language for hreflang=x-default.

{{- if .IsTranslated }}
{{- $defaultContentLanguage := (index .Site.Languages 0).Lang }}
{{- range where .AllTranslations "Language.Lang" "not in" (slice "" "cdn") }}
  <link rel="alternate" hreflang="{{- .Language.Lang -}}" href="{{- .Permalink -}}" title="{{- .Language.LanguageName }}">
{{- if eq .Language.Lang $defaultContentLanguage }}
  <link rel="alternate" hreflang="x-default" href="{{- .Permalink -}}" title="{{- .Language.LanguageName }}">
{{- end }}
{{- end }}

You can set the default value for each dimension in your project configuration:

Then, when iterating over a page collection that contains dimensional variants of the current page, use the relevant IsDefault method to test whether the current dimension value is the default value for that dimension.

Note that the Language object is also accesssible via Page, so you can also check .Page.Language.IsDefault.

These the are the Page methods that return page collections containing dimensional variants of the current page:

The last one, .Page.Rotate, is slick.

1 Like

Thanks, Joe, for the quick reply.
As always, very well-founded, detailed, yet to the point.

1 Like

Great, so this use case is and remains straightforward (and probably has been for quite some time).
Works for me:

{{- if .IsTranslated }}
{{- range where .AllTranslations "Language.Lang" "not in" (slice "" "cdn") }}
  <link rel="alternate" hreflang="{{- .Language.Lang -}}" href="{{- .Permalink -}}" title="{{- .Language.LanguageName }}">
{{- if .Language.IsDefault }}
  <link rel="alternate" hreflang="x-default" href="{{- .Permalink -}}" title="{{- .Language.LanguageName }}">
{{- end }}
{{- end }}
1 Like

I see some potential issue with the deprecactions, especially .Site.BuildDrafts and maybe .Site.Languages, since I can’t see the direct replacements in the table provided by @jmooring

The most obvious is certainly .Site.BuildDrafts - this is not a “odd construct” at all, since there are use cases where the relevant entity in content modeling is not a individual page. One of those are draft menu entries as discussed in:

So, if this shouldn’t be kept, a (less optimal) migration strategy might be something like a environment (-e).

Basically the same point (regarding to the entity in the content model) applies to the language (Site.Languages). I haven’t looked into this that deep but if I unterstand it correctly, the table and example above implies that if a page isn’t translated I can’t access the other languages the site is available in? This can’t be true, right?

Just in case it is: Of course I need to have a language switch, even if no translation (let’s say for a deep linked page in German), I need to have a link to a English and Swedish translation of the Site, they just need to either link to the start or a page that explains that the page is not available as a translation.
So having Site.Languages can be helpful, especially when having a list based on the current page is incomplete.

Regarding .Site.BuildDrafts, the “odd construct” note refers to the fact that this method exists only to report whether you started Hugo with the --buildDrafts flag.

The --buildDrafts flag is an instruction to “include content marked as draft.” In Hugo, “content” means Pages. Using a flag meant for filtering pages to show or hide other parts of your templates was never an intended use case, and we have no plans to include a direct replacement for it in the new API. While we could have moved this to hugo.BuildDrafts, we chose not to because its utility is extremely limited; logic for draft status is properly handled at the page level via .Page.Draft.

As I noted in the discussion you linked, the visibility of menu entries that reference pages with the pageRef property should be controlled by checking for the existence of the page itself… draft pages don’t exist.

If you need to show or hide other elements in your layout, using the environment or a custom parameter is the idiomatic approach. And remember that you can pass custom parameters on the command line, for example:

HUGO_PARAMS_FOO=bar hugo

Then access the value with .Site.Params.foo.

Regarding .Site.Languages, if you need a project-wide list of languages for a language switcher that includes every language even when a specific page translation does not exist, you should range over the slice returned by hugo.Sites.

Each site in that collection gives you access to its respective .Language and its own .Home page for a fallback link.

Thanks @jmooring I’ll update my blog post accordingly. I understand that this a quite niche use case. While the current way is comfortable (by just using -D), your proposal ensures consistency.

Anyways: I think having the available languages for a project might be a good convenience method.

Thanks!

I agree that it would be good to keep hugo.Sites.Languages - but deduplicated - for efficiency.

While you may loop over hugo.Sites in a template to figure it out, you would need awkward (and possibly way slower) filtering logic to de-duplicate across the other dimensions.
Even though hugo.Sites is already sorted language first, the collection ist flat.

So it would be both convenient and efficient to either have .Site.Rotate (same logic as for .Page.Rotate) or top-level .Site collections that de-duplicate along the available dimensions.

I recognize that “convenient”, “awkward”, and “efficient” are subjective measurements, but this is what I would do:

layouts/_partials/get-languages.html

{{ $languages := slice }}
{{ range hugo.Sites }}
  {{ $languages = $languages | collections.Append . }}
{{ end }}
{{ return (collections.Uniq $languages) }}

Then call it like this:

{{ range partialCached "get-languages.html" .}}
  {{ .Language.Label }}
{{ end }}

The metrics look like this:

Start building sites … 
hugo v0.161.0-DEV-c48551677c2504e3f2d7fa53ee42766e0333b957 linux/amd64 BuildDate=2026-04-08T11:19:45Z VendorInfo=jmooring


Template Metrics:

       cumulative       average       maximum      cache  percent  cached  total  
         duration      duration      duration  potential   cached   count  count  template
       ----------      --------      --------  ---------  -------  ------  -----  --------
     148.230316ms     244.604µs    4.671054ms          0        0       0    606  page.html
      10.309861ms     1.71831ms    2.749472ms          0        0       0      6  section.html
       8.145437ms      13.441µs    1.423237ms        100       99     600    606  _partials/get-languages.html
       7.937305ms    1.322884ms    3.384482ms          0        0       0      6  home.html
         45.974µs      45.974µs      45.974µs          0        0       0      1  alias.html


           │ EN  │ DE  │ FR  │ ES  │ IT  │ PT  
───────────┼─────┼─────┼─────┼─────┼─────┼─────
 Pages     │ 103 │ 103 │ 103 │ 103 │ 103 │ 103 
 Paginator │   0 │   0 │   0 │   0 │   0 │   0 
 pages     │     │     │     │     │     │     
 Non-page  │   7 │   0 │   0 │   0 │   0 │   0 
 files     │     │     │     │     │     │     
 Static    │   6 │   6 │   6 │   6 │   6 │   6 
 files     │     │     │     │     │     │     
 Processed │   0 │   0 │   0 │   0 │   0 │   0 
 images    │     │     │     │     │     │     
 Aliases   │   1 │   0 │   0 │   0 │   0 │   0 
 Cleaned   │   0 │   0 │   0 │   0 │   0 │   0 

Total in 81 ms

1 Like

Yes, “convenient” and “awkward” are highly subjective. :smiley:
Thanks a lot for measuring the efficient aspect.
My first idea was this:

{{- $langSites := where hugo.Sites "Version.IsDefault" true -}}
{{- $langSites = where $langSites "Role.IsDefault" true -}}

And then either store $langSites in hugo.Store or use a partialCached as proposed.
A built-in for that would still be convenient to have… ymmv.

Both variants have similar/identical performance in my build, I did not try a lot of samples. In some builds one wins, in other builds the other.

1.864147ms      30.559µs     274.449µs        100       95      58     61  _partials/meta/functions/get-lang-loop.html
1.752547ms       28.73µs     265.839µs        100       95      58     61  _partials/meta/functions/get-lang-where.html
1 Like