Copy as Markdown

The Copy as Markdown button lets a reader copy the canonical article as Markdown instead of pasting rendered HTML into a prompt. The pattern follows thoughtbot’s Copy as Markdown button and fits these sites because the source of truth is already Markdown: articles should be easy to read in a browser, but also easy to hand to an assistant, quote in a working note, or archive as plain text.

The button below is the live component; it copies this page’s own Markdown output to the clipboard and briefly swaps its label to Copied.

When to use#

  • Article pages — blog posts and TIL notes — get the button automatically in the page header; there is nothing to declare per page
  • The reader should be able to hand the canonical article to an assistant, quote it in a working note, or archive it as plain text
  • The feature needs no account, no API, and no special integration: a durable plain-text version of the page and a button for readers who want the Markdown directly
  • A page outside blog and TIL can force the button on or off with showCopyMarkdown under the pageHeader front matter key
  • On blog and TIL pages the head also advertises the Markdown representation to machines, whether or not a reader ever presses the button

Implementation#

The site configuration registers the output, and every regular page renders it:

mediaTypes:
  text/markdown:
    suffixes:
      - md

outputFormats:
  Markdown:
    mediaType: text/markdown
    rel: alternate
    isPlainText: true
    notAlternative: false

outputs:
  page:
    - HTML
    - Markdown

The Markdown template, layouts/_default/single.markdown.md, emits a small front matter block and then the page’s raw Markdown content:

---
title: {{ .Title | jsonify }}
lastmod: {{ .Lastmod.Format "2006-01-02" | jsonify }}
canonical: {{ .Permalink | jsonify }}
---

{{ .RawContent }}

The page header renders the button through the shared partial:

{{ partial "copy-as-markdown.html" $page }}
<button type="button" class="copy-markdown-button" aria-label="Copy as Markdown" title="Copy as Markdown" data-copy-markdown-url="/en/docs/site-implementation/components/copy-as-markdown/index.md" data-copy-label="Copy as Markdown" data-copied-label="Copied" data-error-label="Could not copy">
  <img class="copy-markdown-icon copy-markdown-icon-light" src="/images/markdown-mark-solid.svg" alt="" aria-hidden="true" />
  <img class="copy-markdown-icon copy-markdown-icon-dark" src="/images/markdown-mark.svg" alt="" aria-hidden="true" />
  <span class="sr-only" aria-live="polite">Copy as Markdown</span>
</button>

The handler is the document-level click listener in assets/js/site.js bound to [data-copy-markdown-url]. It fetches that URL, copies the response through the shared copyText helper — the same one code-block copying uses, so there is only one fallback path for older browsers — and briefly swaps the button’s label, aria-label, and title.

Attribute or classRole
data-copy-markdown-urlURL of the page’s Markdown output; also what the handler matches
data-copy-labelResting label, restored after the swap
data-copied-labelLabel shown after a successful copy
data-error-labelLabel shown when the fetch or the copy fails
copy-markdown-button-doneClass toggled on success
copy-markdown-button-errorClass toggled on failure

The shared partial is layouts/partials/copy-as-markdown.html. It renders only when the page has a Markdown output, takes the real output URL from Hugo — so the JavaScript never guesses whether the Markdown lives at post.md or post/index.md — and pulls its three labels from i18n. layouts/partials/entry-title-block.html turns the button on for blog posts and TIL notes, and layouts/_default/baseof.html adds <link rel="alternate" type="text/markdown" href="…"> to the head of those pages. There is no dedicated stylesheet; the button classes live in assets/css/site.css.

Interface manifest

Kind
Component
Category
Action
Status
Implemented
Implementation
Partial:layouts/partials/copy-as-markdown.htmllayouts/partials/entry-title-block.htmlLayout:layouts/_default/baseof.htmllayouts/_default/single.markdown.mdCSS:assets/css/site.cssJavaScript:assets/js/site.jsi18n:copyAsMarkdowncopiedMarkdowncopyMarkdownErrorConfiguration:config/_default/hugo.yml

Accessibility#

  • The control is a native <button type="button">, so click, Enter, and Space all activate it with no custom key handling
  • The icon-only button carries its accessible name three ways: aria-label, the visually hidden sr-only span, and a matching title; both icon images are decorative, with empty alt and aria-hidden="true"
  • The sr-only label span carries aria-live="polite", so the swap to Copied — or to the error label — is announced by screen readers rather than shown only visually
  • Avoid treating the copy-markdown-button-done and copy-markdown-button-error classes as the feedback; they are styling hooks, and the label swap is what makes the result perceivable