Custom Shortcodes & Partial Patterns â Extend Hugo Templates Like a Pro
In this tutorial, you'll learn about Custom Shortcodes & Partial Patterns. We cover key concepts, practical examples, and best practices.
Learn to build custom Hugo shortcodes and partials: reusable template components, parameter handling, nested shortcodes, and performance patterns for content-driven static sites.
What You'll Learn
You will learn to create custom Hugo shortcodes for reusable content components, build efficient partials for layout reuse, handle parameters and conditional logic, and implement patterns that keep your templates DRY and maintainable.
Why It Matters
Shortcodes and partials are the building blocks of a maintainable Hugo site. Without them, the same markup is duplicated across dozens of content files and layout templates. When you need to update a button style, a callout box, or an embedded component, duplicating changes across hundreds of files is error-prone and time-consuming. Shortcodes centralize that logic in one place.
Real-World Use
This DodaTech tutorial site uses over 30 custom shortcodes, including the cards shortcode that renders tutorial listing grids, the ilink shortcode for internal cross-referencing, and the callout shortcode for info boxes. These shortcodes are used thousands of times across the site. A single update to the callout shortcode instantly updates every callout on every page.
Your Learning Path
flowchart LR
A[Hugo Basics] --> B[Custom Shortcodes]
B --> C[Partial Patterns]
C --> D[Asset Pipelines]
D --> E[Production Builds]
B --> F{You Are Here}
style F fill:#f90,color:#fff
Prerequisites: Familiarity with Hugo Tutorials basics (directory structure, frontmatter, basic templates). Understanding of Go Templates syntax is helpful.
Shortcodes vs Partials
| Feature | Shortcode | Partial |
|---|---|---|
| Used in | Content .md files |
Template .html files |
| Syntax | {{</* shortcode */>}} |
{{ partial "file.html" . }} |
| Parameters | Named and positional | Passed as a dict |
| Nesting | Supported | N/A |
| Performance | Slightly slower | Faster (cached) |
Building Custom Shortcodes
Shortcodes live in layouts/shortcodes/. Each file is a Go template that receives parameters from the content file.
Simple Shortcode: Button
{{/* layouts/shortcodes/button.html */}}
{{ $text := .Get "text" | default "Click Me" }}
{{ $url := .Get "url" | default "#" }}
{{ $style := .Get "style" | default "primary" }}
{{ $classes := dict
"primary" "bg-blue-600 text-white hover:bg-blue-700"
"secondary" "bg-gray-200 text-gray-800 hover:bg-gray-300"
"danger" "bg-red-600 text-white hover:bg-red-700"
}}
<a href="{{ $url }}"
class="inline-block px-4 py-2 rounded font-medium
{{ index $classes $style }}">
{{ $text }}
</a>
Expected behavior: Using {{</* button text="Download Now" url="/download" style="primary" */>}} in content renders a styled anchor tag. Changing the button style in one shortcode file updates every button on the site.
Shortcode with Inner Content
{{/* layouts/shortcodes/tip.html */}}
{{ $title := .Get "title" | default "Tip" }}
<div class="tip-box border-l-4 border-green-500 bg-green-50 p-4 my-4">
<strong class="text-green-700">{{ $title }}:</strong>
<div class="mt-1 text-green-900">
{{ .Inner | markdownify }}
</div>
</div>
Expected behavior: Content wrapped in {{</* tip */>}}Your tip here{{</* /tip */>}} renders as a styled tip box with the inner content processed through Markdown.
Shortcode with Inner and Positional Parameters
{{/* layouts/shortcodes/code-with-output.html */}}
{{ $lang := .Get 0 | default "text" }}
{{ $output := .Get 1 }}
<div class="code-block mb-4">
{{ highlight (trim .Inner "\n") $lang "" }}
</div>
{{ if $output }}
<div class="output-block bg-gray-100 border border-gray-300 rounded p-3 mb-4">
<strong>Expected output:</strong>
<pre class="mt-1">{{ $output }}</pre>
</div>
{{ end }}
Expected behavior: {{</* code-with-output "python" "Hello, World!" */>}}print("Hello, World!"){{</* /code-with-output */>}} renders highlighted code followed by an output box.
Using Partials for Layout Reuse
Partials are reusable template snippets called from other templates. Unlike shortcodes, they are used in layout files, not content.
Partial: SEO Meta Tags
{{/* layouts/partials/seo-meta.html */}}
{{ $title := .Title }}
{{ $description := .Description }}
{{ $image := .Params.image | default .Site.Params.ogImage }}
{{ $url := .Permalink }}
<meta property="og:title" content="{{ $title }}" />
<meta property="og:description" content="{{ $description }}" />
<meta property="og:image" content="{{ $image | absURL }}" />
<meta property="og:url" content="{{ $url }}" />
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="{{ $title }}" />
<meta name="twitter:description" content="{{ $description }}" />
{{ if .IsPage }}
<meta name="article:published_time" content="{{ .PublishDate.Format "2006-01-02" }}" />
<meta name="article:modified_time" content="{{ .Lastmod.Format "2006-01-02" }}" />
{{ end }}
Expected behavior: Including {{ partial "seo-meta.html" . }} in the <head> section renders all Open Graph and Twitter Card meta tags, pulling data from the page frontmatter.
Partial: Breadcrumbs
{{/* layouts/partials/breadcrumbs.html */}}
{{ if not .IsHome }}
<nav aria-label="Breadcrumb" class="text-sm text-gray-500 mb-4">
<ol class="list-none p-0 inline-flex flex-wrap">
<li class="flex items-center">
<a href="{{ .Site.BaseURL }}" class="hover:underline">Home</a>
<span class="mx-2">/</span>
</li>
{{ range .Ancestors.Reverse }}
{{ if not .IsHome }}
<li class="flex items-center">
<a href="{{ .RelPermalink }}" class="hover:underline">{{ .Title }}</a>
<span class="mx-2">/</span>
</li>
{{ end }}
{{ end }}
<li class="text-gray-700" aria-current="page">{{ .Title }}</li>
</ol>
</nav>
{{ end }}
Expected behavior: Inserted at the top of a single page template, this partial generates a breadcrumb trail from home through all ancestor sections to the current page.
Advanced Patterns
Nested Shortcodes
{{/* layouts/shortcodes/accordion.html */}}
{{ $id := .Get "id" | default "accordion" }}
<div id="{{ $id }}" class="border rounded mb-4">
{{ .Inner }}
</div>
{{/* layouts/shortcodes/accordion-item.html */}}
{{ $title := .Get "title" }}
{{ $open := .Get "open" | default false }}
{{ $itemId := .Get "id" | default (md5 $title) }}
<div class="border-t first:border-t-0">
<button class="w-full text-left p-3 font-medium hover:bg-gray-50"
onclick="toggleAccordion('{{ $itemId }}')">
{{ $title }}
</button>
<div id="{{ $itemId }}" class="p-3 {{ if not $open }}hidden{{ end }}">
{{ .Inner | markdownify }}
</div>
</div>
Expected behavior: Content authors write nested accordions in Markdown:
{{</* accordion id="faq" */>}}
{{</* accordion-item title="What is Hugo?" */>}}
Hugo is a static site generator written in Go.
{{</* /accordion-item */>}}
{{</* accordion-item title="Is it fast?" open=true */>}}
Yes, Hugo is one of the fastest SSGs available.
{{</* /accordion-item */>}}
{{</* /accordion */>}}
Partial Caching for Performance
{{/* layouts/partials/sidebar.html */}}
{{ $cached := partialCached "sidebar-content.html" . .Section }}
{{ $cached }}
{{/* layouts/partials/sidebar-content.html */}}
{{ $section := .Site.GetPage .Section }}
<aside class="w-64">
<h3 class="font-bold mb-2">{{ $section.Title }}</h3>
<ul>
{{ range $section.Pages.ByWeight }}
<li class="mb-1">
<a href="{{ .RelPermalink }}" class="hover:underline">
{{ .Title }}
</a>
</li>
{{ end }}
</ul>
</aside>
Expected behavior: partialCached stores the rendered output in memory. The sidebar is generated once per section, not once per page, reducing build time significantly for large sites.
Common Shortcode and Partial Mistakes
1. Not Using Default Values
A shortcode that requires every parameter to be specified breaks when content authors forget one. Always provide sensible defaults with default.
2. Hardcoding Inline Styles
Hardcoded colors and spacing in shortcodes make them difficult to theme. Reference CSS classes (via Tailwind or your stylesheet) instead of inline style attributes.
3. Forgetting Markdownify on Inner Content
Shortcode inner content is raw text by default. Without .Inner | markdownify, Markdown syntax inside shortcodes renders as plain text instead of formatted HTML.
4. Overusing Shortcodes for Simple Markup
Not everything needs a shortcode. Simple formatting like bold, italic, links, and images are better handled by native Markdown. Use shortcodes only for complex, reusable components.
5. Ignoring Performance in Partials
Partials called inside loops (like range .Pages) without caching can dramatically slow down builds. Use partialCached with appropriate cache keys for repetitive partials.
6. Passing the Wrong Context
Using {{ partial "sidebar.html" . }} passes the current page context. If you need section-level context, use {{ partial "sidebar.html" .Site }} or construct the right context object.
7. Not Documenting Custom Shortcodes
Content authors cannot use shortcodes they do not know exist. Maintain a reference page listing every custom shortcode with its parameters, examples, and usage notes.
Practice Questions
1. What is the difference between {{</* shortcode */>}} and {{%/* shortcode */%}} in Hugo?
The first variant does not process the inner content as Markdown. The second variant (%) processes inner content through the Markdown renderer. Use % when the shortcode wraps content with Markdown formatting.
2. How do you provide default values for shortcode parameters?
Use the default function: {{ $text := .Get "text" | default "Click Me" }}. If the author omits the parameter, the default value is used.
3. What is the purpose of partialCached and when should you use it?
partialCached renders a partial once and caches the output. Use it for partials that produce the same output for multiple pages (like sidebars, navigation, and footers) to reduce build time.
4. Why should you avoid hardcoding inline styles in shortcodes? Inline styles make theming difficult. If you change your design system, you must update every shortcode file. Using CSS classes keeps styles in one place and allows theme switching.
5. Challenge: Create a shortcode that renders a comparison table with two columns. It should accept a title and rows of data, handle Markdown in cells, and include a caption. Then create a partial that renders the same table for use in templates.
Mini Project: Shortcode Library
Build a library of five reusable Hugo shortcodes: a youtube-lazy shortcode that embeds YouTube videos with lazy loading, a file-tree shortcode that renders a directory tree from nested inner content, a download-button shortcode with file size display, a code-tabs shortcode that switches between multiple language implementations, and a contributors shortcode that lists page authors from frontmatter. Document each shortcode in a reference page.
{{/* layouts/shortcodes/download-button.html */}}
{{ $url := .Get "url" }}
{{ $text := .Get "text" | default "Download" }}
{{ $size := .Get "size" | default "" }}
{{ $platform := .Get "platform" | default "" }}
<a href="{{ $url }}"
class="inline-flex items-center gap-2 px-5 py-3
bg-blue-600 text-white rounded-lg
hover:bg-blue-700 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0
01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293
l5.414 5.414a1 1 0 01.293.707V19a2 2 0
01-2 2z"/>
</svg>
<span>
{{ $text }}
{{ if $size }}
<span class="text-sm opacity-75">({{ $size }})</span>
{{ end }}
</span>
{{ if $platform }}
<span class="text-xs bg-blue-500 px-2 py-0.5 rounded">
{{ $platform }}
</span>
{{ end }}
</a>
Expected behavior: {{</* download-button url="/releases/app-v2.0.exe" text="Download for Windows" size="45 MB" platform="Windows 10+" */>}} renders a styled download button with icon, size, and platform badge.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro