Add copy article button with full Markdown source

This commit is contained in:
2026-09-23 13:21:39 -07:00
parent 98a226f8b6
commit 3d842c4957
17 changed files with 1694 additions and 1623 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -16,8 +16,8 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="alternate" type="application/rss+xml" title="apophenia.news RSS" href="https://apophenia.news/rss.xml" />
<script data-goatcounter="https://apophenia.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script>
<script type="module" crossorigin src="/assets/main-BzJy2slU.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-rN4cgZI2.css">
<script type="module" crossorigin src="/assets/main-COWN3pqS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-BVhQ3ewg.css">
</head>
<body>
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -16,8 +16,8 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="alternate" type="application/rss+xml" title="apophenia.news RSS" href="https://apophenia.news/rss.xml" />
<script data-goatcounter="https://apophenia.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script>
<script type="module" crossorigin src="/assets/main-BzJy2slU.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-rN4cgZI2.css">
<script type="module" crossorigin src="/assets/main-COWN3pqS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-BVhQ3ewg.css">
</head>
<body>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -16,8 +16,8 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="alternate" type="application/rss+xml" title="apophenia.news RSS" href="https://apophenia.news/rss.xml" />
<script data-goatcounter="https://apophenia.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script>
<script type="module" crossorigin src="/assets/main-BzJy2slU.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-rN4cgZI2.css">
<script type="module" crossorigin src="/assets/main-COWN3pqS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-BVhQ3ewg.css">
</head>
<body>
File diff suppressed because one or more lines are too long
+8 -2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -16,8 +16,8 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="alternate" type="application/rss+xml" title="apophenia.news RSS" href="https://apophenia.news/rss.xml" />
<script data-goatcounter="https://apophenia.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script>
<script type="module" crossorigin src="/assets/main-BzJy2slU.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-rN4cgZI2.css">
<script type="module" crossorigin src="/assets/main-COWN3pqS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-BVhQ3ewg.css">
</head>
<body>
+1
View File
@@ -52,6 +52,7 @@ const run = async () => {
articles.push({
...data,
sourceFile: file,
markdown: md,
isUnlisted: unlisted.has(data.slug),
html: fixInternalLinks(marked.parse(content))
});
+6
View File
@@ -65,6 +65,11 @@ ${shellHead({
${nav}
<main class="shell py-10">
<article class="card overflow-hidden">
<div class="flex justify-end px-6 py-3 sm:px-10">
<button type="button" data-copy-article class="inline-flex items-center gap-2 rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm font-medium text-zinc-800 shadow-sm transition hover:bg-zinc-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400">
<i data-lucide="copy" class="h-4 w-4"></i><span aria-live="polite">Copy article</span>
</button>
</div>
<img src="${article.header_image}" alt="${escapeHtml(article.title)}" class="h-64 w-full object-cover" />
<div class="p-6 sm:p-10">
<p class="text-xs uppercase tracking-wide text-zinc-500">${fmtDate(article.date)} • ${renderAuthorInline(article.author)}</p>
@@ -76,6 +81,7 @@ ${nav}
<hr class="my-8 border-zinc-200" />
<div class="article-prose">${article.html}</div>
</div>
<script id="article-markdown" type="application/json">${JSON.stringify(article.markdown).replace(/</g, "\\u003c")}</script>
</article>
${merch}
+16
View File
@@ -7,6 +7,22 @@ Alpine.start();
const bootIcons = () => createIcons({ icons });
const copyArticleButton = document.querySelector("[data-copy-article]");
if (copyArticleButton) {
let resetTimer;
copyArticleButton.addEventListener("click", async () => {
const label = copyArticleButton.querySelector("span");
clearTimeout(resetTimer);
try {
await navigator.clipboard.writeText(JSON.parse(document.querySelector("#article-markdown").textContent));
label.textContent = "Copied!";
} catch {
label.textContent = "Copy failed";
}
resetTimer = setTimeout(() => (label.textContent = "Copy article"), 2500);
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", bootIcons);
} else {