/blog/

2026 0413 Nice autolinks in Hugo

In Google Docs, Confluence, and other heavy document editors, you can just paste a URL and it’ll fetch the page title and make a nice link out of it, at the cost of some tick, tick, tick while it fetches the title from the other page.

I realized some links actually contain enough information to implement an instant (though limited) version of this feature in a Hugo site. All I have to do is paste it as a Markdown autolink, i.e. between angle brackets like <https://example.com>, without specifying a title.

The result:

Markdown Rendered
<https://github.com/mrled/dhd> [GH] mrled/dhd
<https://example.atlassian.net/browse/EXAMPLE-12345> [Jira] EXAMPLE-12345
<https://en.wikipedia.org/wiki/Oblique_Strategies> [WP] Oblique Strategies
More examples
Markdown Rendered
<https://github.com/mrled/suns/pull/2> [GH] mrled/suns#2
<https://github.com/mrled/hedgerules/issues/1> [GH] mrled/hedgerules#1
<https://github.com/mrled/psyops/blob/master/ansible/cloudformation/MicahrlDotCom.cfn.yml> [GH] mrled/psyops@master MicahrlDotCom.cfn.yml
<https://example.atlassian.net/wiki/spaces/EXAMPLE/pages/12345/Important+Company+Announcement> [Confluence: EXAMPLE] Important Company Announcement
<https://x.com/mrled> [Twitter] @mrled
<https://x.com/mrled/status/1761059606502031502> [Twitter] tweet from @mrled
<https://example.slack.com/archives/C0WCVFB5X/p1772997481266379> [Slack] p1772997481266379

Some notes:

  • GitHub happens to use the same namespace for both issue and PR IDs, so if there is a /pull/1 there will not be an /issues/1, and vice versa. It uses the #id syntax for both in its own UI, so we can too.

The secret is Hugo’s render hooks, which can run string matching and munging code on every Markdown link, including autolinks. Here’s source code for an example link render hook:

_markup/render-link.html

{{- /*
Compute display text for <https://example.com> -style "autolinks".
For recognized services, replace the bare URL with a human-readable label.
*/ -}}
{{- $linkText := .Text }}
{{- $useCite := false }}
{{- if and $u.IsAbs (eq .Text .Destination) }}

  {{- /* GitHub: shorten to org/repo, org/repo#number, or org/repo@commit/path */}}
  {{- if hasPrefix .Destination "https://github.com/" }}
    {{- $path := strings.TrimPrefix "https://github.com/" .Destination }}
    {{- $parts := split $path "/" }}
    {{- $org := index $parts 0 }}
    {{- $repo := index $parts 1 }}
    {{- $text := printf "%s/%s" $org $repo }}
    {{- if and (ge (len $parts) 4) (or (eq (index $parts 2) "issues") (eq (index $parts 2) "pull")) }}
      {{- /* Issue or PR URL: append #number */}}
      {{- $text = printf "%s/%s#%s" $org $repo (index $parts 3) }}
    {{- else if and (ge (len $parts) 4) (or (eq (index $parts 2) "tree") (eq (index $parts 2) "blob")) }}
      {{- /* Tree or blob URL: show @ref and last path segment only */}}
      {{- $hash := index $parts 3 }}
      {{- $short := substr $hash 0 7 }}
      {{- $rest := after 4 $parts }}
      {{- if $rest }}
        {{- $last := index $rest (sub (len $rest) 1) }}
        {{- $text = printf "%s/%s@%s %s" $org $repo $short $last }}
      {{- else }}
        {{- $text = printf "%s/%s@%s" $org $repo $short }}
      {{- end }}
    {{- end }}
    {{- $linkText = printf "[GH] %s" $text }}

  {{- /* Confluence wiki: show space key and page title */}}
  {{- else if findRE `^https://example\.atlassian\.net/wiki/spaces/` .Destination }}
    {{- $path := replaceRE `^https://example\.atlassian\.net/wiki/spaces/` "" .Destination }}
    {{- $parts := split $path "/" }}
    {{- $space := index $parts 0 }}
    {{- /* Page title is the 4th path segment; + encodes spaces in Confluence URLs */}}
    {{- $title := index $parts 3 | replaceRE `\+` " " }}
    {{- $linkText = printf "[Confluence: %s] %s" $space $title }}
    {{- $useCite = true }}

  {{- /* Twitter/X profile or tweet; both twitter.com and x.com are supported */}}
  {{- else if or (hasPrefix .Destination "https://twitter.com/") (hasPrefix .Destination "https://x.com/") }}
    {{- $twitterBase := cond (hasPrefix .Destination "https://twitter.com/") "https://twitter.com/" "https://x.com/" }}
    {{- $path := strings.TrimPrefix $twitterBase .Destination }}
    {{- $parts := split $path "/" }}
    {{- $user := index $parts 0 }}
    {{- if and (ge (len $parts) 3) (eq (index $parts 1) "status") }}
      {{- /* Tweet URL: twitter.com/user/status/id */}}
      {{- $linkText = printf "[Twitter] tweet from @%s" $user }}
    {{- else }}
      {{- /* Profile URL: twitter.com/user */}}
      {{- $linkText = printf "[Twitter] @%s" $user }}
    {{- end }}

  {{- /* Wikipedia: convert article slug to title (underscores to spaces) */}}
  {{- else if findRE `^https://[a-z]+\.wikipedia\.org/wiki/` .Destination }}
    {{- $title := replaceRE `^https://[a-z]+\.wikipedia\.org/wiki/` "" .Destination | replaceRE `_` " " }}
    {{- $linkText = printf "[WP] %s" $title }}
    {{- $useCite = true }}

  {{- /* Jira ticket: extract and show the ticket ID (e.g. PROJ-123) */}}
  {{- else if findRE `^https://example\.atlassian\.net/.*[A-Z]+-\d+` .Destination }}
    {{- $ticket := replaceRE `.*?([A-Z]+-\d+).*` "$1" .Destination }}
    {{- $linkText = printf "[Jira] %s" $ticket }}

  {{- /* Slack: show message ID (p-number) from archives URL; query params ignored via $u.Path */}}
  {{- else if findRE `^https://example\.slack\.com/archives/` .Destination }}
    {{- $pathParts := split (strings.TrimPrefix "/" $u.Path) "/" }}
    {{- $msgId := index $pathParts (sub (len $pathParts) 1) }}
    {{- $linkText = printf "[Slack] %s" $msgId }}

  {{- end }}
{{- end }}

On this public site, I use this pretty conservatively, because I have plenty of time and a hand-written title is often better. But on a private site that I use for notes, probably 90% of my outbound links are autolinks. The friction of deciding on a title and writing it out the Markdown way matters sometimes, like when taking notes real time during a meeting, and a title adapted from a URL ends up looking good enough, sometimes even great.

I also like that, unlike Confluence and Google docs, there is zero waiting when authoring the document. And furthermore, there’s no nondeterminism or delay at build time either.

If you’re building a web application: please, use nice human-readable URLs!

Responses

Webmentions

Hosted on remote sites, and collected here via Webmention.io (thanks!).

Comments

Comments are hosted on this site and powered by Remark42 (thanks!).