Adding query strings to markdown links automatically in Hugo

Introduction

Some time ago I added MVP referral codes as query parameters to outgoing links to Microsoft websites in my blog posts. I had to go through all of my blog posts to find links to Microsoft websites and add them manually. I found this rather boring, so I wanted to automate this! By automating this process I will save time and effort, especially because any future references to those Microsoft domains will contain these query parameters automatically.

And while I was updating all these links, I realized that a lot of my links didn’t open a new tab; they would actually navigate the user away from my site. I don’t like that behaviour for all of my links, so I wanted to fix this.

So, In this blog post I will share how this is set up so you can use this for your own website! I built it in a generic way, so this will work with any domain and all query parameters.

info
I am documenting my approach and solution to this problem because I think it will be helpful for others! If you aren’t interested in my approach, you can scroll down or click here to go straight to the code!

Approaching the problem

Identifying the problem

When I was working on my blog post about using Bogus to create seed data for EF Core, I stumbled upon a small issue with how my links are generated.

I use Markdown with the default Goldmark renderer to write these blog posts because I find it very convenient. Markdown allows you to use the following syntax to create a link: [Link text]( https://example.com).

Clicking on such a link would normally open the link in the current tab which means you leave the current website. This can be very annoying if you are reading a blog post and want to learn more about something and want to continue reading as well. So, we want to open links to other websites in a new tab.

Markdown supports HTML, so I could simply stop using the Markdown syntax and use <a href="https://example.com" target="_blank">Link text</a> instead. But I find that this makes my blog posts messy and more difficult to write. I could also use a Custom shortcode to clean that up, but it’s still a bit messier than the markdown syntax.

I wanted to fix this problem by just changing the way Markdown renders my links. So, how can we do that?

Adding query parameters automatically for specific domains

In case the problem may not be very clear yet, I mean something like this: Imagine that I want to add ?hello=world to all outgoing https://example.com and https://something.com links. Not only would I need to update all my blog posts by hand, I would also need to remember to do this for all new blog posts and future edits.

If we look into solving this problem, it seems quite similar to the previous one; I don’t want to do this manually, and I don’t want to create shortcodes. It would be best if I could change the link renderer itself.

Looking for the solution

I quickly found others online who had the same ‘opening links in a new tab’ problem. However, I still needed to modify this code to modify these links with custom query parameter.

Luckily, ChatGPT came to my rescue because I had to write some Go Templates for this, and I had no experience with this. So if you have any feedback on the code below, let me know!

The solution

It’s time to present the solution!

  1. You need to create a render-link.html file in layouts/_default/_markup/ folder, which will override the default link renderer behaviour.

  2. Put the following code in it.

    <!-- Put a list of your domains here -->
    {{ $domains := slice "example.com" "something.com" }}
    <!-- Put your query parameter(s) here -->
    {{ $param := "hello=world" }}
    
    {{ $destination := .Destination }}
    
    {{ $found := false }}
    {{ range $domains }}
      {{ if not $found }}
        {{ $domain := . }}
        {{ $splitDest := split $destination "://" }}
        {{ $splitPath := split (index $splitDest 1) "#" }}
        {{ $splitPathFragments := split (index $splitPath 0) "?" }}
        {{ $splitDomain := split (index $splitPathFragments 0) "/" }}
        {{ $found = hasPrefix (index $splitDomain 0) $domain }}
        {{ if $found }}
          {{ $query := index $splitPathFragments 1 }}
          {{ $path := printf "%s?%s" (index $splitPathFragments 0) $param }}
          {{ $fragment := index $splitPath 1 }}
          {{ if ne $fragment nil }}
            {{ $destination = printf "%s://%s#%s" (index $splitDest 0) (printf "%s%s" $path "") $fragment }}
          {{ else }}
            {{ $destination = printf "%s://%s" (index $splitDest 0) $path }}
          {{ end }}
        {{ end }}
      {{ end }}
    {{ end }}
    
    <a href="{{ $destination | safeURL }}"{{ with .Title}} title="{{ . }}"{{ end }}{{ if strings.HasPrefix $destination "http" }} target="_blank"{{ end }}>{{ .Text }}</a>
    

The result: If you type the following markdown [Link text]( https://example.com), you will be presented with the following HTML: <a href="https://example.com?hello=world" target="_blank">Link text</a>. If you had any existing query parameters in your original markdown link, the new query parameter will be appended. The same goes for the hash.

Explanation

The code does the following:

  • Defines a list of domain names and assigns it to a variable $domains.
  • Defines a query parameter with a key-value pair and assigns it to a variable $param.
  • Assigns the current page’s destination URL to a variable $destination.
  • Initializes a boolean variable $found to false.
  • Loops through each domain in the $domains list.
  • If $found is false, it performs the following operations:
    • Assigns the current domain to a variable $domain.
    • Splits the $destination URL into its components (scheme, path, fragment) and assigns them to variables $splitDest, $splitPath, and $fragment.
    • Extracts the path fragments from the $splitPath variable and assigns them to $splitPathFragments.
    • Extracts the domain name from the path fragments and assigns it to $splitDomain.
    • Checks if the domain name starts with the current $domain using hasPrefix function and assigns the result to $found.
  • If $found is true, it modifies the URL by adding the query parameter $param to the URL’s path.
    • If the URL has a fragment, it appends the modified path and fragment back to the URL.
    • If the URL has no fragment, it appends only the modified path to the URL.
    • Assigns the modified URL back to $destination.
  • Outputs an anchor tag with the modified URL.

Finishing up

If you have any questions or know of any improvements, let me know in the comments below!