This page looks best with JavaScript enabled

How-To add link icons in Hugo markdown links

How to add link icons for Markdown links in Hugo

 ·   ·  ☕ 9 min read  · 
❄️ Yohan Yukiya Sese-Cuneta
Fediverse Follow

Link icons are great. It signals to the reader what a link is. It is external? Or perhaps a video? If the link is clicked, will it start a download or will it open the default mail program? Link icons also helps a developer or content creator to easily find links, or the lack thereof.

Link icons started with and was popularised by Wikipedia a decade ago 1. Everyone were looking for CMS plugins to add link icons to their websites and blogs. The method back then was to use a small .png image file as the icon. But today? We are going to use Unicode emojis and only use .svg if an appropriate emoji is not available.

In this post, we will add link icons support in Hugo through Markdown links. No shortcode needed, just plain regular [text](https://example.com "Title") links, thanks to the power of render hooks 2.

Features

What’s new

  • 2022-06-17:

    • ftp icon changed to: ↔️
    • sftp:// protocol moved to ftp category
    • ref: switched to Hugo’s findRE where appropriate
    • fix: [text](./path/to/content/) and [text.ext](./path/to/file.ext) formats
  • 2022-05-27:

    • Same (sub)-domain no longer have external icon.
    • More external link support like audio, video, fonts, disk images, documents, presentations, spreadsheets, and more!

Steps

To add link icons, follow the steps below:

  1. Create a file called render-link.html in this directory /layouts/_default/_markup/

  2. Copy and paste this code:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    
    {{- $baseurl := urls.Parse site.BaseURL -}}
    {{- $url := urls.Parse .Destination -}}
    {{- $getpage := site.GetPage .Destination -}}
    {{- $internal := lt (len $url.Host) 1 -}} {{/* NOTE: internal links will always have an empty $url.Host */}}
    
    {{- $fragment := $url.Fragment -}}
    {{- with $fragment -}}{{ $fragment = printf "#%s" $fragment }}{{- end -}}
    
    {{- $destination := "" -}}
    {{- if $internal -}}
      {{- if (strings.HasPrefix $url.Path "./") -}}
        {{/* NOTE: for links starting with ./ */}}
        {{- $urltrimmed := strings.TrimPrefix "./" $url -}}
        {{- $destination = printf "%s://%s/%s%s" $baseurl.Scheme $baseurl.Host $urltrimmed $fragment -}}
      {{- else -}}
        {{/* NOTE: for internal links */}}
        {{- $destination = printf "%s%s" $getpage.RelPermalink $fragment -}}
      {{- end -}}
    {{- else -}}
      {{- $destination = .Destination -}}
    {{- end -}}
    
    {{/* PROTOCOLS */}}
      {{- $chat := findRE "^(?:discord|irc[s6]?|jabber|skype|xmpp)://" .Destination -}}
      {{- $ftp := findRE "^(?:[as]?ftp)://" .Destination -}}
      {{- $magnet := strings.HasPrefix .Destination "magnet://" -}}
      {{- $mail := strings.HasPrefix .Destination "mailto:" -}}
      {{- $remote := findRE "^(?:bzr|git|s(?:sh|vn)|telnet)://" .Destination -}}
      {{- $tel := strings.HasPrefix .Destination "tel:" -}}
    
    {{/* READING */}}
      {{- $books := or (strings.HasPrefix .Destination "doi://") (findRE "\\.(?:epub|mobi|pdf)$" .Destination) -}}
      {{- $document := findRE "\\.(?:docx?|odt|s(?:dw|xw)|sxw|uo[ft])$" .Destination -}}
      {{- $text := findRE "\\.(?:csv|txt)$" .Destination -}}
      {{- $presentation := findRE "\\.(?:f?odp|pptx?|s(?:d[dp]|xi)|uop)$" .Destination -}}
      {{- $spreadsheet := findRE "\\.(?:f?ods|s(?:d[cx]|xc)|uos|xlsx?)$" .Destination -}}
    
    {{/* MEDIA */}}
      {{- $audio := findRE "\\.(?:(?:fl|a)ac|mka|og[ag]|opus|mp[3a]|midi?|wave?|wma)$" .Destination -}}
      {{- $video := findRE "\\.(?:av[1i]|divx|mk(?:3d|v)|mp(?:(?:e?g)?4?|v)|og[mv]|xvid|webm)$" .Destination -}}
      {{- $subtitle := findRE "\\.(?:dfxp|mks|s(?:bv|cc|rt|ub)|ttml|vtt)$" .Destination -}}
    
    {{/* EXECUTABLES */}}
      {{- $executable := findRE "\\.(?:apk|com|deb|exe|msi)$" .Destination -}}
      {{- $scripts := findRE "\\.(?:bat|sh)$" .Destination -}}
    
    {{/* OTHERS */}}
      {{- $fonts := findRE "\\.(?:otf|tt[fc]|woff2?)$" .Destination -}}
      {{- $compressed := findRE "\\.(?:[7g]?z(?:ip)?|bz(?:ip)?2?|[rt]ar)$" .Destination -}}
      {{- $diskimage := findRE "\\.(?:[di]mg|iso|md[sfx])$" .Destination -}}
      {{- $imagediting := findRE "\\.(?:psd|xcf)$" .Destination -}}
    
    {{- $icon := "" -}}
    {{- if $chat -}}{{ $icon = "chat" }}
      {{- else if $ftp -}}{{ $icon = "ftp" }}
      {{- else if $magnet -}}{{ $icon = "magnet" }}
      {{- else if $mail -}}{{ $icon = "mail" }}
      {{- else if $remote -}}{{ $icon = "remote" }}
      {{- else if $tel -}}{{ $icon = "tel" }}
    
      {{- else if $books -}}{{ $icon = "books" }}
      {{- else if $document -}}{{ $icon = "document" }}
      {{- else if $text -}}{{ $icon = "text" }}
      {{- else if $presentation -}}{{ $icon = "presentation" }}
      {{- else if $spreadsheet -}}{{ $icon = "spreadsheet" }}
    
      {{- else if $audio -}}{{ $icon = "audio" }}
      {{- else if $video -}}{{ $icon = "video" }}
      {{- else if $subtitle -}}{{ $icon = "subtitle" }}
    
      {{- else if $executable -}}{{ $icon = "executable" }}
      {{- else if $scripts -}}{{ $icon = "scripts" }}
    
      {{- else if $fonts -}}{{ $icon = "fonts" }}
      {{- else if $compressed -}}{{ $icon = "compressed" }}
      {{- else if $diskimage -}}{{ $icon = "diskimage" }}
      {{- else if $imagediting -}}{{ $icon = "imagediting" }}
    
      {{- else if and (not $internal) (ne $url.Host $baseurl.Host) -}}{{ $icon = "external" }}
    {{- end -}}
    <a href="{{ $destination | safeURL }}"{{ with or .Title $getpage.LinkTitle .Text }} title="{{ . }}"{{ end }}{{ with $icon }} class="icon_{{ . }}"{{ end }}{{ if not $internal }} rel="noopener external"{{ end }}>{{ or .Text .Title $getpage.LinkTitle | safeHTML }}</a>
    
  3. In your stylesheet file add:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    
    /********************
    ** BGN: Link icons **
    ********************/
      [class^="icon_"]::after {
        display: inline-block;
        text-decoration: none;
        white-space: pre-wrap;
        margin-left: 0.25em;
        width: 1em;
      }
      .icon_external::after     { content: "\1F517";                                    /* 🔗 */ }
    
      .icon_chat::after         { content: "\1F4AC";                                    /* 💬 */ }
      .icon_ftp::after          { content: "\2194\FE0F";                                /* ↔️ */ }
      .icon_magnet::after       { content: "\1F9F2";                                    /* 🧲 */ }
      .icon_mail::after         { content: "\1F4E7";                                    /* 📧 */ }
      .icon_remote::after       { content: "\1F4BB";                                    /* 💻 */ }
      .icon_tel::after          { content: "\260E\FE0F";                                /* ☎️ */ }
    
      .icon_books::after        { content: "\1F4D6";                                    /* 📖 */ }
      .icon_document::after     { content: "\1F4C4";                                    /* 📄 */ }
      .icon_text::after         { content: "\1F4DD";                                    /* 📝 */ }
      .icon_presentation::after { content: url("../img/link-icons-presentation.svg");   /* https://openclipart.org/detail/36505/tango-x-office-presentation */ }
      .icon_spreadsheet::after  { content: url("../img/link-icons-spreadsheet.svg");    /* https://openclipart.org/detail/36517/tango-x-office-spreadsheet */ }
    
      .icon_audio::after        { content: "\1F3B5";                                    /* 🎵 */ }
      .icon_video::after        { content: "\1F4FD\FE0F";                               /* 📽️ */ }
      .icon_subtitle::after     { content: url("../img/link-icons-subtitle.svg");       /* https://openclipart.org/detail/212110/mimetype-subtitle */ }
    
      .icon_executable::after   { content: url("../img/link-icons-executable.svg");     /* https://openclipart.org/detail/212161/mimetype-binary */ }
      .icon_scripts::after      { content: url("../img/link-icons-scripts.svg");        /* https://openclipart.org/detail/36175/tango-text-x-script */ }
    
      .icon_fonts::after        { content: url("../img/link-icons-fonts.svg");          /* https://openclipart.org/detail/35257/tango-preferences-desktop-font */ }
      .icon_compressed::after   { content: "\1F5DC\FE0F";                               /* 🗜️ */ }
      .icon_diskimage::after    { content: "\1F4BD";                                    /* 💽 */ }
      .icon_imagediting::after  { content: url("../img/link-icons-imageediting.svg");   /* https://openclipart.org/detail/231061/artists-brush-and-paint */ }
    /********************
    ** END: Link icons **
    ********************/
    
  4. Download .svg icons: link-icons.7z

  5. Extract the .svg files in /static/img/ folder.

How to use

The following Markdown links

- External links
  - [https://example.com/#fragment](https://example.com/#fragment "https://example.com/#fragment")
- Chat
  - [irc://](irc://example.com "irc://") | [ircs://](ircs://example.com "ircs://") | [irc6://](irc6://example.com "irc6://") | [xmpp://](xmpp://example.com "xmpp://") | [jabber://](jabber://example.com "jabber://") | [discord://](discord://example.com "discord://") | [skype://](skype://example.com "skype://")
- FTP
  - [sftp://](sftp://example.com "sftp://") | [ftp://](ftp://example.com "ftp://") | [aftp://](aftp://example.com "aftp://")
- Magnet
  - [magnet://](magnet://example.com "magnet://")
- Mail
  - [mailto:](mailto:[email protected] "mailto:")
- Remote
  - [telnet://](telnet://example.com "telnet://") | [ssh://](ssh://example.com "ssh://") | [git://](git://example.com "git://") | [svn://](svn://example.com "svn://") | [bzr://](bzr://example.com "bzr://")
- Tel
  - [tel:](tel:123-456-7890 "tel:")
- Books
  - [doi://](doi://example.com "doi://") | [.epub](https://example.com/file.epub ".epub") | [.mobi](https://example.com/file.mobi ".mobi") | [.pdf](https://example.com/file.pdf ".pdf")
- Document
  - [.odt](https://example.com/file.odt ".odt") | [.sdw](https://example.com/file.sdw ".sdw") | [.sxw](https://example.com/file.sxw ".sxw") | [.uof](https://example.com/file.uof ".uof") | [.uot](https://example.com/file.uot ".uot") | [.doc](https://example.com/file.doc ".doc") | [.docx](https://example.com/file.docx ".docx")
- Text
  - [.txt](https://example.com/file.txt ".txt") | [.csv](https://example.com/file.csv ".csv")
- Presentation
  - [.odp](https://example.com/file.odp ".odp") | [.fodp](https://example.com/file.fodp ".fodp") | [.sdd](https://example.com/file.sdd ".sdd") | [.sdp](https://example.com/file.sdp ".sdp") | [.sxi](https://example.com/file.sxi ".sxi") | [.uop](https://example.com/file.uop ".uop") | [.ppt](https://example.com/file.ppt ".ppt") | [.pptx](https://example.com/file.pptx ".pptx")
- Spreadsheet
  - [.ods](https://example.com/file.ods ".ods") | [.fods](https://example.com/file.fods ".fods") | [.sdc](https://example.com/file.sdc ".sdc") | [.sxc](https://example.com/file.sxc ".sxc") | [.uos](https://example.com/file.uos ".uos") | [.xls](https://example.com/file.xls ".xls") | [.xlsx](https://example.com/file.xlsx ".xlsx")
- Audio
  - [.flac](https://example.com/file.flac ".flac") | [.aac](https://example.com/file.aac ".aac") | [.mka](https://example.com/file.mka ".mka") | [.ogg](https://example.com/file.ogg ".ogg") | [.oga](https://example.com/file.oga ".oga") | [.opus](https://example.com/file.opus ".opus") | [.mp3](https://example.com/file.mp3 ".mp3") | [.mpa](https://example.com/file.mpa ".mpa") | [.mid](https://example.com/file.mid ".mid") | [.midi](https://example.com/file.midi ".midi") | [.wav](https://example.com/file.wav ".wav") | [.wave](https://example.com/file.wave ".wave") | [.wma](https://example.com/file.wma ".wma")
- Video
  - [.av1](https://example.com/file.av1 ".av1") | [.webm](https://example.com/file.webm ".webm") | [.xvid](https://example.com/file.xvid ".xvid") | [.mkv](https://example.com/file.mkv ".mkv") | [.mk3d](https://example.com/file.mk3d ".mk3d") | [.ogm](https://example.com/file.ogm ".ogm") | [.ogv](https://example.com/file.ogv ".ogv") | [.divx](https://example.com/file.divx ".divx") | [.avi](https://example.com/file.avi ".avi") | [.mp4](https://example.com/file.mp4 ".mp4") | [.mpeg4](https://example.com/file.mpeg4 ".mpeg4") | [.mpv](https://example.com/file.mpv ".mpv") | [.mpeg](https://example.com/file.mpeg ".mpeg") | [.mpg](https://example.com/file.mpg ".mpg")
- Subtitle
  - [.vtt](https://example.com/file.vtt ".vtt") | [.ttml](https://example.com/file.ttml ".ttml") | [.dfxp](https://example.com/file.dfxp ".dfxp") | [.srt](https://example.com/file.srt ".srt") | [.sub](https://example.com/file.sub ".sub") | [.sbv](https://example.com/file.sbv ".sbv") | [.scc](https://example.com/file.scc ".scc") | [.mks](https://example.com/file.mks ".mks")
- Executables
  - [.deb](https://example.com/file.deb ".deb") | [.apk](https://example.com/file.apk ".apk") | [.exe](https://example.com/file.exe ".exe") | [.com](https://example.com/file.com ".com") | [.msi](https://example.com/file.msi ".msi")
- Scripts
  - [.bat](https://example.com/file.bat ".bat") | [.sh](https://example.com/file.sh ".sh")
- Fonts
  - [.woff](https://example.com/file.woff ".woff") | [.woff2](https://example.com/file.woff2 ".woff2") | [.otf](https://example.com/file.otf ".otf") | [.ttf](https://example.com/file.ttf ".ttf") | [.ttc](https://example.com/file.ttc ".ttc")
- Compressed files
  - [.7z](https://example.com/file.7z ".7z") | [.7zip](https://example.com/file.7zip ".7zip") | [.tar](https://example.com/file.tar ".tar") | [.gz](https://example.com/file.gz ".gz") | [.gzip](https://example.com/file.gzip ".gzip") | [.bz2](https://example.com/file.bz2 ".bz2") | [.bzip2](https://example.com/file.bzip2 ".bzip2") | [.zip](https://example.com/file.zip ".zip") | [.rar](https://example.com/file.rar ".rar")
- Disk images
  - [.img](https://example.com/file.img ".img") | [.iso](https://example.com/file.iso ".iso") | [.dmg](https://example.com/file.dmg ".dmg") | [.mds](https://example.com/file.mds ".mds") | [.mdf](https://example.com/file.mdf ".mdf") | [.mdx](https://example.com/file.mdx ".mdx")
- Image editing
  - [.xcf](https://example.com/file.xcf ".xcf") | [.psd](https://example.com/file.psd ".psd")

Will render as:

An official list of Uniform Resource Identifier (URI) Schemes can be found at the IANA official website. 3 However, not all popular URI Schemes were registered and/or submitted, for example, discord:// and bzr:// (as of IANA document dated 2022-05-13). Regardless, these popular unregistered URI Schemes were included in the above code.

I hope you find it useful!


Did you like it? Do share this post, leave a comment below, and send me a gift! (opens in a new tab/window)