Right, so you’ve outgrown the built-in shortcodes. Good. They’re fine for a quick hello, but you’re building something real. That means writing your own. And Hugo, in its infinite wisdom, gives us a wonderfully simple yet powerful way to do this: the layouts/shortcodes directory.

Think of this directory as your personal toolbox. Every file you drop in here becomes a new shortcode you can call from your content. Create a file named cat-picture.html? Boom, you now have a &#123;&#123;< cat-picture >&#125;&#125; shortcode. The name of the file is the name of the shortcode. It’s so straightforward it’s almost absurd, a rare moment of sheer clarity in the world of tech.

The Absolute Basics: Your First Shortcode

Let’s say you’re tired of writing the same complex HTML for a styled warning box. Stop copying and pasting. Make it a shortcode.

Create a file at layouts/shortcodes/warning.html. In it, you might write:

<div class="bg-yellow-100 border-l-4 border-yellow-500 p-4 my-4">
  <p class="font-bold">Heads up!</p>
  <p>{{ .Inner }}</p>
</div>

Now, in your Markdown, instead of that div soup, you write:

{{< warning >}}
This is a warning message. The server is on fire, but it's probably fine.
{{< /warning >}}

See what happened? The content between the opening and closing shortcode tags—the .Inner—gets injected right into that template. Hugo handles the parsing; you just provide the wrapper. It’s like a fancy copy-paste that follows the DRY principle (Don’t Repeat Yourself, which is something you should never repeat).

Leveling Up: Using Parameters

Static boxes are cool, but parameters are where the real power lies. Your shortcode can accept named parameters, which you access inside the template via the .Get method. Let’s make that warning box more flexible.

Let’s improve our warning.html:

<div class="bg-{{ .Get "color" }}-100 border-l-4 border-{{ .Get "color" }}-500 p-4 my-4">
  <p class="font-bold">{{ .Get "title" | default "Note" }}</p>
  <p>{{ .Inner }}</p>
</div>

Now you can call it with parameters:

{{< warning color="blue" title="Pro Tip" >}}
You can now pass in a color and title. Fancy.
{{< /warning >}}

Why this works: When Hugo renders the page, it parses your shortcode call, plucks out the parameters into a map, and makes them available to your template. The .Get function is how you ask for a value back from that map. The | default "Note" part is a Hugo pipe that says “if the ’title’ parameter is empty or doesn’t exist, use ‘Note’ instead.” It’s your safety net.

The Gotchas: Where They Get You

This simplicity hides a few traps. Let’s navigate them.

  1. Positional vs. Named Parameters: You can use .Get 0 to get the first parameter without a name (e.g., &#123;&#123;< shortcode "value" >&#125;&#125;). Don’t. It’s a nightmare to maintain. Always use named parameters. Your future self, trying to remember what that first magical “value” was for, will thank you.

  2. .Inner and Markdown: By default, .Inner is a string. If you write Markdown inside your shortcode, it will not be rendered unless you tell Hugo to do so. You must use the {{ .Inner | markdownify }} pipeline filter to process it. This is a classic “wait, why isn’t this working?” moment for everyone.

  3. HTML in .Inner: Conversely, if you put raw HTML inside your shortcode, markdownify will escape it. If you want raw HTML to pass through, you use {{ .Inner | safeHTML }}. Use this with extreme caution. Only do it if you fully trust the content source.

A More Complex, Real-World Example

Let’s build something genuinely useful: a responsive image shortcode that handles srcset, lazy loading, and captions all at once. This is where you save yourself hours of grunt work.

Create layouts/shortcodes/img.html:

{{/* Get the required image resource */}}
{{- $image := .Page.Resources.GetMatch (printf "%s" (.Get "src")) -}}
{{- with $image -}}
  {{/* Create multiple sizes for srcset */}}
  {{- $tiny := $image.Resize "320x" -}}
  {{- $small := $image.Resize "640x" -}}
  {{- $medium := $image.Resize "1200x" -}}

<figure class="my-8">
  <img
    srcset="{{ $tiny.RelPermalink }} 320w,
            {{ $small.RelPermalink }} 640w,
            {{ $medium.RelPermalink }} 1200w"
    sizes="(max-width: 640px) 100vw, 1200px"
    src="{{ $medium.RelPermalink }}"
    alt="{{ .Get "alt" }}"
    loading="lazy"
    class="rounded-md shadow-lg"
  />
  {{- with .Get "caption" -}}
    <figcaption class="text-center italic text-gray-600 mt-2">{{ . | markdownify }}</figcaption>
  {{- end -}}
</figure>
{{- else -}}
  {{- errorf "Shortcode 'img' in page '%s': Image resource '%s' not found. Please check the path." .Page.Path (.Get "src") -}}
{{- end -}}

Now, in your content:

{{< img src="my-beautiful-cat.jpg" alt="A very fluffy cat" caption="This is *Mittens*, the fluffiest" >}}

Why this is brilliant: This single shortcode automatically generates multiple image sizes for performance, uses modern lazy loading, wraps it in a semantic <figure> element, and handles Markdown in the caption. The errorf at the end is a best practice—it will loudly fail the build if you mistype the image path, rather than silently rendering a broken image. That’s being a good friend to your future self.

This is the power of Hugo’s shortcodes. You’re not just templating HTML; you’re extending the Markdown language itself to include your own domain-specific instructions. It’s the kind of feature that turns a static site generator from a tool into a workshop.