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.

7 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.

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