Make Copyable Command Blocks in Hugo

Published February 13, 2023

Reading time: 5 minutes.

If you’re developing content that uses a lot of Terminal commands, you may want to prefix those commands with a prompt character like $, but you don’t want visitors to copy that prompt.

In previous versions of Hugo, you had to use various hacks to make this work, but recent versions of Hugo let you override the rendering functions for code fences based on the language you specify.

In this tutorial you’ll add a new renderer for a new command language which you can use to clearly differentiate commands from other shell output or scripts. Then you’ll style the prompt with CSS and add a copy to clipboard button.

Creating the Custom Renderer

You’ll place custom Markdown renderers in the _default/_markup directory in your layout or your theme folder. I recommend keeping this with your theme.

Create a new file within your theme folder called _default/_markup/render-codeblock-command.html and add the following content which generates the structure that Hugo should render when it encounters command blocks:

<div class="command">
  {{- highlight .Inner "bash" -}}
</div>

This code is almost identical to the output that regular bash code blocks generate when they’re converted to HTML. The highlight function takes the code, represented by .Inner, and highlights it using the bash language highlighter. The only thing that’s different is that the output is wrapped with another <div> called command, which will help you identify it later.

When Hugo renders a code block, it renders each line within a span element. Depending on your Hugo configuration, the code blocks may render with inline styles or with classes. Either way, the structure of the code block looks like the following:

<code class="language-html" data-lang="html">
  <span class="line">
    <span class="cl">code goes here</span>
  </span>
</code>

Now that you can identify these command blocks, you can add the prompt using CSS. Add a new selector to your stylesheet that looks for these special code blocks inside of elements with command classes and prepend the prompt to each

.command code > span::before {
  color: #00f2c3;
  content: "$ ";
}

With your new renderer and style in place, you can start marking up commands like this:

```command
mkdir -p ~/tmp/test
```

Next you’ll add a little bit of JavaScript to copy the command to the clipboard.

Adding a Copy button

Create a new file called assets/js/clipboard.js. In the file, add the following code that identifies all of the elements with the command class and adds Copy to Clipboard buttons to them:

(function() {
  function addButtons() {
    var snippets = document.getElementsByClassName('command');
    var numberOfSnippets = snippets.length;
    for (var i = 0; i < numberOfSnippets; i++) {
      var b = document.createElement("button");
      b.classList.add('copy-btn')
      b.innerText="Copy";

      b.addEventListener("click", function () {
        this.innerText = 'Copying..';
        code = this.nextSibling.innerText;
        console.log(code)
        navigator.clipboard.writeText(code);
        this.innerText = 'Copied!';
        var that = this;
        setTimeout(function () {
          that.innerText = 'Copy';
        }, 1000)
      });
      snippets[i].prepend(b)
    }
  }

  addButtons();
})();

This code scans for the command blocks on the page and adds a button inside of each block. The button has an event handler that, when clicked, grabs the text out of the code block and copies it to the clipboard. Using .innerText copies only the text. This way none of the inner <span> tags inserted by the highlighting engine get copied over.

This code uses navigator.clipboard.writeText to write to the system clipboard. This built-in browser API is only available over HTTPS connections, meaning that it won’t work when testing locally. But you’ll work around that in the next section.

The button will show up over the top of your command, so add the following CSS to your stylesheet to move the button to the right side of the code block:

.command .copy-btn {
  float: right;
  cursor: pointer;
}

Typically, you’d load JavaScript in the <head> section of your page, but this code can only work when the elements on the page are present. Loading this in the <head> would fire too early. There are solutions to wait until the page has loaded, but the solution that involves the least amount of effort is to add the script right before the closing <body> tag. This way all of the code blocks will be rendered but you don’t have to wait for other things to load like images or other assets.

Add the following two lines above the closing <body> tag in your Hugo layout. If you’ve used a footer partial, you can add it there, or you can add it to your overall layout.

    {{ $js := resources.Get "js/clipboard.js" | minify | fingerprint }}
    <script src="{{ $js.RelPermalink }}"></script>

This uses Hugo’s asset pipeline to minify and fingerprint the script and then add it into the page.

Test it with local tunnel

If you test your Hugo site locally, the clipboard functionality won’t work because that feature requires a secure connection. You can get one by using the local tunnel service.

First, add some command blocks to your site. Use code fences with command as the language type.

Now start your Hugo server:

hugo serve

Assuming you have Node.js installed, run local tunnel using nix and forward requests to port 1313:

npx localtunnel -p 1313

Let it download and install the components it needs. It’ll provide you with a URL similar to the following:

https://fruity-bears-cheer-12-34-56-240.loca.lt

Visit that URL and test out your site’s functionality. Find your code block, press the Copy button, and the text gets placed on your clipboard.

Wrapping Up

You’ve built your own custom renderer in Hugo for code blocks and you added functionality to copy the code to the clipboard with a small amount of JavaScript.

You can make the JavaScript clipboard code work for any code block, too. Change the selector in the script so it looks for highlight instead of command:


  function addButtons() {
    var snippets = document.getElementsByClassName('highlight');
...

Then change the CSS selector that positions the button to use highlight instead of command:

.highlight .copy-btn {
  float: right;
  cursor: pointer;
}

Now you’ll have clipboard copying anywhere you have highlighted code.


I don't have comments enabled on this site, but I'd love to talk with you about this article on Mastodon, Twitter, or LinkedIn. Follow me there and say hi.


Liked this? I have a newsletter.