An Easy and Effective Way to Generate sitemap.xml in Hugo for Maximum SEO
14 min read

An Easy and Effective Way to Generate sitemap.xml in Hugo for Maximum SEO

Every time you build a site with Hugo and run hugo build, an XML file is generated automatically in the public/ directory — and many developers pass it by without ever opening it. That file is sitemap.xml, and understanding how it works in depth can be the difference between your content pages getting indexed by Google in hours versus getting indexed in weeks.

A sitemap isn’t just a list of URLs. It’s a signal you send to search engine crawlers: which pages exist, when they were last modified, how often they change, and how important they are relative to other pages on the same site. Hugo provides all these foundations out of the box, but most of the default configuration isn’t optimal for production needs. This article covers everything from the beginning to deep customization — including rarely discussed scenarios like sitemaps for multilingual sites and sitemap indexes for large-scale sites.


How Sitemaps Work and Why They Matter

Before getting into Hugo configuration, it’s important to understand why sitemaps exist and how search engines use them.

Search engine crawlers — Googlebot, Bingbot, and others — find pages in two ways: by following links from already-indexed pages, and by reading sitemaps. The first method is called organic crawling, while a sitemap gives the crawler an explicit roadmap of your site’s structure.

flowchart TD
    A[Search Engine Crawler] --> B{Ways to Find Pages}
    B --> C[Organic Crawling\nFollowing links between pages]
    B --> D[Sitemap Discovery\nReading sitemap.xml]
    C --> E[Slow for new pages\nDepends on internal linking]
    D --> F[Fast for new pages\nYou have full control]
    F --> G[URL found]
    E --> G
    G --> H[Crawling & Indexing]

Sitemaps are very useful in the following conditions:

  • New sites without many external backlinks yet
  • Pages not linked from the main navigation
  • Frequently updated content you want Google to know about as soon as possible
  • Sites with hundreds or thousands of pages where organic crawling could miss some URLs

It’s also important to understand what a sitemap doesn’t do: it doesn’t guarantee a page will be indexed. A sitemap is a recommendation, not a command. Google still decides for itself whether a page is worth indexing based on its content quality.

The Anatomy of a sitemap.xml File

Sitemap files follow the standard defined by sitemaps.org and accepted by all major search engines:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://example.com/artikel-pertama/</loc>
    <lastmod>2026-01-15</lastmod>
    <changefreq>weekly</changefreq>
    <priority>0.8</priority>
  </url>
  <url>
    <loc>https://example.com/tentang/</loc>
    <lastmod>2025-06-01</lastmod>
    <changefreq>monthly</changefreq>
    <priority>0.5</priority>
  </url>
</urlset>

Four elements in each <url>:

ElementRequiredFunction
<loc>YesThe full page URL, including protocol and trailing slash
<lastmod>NoThe date the content was last modified, ISO 8601 format
<changefreq>NoA hint for how often the page changes
<priority>NoThe page’s relative priority (0.0–1.0)
Important note: Google officially states that they’ve ignored changefreq and priority for several years, using only <loc> and <lastmod> as meaningful signals. Bing still considers both. Include all elements for compatibility, but don’t obsess over their values.

Hugo’s Built-in Sitemap: What You Get by Default

Hugo generates sitemap.xml automatically with no configuration at all. This is one of Hugo’s advantages over other static site generators that require additional plugins.

After running hugo build, you’ll find the file at:

public/
  sitemap.xml
  index.html
  artikel-pertama/
    index.html
  ...

By default, Hugo includes all pages in the sitemap: regular articles, section pages, taxonomy pages (tags, categories), and static pages like /about/. Pages set to draft: true in front matter aren’t included.

To see what Hugo generates by default, run the build and open public/sitemap.xml. You’ll see every page listed with a <lastmod> taken from the content file’s modification date.


Global Sitemap Configuration

Sitemap configuration is done in Hugo’s main configuration file. Hugo supports three formats: TOML, YAML, and JSON.

TOML (hugo.toml or config.toml)

baseURL = "https://example.com/"
languageCode = "id-ID"
title = "My Blog"
enableRobotsTXT = true

[sitemap]
  changefreq = "weekly"
  priority = 0.5
  filename = "sitemap.xml"

YAML (hugo.yaml or config.yaml)

baseURL: "https://example.com/"
languageCode: "id-ID"
title: "My Blog"
enableRobotsTXT: true

sitemap:
  changefreq: "weekly"
  priority: 0.5
  filename: "sitemap.xml"

Available Values for changefreq

ValueWhen to Use
alwaysPages that change on every access (very rarely relevant)
hourlyLive dashboards, breaking news
dailyBlogs with daily posts, news aggregators
weeklyBlogs with weekly updates
monthlyStable content pages like documentation
yearlyPages that almost never change
neverArchived pages that will never change

For a typical technical blog, weekly or monthly is the most sensible choice.

priority Values

Priority is a scale from 0.0 to 1.0 indicating the relative importance of a page within your own site. Hugo’s default is 0.5. The home page is usually given 1.0, main articles around 0.8, tag/category pages around 0.3.

# A more differentiated configuration
[sitemap]
  changefreq = "weekly"
  priority = 0.5  # default for all pages
  filename = "sitemap.xml"

This global value applies to all pages. For per-page overrides, we’ll cover that in the front matter section.


Excluding Pages from the Sitemap

Not every page belongs in the sitemap. Pages like search results, custom error pages, or utility pages shouldn’t be sent to crawlers.

Method 1: Per-Page Front Matter

---
title: "Search Page"
sitemap:
  exclude: true
---

Or with the TOML format in front matter:

+++
title = "Search Page"
[sitemap]
  exclude = true
+++

Method 2: Via robots.txt (A Different Approach)

If a page is already in the sitemap but you don’t want it crawled at all, combine it with robots.txt:

User-agent: *
Disallow: /search/
Disallow: /private/

Sitemap: https://example.com/sitemap.xml
Don’t confuse sitemap exclusion with robots.txt Disallow. A Disallow in robots.txt prevents crawlers from accessing a page. Removing it from the sitemap only means you’re not actively recommending that page. Both have different purposes and often need to be used together.

Per-Page Priority Overrides via Front Matter

Global configuration provides default values for all pages. But main articles, the home page, and landing pages deserve higher priority than tag or archive pages.

Hugo allows this override directly in each page’s front matter:

---
title: "Complete Kubernetes Guide for Production"
date: 2026-01-15
lastmod: 2026-03-20
sitemap:
  changefreq: "monthly"
  priority: 0.9
---

Or in TOML format:

+++
title = "Complete Kubernetes Guide for Production"
date = 2026-01-15
lastmod = 2026-03-20

[sitemap]
  changefreq = "monthly"
  priority = 0.9
+++

Important note: lastmod in front matter is the most accurate way to tell search engines when content was last updated. Hugo uses this value when available, falling back to the file’s modification date on the filesystem.

A Sensible Priority Strategy

Home page (/)                     → priority: 1.0
Pillar/featured articles          → priority: 0.9
Regular articles                  → priority: 0.7
Section/category pages            → priority: 0.5
Tag pages                         → priority: 0.3
About/contact pages               → priority: 0.4
Archive/search pages              → priority: 0.1 or exclude

Custom Sitemap Templates

This is a feature that’s often overlooked but very powerful. Hugo lets you replace the default sitemap template with a custom version that gives you full control over the XML output.

Create a file at this path:

layouts/
  sitemap.xml      ← replaces the global sitemap template

Basic Template

The following template replicates Hugo’s built-in behavior more explicitly:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  {{- range .Data.Pages }}
  {{- if not .Params.sitemap.exclude }}
  <url>
    <loc>{{ .Permalink }}</loc>
    {{- if not .Lastmod.IsZero }}
    <lastmod>{{ .Lastmod.Format "2006-01-02" }}</lastmod>
    {{- end }}
    <changefreq>{{ with .Params.sitemap.changefreq }}{{ . }}{{ else }}{{ $.Site.Sitemap.ChangeFreq }}{{ end }}</changefreq>
    <priority>{{ with .Params.sitemap.priority }}{{ . }}{{ else }}{{ $.Site.Sitemap.Priority }}{{ end }}</priority>
  </url>
  {{- end }}
  {{- end }}
</urlset>

Template with Content Filtering

For sites that want to control which pages are sent to the sitemap — for example only articles above a certain length, or only pages not tagged as drafts — you can add conditions:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  {{- range .Data.Pages }}
  {{- if and (not .Params.sitemap.exclude) (not .Draft) }}
  <url>
    <loc>{{ .Permalink }}</loc>
    {{- if not .Lastmod.IsZero }}
    <lastmod>{{ .Lastmod.Format "2006-01-02" }}</lastmod>
    {{- end }}
    {{- with .Params.sitemap.changefreq }}
    <changefreq>{{ . }}</changefreq>
    {{- else }}
    <changefreq>{{ $.Site.Sitemap.ChangeFreq }}</changefreq>
    {{- end }}
    {{- with .Params.sitemap.priority }}
    <priority>{{ . }}</priority>
    {{- else }}
    <priority>{{ $.Site.Sitemap.Priority }}</priority>
    {{- end }}
  </url>
  {{- end }}
  {{- end }}
</urlset>

Template with the Image Sitemap Extension

For sites whose content contains many important images (portfolios, galleries, tutorials with screenshots), Google recommends including image information in the sitemap using a special namespace extension:

<?xml version="1.0" encoding="UTF-8"?>
<urlset
  xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
  xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
  {{- range .Data.Pages }}
  {{- if not .Params.sitemap.exclude }}
  <url>
    <loc>{{ .Permalink }}</loc>
    {{- if not .Lastmod.IsZero }}
    <lastmod>{{ .Lastmod.Format "2006-01-02" }}</lastmod>
    {{- end }}
    {{- if .Params.cover_image }}
    <image:image>
      <image:loc>{{ .Params.cover_image | absURL }}</image:loc>
      <image:title>{{ .Title }}</image:title>
    </image:image>
    {{- end }}
  </url>
  {{- end }}
  {{- end }}
</urlset>

This requires front matter with a cover_image field on every article that has a main image.


Sitemap Indexes: For Large-Scale Sites

The sitemap standard limits a single sitemap file to a maximum of 50,000 URLs and a maximum of 50 MB after compression. For sites approaching or exceeding these limits, the solution is a sitemap index — an XML file that contains a list of other sitemaps, instead of a direct URL list.

flowchart TD
    A[sitemap.xml\nSitemap Index] --> B[sitemap-articles.xml\nAll articles]
    A --> C[sitemap-pages.xml\nStatic pages]
    A --> D[sitemap-tags.xml\nTaxonomy pages]
    B --> E[URL 1..N]
    C --> F[about, contact, ... URLs]
    D --> G[tag/golang, tag/docker, ... URLs]

Hugo has built-in support for sitemap indexes via the layouts/sitemapindex.xml template. But for more granular control — for example creating separate sitemaps per section — you need a custom output format approach.

Manual Sitemap Index with Hugo Outputs

In hugo.toml, define a custom output format:

[outputs]
  home = ["HTML", "RSS", "SitemapIndex"]

[outputFormats]
  [outputFormats.SitemapIndex]
    mediaType = "application/xml"
    baseName = "sitemap"
    isPlainText = false
    notAlternative = true

Then create a template at layouts/index.sitemapindex.xml:

<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  {{ range .Site.Pages }}
  {{ if eq .Section "articles" }}
  <sitemap>
    <loc>{{ "articles-sitemap.xml" | absURL }}</loc>
    <lastmod>{{ now.Format "2006-01-02" }}</lastmod>
  </sitemap>
  {{ end }}
  {{ end }}
  <sitemap>
    <loc>{{ "pages-sitemap.xml" | absURL }}</loc>
  </sitemap>
</sitemapindex>

For a typical site with hundreds of pages, a sitemap index isn’t needed yet. Start considering it when the page count approaches 10,000+.


Sitemaps for Multilingual Sites

This is one of the areas that most often causes confusion. If your Hugo site supports more than one language, Hugo generates a sitemap per language by default.

Basic Multilingual Configuration

# hugo.yaml
defaultContentLanguage: "id"
defaultContentLanguageInSubdir: false

languages:
  id:
    languageName: "Indonesia"
    weight: 1
    baseURL: "https://example.com/"
  en:
    languageName: "English"
    weight: 2
    baseURL: "https://example.com/en/"

With this configuration, Hugo generates:

public/
  sitemap.xml          ← a sitemap index referencing both
  id/
    sitemap.xml        ← a sitemap for Indonesian content
  en/
    sitemap.xml        ← a sitemap for English content

Adding hreflang Tags in the Sitemap

For multilingual sites, Google recommends including hreflang tags in the sitemap so crawlers know that two pages are translations of each other. This requires a custom template:

<?xml version="1.0" encoding="UTF-8"?>
<urlset
  xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
  xmlns:xhtml="http://www.w3.org/1999/xhtml">
  {{- range .Data.Pages }}
  {{- if not .Params.sitemap.exclude }}
  <url>
    <loc>{{ .Permalink }}</loc>
    {{- if not .Lastmod.IsZero }}
    <lastmod>{{ .Lastmod.Format "2006-01-02" }}</lastmod>
    {{- end }}
    {{- range .Translations }}
    <xhtml:link
      rel="alternate"
      hreflang="{{ .Language.Lang }}"
      href="{{ .Permalink }}"/>
    {{- end }}
    <xhtml:link
      rel="alternate"
      hreflang="{{ .Language.Lang }}"
      href="{{ .Permalink }}"/>
  </url>
  {{- end }}
  {{- end }}
</urlset>

Integrating with robots.txt

Hugo can generate robots.txt automatically if you enable this option:

# hugo.toml
enableRobotsTXT = true

Hugo’s default robots.txt only contains one line:

User-agent: *

To include the sitemap location, create a custom template at layouts/robots.txt:

User-agent: *
Allow: /

# Crawlers you want to block entirely
User-agent: AhrefsBot
Disallow: /

# Pages you don't want crawled
Disallow: /search/
Disallow: /private/
Disallow: /admin/

# Sitemap
Sitemap: {{ .Site.BaseURL }}sitemap.xml
Why enableRobotsTXT = true matters: Without the Sitemap: line in robots.txt, crawlers can still find your sitemap but have to know its URL. By listing the sitemap in robots.txt, every crawler visiting your site immediately knows where the sitemap is — even before finding any page.

Submitting to Google Search Console

Generating a perfect sitemap isn’t enough if search engines don’t know it exists. There are two ways to tell Google about your sitemap.

Method 1: Via robots.txt

This was covered above — by adding Sitemap: https://example.com/sitemap.xml to robots.txt, Google will find your sitemap automatically when visiting the site.

Method 2: Manual Submission via Google Search Console

flowchart LR
    A[Google Search Console] --> B[Select Property]
    B --> C[Sitemaps in the left menu]
    C --> D[Enter the sitemap URL]
    D --> E[sitemap.xml]
    E --> F[Click Submit]
    F --> G[Google starts processing]
    G --> H[Status: Success / Error]

The steps:

  1. Open search.google.com/search-console
  2. Select your site’s property (or add one if you haven’t)
  3. In the left menu, click Sitemaps
  4. In the “Add a new sitemap” field, enter the relative path: sitemap.xml
  5. Click Submit

Google will process the sitemap and show its status: how many URLs were found, how many are indexed, and whether there are errors.

What to Check in Search Console

After submitting, monitor a few things:

  • Discovered URLs vs Indexed URLs: If there’s a big gap, Google found pages in the sitemap but chose not to index them — usually because of content quality or duplicate content issues
  • Errors: URLs that can’t be accessed, redirect chains, or wrong sitemap format
  • Last read: When Google last read your sitemap — ideally within the last few days

Anti-Patterns to Avoid

1. Including Non-Canonical URLs

<!-- ANTI-PATTERN: URLs with query strings or UTM parameters -->
<url>
  <loc>https://example.com/artikel/?utm_source=twitter</loc>
</url>
<url>
  <loc>https://example.com/artikel/?page=2</loc>
</url>

<!-- CORRECT: only canonical URLs without tracking parameters -->
<url>
  <loc>https://example.com/artikel/</loc>
</url>

If you have URLs with query strings in the sitemap, Google will see them as different URLs and could create duplicate content problems.

2. Including Pages Blocked by robots.txt

# robots.txt
Disallow: /admin/
Disallow: /private/
<!-- ANTI-PATTERN: including URLs disallowed in robots.txt -->
<url>
  <loc>https://example.com/admin/dashboard/</loc>
</url>

<!-- Google will complain about this inconsistency in Search Console -->

Sitemap and robots.txt must be consistent with each other.

3. Inaccurate lastmod

# ANTI-PATTERN: lastmod in front matter is never updated
---
title: "An Article Updated Many Times"
date: 2024-01-01
lastmod: 2024-01-01  # never updated even though content was revised
---

# CORRECT: lastmod is always updated whenever content changes significantly
---
title: "An Article Updated Many Times"
date: 2024-01-01
lastmod: 2026-03-15  # reflects the latest substantial revision
---

If lastmod is inaccurate, Google loses an important signal about when content was updated and may not prioritize re-crawling.

4. Giving Every Page the Same Priority

# ANTI-PATTERN: uniform priority for all pages
[sitemap]
  priority = 0.8  # every page 0.8? this sends no signal at all

If all pages have the same priority, you lose the benefit of that field. Better to use variation that reflects real importance, or leave it at the default 0.5 and override only for truly critical pages.

5. Not Verifying the Sitemap Output After Build

# ANTI-PATTERN: build and deploy immediately without checking the sitemap
hugo build
# immediately upload to the server

# CORRECT: verify first
hugo build
cat public/sitemap.xml | head -50
# or open in a browser: hugo server, then visit /sitemap.xml

Errors in the sitemap template can produce invalid XML, which will be rejected by Google Search Console.


The Complete Workflow: From Build to Submission

Here’s a complete workflow to ensure your Hugo sitemap is correct end to end:

flowchart TD
    A[Configure sitemap\nin hugo.toml/yaml] --> B[Add per-page sitemap\nfront matter if needed]
    B --> C{Custom template\nneeded?}
    C -- Yes --> D[Create layouts/sitemap.xml]
    C -- No --> E[Use Hugo's default template]
    D --> F[hugo build]
    E --> F
    F --> G[Verify public/sitemap.xml]
    G --> H{XML valid?}
    H -- No --> I[Debug the template]
    I --> F
    H -- Yes --> J[Update robots.txt\ninclude the Sitemap: URL]
    J --> K[Deploy to production]
    K --> L[Submit to Google Search Console]
    L --> M[Monitor indexing status]
    M --> N{Any errors?}
    N -- Yes --> O[Investigate and fix]
    N -- No --> P[Monitor periodically]

Sitemap Checklist Before Deploy

CONFIGURATION:
  □ baseURL in hugo.toml uses the production domain (not localhost)
  □ enableRobotsTXT = true is enabled
  □ The changefreq value matches your content update frequency
  □ The default priority is sensible (0.5 for regular pages)

FRONT MATTER:
  □ Important pages have accurate lastmod
  □ Pages that don't need indexing have sitemap.exclude = true
  □ Featured articles have a higher priority

OUTPUT:
  □ public/sitemap.xml exists after the build
  □ The XML opens in a browser without parsing errors
  □ The URL count in the sitemap is sensible (not too few, no duplicate pages)
  □ All URLs use HTTPS (not HTTP)
  □ All URLs use the correct domain

ROBOTS.TXT:
  □ The Sitemap: https://domain.com/sitemap.xml line exists
  □ Disallowed pages don't appear in the sitemap

SEARCH CONSOLE:
  □ The sitemap has been submitted
  □ The status shows success
  □ No errors are reported

Summary

  • A sitemap is a signal, not a command — Google uses it as a crawling guide, but still decides for itself whether a page deserves indexing based on content quality.
  • Hugo generates sitemap.xml automatically with no configuration at all — located at public/sitemap.xml after the build. But the default configuration isn’t always optimal.
  • Global configuration in hugo.toml/yaml sets changefreq, priority, and filename for all pages at once.
  • Per-page overrides via the sitemap.changefreq, sitemap.priority, and sitemap.exclude front matter give granular control for specific pages.
  • lastmod is the most important element that Google actually reads — always update this field in front matter whenever there’s a substantial content revision.
  • Custom templates at layouts/sitemap.xml give full control over the XML output — useful for adding image sitemaps, hreflang for multilingual sites, or complex filtering logic.
  • robots.txt must be consistent with the sitemap — don’t put URLs in the sitemap that are Disallowed in robots.txt.
  • Enable enableRobotsTXT = true and add a Sitemap: line in robots.txt so crawlers find the sitemap automatically.
  • Submit to Google Search Console to make sure Google knows about your sitemap and actively monitor indexing status.
  • Main anti-patterns: URLs with query strings, inaccurate lastmod, uniform priority for all pages, and not verifying XML validity after the build.

Portfolio