From 79eba1acfc00d81b903c7c7c3f5c6a2662a267b8 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sat, 7 Mar 2026 03:31:34 +0000 Subject: [PATCH] Add Copy Page button --- src/components/PageHeading.tsx | 49 +++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/components/PageHeading.tsx b/src/components/PageHeading.tsx index ee92f5e5559..f488f4a98be 100644 --- a/src/components/PageHeading.tsx +++ b/src/components/PageHeading.tsx @@ -14,8 +14,12 @@ import Tag from 'components/Tag'; import {H1} from './MDX/Heading'; import type {RouteTag, RouteItem} from './Layout/getRouteMeta'; import * as React from 'react'; +import {useState, useEffect} from 'react'; +import {useRouter} from 'next/router'; import {IconCanary} from './Icon/IconCanary'; import {IconExperimental} from './Icon/IconExperimental'; +import {IconCopy} from './Icon/IconCopy'; +import {Button} from './Button'; interface PageHeadingProps { title: string; @@ -27,6 +31,44 @@ interface PageHeadingProps { breadcrumbs: RouteItem[]; } +function CopyAsMarkdownButton() { + const {asPath} = useRouter(); + const [copied, setCopied] = useState(false); + + useEffect(() => { + if (!copied) return; + const timer = setTimeout(() => setCopied(false), 2000); + return () => clearTimeout(timer); + }, [copied]); + + async function handleCopy() { + const cleanPath = asPath.split(/[?#]/)[0]; + try { + const res = await fetch(cleanPath + '.md'); + if (!res.ok) return; + const text = await res.text(); + await navigator.clipboard.writeText(text); + setCopied(true); + } catch { + // Silently fail + } + } + + return ( + + ); +} + function PageHeading({ title, status, @@ -37,7 +79,12 @@ function PageHeading({ return (
- {breadcrumbs ? : null} +
+
+ {breadcrumbs ? : null} +
+ +

{title} {version === 'canary' && (