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
orassets/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 ashighlight
. - 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>