Custom Hugo Template Development (Go Templates)
Hugo uses the standard html/template package from Go's standard library, extended with its own functions. The syntax differs from Twig, Jinja2, Liquid — it has its own logic that needs to be understood once, then work efficiently. A custom template gives you full control over markup without dependence on third-party themes.
Template Structure
Hugo resolves templates by lookup order. For a page /blog/my-post/, Hugo searches for a template in this order:
layouts/blog/single.html
layouts/blog/single.baseof.html
layouts/_default/single.html
layouts/_default/baseof.html
For a section /services/ (list of pages):
layouts/services/list.html
layouts/_default/list.html
The base template baseof.html defines structure through blocks:
{{/* layouts/_default/baseof.html */}}
<!DOCTYPE html>
<html lang="{{ .Site.Language.Lang }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ block "title" . }}{{ .Site.Title }}{{ end }}</title>
{{ block "head" . }}{{ end }}
{{ partial "head/styles.html" . }}
</head>
<body class="{{ block "body-class" . }}{{ end }}">
{{ partial "header.html" . }}
<main>
{{ block "main" . }}{{ end }}
</main>
{{ partial "footer.html" . }}
{{ partial "head/scripts.html" . }}
</body>
</html>
A child template overrides blocks:
{{/* layouts/blog/single.html */}}
{{ define "title" }}{{ .Title }} | {{ .Site.Title }}{{ end }}
{{ define "body-class" }}page-post{{ end }}
{{ define "main" }}
<article class="post">
<header class="post__header">
<h1>{{ .Title }}</h1>
<time datetime="{{ .Date.Format "2006-01-02" }}">
{{ .Date.Format "2 January 2006" }}
</time>
{{ with .Params.authors }}
<div class="post__authors">
{{ range . }}
{{ $author := index $.Site.Data.authors . }}
<span>{{ $author.name }}</span>
{{ end }}
</div>
{{ end }}
</header>
{{ if .Params.featured_image }}
<figure class="post__cover">
{{ $img := resources.Get .Params.featured_image }}
{{ if $img }}
{{ $webp := $img | images.Resize "1200x630 WebP" }}
{{ $fallback := $img | images.Resize "1200x630" }}
<picture>
<source srcset="{{ $webp.Permalink }}" type="image/webp">
<img src="{{ $fallback.Permalink }}" alt="{{ .Title }}" loading="eager">
</picture>
{{ end }}
</figure>
{{ end }}
<div class="post__body">{{ .Content }}</div>
{{ partial "blog/related-posts.html" . }}
</article>
{{ end }}
Working with Data and Variables
Go Templates are strictly typed. Main patterns:
{{/* Variable assignment */}}
{{ $total := 0 }}
{{ range .Pages }}
{{ $total = add $total 1 }}
{{ end }}
Total posts: {{ $total }}
{{/* Working with dictionaries */}}
{{ $meta := dict "og:type" "article" "og:title" .Title }}
{{ range $key, $val := $meta }}
<meta property="{{ $key }}" content="{{ $val }}">
{{ end }}
{{/* Conditions with AND/OR */}}
{{ if and .Params.featured (not .Draft) }}
<span class="badge">Featured</span>
{{ end }}
{{/* Ternary operator via cond */}}
{{ $class := cond .Params.dark "section--dark" "section--light" }}
<section class="{{ $class }}">
Partial Functions with Parameters
Partials accept context (.) or arbitrary dictionaries:
{{/* Call partial with custom context */}}
{{ partial "components/card.html" (dict
"title" .Title
"url" .Permalink
"image" .Params.thumbnail
"excerpt" .Summary
"date" .Date
"tags" .Params.tags
) }}
{{/* layouts/partials/components/card.html */}}
{{ $ctx := . }}
<article class="card">
{{ with $ctx.image }}
<div class="card__image">
<img src="{{ . }}" alt="{{ $ctx.title }}">
</div>
{{ end }}
<div class="card__body">
<h3 class="card__title">
<a href="{{ $ctx.url }}">{{ $ctx.title }}</a>
</h3>
{{ with $ctx.excerpt }}
<p class="card__excerpt">{{ . }}</p>
{{ end }}
{{ if $ctx.tags }}
<ul class="card__tags">
{{ range $ctx.tags }}
<li><a href="/tags/{{ . | urlize }}/">{{ . }}</a></li>
{{ end }}
</ul>
{{ end }}
</div>
</article>
Custom Pages with Complex Logic
Example: team page with grouping by departments:
{{/* layouts/team/list.html */}}
{{ define "main" }}
{{ $pages := .Pages.ByParam "order" }}
{{/* Grouping by department */}}
{{ $departments := slice }}
{{ range $pages }}
{{ $dept := .Params.department }}
{{ if not (in $departments $dept) }}
{{ $departments = $departments | append $dept }}
{{ end }}
{{ end }}
{{ range $departments }}
{{ $dept := . }}
<section class="team-department">
<h2>{{ $dept }}</h2>
<div class="team-grid">
{{ range where $pages "Params.department" $dept }}
{{ partial "components/team-card.html" . }}
{{ end }}
</div>
</section>
{{ end }}
{{ end }}
SEO Templates
{{/* layouts/partials/head/seo.html */}}
{{ $title := cond .IsHome .Site.Title (printf "%s | %s" .Title .Site.Title) }}
{{ $description := cond .Params.description .Params.description .Site.Params.description }}
{{ $image := cond .Params.featured_image .Params.featured_image .Site.Params.defaultOGImage }}
<title>{{ $title }}</title>
<meta name="description" content="{{ $description }}">
<meta property="og:title" content="{{ $title }}">
<meta property="og:description" content="{{ $description }}">
<meta property="og:url" content="{{ .Permalink }}">
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}">
{{ with $image }}
<meta property="og:image" content="{{ absURL . }}">
{{ end }}
{{ if .IsPage }}
<meta property="article:published_time" content="{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}">
<meta property="article:modified_time" content="{{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" }}">
{{ range .Params.tags }}
<meta property="article:tag" content="{{ . }}">
{{ end }}
{{ end }}
<link rel="canonical" href="{{ .Permalink }}">
{{ range .AlternativeOutputFormats }}
<link rel="{{ .Rel }}" type="{{ .MediaType.Type }}" href="{{ .Permalink | safeURL }}">
{{ end }}
Template Performance
Hugo caches partial results via partialCached:
{{/* Cache by page slug */}}
{{ partialCached "components/sidebar.html" . .Page.Slug }}
{{/* Cache globally (once for entire site) */}}
{{ partialCached "components/footer-nav.html" . }}
partialCached is especially important for partials that do heavy computations or access large datasets.
Timeline
Custom template for a site with 5–15 pages, blog, and basic SEO markup — 1–2 weeks. Complex template with multiple content types, custom taxonomies, multilingual support — 3–5 weeks.







