This all began with a post on my socials...
https://mastodon.neilzone.co.uk/@neil/113244632967157645
The consensus certainly seemed to be that it's not possible to do this solely with CSS (which is a shame), but Neil did mention that they were investigating using Hugo to solve this.
My advice was to use Render Hooks, but I couldn't stop thinking about how I might do this, as it seemed like a neat way to present those long external links...
This is where I started
You can jump straight to the solution if you want. Otherwise, read on.
I spun up a standard Hugo quickstart project, and added some links to my site index page:
Here is a variety of links to test Hugo and/or CSS to show just the domain for external links...
**Internal links**
* [Custom text](/i_love_cats)
* [Custom text, with title](/i_love_cats "I love cats")
* [Custom text, absolute path](http://localhost:1313/i_love_cats)
**External links**
* Top-level domain, auto-generated text https://cats.org
* Deep link, auto-generated text https://www.cats.org.uk/help-and-advice/lost-found-and-feral-cats/lost-found-and-feral-cats
* [Custom text, Top-level domain](https://cats.org)
* [Custom text, Top-level domain, with title ](https://cats.org "I love rescue cats")
* [Custom text, deep link](https://www.cats.org.uk/help-and-advice/lost-found-and-feral-cats/lost-found-and-feral-cats)
* [Custom text, deep link, with title](https://www.cats.org.uk/help-and-advice/lost-found-and-feral-cats/lost-found-and-feral-cats "Lost & found cats")
I wanted to account for all the ways you can present links in markdown, in Hugo. It looks something like this:
I think it's those pesky auto-generated deeplinks that Neil wants to make a little prettier.
The rendered link looks like:
<a href="https://www.cats.org.uk/help-and-advice/lost-found-and-feral-cats/lost-found-and-feral-cats">
https://www.cats.org.uk/help-and-advice/lost-found-and-feral-cats/lost-found-and-feral-cats
</a>
Which doesn't give you much to hook into with CSS.
This is where Hugo's render hooks come in. For a certain selection of commonly used html elements, Hugo gives you the chance to override the default rendering, and define how you want it to render yourself using the Hugo template system. Hugo provides you with 3 variables for a link.
[Post 1](/posts/post-1 "My first post")
------ ------------- -------------
text destination title
My first step was to add a data-domain
attribute to see if I could use it to replace the content. I created a render-link.html
file in
layouts/
_default/
_markup/
render-link.html
This now overrides the default markdown rendering for links. I started with:
{{ $u := urls.Parse .Destination }}
<a href="{{ .Destination | safeURL }}"
data-domain="{{ $u.Hostname }}">{{ .Text | safeHTML }}
</a>
It uses Hugo's urls.Parse function to take the destination and convert to an object of the various parts of a url. In our case, we just need the Hostname
. I added it to the data-domain
attribute, and our deep link now looks like
<a href="https://www.cats.org.uk/help-and-advice/lost-found-and-feral-cats/lost-found-and-feral-cats" data-domain="www.cats.org.uk">https://www.cats.org.uk/help-and-advice/lost-found-and-feral-cats/lost-found-and-feral-cats</a>
Then I added a bit to the CSS for links, to show the data-domain attribute as extended content for the link, like this:
a[data-domain]::after {
content: ' - ' attr(data-domain);
}
But, this was a bit messy, as the data-domain
was missing for relative links, and rendered some odd dashes at the end of lines. Also, where the markdown had infered the link test from the url, it looked even messier. I couldn't find a neat way to reliably style out the actual link text leaving only the shorter hostname.
Given I had the urls.Parse
method, I now realised I could do something a little more fancy-schmancy, and build up some additional elements, easily styled by CSS. Something like this:
{{ $u := urls.Parse .Destination }}
<a href="{{ .Destination | safeURL }}"
data-domain="{{ $u.Hostname }}">
<span class="link-text">{{ .Text | safeHTML }}</span>
<span class="hostname-text">{{ $u.Hostname }}</span>
</a>
Which didn't really change the output, but now I had some CSS selectors to work with.
I created this monstrosity (you would swap out localhost
for your site's domain)
a[href]:not([href*="://localhost"]) .link-text {
display: none;
}
Thinking this would pretty much do the trick, but... it fails on relative links, because they too don't match the ://localhost
domain. Looks like:
Oops 😦
I needed to provide a few more class names for the CSS to be able to hook into.
First up - is this a relative link? For this I need to check if the hostname is empty.
{{ if not (strings.ContainsNonSpace $u.Hostname )}}
Basically this checks if the hostname is empty using the strings.ContainsNonSpace functionality.
So I added a $class
variable to store some output, and our template looks like this:
{{ $u := urls.Parse .Destination }}
{{ $class := "" }}
{{ if not (strings.ContainsNonSpace $u.Hostname)}}
{{ $class = "relative" }}
{{ end }}
<a href="{{ .Destination | safeURL }}"
class="{{ $class }}"
data-domain="{{ $u.Hostname }}">
<span class="link-text">{{ .Text | safeHTML }}</span>
<span class="hostname-text">{{ $u.Hostname }}</span>
</a>
the monster css selector looks like:
a[href]:not([href*="://localhost"]):not(.relative) .link-text {
display: none;
}
And this renders like this:
Which is looking loads better, but has nixed the explicit text on the last four of those links 😢 and is still showing the domain for the absolute link too.
I tried a few variations with the CSS, but this is the best it got.
I needed to make sure I could also explicitly detect an external link AND / OR one with explicit text.
For this, I needed some more template magic.
{{ $u := urls.Parse .Destination }}
{{ $yourDomain := "localhost" }}
{{ $class := "" }}
{{ if not (strings.ContainsNonSpace $u.Hostname)}}
{{ $class = "relative" }}
{{ else }}
{{ if not (compare.Eq $u.Hostname $yourDomain) }}
{{ $class = "external" }}
{{ if (compare.Eq $u.String .Text) }}
{{ $class = "external implied"}}
{{ end }}
{{ end }}
{{ end }}
<a href="{{ .Destination | safeURL }}"
class="{{ $class }}"
data-domain="{{ $u.Hostname }}">
<span class="link-text">{{ .Text | safeHTML }}</span>
<span class="hostname-text">{{ $u.Hostname }}</span>
</a>
Now we have a way, by comparing the $u.Hostname
to a $yourDomain
variable, to see if it explicitly external
{{ if not (compare.Eq $u.Hostname $yourDomain) }}
AND we subsequently check to see if the url is the same as the .Text
{{ if (compare.Eq $u.String .Text) }}
And change the class appropriately. Now we're getting closer! Our rendered deeplink looks like this:
<a href="https://www.cats.org.uk/help-and-advice/lost-found-and-feral-cats/lost-found-and-feral-cats"
class="external implied"
data-domain="www.cats.org.uk">
<span class="link-text">https://www.cats.org.uk/help-and-advice/lost-found-and-feral-cats/lost-found-and-feral-cats</span>
<span class="hostname-text">www.cats.org.uk</span>
</a>
We can update our CSS now, to be a little more readable:
a.relative {
.hostname-text {
display: none;
}
}
a.external:not(.implied) {
.hostname-text { display:none }
}
a.external.implied {
.link-text { display: none; }
}
And this renders like this. Pretty much nails it...
Bingo!
Grr, then I noticed the absolute link is not accounted for. A simple default on the $class
variable, and an additional definition in the CSS, and it's fixed.
{{ $u := urls.Parse .Destination }}
{{ $yourDomain := "localhost" }}
{{ $class := "internal" }}
{{ if not (strings.ContainsNonSpace $u.Hostname)}}
{{ $class = "relative" }}
{{ else }}
{{ if not (compare.Eq $u.Hostname $yourDomain) }}
{{ $class = "external" }}
{{ if (compare.Eq $u.String .Text) }}
{{ $class = "external implied"}}
{{ end }}
{{ end }}
{{ end }}
<a href="{{ .Destination | safeURL }}"
class="{{ $class }}"
data-domain="{{ $u.Hostname }}">
<span class="link-text">{{ .Text | safeHTML }}</span>
<span class="hostname-text">{{ $u.Hostname }}</span>
</a>
And the CSS...
a.internal,
a.relative {
.hostname-text {
display: none;
}
}
a.external:not(.implied) {
.hostname-text { display:none }
}
a.external.implied {
.link-text { display: none; }
}
...and bingo.
Over-engineering
Yep, that's it. I think this meets the needs of what we wanted in the first place. Namely that it only shows the top-level domain for those implied-text, deeplink urls, and everything else looks as expected...
But there's a little more...
Don't forget, Hugo allows us to pass in a 'title' into our markdown, like this:
[Custom text, deep link, with title](https://www.cats.org.uk/help-and-advice/lost-found-and-feral-cats/lost-found-and-feral-cats "Lost & found cats")
But that title attribute is not currently rendered by our customer render hook. Let's fix that:
{{ $u := urls.Parse .Destination }}
{{ $yourDomain := "localhost" }}
{{ $class := "internal" }}
{{ if not (strings.ContainsNonSpace $u.Hostname)}}
{{ $class = "relative" }}
{{ else }}
{{ if not (compare.Eq $u.Hostname $yourDomain) }}
{{ $class = "external" }}
{{ if (compare.Eq $u.String .Text) }}
{{ $class = "external implied"}}
{{ end }}
{{ end }}
{{ end }}
<a href="{{ .Destination | safeURL }}"
{{ with .Title}}title="{{ . }}"{{ end }}
class="{{ $class }}"
data-domain="{{ $u.Hostname }}">
<span class="link-text">{{ .Text | safeHTML }}</span>
<span class="hostname-text">{{ $u.Hostname }}</span>
</a>
We've simply tested to see if the .Title
variable exists, and if it does, add the title attribute. Our link now renders like:
<a href="https://www.cats.org.uk/help-and-advice/lost-found-and-feral-cats/lost-found-and-feral-cats"
title="Lost & found cats"
class="external"
data-domain="www.cats.org.uk">
<span class="link-text">Custom text, deep link, with title</span>
<span class="hostname-text">www.cats.org.uk</span>
</a>
Not a big deal, but what's nice is that you can use the existence of the title attribute to give some of those deeplinks a bit more context, without showing the whole URL. We add this to the end of our CSS:
a[title]::after {
content: '(' attr(title) ')';
}
and we get titles. Which you can style if you want. It also gives you the title on when the mouse hovers over the link.
One more thing
Last of all, I want my external links to always open in a new tab, so we can add a target
attribute to our link. And that is the solution!
Solution
/layouts/_default/_markup/render-link.html
{{ $u := urls.Parse .Destination }}
{{ $yourDomain := "localhost" }}
{{ $class := "internal" }}
{{ $target := "" }}
{{ if not (strings.ContainsNonSpace $u.Hostname)}}
{{ $class = "relative" }}
{{ else }}
{{ if not (compare.Eq $u.Hostname $yourDomain) }}
{{ $class = "external" }}
{{ $target = "_blank" }}
{{ if (compare.Eq $u.String .Text) }}
{{ $class = "external implied"}}
{{ end }}
{{ end }}
{{ end }}
<a href="{{ .Destination | safeURL }}"
{{ with .Title}}title="{{ . }}"{{ end }}
target="{{ $target }}"
class="{{ $class }}"
data-domain="{{ $u.Hostname }}"><span class="link-text">{{ .Text | safeHTML }}</span> <span class="hostname-text">{{ $u.Hostname }}</span></a>
And the final CSS...
a.internal,
a.relative {
.hostname-text {
display: none;
}
}
a.external:not(.implied) {
.hostname-text { display:none }
}
a.external.implied {
.link-text { display: none; }
}
a[title]::after {
content: '(' attr(title) ')';
}
Obviously, this is a little over-engineered, but it gives you a lot of flexibility to display links however you like.
Enjoy!
If you found this useful, or have feedback please drop me a line @toychicken
Finally, if you too love cats, make a donation to the Cats protection league today. 🐈⬛
Top comments (0)