I learned recently that Hugo considers
the ref and relref shortcodes “obsolete” for use in Markdown files.
This surprised me as I have been using them on this site and others for many years.
Instead, we should use render hooks.
Note that this is limited to Markdown only;
ref and relref continue to be necessary for HTML and other kinds of input files.
The result is just regular Markdown that’s more portable to other renderers, and it’s shorter and looks nicer when editing as well.
Hugo now ships simple render templates for links and images, and uses them by default under some circumstances (see the duplicateResourceFiles setting).
This post is about links, but render hooks are also used for blockquotes, code blocks, headings, and tables; see the docs for these.
Validating links with render hooks
When using ref, it will emit an error if that ref doesn’t resolve to a content page.
I rely on this to make sure I’m not linking to nonexistent pages on my own site.
The default render hooks do not do this
(yet)
but you can add customizations to do so.
I am using these hooks
from jmooring, one of the Hugo maintainers,
which do extensive validation.
(They even validate that anchor tags actually link to an ID on the page now!)
Switching to render hooks
I switched to using render hooks earlier this week. I started with this (uses macOS sed):
# Convert all {{< ref 'some/thing' >}} to {{ ref "some/thing" }}
# Ensure that consistent spacing is enforced: one space after < and before >
find content -type f -name '*.md' -exec sed -i '' 's/{{< ?ref "?\([^"]*\)"? ?>}}/\1/g' {} +
find content -type f -name '*.md' -exec sed -i '' 's/{{< ?relref "?\([^"]*\)"? ?>}}/\1/g' {} +
# Convert all {{< ref "/some/thing" >}} to /some/thing
# Also handle unquoted paths
find content -type f -name '*.md' -exec sed -E -i '' 's/{{<[[:space:]]*(relref|ref)[[:space:]]*"([^"]*)"[[:space:]]*>}}/\2/g' {} +This required some manual fixes because the lookup rules for ref vs the render hook are different.
I used the errors from the jmooring render hook to show me what needed to be changed.
Intentionally linking to pages that cannot be validated
Sometimes with render hooks you might want to intentionally link to a page that can’t be validated when the site builds, like the 404 page, the RSS feed or sitemap, etc.
When I was using ref for most links, I just used regular Markdown links for these non-validatable links,
e.g. [RSS feed](/rss.xml).
But when using validating render hooks, this will fail.
There are two solutions:
- Use HTML links instead of native Markdown links, like
<a href=/rss.xml>RSS feed</a>. This is the method I’m using. I’m adding an attributedata-linkcheck-exemptto indicate that the link is HTML to bypass validation, like<a data-linkcheck-exempt href=/rss.xml>RSS feed</a>. One downside of this method is that it requires permitting HTML in your Markdown files withunsafe = true. - Modify the render hook to skip validation if the link contains a certain query string like
[RSS feed](/rss.xml?validate=false). This is probably a nicer solution for the general case. jmooring suggested it on the Hugo forums.
If render hooks are enabled, use % for shortcodes
You can enable render hooks and continue to use the ref shortcode,
but there is one caveat:
you have to convert ref and any other shortcodes in Markdown links to the % syntax.
| Ok | [click here]({{% ref "blog" %}}) |
| Broken | [click here]({{< ref "blog" >}}) |
The < style shortcodes cause the render hook to see values like
HAHAHUGOSHORTCODE858s1HBHB as the link destination.
This means any validation that inspects the link destination will not work as expected.
The % style shortcodes get handled properly by render hooks.
This applies to all shortcodes, not just ref —
if you have anything custom like [click here]({{< yourCustomShortCode >}}),
you’ll need to convert that to the % version too.
A note on HAHAHUGOSHORTCODE placeholder values
You cannot include this string anywhere in your site, even Markdown content, or Hugo will fail to build with an error
illegal state in content; shortcode token missing end delim
I’m including it above with an invisible space, literally
<code>HAHAHUGO‍SHORTCODE</code>,
so that it doesn’t cause this problem.
If you select and copy that text, you’ll also copy that
zwj character.
If you paste it into the terminal you’ll get something like:
> printf 'HAHAHUGO<200d>SHORTCODE' | xxd
00000000: 4841 4841 4855 474f e280 8d53 484f 5254 HAHAHUGO...SHORT
00000010: 434f 4445 CODE
As discussed in #7416, Hugo didn’t always check for this, and sometimes the placeholder could sneak into production sites, especially as Hugo shipped new releases that changed behavior.