Ajouter un bouton « Copy as Markdown » à votre blog Docusaurus
Créez un bouton « Copy as Markdown » et un lien « View raw » pour les articles de votre blog Docusaurus — un plugin exécuté au build qui écrit un miroir en Markdown pur à côté de chaque article, plus le petit composant React qui le récupère et le copie dans le presse-papiers. Code source complet inclus.
Traduction automatique
Cet article fait partie de la série Créer des composants Docusaurus :
- Modifier la façon dont Docusaurus génère les balises img
- Surcharger la génération des balises img avec Docusaurus
- Organisez votre contenu Docusaurus avec un composant Series personnalisé
- Ajouter un bouton « retour en haut » dans les articles Docusaurus
- Créer un plugin de recherche/remplacement pour Docusaurus
- Un composant pour afficher des snippets de code dans un blog Docusaurus
- Un composant Docusaurus qui prévient le lecteur que l'article a plus d'un an
- Afficher le changelog de votre article
- Injecter de l'ASCII art dans toutes les pages HTML générées par Docusaurus
- Best Practice - Personnaliser le flux RSS de Docusaurus pour un contenu complet et des images
- Quelques vérifications sur les images de votre site Docusaurus
- Gestion des tags dans Docusaurus
- Automatiser les résumés TL;DR avec Gemini AI
- Le composant ProjectSetup - Une façon standardisée de partager des structures de projet
- Comment signaler un contenu assisté par IA dans un blog Docusaurus
- Donnez vie à vos tutoriels CLI avec un terminal machine à écrire
- Espiègleries de suricate : semer des easter eggs partout sur mon blog
- Tooltips de code assistés par l'IA dans Docusaurus — Explique-moi comme à un enfant de cinq ans
- Ajouter des réactions de lecteurs à votre blog Docusaurus
- Votre blog est illisible pour une IA. Voici la solution.
- Secouez votre téléphone, surprenez le suricate
- Ajouter un bouton « Copy as Markdown » à votre blog Docusaurus
- Ask My Blog : transformer 259 articles en 2 160 questions cherchables
- La troisième porte : une bulle de chat flottante, et la fonctionnalité que personne ne trouvait
- Dessiner une carte de mon propre blog
- Laissez vos lecteurs signaler les problèmes — un widget de feedback multi-types pour Docusaurus

Ce guide ajoute un bouton « Copy as Markdown » et un lien « View raw » à chaque article d'un blog Docusaurus. Deux petites pièces suffisent : un plugin exécuté au build qui écrit un miroir en Markdown pur à côté de la page HTML de chaque article (/blog/my-post → /blog/my-post.md), et un composant React qui récupère ce miroir et le copie dans le presse-papiers. Pas de serveur, pas de base de données, environ 90 lignes de code au total — et les lecteurs, ou le LLM dans lequel ils collent l'URL, obtiennent le texte intégral de l'article sans React, sans JSX, et sans les accordéons repliés que la page HTML masque par défaut.
Chaque fois que je veux coller un de mes propres articles dans un LLM pour lui poser une question, je passe par le même rituel agaçant : ouvrir la page, tout sélectionner, copier, coller, puis passer une minute à supprimer le menu de navigation, le widget « Was this helpful? » et une demi-douzaine de libellés d'interface venus avec le reste. Le vrai contenu de l'article — la partie que je voulais — est enfoui là-dedans quelque part.
La source d'un article de blog est déjà du Markdown. Il n'y a aucune raison qu'un lecteur doive se battre avec le HTML rendu pour la récupérer.
J'ai donc construit un petit bouton « Copy as Markdown » placé directement dans l'en-tête de l'article : un clic, et la source en texte brut arrive dans le presse-papiers, prête à être collée n'importe où. Cet article parcourt les deux pièces nécessaires pour le reproduire — un générateur de miroir au build et le bouton lui-même — avec le code source réel et actuel du bouton.
Ce qui est copié
Voici le fichier que le bouton récupère réellement, produit par une étape postBuild que vous écrirez dans un instant — c'est le texte littéral qui arrive dans le presse-papiers quand un lecteur clique sur le bouton :
Même la balise <TLDR> survit intacte — suffisant pour qu'un humain parcoure le texte ou qu'un LLM l'analyse. Remarquez aussi le commentaire HTML tout en haut : c'est ainsi que le miroir renvoie vers la vraie page entièrement rendue.
Pourquoi il n'y a que deux pièces mobiles
- Un bouton React qui récupère une URL dérivée du permalien de la page courante et copie le texte renvoyé — il n'a aucune idée de ce que « Markdown » veut dire.
- Une étape de build qui fait en sorte que cette URL réponde vraiment quelque chose qui vaille la peine d'être copié : un fichier en texte brut par article, posé tranquillement à côté de la page HTML qu'il reflète.
- Comme les deux ne communiquent qu'à travers une simple URL, l'un ou l'autre peut être remplacé indépendamment plus tard — un générateur de miroir plus intelligent, un bouton plus élaboré — sans toucher à l'autre fichier.
- Un miroir manquant (mauvais build, serveur de dev, faute de frappe dans l'URL) se dégrade en un message clair « Could not copy » plutôt qu'en un échec silencieux ou un crash.
Étape 1 — Générer un miroir en Markdown pur au build
Le bouton n'est que la moitié de l'histoire : sans fichier .md à récupérer, il finit toujours dans l'état « Could not copy ». Ce fichier doit exister avant que le bouton soit utile, donc construisez-le d'abord.
Créez plugins/markdown-mirror-plugin/index.cjs :
Quatre points méritent d'être soulignés :
function buildMetadataComment(url, buildDate) {
return [
"<!--",
` canonical-url: ${url}`,
` generated-at: ${buildDate}`,
" This is a static plain-Markdown mirror generated at build time.",
" Visit the canonical URL above for the fully rendered page, with images and interactive components.",
"-->",
"",
].join("\n");
}
C'est la pièce qui répond à une question que tout miroir en texte brut finit par soulever : d'où vient ce fichier ? Un commentaire HTML est le bon contenant pour ça — invisible dès que le fichier est collé dans quelque chose qui rend vraiment le Markdown (Notion, Obsidian, une zone de commentaire GitHub), mais toujours en texte brut dans une vue brute ou dans ce qu'on donne à un LLM, si bien que l'URL survit exactement là où un lecteur ayant perdu la trace de la page d'origine en a besoin. generated-at utilise un seul horodatage partagé par tous les miroirs de l'exécution (calculé une fois dans postBuild, pas par fichier), ce qui reflète « ce build » et non « cette milliseconde ».
async postBuild({ siteDir, outDir, siteConfig, routesPaths }) {
postBuild est un lifecycle hook Docusaurus — il se déclenche une seule fois, après que yarn build a fini d'écrire le HTML, et jamais pendant yarn start. C'est exactement le bon moment : le miroir n'a besoin d'exister qu'en production. siteConfig.url est ce qui transforme un permalien nu en URL absolue dont le commentaire ci-dessus a besoin.
if (!knownRoutes.has(permalink)) continue;
routesPaths est la liste des URLs que Docusaurus lui-même a décidé de publier. La recouper — au lieu de réimplémenter à la main la logique « cet article est-il un brouillon ? » — est ce qui fait que les articles en draft: true disparaissent du miroir gratuitement.
const content = body.replace("<!-- truncate -->\n", "").trim() + "\n";
const markdown = buildMetadataComment(url, buildDate) + content;
Le bloc de frontmatter est retiré par le package front-matter avant même que cette ligne s'exécute ; le replace ici ne supprime que le marqueur <!-- truncate -->, qui n'a aucun sens en dehors de la logique de découpage des résumés propre à Docusaurus. Le commentaire de métadonnées vu plus haut est ajouté en dernier, juste avant l'écriture du fichier.
Branchez-le dans votre configuration :
const config = {
// ...
plugins: ["./plugins/markdown-mirror-plugin/index.cjs"],
};
front-matter est un petit package sans dépendances
npm install front-matter si votre projet ne l'a pas déjà — c'est un parseur de frontmatter YAML à usage unique, pas une chaîne d'outils Markdown complète.
Cette version est volontairement simple
Elle recopie votre source Markdown telle quelle, donc tout composant MDX personnalisé (un <TLDR>, une <Card>, vos propres widgets) apparaît dans le miroir sous forme de texte JSX brut plutôt qu'en équivalent Markdown. Pour une poignée de composants, c'est parfaitement lisible, comme le montre le miroir ci-dessus. Si votre blog s'appuie sur des dizaines de composants personnalisés comme celui-ci, voyez « Sous le capot » plus bas pour ce qu'une version plus complète doit gérer.
Étape 2 — Le composant CopyAsMarkdown
Voici le code source réel et actuel qui tourne sur ce blog — pas un extrait simplifié :
2.1 — L'état et l'URL dérivée
const [status, setStatus] = useState("idle"); // idle | copying | copied | error
const mdUrl = `${metadata.permalink.replace(/\/$/, "")}.md`;
status est une petite machine à quatre états qui pilote tout ce que le bouton affiche. mdUrl retire le slash final éventuel du permalien de l'article et ajoute .md — ce qui correspond exactement à ce que le plugin de l'étape 1 écrit sur le disque.
2.2 — Le gestionnaire de copie
const handleCopy = useCallback(async () => {
setStatus("copying");
try {
const res = await fetch(mdUrl);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const text = await res.text();
await navigator.clipboard.writeText(text);
setStatus("copied");
} catch (err) {
console.error("CopyAsMarkdown: failed to copy", err);
setStatus("error");
}
}, [mdUrl]);
Un fetch pour le miroir, un writeText vers le presse-papiers, et un seul catch autour des deux. Ce bloc catch fait double emploi : il attrape un miroir manquant (404, !res.ok) et un rejet de l'API Clipboard (permission refusée, contexte non sécurisé) avec le même résultat « error » — le lecteur n'a pas besoin de savoir lequel s'est produit, seulement que la copie n'a pas fonctionné.
L'API Clipboard exige un contexte sécurisé
navigator.clipboard n'est disponible qu'en HTTPS, ou sur localhost. Testez le bouton sur une simple adresse http:// depuis n'importe quel autre host et writeText échouera à chaque fois — c'est le navigateur, pas un bug dans ce composant.
2.3 — Remise à zéro automatique
useEffect(() => {
if (status !== "copied" && status !== "error") return;
const timer = setTimeout(() => setStatus("idle"), 2000);
return () => clearTimeout(timer);
}, [status]);
Dès que status passe à copied ou error, cet effet programme un retour à idle deux secondes plus tard, pour que le bouton ne reste pas bloqué sur « ✓ Copied » indéfiniment. La fonction de nettoyage annule un timer en attente si le lecteur reclique avant la fin de ces deux secondes.
2.4 — Le rendu
return (
<div className={styles.wrapper}>
<button
type="button"
className={styles.copyBtn}
onClick={handleCopy}
disabled={status === "copying"}
>
{status === "copied"
? "✓ Copied"
: status === "error"
? "Could not copy"
: "📋 Copy as Markdown"}
</button>
<a href={mdUrl} className={styles.rawLink} target="_blank" rel="noopener noreferrer">
View raw
</a>
</div>
);
Le libellé du bouton est une simple chaîne de ternaires basée sur status ; il est désactivé pendant qu'un fetch est en cours pour qu'un lecteur ne puisse pas lancer une seconde requête en pleine copie. Le lien « View raw » n'a besoin d'aucun JavaScript — il pointe directement vers mdUrl et laisse le navigateur faire le travail, target="_blank" et rel="noopener noreferrer" empêchant le nouvel onglet de garder une référence vers celui-ci.
Étape 3 — Le module CSS
Chaque couleur ici est une propriété personnalisée Infima (--ifm-color-emphasis-*), donc le bouton s'adapte au mode clair, au mode sombre ou à un thème Docusaurus entièrement personnalisé, sans une seule valeur hexadécimale en dur ni surcharge [data-theme="dark"] à maintenir.
Étape 4 — L'intégrer dans l'en-tête de l'article
Docusaurus rend le contenu de votre article via un composant de thème appelé BlogPostItem. Pour faire apparaître le bouton, il faut le swizzler :
yarn run swizzle @docusaurus/theme-classic BlogPostItem --eject
Confirmez l'eject quand on vous le demande — un --wrap ne vous donnerait pas d'endroit pour insérer le bouton entre l'en-tête et le contenu.
Ouvrez le src/theme/BlogPostItem/index.js obtenu et ajoutez trois choses : l'import, la garde isBlogPostPage (pour que le bouton n'apparaisse jamais sur une carte en vue liste), et le composant lui-même.
import CopyAsMarkdown from "@site/src/components/CopyAsMarkdown";
import { useBlogPost } from "@docusaurus/plugin-content-blog/client";
import BlogPostItemContainer from "@theme/BlogPostItem/Container";
import BlogPostItemContent from "@theme/BlogPostItem/Content";
import BlogPostItemFooter from "@theme/BlogPostItem/Footer";
import BlogPostItemHeader from "@theme/BlogPostItem/Header";
export default function BlogPostItem({ children, className }) {
const { metadata, isBlogPostPage } = useBlogPost();
return (
<BlogPostItemContainer className={className}>
<BlogPostItemHeader />
{isBlogPostPage && <CopyAsMarkdown metadata={metadata} />}
<BlogPostItemContent>{children}</BlogPostItemContent>
<BlogPostItemFooter />
</BlogPostItemContainer>
);
}
isBlogPostPage vaut true uniquement quand un lecteur consulte un article isolé, jamais sur la page d'accueil du blog ou la grille d'archives — la même garde utilisée pour placer correctement le widget de réactions des lecteurs et le bouton de retour en haut de page sur ce blog même.
Sur mon propre site, il passe plutôt par l'en-tête
Mon BlogPostItem/index.js transmet <CopyAsMarkdown> à BlogPostItem/Header sous forme de prop au lieu de le rendre en ligne, parce que cet en-tête gère déjà un badge « assisté par IA » et la liste des auteurs. Vous n'avez besoin de cette indirection que si votre propre en-tête fait quelque chose de similaire — pour la plupart des blogs, le rendre directement comme montré ci-dessus est plus simple et tout aussi correct.
Tous les fichiers en un coup d'œil
Besoin d'aide ? Cliquez sur le bouton « Aide » ci-dessus pour les instructions de mise en place.
src/theme/BlogPostItem/index.js n'est pas inclus ci-dessus — comme indiqué à l'étape 4, ce fichier existe déjà sous une forme ou une autre sur votre site, et vous le modifiez, vous ne le créez pas de zéro. Affiché ici avec le même composant de snippets de code utilisé tout au long de cet article.
À vous d'essayer
- Ajoutez le plugin à
docusaurus.config.jset créez les trois fichiers ci-dessus. - Lancez
yarn build— pasyarn start. Les hookspostBuildne se déclenchent jamais sur le serveur de dev, donc le miroir n'existe réellement pas avant un vrai build. - Servez la sortie localement, par exemple
npx serve build, et ouvrez n'importe quel article. - Cliquez sur Copy as Markdown, puis collez dans un éditeur de texte. Vous devriez voir la source Markdown brute, balises
<TLDR>comprises. - Cliquez sur View raw. Le fichier
.mddevrait s'ouvrir en texte brut dans un nouvel onglet.
Auto-hébergé sur Apache ? Attention au type MIME
Si l'étape 5 déclenche une invite de téléchargement au lieu d'afficher le fichier, votre serveur ne sait pas que .md est du texte. Ajoutez AddType text/markdown .md à votre .htaccess (ou l'équivalent Nginx, un bloc types) pour qu'il soit servi en ligne.
Sous le capot (passez si vous voulez juste l'utiliser)
Pourquoi le miroir ne peut pas entrer en collision avec l'URL de l'article. /blog/my-post est un répertoire contenant un index.html — c'est ainsi que Docusaurus sert des URLs propres. /blog/my-post.md est un fichier voisin dans ce même répertoire. Même dossier, fichiers différents, zéro conflit de routage.
Pourquoi postBuild, et pas un loader webpack ou une route en dev. Un loader devrait rejouer la même logique MDX-vers-texte à chaque hot reload sans aucun bénéfice — personne ne copie un article depuis un blog qu'il est en train d'éditer. Restreindre le miroir à la production garde le serveur de dev rapide et la logique en un seul endroit.
Là où la version simple ne suffit plus. Le plugin de l'étape 1 fait passer tout composant personnalisé tel quel, en texte JSX littéral. C'est acceptable pour un bloc <TLDR> isolé, mais ça cesse vite de ressembler à du Markdown dès qu'un article s'appuie sur des dizaines de composants — snippets de code, alertes, cartes étape par étape, etc. Le faire correctement implique de parser le MDX en véritable arbre syntaxique et de remplacer chaque nœud de composant par un équivalent en Markdown pur, composant par composant. C'est un projet nettement plus gros qu'un bouton et une boucle de copie — le genre de chose pour laquelle existe le pipeline de dégradation, beaucoup plus imposant, de ce blog — et un bon candidat pour un article de suivi à part entière.
Une date de frontmatter n'est pas une chaîne. Si vous étendez le commentaire de métadonnées pour afficher aussi le champ date: de l'article, ne le concaténez pas directement comme une chaîne. YAML type automatiquement un date: 2024-02-23 non quoté en objet Date JS, et `${date}` appelle son toString() par défaut, très verbeux (Mon Jul 27 2026 00:00:00 GMT+0000 (Coordinated Universal Time)). Normalisez-le d'abord : date instanceof Date ? date.toISOString().slice(0, 10) : String(date).
Conclusion
Une petite centaine de lignes de code, réparties entre un plugin de build et un composant React, suffisent pour arrêter de se battre avec le HTML rendu chaque fois que vous voulez le texte brut d'un article. Le bouton ne sait pas ce qu'est le Markdown, et le plugin ne sait pas ce qu'est un bouton — chacun fait un petit travail, et l'ensemble tient debout parce que ni l'un ni l'autre n'a besoin de connaître les entrailles de l'autre.
Si vous partez de là, l'étape suivante naturelle est celle signalée dans Sous le capot : apprendre au générateur de miroir à vraiment dégrader vos composants personnalisés au lieu de les recopier en JSX brut. En attendant, cette version résout déjà le problème qui a tout déclenché — plus de chirurgie manuelle de copier-coller, ni pour moi ni pour quiconque veut donner un de mes articles à un LLM.
Cet article fait partie de la série Créer des composants Docusaurus :
- Modifier la façon dont Docusaurus génère les balises img
- Surcharger la génération des balises img avec Docusaurus
- Organisez votre contenu Docusaurus avec un composant Series personnalisé
- Ajouter un bouton « retour en haut » dans les articles Docusaurus
- Créer un plugin de recherche/remplacement pour Docusaurus
- Un composant pour afficher des snippets de code dans un blog Docusaurus
- Un composant Docusaurus qui prévient le lecteur que l'article a plus d'un an
- Afficher le changelog de votre article
- Injecter de l'ASCII art dans toutes les pages HTML générées par Docusaurus
- Best Practice - Personnaliser le flux RSS de Docusaurus pour un contenu complet et des images
- Quelques vérifications sur les images de votre site Docusaurus
- Gestion des tags dans Docusaurus
- Automatiser les résumés TL;DR avec Gemini AI
- Le composant ProjectSetup - Une façon standardisée de partager des structures de projet
- Comment signaler un contenu assisté par IA dans un blog Docusaurus
- Donnez vie à vos tutoriels CLI avec un terminal machine à écrire
- Espiègleries de suricate : semer des easter eggs partout sur mon blog
- Tooltips de code assistés par l'IA dans Docusaurus — Explique-moi comme à un enfant de cinq ans
- Ajouter des réactions de lecteurs à votre blog Docusaurus
- Votre blog est illisible pour une IA. Voici la solution.
- Secouez votre téléphone, surprenez le suricate
- Ajouter un bouton « Copy as Markdown » à votre blog Docusaurus
- Ask My Blog : transformer 259 articles en 2 160 questions cherchables
- La troisième porte : une bulle de chat flottante, et la fonctionnalité que personne ne trouvait
- Dessiner une carte de mon propre blog
- Laissez vos lecteurs signaler les problèmes — un widget de feedback multi-types pour Docusaurus






