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

Updated 2026-09-14 · Source: https://porteur.ai/guides/hugo-seo

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

```html
{{/* 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.

```html
{{ 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.

> Do not output duplicate head partials. Two canonicals or two titles confuse crawlers.

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

```toml
# 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.

```xml
{{- 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>
```

> The numbers in the Format string are Go’s reference date tokens, typical for the language, not site measurements.

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

> If you cannot confirm an exclusion method from docs, filter by your own front matter flags in a custom template and test the output.

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

```text
# 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.

> Never deploy Disallow: / to production. If in doubt, check the live file at yourproduct.com/robots.txt after each release.

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

```toml
# 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.

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

```html
{{/* 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.

```yaml
# 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.

> If Search Console shows Duplicate without user-selected canonical, audit your canonicals and internal links first.

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

```html
{{/* 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.

```toml
{{/* 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

### Do I need an SEO plugin or module for Hugo?

No. Hugo does not have a native plugin layer for SEO. You write layouts and partials for metadata, sitemap and robots. Community modules exist, but you can ship the small set you need with a few templates you control.

### How do I exclude a page from the Hugo sitemap?

Use a custom sitemap template and filter on a front matter flag, for example noindex true. Hugo’s built-in template is generic, so override it and test the output. Keep the file valid XML and link absolute URLs.

### Should I index tag and category pages on a small site?

Usually no. They are thin until you add copy and curation. Default them to noindex in the head partial. When a term page has unique content and demand, let it index and link it from templates.

### Where do I set robots.txt in Hugo?

Create layouts/robots.txt as a template and use hugo.Environment to vary rules. Allow on production and disallow on staging. Point to your sitemap with the BaseURL. Rebuild and check the live file after deploy.

### How do I handle canonical URLs on paginated lists?

Use self canonicals. Each page of a series should point to itself, not to page 1. If you do not want page 2 indexed, noindex it and keep it out of the sitemap.

### How do I improve Core Web Vitals on a Hugo site?

Keep pages light. Set width and height on images to avoid layout shift. Preload the LCP image. Avoid heavy scripts that hurt INP. Aim for LCP under 2.5 seconds, CLS under 0.1 and INP under 200 ms.

## Read next

- [Technical SEO checklist for a small site](https://porteur.ai/guides/technical-seo-checklist): Run these 20 technical SEO checks, in order. Each shows how to check it free and what fixed looks like for a site under 1,000 pages.
- [On-page SEO checklist: the twelve things on the page](https://porteur.ai/guides/on-page-seo-checklist): The twelve page elements that move clicks and rankings, with examples you can copy to your own product pages today.
- [Canonical tags: what they do and the mistakes that cost rankings](https://porteur.ai/guides/canonical-tag): What a canonical tag does, when to use one, the mistakes that cost rankings, and how to check and fix canonicals on a small site.
- [The Sitemaps report: what Success, Has errors and Couldn’t fetch mean](https://porteur.ai/guides/search-console-sitemaps-report): How to submit your sitemap URL, read Success, Has errors and Couldn’t fetch, and fix invalid XML, 404s, cross‑host URLs and missing child sitemaps.
- [XML sitemap](https://porteur.ai/glossary/xml-sitemap): What to put in sitemap.xml, how lastmod works, how to create, validate and submit your XML sitemap, and the traps to avoid.
- [robots.txt](https://porteur.ai/glossary/robots-txt): robots.txt tells crawlers which URLs they may fetch. See what it does not do, how to test it, what to put in it, and how to handle AI bots.
- [Framer SEO: the settings, the staging trap, and what to check](https://porteur.ai/guides/framer-seo): Set up Framer SEO the right way: page settings, sitemap and robots, redirects, CMS fields, staging that stays noindex, speed, and Search Console checks.
- [Nuxt SEO: useSeoMeta, the SEO modules, and the client-rendering trap](https://porteur.ai/guides/nuxt-seo): Ship Nuxt pages that Google can crawl: server-rendered HTML, correct meta, a clean sitemap and robots, schema, fast images, and no client-only content.

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: https://porteur.ai/
