29.1 Built-in Output Formats: HTML, RSS, JSON, CSV, robots.txt, sitemap
Right, let’s talk about the freebies. Hugo doesn’t make you build everything from scratch. It comes packing a set of built-in output formats that cover about 90% of what a typical website needs to do. These aren’t just afterthoughts; they’re core to its philosophy of being a full-fledged web engine, not just a fancy blog generator.
We’ll get to the cool custom ones later, but first, you need to understand the tools already in your belt. The big ones are HTML, RSS, JSON, and CSV. But we’ll also touch on the two special-purpose ones: robots.txt and sitemap.xml.
The Workhorse: HTML
This is the obvious one. Every page kind template (like single.html) you create in your layouts directory inherently outputs to Hugo’s HTML format. You don’t have to configure a thing. It’s the default, and it just works. The magic is that you can have multiple HTML output formats. Why would you do that? Maybe you want a minimalist, text-only version of your article for a specific audience, or a dedicated print stylesheet version. You’d define a new format (we’ll cover that later) and create a corresponding template like single.print.html. For now, just know that the basic HTML you’re used to is the primary, built-in format.
RSS: The Relic That Refuses to Die (Thankfully)
RSS is a fantastic, no-nonsense way to let machines know you’ve published something new. Hugo’s built-in RSS template is… functional. It’ll get the job done, emitting a valid RSS 2.0 feed. You can find it at /index.xml by default.
The real power move is overriding it. You don’t like my boring default RSS feed? Of course you don’t. Create a layouts/index.rss.xml file. Now you’re in control. Here’s a minimal example that spits out your last 10 articles:
<!-- layouts/index.rss.xml -->
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{{ .Site.Title }}</title>
<link>{{ .Site.BaseURL }}</link>
<description>Recent content on {{ .Site.Title }}</description>
<atom:link href="{{ .Permalink }}" rel="self" type="application/rss+xml" />
{{ range ( where .Site.RegularPages "Type" "in" site.Params.mainSections | first 10 ) }}
<item>
<title>{{ .Title }}</title>
<link>{{ .Permalink }}</link>
<pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</pubDate>
<guid>{{ .Permalink }}</guid>
<description>{{ .Summary | html }}</description>
</item>
{{ end }}
</channel>
</rss>
Pitfall Alert: That .Date field uses the front matter date, not the filesystem date. And note the very specific, very weird format string for pubDate—it’s not RFC3339, it’s the exact way RSS 2.0 expects it. Blame the 2004 spec, not me.
JSON and CSV: Your Data’s Escape Hatch
This is where Hugo starts to feel like a superpower. Need a JSON API endpoint for your posts? Done. Want to export your speaker data as a CSV spreadsheet? Trivial.
Hugo can render any template as JSON or CSV. You just need to tell it which template to use for which output format. The key is the baseName in the output format definition. But for a quick win, create a template file with the right extension. Let’s say you have a ‘speakers’ section. Want a JSON list? Create layouts/speakers/list.json.json (yes, the double .json is correct—the first is the template name, the second is the output format extension).
{{- /* layouts/speakers/list.json.json */ -}}
{{- $.Scratch.Add "json" slice -}}
{{- range .Pages -}}
{{- $.Scratch.Add "json" (dict "name" .Title "bio" .Content "link" .Permalink) -}}
{{- end -}}
{{- ($.Scratch.Get "json") | jsonify -}}
This little template loops through your speaker pages, builds a slice of dictionaries, and then uses the jsonify function to output it as pristine JSON. The Content field is full HTML, which might be overkill, but you see the point. You have the full content of your pages to play with.
Best Practice: For anything more than a trivial example, use $.Scratch or dict to build your data structure before passing it to jsonify. Trying to build JSON strings by hand is a one-way ticket to encoding hell.
The Specialists: robots.txt and sitemap.xml
These are clever. Hugo treats them as output formats, which means they get the full templating treatment. Your robots.txt isn’t a static file; it’s a generated endpoint.
The built-in sitemap is actually pretty good, but you might want to exclude certain pages. Easy. Override layouts/sitemap.xml and add your logic. Don’t want tag pages in the sitemap? Filter them out.
<!-- layouts/sitemap.xml -->
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{{ range .Data.Pages }}
{{ if and (ne .Kind "term") (ne .Params.sitemap_exclude true) }}
<url>
<loc>{{ .Permalink }}</loc>
<lastmod>{{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}</lastmod>
</url>
{{ end }}
{{ end }}
</urlset>
This version excludes any page with sitemap_exclude: true in its front matter and all taxonomy term pages (the list pages for tags/categories). You have complete control.
The beauty of all this is consistency. You’re using the same content, the same front matter, the same templating logic—just for a different output. You’re not maintaining separate systems. It’s all just Hugo.