I finally had some time to look at this again, and managed to implement a relatively simple solution, by extending the html
export backend.
The code to do it is:
(require 'ox)
(require 's)
(require 'uuidgen)
(defun mvr-html-src-block (src-block contents info)
"Transcode a SRC-BLOCK element from Org to HTML, adding a 'copy to clipboard' button."
(if (not (org-export-read-attribute :attr_html src-block :copy-button))
(org-export-with-backend 'html src-block contents info)
(let*((b-id (concat "btn_" (s-replace "-" "" (uuidgen-4))))
(content (let ((print-escape-newlines t))(prin1-to-string (org-export-format-code-default src-block info))))
(content- (s-chop-prefix "\"" (s-chop-suffix "\"" (s-replace "`" "\\`" content))))
(btn- "button")
(scr- "script")
(bquote- "`")
(script (concat "\n<" scr- " type='text/javascript'>\n var copyBtn" b-id "=document.querySelector('" btn- "[name=" b-id "]');\n"
"copyBtn" b-id ".addEventListener('click', function(event) {\n"
"copyTextToClipboard(" bquote- content- bquote- ");\n});\n</" scr- ">\n"))
(button (concat "<" btn- " class='copyBtn' name=" b-id ">Copy to clipboard</" btn- ">")))
(concat (org-export-with-backend 'html src-block contents info) button script))))
(org-export-define-derived-backend 'mvr-html 'html
:translate-alist '((src-block . mvr-html-src-block)))
(defun org-export-to-html-with-button (file)
"Exports the current org-mode buffer to an HTML file, adding 'copy to clipboard'
buttons to source code blocks."
(interactive "FFile Name: ")
(org-export-to-file 'mvr-html file))
The copy button is then enabled by adding #+ATTR_HTML: :copy-button t
to the relevant source blocks, like so
#+ATTR_HTML: :copy-button t
#+BEGIN_SRC....
Remember to add the JS code referenced in the linked SO answer (the "complex example"), and you're good to go. The buttons are added with a "copyBtn" class, so they can be styled as needed.