Hugo SEO: what the templates must do, since nothing is automatic

You run Hugo. SEO only happens if your templates do it. Here is what to put in the head partial, the sitemap and robots templates. How to handle taxonomy pages, canonicals and images. A checklist to ship with confidence.

By , founder of Porteur · Updated 14 September 2026 · Markdown

What good Hugo SEO looks like

Hugo is fast. It does not do SEO for you. The theme decides, or you write the templates.

  • The head partial sets titles, descriptions, canonicals and robots meta.
  • The sitemap.xml is generated and scoped to indexable URLs.
  • robots.txt is explicit and safe in staging.
  • Taxonomy pages do not bloat the index.
  • Images have alt text, correct sizes and fast delivery.

You control this in layouts, partials and config. Nothing is automatic beyond what your theme already ships, as of 2026.

The head partial: titles, descriptions, canonicals and robots

Put all metadata in one partial. Call it from every layout. Keep logic simple and predictable.

{{/* layouts/partials/head.html */}}
<title>{{ with .Params.seo_title }}{{ . }}{{ else }}{{ .Title }}{{ if ne .Kind "home" }} | {{ .Site.Title }}{{ end }}{{ end }}</title>

{{ with or .Params.meta_description .Summary }}
  <meta name="description" content="{{ . | plainify | truncate 155 }}">
{{ end }}

{{ $canonical := .Permalink }}
{{ with .Params.canonical }}{{ $canonical = . }}{{ end }}
<link rel="canonical" href="{{ $canonical }}">

{{/* Robots meta: default index,follow. Noindex for drafts, private, or thin lists you choose */}}
{{ $robots := "index,follow" }}
{{ if or .Draft .Params.noindex }}{{ $robots = "noindex,follow" }}{{ end }}
{{ if or (eq .Kind "taxonomy") (eq .Kind "term") }}
  {{ if not .Params.index }}{{ $robots = "noindex,follow" }}{{ end }}
{{ end }}
<meta name="robots" content="{{ $robots }}">

{{/* Basic Open Graph */}}
<meta property="og:title" content="{{ with .Params.og_title }}{{ . }}{{ else }}{{ .Title }}{{ end }}">
<meta property="og:description" content="{{ with .Params.og_description }}{{ . }}{{ else }}{{ with .Params.meta_description }}{{ . }}{{ end }}{{ end }}">
<meta property="og:url" content="{{ $canonical }}">
<meta property="og:type" content="{{ if eq .Kind "page" }}article{{ else }}website{{ end }}">

{{/* Twitter Card */}}
<meta name="twitter:card" content="summary_large_image">
{{ with .Params.og_image }}
  <meta property="og:image" content="{{ . }}">
  <meta name="twitter:image" content="{{ . }}">
{{ end }}

In each layout, include the partial once.

{{ partial "head.html" . }}
  • Title: use a page field, then the page title. Add the site name except on the home page. Keep near 60 characters.
  • Description: prefer a field. Fall back to a summary. Trim to 155 characters.
  • Canonical: default to .Permalink. Allow a per page override.
  • Robots: default index,follow. Allow noindex per page and for taxonomy kinds you mark as thin.
  • Social: basic Open Graph and Twitter help sharing and search features.

Hugo sitemap: ship a clean sitemap.xml

Hugo has a built-in sitemap template. You can override it. Use a custom template if you need control over what is included.

# config.toml (or .yaml/.json)
baseURL = "https://yourproduct.com/"
[sitemap]
  changefreq = "weekly"
  priority = 0.5
  filename = "sitemap.xml"

Override the template to skip noindex pages, drafts and thin taxonomies. Put this at layouts/_default/sitemap.xml. Keep the XML valid. Link absolute URLs only.

{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>\n" -}}
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  {{/* Regular pages */}}
  {{ range .Site.RegularPages }}
    {{ if and (not .Draft) (ne (index .Params "noindex") true) }}
      <url>
        <loc>{{ .Permalink }}</loc>
        {{ with .Lastmod }}<lastmod>{{ .Format "2006-01-02T15:04:05Z07:00" }}</lastmod>{{ end }}
      </url>
    {{ end }}
  {{ end }}

  {{/* Taxonomy list pages, only if you mark them indexable */}}
  {{ range $taxonomy, $terms := .Site.Taxonomies }}
    {{ range $term, $pages := $terms }}
      {{ $termPage := index $.Site.GetPage (printf "/%s/%s" $taxonomy $term) }}
      {{ if and $termPage (ne (index $termPage.Params "index") false) }}
        <url>
          <loc>{{ $termPage.Permalink }}</loc>
          {{ with $termPage.Lastmod }}<lastmod>{{ .Format "2006-01-02T15:04:05Z07:00" }}</lastmod>{{ end }}
        </url>
      {{ end }}
    {{ end }}
  {{ end }}
</urlset>
  • Submit sitemap.xml in Search Console. Watch the Sitemaps report for errors.
  • Do not include paginated list pages unless you need them indexed.
  • If you run multiple languages or sections, link to one sitemap index and split per section if needed.

robots.txt: clear rules and safe staging

Hugo can render a robots.txt from a template. Keep it simple. Disallow what must stay out. Point to the sitemap. Lock staging to avoid index leaks.

# layouts/robots.txt
User-agent: *
{{ if eq hugo.Environment "production" }}
Allow: /
Sitemap: {{ .Site.BaseURL }}sitemap.xml
{{ else }}
Disallow: /
# Staging or local. Block all crawling.
{{ end }}
  • Set HUGO_ENV=production on real deploys. Test the built robots.txt before you ship.
  • Do not try to fix index issues with robots.txt. Use noindex in the head for pages that should be public to users but not indexed.
  • If you run multiple hosts, make sure BaseURL matches the live domain so the sitemap URL is correct.

Taxonomy pages: avoid thin archives

Hugo creates taxonomy list pages for tags and categories if enabled. Out of the box, these pages can be thin. Thin archives bloat the index and waste crawl.

  • Disable taxonomies you do not use.
  • Noindex term and taxonomy pages until you curate them.
  • If you keep them indexable, add unique copy and internal links.
  • Cap pagination. Do not create hundreds of near empty pages.
# config.toml
[taxonomies]
  tag = "tags"
  category = "categories"
# Remove a line to disable that taxonomy entirely

Noindex in the head partial when the kind is taxonomy or term, unless you opt in with front matter. The head example above shows this pattern.

---
title: "Kotlin tools"
index: true            # you allow this term page to be indexed
meta_description: "Tools for Kotlin devs we recommend."
---

Content above the list adds context. Use the term template to render it.
{{/* layouts/_default/terms.html or taxonomy.html */}}
<h1>{{ .Title }}</h1>
{{ with .Content }}<div class="term-intro">{{ . }}</div>{{ end }}
<ul>
  {{ range .Pages }}
    <li><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></li>
  {{ end }}
</ul>

Canonicals: one URL per page

Set a canonical for every page. Hugo does not add one for you. Use .Permalink by default. Allow an override for true duplicates you must keep live.

# In front matter when you need to point elsewhere
---
canonical: "https://yourproduct.com/guides/getting-started/"
---
  • Do not point canonicals across languages or products unless the content is the same.
  • Paginated lists should self canonical. Do not point page 2 to page 1 unless you remove page 2 from the index.
  • Avoid query string variants at the server. Hugo builds static paths, so keep links consistent in templates.

Images: alt text, dimensions and fast delivery

Hugo’s image pipeline helps, but templates must call it. Ship correct dimensions to prevent layout shift. Generate responsive formats. Keep the LCP under 2.5 seconds and CLS under 0.1.

{{/* Example for a post hero image in a layout */}}
{{ $img := .Resources.GetMatch .Params.hero }}
{{ with $img }}
  {{ $w1200 := .Fit "1200x630" }}
  {{ $w800  := .Fit "800x420" }}
  {{ $w400  := .Fit "400x210" }}
  <link rel="preload" as="image" href="{{ $w1200.RelPermalink }}" imagesrcset="{{ $w400.RelPermalink }} 400w, {{ $w800.RelPermalink }} 800w, {{ $w1200.RelPermalink }} 1200w" imagesizes="(min-width: 768px) 800px, 100vw">
  <img
    src="{{ $w800.RelPermalink }}"
    srcset="{{ $w400.RelPermalink }} 400w, {{ $w800.RelPermalink }} 800w, {{ $w1200.RelPermalink }} 1200w"
    sizes="(min-width: 768px) 800px, 100vw"
    width="{{ $w800.Width }}" height="{{ $w800.Height }}"
    alt="{{ with .Page.Params.hero_alt }}{{ . }}{{ else }}{{ $.Title }} hero image{{ end }}"
    loading="lazy" decoding="async">
{{ end }}
  • Always set width and height attributes. That prevents layout shift.
  • Set alt text in front matter or infer from the caption. Do not leave it empty unless the image is decorative.
  • Preload the LCP image on templates where the hero is the LCP.
  • Compress originals. Hugo can resize, but the input size still affects build and transfer.
  • Audit INP too. Keep it under 200 ms. Heavy client scripts slow interaction, even on static HTML.

Social previews and basics you forget

Social images and structured data improve how your pages appear. Add a default image and site name. Set favicons and a web app manifest once. Then stop touching it.

{{/* config params for site-wide fallbacks */}}
# config.toml
[params]
  default_og_image = "/images/social-default.jpg"
  organization_name = "Your Product"

{{/* in head partial */}}
{{ $og := or .Params.og_image .Site.Params.default_og_image }}
{{ with $og }}
  <meta property="og:image" content="{{ . }}">
  <meta name="twitter:image" content="{{ . }}">
{{ end }}
  • Add a 1200 by 630 social image per key page. Store paths in front matter.
  • Set a site favicon set. Link rel icons once in the base layout.
  • If you add JSON-LD, keep it minimal and accurate. Do not fake reviews or ratings.

A Hugo SEO checklist to ship

  1. Head partial included on all layouts

    Title, description, canonical, robots meta, Open Graph and Twitter. No duplicates. Home page uses a custom title.

  2. Index rules are explicit

    Front matter supports noindex. Taxonomies default to noindex until you opt in. Drafts never index. Staging blocks crawling.

  3. Sitemap.xml includes only indexable URLs

    Custom sitemap template filters drafts and noindex. It excludes thin archives and unnecessary pagination. Absolute URLs only.

  4. robots.txt matches the environment

    Production allows crawling and links the sitemap. Staging blocks all. BaseURL is correct. You test the live file after deploy.

  5. Canonicals are correct

    Default to .Permalink. Overrides exist for true duplicates only. No cross-site canonicals unless content is the same.

  6. Images are optimised

    Hero has preload, srcset, width and height. Alt text is set. Binary sizes are compressed. LCP is under 2.5 s, CLS under 0.1.

  7. Internal links are clear

    No orphan pages. Primary pages are within two clicks. Navigation and footer link to money pages and key guides.

  8. Submit and monitor in Search Console

    Verify the property. Submit sitemap.xml. Check the Sitemaps and Page indexing reports. Fix errors before adding more pages.

  • Keep URLs stable. Avoid renames. If you must change, ship 301 redirects at the host.
  • Avoid index bloat from autogenerated pages and feeds you do not need.
  • Do not ship heavy client scripts. Static HTML is fast. Protect INP and LCP by default.

Questions

Sources

Check my site, free

Want a second pair of eyes on your Hugo setup? Run a free check from a URL and see three findings on your site, the searches around it and the rivals in about thirty seconds.

  • Free check, no card
  • Read-only, your own accounts
  • Readable by your agent

Read next