16.2 Home Page Template: layouts/index.html
Alright, let’s talk about the home page. This is your digital front door, the first thing most people will see. And because of that, Hugo, in its infinite wisdom, gives you a few ways to build it, which is both a blessing and a curse. The main event is layouts/index.html. This file sits at the root of your layouts directory and is the default template for when someone hits the root of your domain (yoursite.com/).
Now, here’s the first bit of Hugo voodoo you need to understand: your home page isn’t a “page” in the content sense. It doesn’t have a content file in content/ with front matter. It’s a kind of list page. Specifically, it’s the list page for the entire site. This means it has access to all the site-wide variables (.Site) and, crucially, it can range over the site’s regular pages. This is both incredibly powerful and a common source of confusion.
The Bare Minimum (That Actually Works)
Let’s start with something that won’t embarrass you. A simple home page that lists your most recent content. This is the “blog-style” home page, and it’s a classic for a reason.
{{ define "main" }}
<article class="homepage">
<header>
<h1>{{ .Site.Title }}</h1>
<p>{{ .Site.Params.Description | default "Just another Hugo site" }}</p>
</header>
<section>
<h2>Latest Musings</h2>
<ul>
{{ range first 5 (where site.RegularPages "Type" "in" "posts") }}
<li>
<a href="{{ .Permalink }}">{{ .Title }}</a>
<span class="date">{{ .Date.Format "January 2, 2006" }}</span>
</li>
{{ end }}
</ul>
</section>
</article>
{{ end }}
Let’s break down the magic trick here. We’re using {{ define "main" }} because we’re injecting our content into the baseof template, which is just good manners. The range statement is where the real work happens: {{ range first 5 (where site.RegularPages "Type" "in" "posts") }}.
site.RegularPages: This is a goldmine. It’s a collection of all pages on your site that are not sections, taxonomies, or terms. It excludes all the scaffolding and gets right to the content you wrote.where ... "Type" "in" "posts": We’re filtering that list further. We only want pages from the “posts” section (or whatever you’ve called your main content type). The"in"operator is useful here because you can provide a slice of sections:"in" (slice "posts" "projects").first 5: This function plucks the first 5 items from the resulting filtered list. Since Hugo lists pages by date in descending order by default, this gives us the five most recent posts.
Why This Is a Solid Default
This approach is robust. By using site.RegularPages, you’re future-proofing yourself. If you add a new section like “notes” or “links”, they won’t accidentally show up on your home page unless you explicitly add them to that where clause. It’s a conscious design choice, not a happy accident.
Getting Fancy: Using a _index.md File
“But wait,” you say, “I want a cool hero section with some custom markdown text above my post list!” I hear you. This is where things get interesting. You can create a content file at content/_index.md and give it some front matter. This file will then be available to your layouts/index.html template as the .Title, .Content, etc.
This is Hugo’s “sections” model leaking into the home page, and it’s… fine. It works.
{{ define "main" }}
<article class="homepage">
<header class="hero">
<h1>{{ .Title }}</h1> <!-- Pulls from _index.md -->
<div class="intro">
{{ .Content }} <!-- Pulls from _index.md -->
</div>
</header>
<section>
<h2>Recent Writing</h2>
<ul class="post-list">
<!-- But we still range over the global site list -->
{{ range first 5 (where site.RegularPages "Type" "in" "posts") }}
<li>
<a href="{{ .Permalink }}">{{ .Title }}</a>
<span class="date">{{ .Date.Format "Jan 2, 2006" }}</span>
</li>
{{ end }}
</ul>
</section>
</article>
{{ end }}
The potential pitfall here is a cognitive one: your template is now a hybrid. The .Title and .Content come from one source (content/_index.md), but the list of pages comes from another, completely unrelated source (site.RegularPages). It’s not wrong, it’s just conceptually messy. Keep it straight in your head, and you’ll be fine.
The “Oops, I Broke Pagination” Trap
Let’s say you want to show more than just a handful of posts. You think, “I’ll just paginate the home page like any other list!” So you add {{ template "_internal/pagination.html" . }} to the bottom of your template.
It might even work. Until it doesn’t.
Remember, the home page is a list of site.RegularPages. If you have a lot of content, that’s a massive list to paginate through on every build. Hugo’s pagination is efficient, but you’re asking it to paginate the entire universe of your content. For a large site, this can noticeably slow down your build times. The best practice? Don’t paginate your entire site on the home page. Use first to grab a curated number of items (5, 10, 15). If you want a full archive, create a dedicated “Archive” section page and paginate that instead. Your build times will thank you.
The Nuclear Option: It’s Your Party
Never forget: layouts/index.html is just a template. You can do whatever you want in there. It doesn’t have to be a list of recent posts. You could hardcode a landing page, pull data from a headless CMS, or display a single “featured” post. The key is understanding that its default superpower is acting as the list for site.RegularPages. Whether you use that power or not is entirely up to you. Just know the rules before you break them.