Scramblings

Dev scratchpad. Digital garden

Hugo - Introduce a Copy Button for Code Blocks

May 28, 2024 | Reading Time: 4 min

One way to enhance the usability of your documentation or blog is by adding a copy button to your code blocks, allowing users to easily copy code snippets to their clipboard. In this post, we will integrate a copy button for code blocks in a Hugo-based static site.

The implementation involves creating a JavaScript function to add the button and then integrating that into your page layouts. The example provided uses Bootstrap 5 but can be adapted as needed.

Step 1: JavaScript: Adding the Copy Button

  • Create a new JavaScript file, copybutton.js with below code.
  • This can be placed in the static/js or assets/js directory of your Hugo project.
  • The implementation introduces a header row with language and copy button. It will do this for all code blocks with the language-* class and with some parent as highlight.
  • The business logic can be easily modified to suit any other class layout (e.g: all pre code blocks, or all code blocks etc)
  • In this current form, it would work for both table based class generation in hugo or normal one.
  • Please note that you would need to adjust the styling to your theme.
  1function addCopyButtonToCodeBlocks() {
  2  // Function to determine if the background color is light or dark
  3  function isColorDark(color) {
  4    const rgb = color.match(/\d+/g);
  5    const r = parseInt(rgb[0], 10);
  6    const g = parseInt(rgb[1], 10);
  7    const b = parseInt(rgb[2], 10);
  8    // Calculate luminance
  9    const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
 10    return luminance < 0.5;
 11  }
 12
 13  // Function to adjust color brightness significantly
 14  function adjustColorBrightness(color, amount) {
 15    const rgb = color.match(/\d+/g);
 16    const r = Math.min(255, Math.max(0, parseInt(rgb[0], 10) + amount));
 17    const g = Math.min(255, Math.max(0, parseInt(rgb[1], 10) + amount));
 18    const b = Math.min(255, Math.max(0, parseInt(rgb[2], 10) + amount));
 19    return `rgb(${r}, ${g}, ${b})`;
 20  }
 21
 22  // Get all code blocks with a class of "language-*"
 23  const codeBlocks = document.querySelectorAll(
 24    'pre > code[class^="language-"]'
 25  );
 26  const copyIcon = '<i class="fas fa-copy"></i> copy code';
 27  const copiedIcon = '<i class="fas fa-check"></i> copied!';
 28
 29  // For each code block, add a copy button inside a header
 30  codeBlocks.forEach((codeBlock) => {
 31    // Get the background color of the code block
 32    const computedStyle = window.getComputedStyle(codeBlock);
 33    const backgroundColor = computedStyle.backgroundColor;
 34
 35    // Adjust the header color to be significantly lighter or darker than the background color
 36    const headerColor = isColorDark(backgroundColor)
 37      ? adjustColorBrightness(backgroundColor, 65)
 38      : adjustColorBrightness(backgroundColor, -65);
 39    const textColor = isColorDark(backgroundColor) ? "#d1d1d1" : "#606060";
 40
 41    // Create the header div
 42    const header = document.createElement("div");
 43    header.style.backgroundColor = headerColor;
 44    header.style.display = "flex";
 45    header.style.justifyContent = "space-between";
 46    header.style.alignItems = "center";
 47    header.style.paddingRight = "0.5rem";
 48    header.style.paddingLeft = "0.5rem";
 49    header.style.borderTopLeftRadius = "5px";
 50    header.style.borderTopRightRadius = "5px";
 51    header.style.color = textColor;
 52    header.style.borderBottom = `1px solid ${headerColor}`;
 53    header.classList.add("small");
 54
 55    // Create the copy button
 56    const copyButton = document.createElement("button");
 57    copyButton.classList.add("btn", "copy-code-button");
 58    copyButton.style.background = "none";
 59    copyButton.style.border = "none";
 60    copyButton.style.color = textColor;
 61    copyButton.style.fontSize = "100%"; // Override the font size
 62    copyButton.style.cursor = "pointer";
 63    copyButton.innerHTML = copyIcon;
 64    copyButton.style.marginLeft = "auto";
 65
 66    // Add a click event listener to the copy button
 67    copyButton.addEventListener("click", () => {
 68      // Copy the code inside the code block to the clipboard
 69      const codeToCopy = codeBlock.innerText;
 70      navigator.clipboard.writeText(codeToCopy);
 71
 72      // Update the copy button text to indicate that the code has been copied
 73      copyButton.innerHTML = copiedIcon;
 74      setTimeout(() => {
 75        copyButton.innerHTML = copyIcon;
 76      }, 1500);
 77    });
 78
 79    // Get the language from the class
 80    const classList = Array.from(codeBlock.classList);
 81    const languageClass = classList.find((cls) => cls.startsWith("language-"));
 82    const language = languageClass
 83      ? languageClass.replace("language-", "")
 84      : "";
 85
 86    // Create the language label
 87    const languageLabel = document.createElement("span");
 88    languageLabel.textContent = language ? language.toLowerCase() : "";
 89    languageLabel.style.marginRight = "10px";
 90
 91    // Append the language label and copy button to the header
 92    header.appendChild(languageLabel);
 93    header.appendChild(copyButton);
 94
 95    // Find the parent element with the "highlight" class and insert the header before it
 96    const highlightParent = codeBlock.closest(".highlight");
 97    if (highlightParent) {
 98      highlightParent.parentNode.insertBefore(header, highlightParent);
 99    }
100  });
101}
102
103// Call the function to add copy buttons to code blocks
104document.addEventListener("DOMContentLoaded", addCopyButtonToCodeBlocks);

Step 2: Integration in layouts

  • To include this JavaScript file in your Hugo site, you need to modify a layout page where you want to have the copybutton present.
  • If you want it to be present in all your pages, it could be the baseof.html file.
  • If you place it in the assets/js directory, it can integrated as:
1{{ with resources.Get "js/copybutton.js" }}
2    {{ $minifiedScript := . | minify | fingerprint }}
3    <script src="{{ $minifiedScript.Permalink }}" integrity="{{ $minifiedScript.Data.Integrity }}" defer></script>
4{{ else }}
5    {{ errorf "copybutton.js not found in assets/js/" }}
6{{ end }}
  • If you place it in the static/js directory, it can integrated as:
1<script src="{{ "js/copybutton.js" | relURL }}" defer></script>