Hey everyone ,
I’ve been experimenting with adding Obsidian-style wikilinks ([[Page]]
, [[Page|Alias]]
, ![[Image]]
) into Hugo.
The idea is:
-
Mask inline code and fenced code blocks so wikilinks there don’t get parsed.
-
Convert image wikilinks first, then normal wikilinks.
-
Map wikilinks to Hugo pages, aliases, or paths.
-
Render them as internal/external links (with fallback to a “broken link” style if unresolved).
Here are the core templates I’ve got so far:
Wikilink dictionary & rendering logic
render-link.html
{{/* --- Utility: build dictionary for wikilink resolution --- */}}
{{- $wikilinkMap := dict -}} {{/* Create an empty dictionary to store link lookups */}}
{{- range $p := union site.RegularPages site.Sections }} {{/* Loop through all regular pages + section pages */}}
{{- if $p.File }} {{/* Only process if the page has an associated file */}}
{{- $base := lower $p.File.BaseFileName }} {{/* Get file base name, lowercase (e.g. "Note.md" -> "note") */}}
{{- $path := lower (replace (replace $p.File.Path "\\" "/") ".md" "") }}
{{/* Normalize path: replace backslashes with slashes, remove .md, lowercase */}}
{{/* Save aliases into the lookup dictionary */}}
{{- $wikilinkMap = merge $wikilinkMap (dict $base $p) -}} {{/* Allow lookup by base filename */}}
{{- $wikilinkMap = merge $wikilinkMap (dict $path $p) -}} {{/* Allow lookup by normalized path */}}
{{- range $p.Aliases }} {{/* If page has defined aliases, also store them */}}
{{- $wikilinkMap = merge $wikilinkMap (dict (lower .) $p) -}}
{{- end -}}
{{- end }}
{{- end }}
{{/* --- Templates for different link rendering cases --- */}}
{{ define "render-link-internal" }}
<a class="link link--internal" href="{{ .href | safeURL }}">{{ .text }}</a>
{{ end }}
{{ define "render-external-link" }}
<a class="link link--external"
href="{{ .href | safeURL }}"
rel="external noopener noreferrer nofollow" {{/* Security: prevent leaking referrer and opener */}}
target="_blank" {{/* Open external links in new tab */}}
{{- with .title }}title="{{ . }}"{{ end -}}>
{{- with .text }}{{ . }}{{ end -}}
</a>
{{ end }}
{{ define "render-broken" }}
<span class="link link--broken" title="Link to {{ .text }} is missing">{{ .text }}</span>
{{ end }}
{{/* --- Main link resolution logic --- */}}
{{- $destination := .Destination | default "" -}} {{/* Destination (href) provided by Markdown parser */}}
{{- $text := .Text | safeHTML | default "" -}} {{/* Text inside the link */}}
{{- $title := .Title | default "" -}} {{/* Optional title attribute */}}
{{- $isWikilink := eq $destination "wikilink" -}} {{/* Check if this link is a custom "wikilink" type */}}
{{- $isExternal := or
(strings.HasPrefix $destination "http://")
(strings.HasPrefix $destination "https://")
(strings.HasPrefix $destination "mailto:")
-}} {{/* Detect if the link is external (http/https/mailto) */}}
{{ if $isWikilink }}
{{ $rawTitle := cond (ne $title "") $title $text }} {{/* Prefer title if provided, else use text */}}
{{ $parts := split $rawTitle "#" }} {{/* Split on "#" to separate anchor */}}
{{ $targetRaw := index $parts 0 }} {{/* First part = target page */}}
{{ $anchor := cond (gt (len $parts) 1) (printf "#%s" (index $parts 1)) "" }}
{{/* If anchor exists, prepend "#" */}}
{{/* Normalize target: lowercase and strip ".md" extension */}}
{{ $target := lower (replace $targetRaw ".md" "") }}
{{ $page := index $wikilinkMap $target }} {{/* Try to find page from dictionary */}}
{{ if $page }}
{{/* If found, render as internal link */}}
{{ template "render-link-internal" (dict "href" (print $page.RelPermalink $anchor) "text" $text) }}
{{ else }}
{{/* If not found, mark as broken */}}
{{ template "render-broken" (dict "text" $text) }}
{{ end }}
{{ else if $isExternal }}
{{/* Render as external link */}}
{{ template "render-external-link" (dict "href" $destination "text" $text "title" $title) }}
{{ else }}
{{- $u := urls.Parse $destination -}} {{/* Parse the URL for further checks */}}
{{ if $u.IsAbs }}
{{/* Absolute URL: treat as external */}}
{{ template "render-external-link" (dict "href" $destination "text" $text "title" $title) }}
{{ else }}
{{/* Otherwise, treat as internal (relative link) */}}
{{ template "render-link-internal" (dict "href" $destination "text" $text) }}
{{ end }}
{{ end }}
_partials/_wikilink-cache.html
{{- $regularPages := where site.RegularPages "Lang" site.Language.Lang -}}
{{- $sections := where site.Sections "Lang" site.Language.Lang -}}
{{- $allPages := union $regularPages $sections -}}
{{- $wikilinks := dict -}}
{{- range $allPages }}
{{- if .File }}
{{- $slug := .File.BaseFileName -}}
{{- $wikilinks = merge $wikilinks (dict $slug .) -}}
{{- $wikilinks = merge $wikilinks (dict .File.TranslationBaseName .) -}}
{{- range .Aliases }}
{{- $wikilinks = merge $wikilinks (dict . $) -}}
{{- end }}
{{- end }}
{{- end }}
{{- return $wikilinks -}}
Content preprocessing (mask → convert → restore)
{{- /* Initialize content and placeholder storage */ -}}
{{- $content := .RawContent -}} {{/* Get the raw Markdown content */}}
{{- $placeholders := slice -}} {{/* Store masked sections (inline/code blocks) */}}
{{- $i := 0 -}} {{/* Counter for unique placeholder IDs */}}
{{- /* ------------------------------------ */ -}}
{{- /* 1. Mask inline code containing [[...]] or ![[...]] */ -}}
{{- $inlineMatches := findRE "`!?\\[\\[[^\\[\\]]+\\]\\]`" $content -}}
{{/* Regex: match inline code wrapped in backticks that contains wikilink or image wikilink */}}
{{- range $inlineMatches -}}
{{- $placeholder := printf "WIKILINK_PLACEHOLDER_%d" $i -}} {{/* Create placeholder name */}}
{{- $placeholders = $placeholders | append (dict "original" . "placeholder" $placeholder) -}}
{{/* Save mapping: original text → placeholder */}}
{{- $content = replace $content . $placeholder -}} {{/* Replace inline code with placeholder */}}
{{- $i = add $i 1 -}} {{/* Increment placeholder index */}}
{{- end -}}
{{- /* ------------------------------------ */ -}}
{{- /* 2. Mask entire code blocks containing [[...]] or ![[...]] */ -}}
{{- $codeBlockMatches := findRE "(?s)```.*?```" $content -}} {{/* Regex: match fenced code blocks (triple backticks, multiline) */}}
{{- range $codeBlockMatches -}}
{{- if or (findRE "\\[\\[.*?\\]\\]" .) (findRE "!\\[\\[.*?\\]\\]" .) -}}
{{/* Only replace if wikilink syntax is found inside the code block */}}
{{- $placeholder := printf "WIKILINK_PLACEHOLDER_%d" $i -}}
{{- $placeholders = $placeholders | append (dict "original" . "placeholder" $placeholder) -}}
{{- $content = replace $content . $placeholder -}} {{/* Mask the code block */}}
{{- $i = add $i 1 -}}
{{- end -}}
{{- end -}}
{{- /* ------------------------------------ */ -}}
{{- /* 3. Convert Image Wikilinks first */ -}}
{{- $content = replaceRE `!\[\[([^\|\]]+)\|([^\|\]]+)\|([^\]]+)\]\]` `` $content -}}
{{/* Pattern: ![[file|alt|title]] →  */}}
{{- $content = replaceRE `!\[\[([^\|\]]+)\|([^\]]+)\]\]` `` $content -}}
{{/* Pattern: ![[file|alt]] →  */}}
{{- $content = replaceRE `!\[\[([^\]]+)\]\]` `` $content -}}
{{/* Pattern: ![[file]] →  */}}
{{- /* 4. Convert Regular Wikilinks to Markdown Links */ -}}
{{- $content = replaceRE `\[\[(https?://[^\|\]]+)\|([^\]]+)\]\]` `[$2]($1)` $content -}}
{{/* [[https://url|text]] → [text](https://url) */}}
{{- $content = replaceRE `\[\[(https?://[^\]]+)\]\]` `[$1]($1)` $content -}}
{{/* [[https://url]] → [url](https://url) */}}
{{- $content = replaceRE `\[\[([^\|\]]+)\|([^\]]+)\]\]` `[$2](wikilink "$1")` $content -}}
{{/* [[page|text]] → [text](wikilink "page") */}}
{{- $content = replaceRE `\[\[([^\]]+)\]\]` `[$1](wikilink "$1")` $content -}}
{{/* [[page]] → [page](wikilink "page") */}}
{{- /* ------------------------------------ */ -}}
{{- /* 5. Restore all placeholders to original content */ -}}
{{- range $placeholders -}}
{{- $content = replace $content .placeholder .original -}}
{{- end -}}
{{- /* 6. Render the final processed content */ -}}
{{- .RenderString $content -}} {{/* Pass the modified content back into Hugo’s Markdown renderer */}}
Example input (Markdown):
[[Page One]]
[[Page Two|Custom Text]]
![[image.png|Alt Text]]
`inline code with [[notalink]]`
Output (HTML):
<a class="link link--internal" href="/page-one/">Page One</a>
<a class="link link--internal" href="/page-two/">Custom Text</a>
<img alt="Alt Text" src="/image.png">
<code>inline code with [[notalink]]</code>
Disclaimer:
This code was originally generated with ChatGPT (and then I tweaked it a bit). I’m sharing here mainly to get feedback, best practices, or improvements from the community.
Would love to hear your thoughts — e.g.
-
Is this approach efficient enough?
-
Any edge cases I might be missing?
-
Better regex / Hugo idioms to simplify this?
Thanks in advance!