How I crafted TL;DRs with LLMs and modernized my blog (part 3)

tl;dr: I added a "Copy page" button to my blog, just like modern docs have. The Markdown version of the page is fetched in background and copied on click. Implementing this feature felt like a necessary step!
View the series
  1. See how I crafted story-like tl;drs for my posts with LLMs
  2. Learn how I generated my llms.txt summary with LLMs
  3. Grab the "Copy page" button's code, as seen in OpenAI docs
  4. Check how I optimized images for better blog performance
  5. See how impressed I was by GPT-4.1's meta descriptions

Nowadays, technical documentation sites, like OpenAI Platform ↗, often have a "Copy page" button that lets you copy the content of a page in Markdown format.

Screenshot of the 'Copy page as markdown' button highlighted on the OpenAI Platform documentation page

Some sites go further. They include multiple tabs or a dropdown, letting you ask AI about the page, or view it directly as Markdown.

Screenshot collage of user interfaces from Anthropic, Zapier, and Stripe documentation, displaying example tabs and dropdown menus that enable users to interact with AI assistants about the current page content or switch to viewing documentation as Markdown format

Often, you can access the Markdown version by simply adding .md to the page's URL. Anthropic and Zapier use the Mintlify ↗ platform for this; Stripe does it too, but with Markdoc ↗:

While this blog isn't documentation, it is a technical blog. I thought adding a "Copy page" as Markdown could be helpful: maybe for readers who want to ask an LLM about a post, or check the truthfulness of a statement. So, I added one.

Side-by-side comparison of the 'Copy Page as Markdown' UI button on tonyaldon.com, showing both the default interface and the pop-up error message in an AI automation workflow blog context
Get my latest AI automation experiments — free

No spam. Unsubscribe anytime.

Since my pages are already converted to Markdown following the same pattern (see the Markdown version of this page) to mention them in my llms.txt file, I just needed a bit more logic:

  1. Compute the path to the Markdown file:

    https://.../foo/ => https://.../foo.md
  2. Fetch the Markdown page on load using the markdownPageGet function, and store its content in markdownPage.

  3. Define the copyPageToClipboard function to listen for the onclick event on the "Copy page" button. It writes markdownPage to the clipboard when ready. If it can't fetch, it pops up the message "Unable to copy" at the bottom.

:root {
  --bg-page:  #ffffff;
  --fg-text:  #24292f;
  --fg-muted: #57606a;
}

.date {
  color: var(--fg-muted);
  letter-spacing: 0.01em;
  text-decoration: none;
  padding: 0.3em 0;
}

.date-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 1.5em;
  gap: 1em;
}

.copy-page-btn {
  display: flex;
  align-items: center;
  gap: 0.3em;
  background: var(--bg-page);
  color: var(--fg-text);
  border-radius: 6px;
  padding: 0.3em 0.4em;
  font-size: 1em;
  cursor: pointer;
  border: none;
  transition: background 0.15s;
}

#error-popup {
  position: fixed;
  left: 50%;
  bottom: 32px;
  transform: translateX(-50%) translateY(30px);
  background: var(--fg-text);
  color: var(--bg-page);
  font-size: 1rem;
  border-radius: 6px;
  padding: 0.6em 1.2em;
  opacity: 0;
  transition:
    opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1),
    transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  z-index: 3000;
  text-align: center;
}

#error-popup.show {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}
<svg style="display: none">
  <symbol id="icon-copy" viewBox="0 0 24 24">
    <path d="M12.7587 2H16.2413C17.0463 1.99999 17.7106 1.99998 18.2518 2.04419C18.8139 2.09012 19.3306 2.18868 19.816 2.43597C20.5686 2.81947 21.1805 3.43139 21.564 4.18404C21.8113 4.66937 21.9099 5.18608 21.9558 5.74817C22 6.28936 22 6.95372 22 7.75868V11.2413C22 12.0463 22 12.7106 21.9558 13.2518C21.9099 13.8139 21.8113 14.3306 21.564 14.816C21.1805 15.5686 20.5686 16.1805 19.816 16.564C19.3306 16.8113 18.8139 16.9099 18.2518 16.9558C17.8906 16.9853 17.4745 16.9951 16.9984 16.9984C16.9951 17.4745 16.9853 17.8906 16.9558 18.2518C16.9099 18.8139 16.8113 19.3306 16.564 19.816C16.1805 20.5686 15.5686 21.1805 14.816 21.564C14.3306 21.8113 13.8139 21.9099 13.2518 21.9558C12.7106 22 12.0463 22 11.2413 22H7.75868C6.95372 22 6.28936 22 5.74818 21.9558C5.18608 21.9099 4.66937 21.8113 4.18404 21.564C3.43139 21.1805 2.81947 20.5686 2.43597 19.816C2.18868 19.3306 2.09012 18.8139 2.04419 18.2518C1.99998 17.7106 1.99999 17.0463 2 16.2413V12.7587C1.99999 11.9537 1.99998 11.2894 2.04419 10.7482C2.09012 10.1861 2.18868 9.66937 2.43597 9.18404C2.81947 8.43139 3.43139 7.81947 4.18404 7.43598C4.66937 7.18868 5.18608 7.09012 5.74817 7.04419C6.10939 7.01468 6.52548 7.00487 7.00162 7.00162C7.00487 6.52548 7.01468 6.10939 7.04419 5.74817C7.09012 5.18608 7.18868 4.66937 7.43598 4.18404C7.81947 3.43139 8.43139 2.81947 9.18404 2.43597C9.66937 2.18868 10.1861 2.09012 10.7482 2.04419C11.2894 1.99998 11.9537 1.99999 12.7587 2ZM9.00176 7L11.2413 7C12.0463 6.99999 12.7106 6.99998 13.2518 7.04419C13.8139 7.09012 14.3306 7.18868 14.816 7.43598C15.5686 7.81947 16.1805 8.43139 16.564 9.18404C16.8113 9.66937 16.9099 10.1861 16.9558 10.7482C17 11.2894 17 11.9537 17 12.7587V14.9982C17.4455 14.9951 17.7954 14.9864 18.089 14.9624C18.5274 14.9266 18.7516 14.8617 18.908 14.782C19.2843 14.5903 19.5903 14.2843 19.782 13.908C19.8617 13.7516 19.9266 13.5274 19.9624 13.089C19.9992 12.6389 20 12.0566 20 11.2V7.8C20 6.94342 19.9992 6.36113 19.9624 5.91104C19.9266 5.47262 19.8617 5.24842 19.782 5.09202C19.5903 4.7157 19.2843 4.40973 18.908 4.21799C18.7516 4.1383 18.5274 4.07337 18.089 4.03755C17.6389 4.00078 17.0566 4 16.2 4H12.8C11.9434 4 11.3611 4.00078 10.911 4.03755C10.4726 4.07337 10.2484 4.1383 10.092 4.21799C9.7157 4.40973 9.40973 4.7157 9.21799 5.09202C9.1383 5.24842 9.07337 5.47262 9.03755 5.91104C9.01357 6.20463 9.00489 6.55447 9.00176 7ZM5.91104 9.03755C5.47262 9.07337 5.24842 9.1383 5.09202 9.21799C4.7157 9.40973 4.40973 9.7157 4.21799 10.092C4.1383 10.2484 4.07337 10.4726 4.03755 10.911C4.00078 11.3611 4 11.9434 4 12.8V16.2C4 17.0566 4.00078 17.6389 4.03755 18.089C4.07337 18.5274 4.1383 18.7516 4.21799 18.908C4.40973 19.2843 4.7157 19.5903 5.09202 19.782C5.24842 19.8617 5.47262 19.9266 5.91104 19.9624C6.36113 19.9992 6.94342 20 7.8 20H11.2C12.0566 20 12.6389 19.9992 13.089 19.9624C13.5274 19.9266 13.7516 19.8617 13.908 19.782C14.2843 19.5903 14.5903 19.2843 14.782 18.908C14.8617 18.7516 14.9266 18.5274 14.9624 18.089C14.9992 17.6389 15 17.0566 15 16.2V12.8C15 11.9434 14.9992 11.3611 14.9624 10.911C14.9266 10.4726 14.8617 10.2484 14.782 10.092C14.5903 9.7157 14.2843 9.40973 13.908 9.21799C13.7516 9.1383 13.5274 9.07337 13.089 9.03755C12.6389 9.00078 12.0566 9 11.2 9H7.8C6.94342 9 6.36113 9.00078 5.91104 9.03755Z"></path>
  </symbol>
</svg>
<svg style="display: none">
  <symbol id="icon-check" viewBox="0 0 24 24">
    <path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"></path>
  </symbol>
</svg>

<div class="date-row">
  <span class="date">July 14, 2025</span>
  <button class="copy-page-btn"
          type="button"
          title="Copy page markdown"
          onclick="copyPageToClipboard(this)">
    <span class="copy-icon">
      <svg fill="currentColor" width="1.2em" height="1.2em" style="vertical-align: -0.3em">
        <use href="#icon-copy"></use>
      </svg>
    </span>
    <span class="check-icon" style="display: none">
      <svg fill="currentColor" width="1.2em" height="1.2em" style="vertical-align: -0.3em">
        <use href="#icon-check"></use>
      </svg>
    </span>
    <span>Copy page</span>
  </button>
</div>

<div id="error-popup"></div>
function showError(msg) {
  const popup = document.getElementById("error-popup");
  popup.textContent = msg;
  popup.classList.add("show");
  // Remove if already pending
  if (popup._timeoutId) clearTimeout(popup._timeoutId);
  popup._timeoutId = setTimeout(() => {
    popup.classList.remove("show");
    popup._timeoutId = null;
  }, 1500);
}

async function markdownPageGet() {
  const p = document.location.pathname;
  if (p === "/") {
    return "";
  } else {
    const url = `${p.slice(0, -1)}.md`;
    const resp = await fetch(url);
    if (!resp.ok) {
      throw new Error(`Couldn't fetch ${url}`);
    } else {
      return resp.text();
    }
  }
}

let markdownPage = markdownPageGet();

function copyPageToClipboard(btn) {
  markdownPage
    .then((txt) => {
      navigator.clipboard.writeText(txt).then(function () {
        // swap icons ...
        const copyIcon = btn.querySelector(".copy-icon");
        const checkIcon = btn.querySelector(".check-icon");
        if (copyIcon && checkIcon) {
          copyIcon.style.display = "none";
          checkIcon.style.display = "inline-flex";
          btn.disabled = true;
          setTimeout(function () {
            copyIcon.style.display = "inline-flex";
            checkIcon.style.display = "none";
            btn.disabled = false;
          }, 1500);
        }
      });
    })
    .catch((err) => {
      console.log(err);
      showError("Unable to copy :(");
    });
}

That's all I have for today! Talk soon 👋

Recent posts
latestHow I crafted TL;DRs with LLMs and modernized my blog (part 5)
See how impressed I was by GPT-4.1's meta descriptions
prompt
How I crafted TL;DRs with LLMs and modernized my blog (part 4)
Check how I optimized images for better blog performance
code
How I explored Google Sheets to Gmail automation through Zapier before building it in Python (part 1)
See how I built my first Zapier Gmail alert from Sheets updates
no-code
How I realized AI automation is all about what you automate
Check my favorite CRM AI automation (so far) from Zapier blog
misc
Curious about the tools I use?