From d076cfac18bfbd958772a5af0bcc480e085958f9 Mon Sep 17 00:00:00 2001 From: PaulieB14 <94752445+PaulieB14@users.noreply.github.com> Date: Tue, 31 Mar 2026 22:30:54 -0400 Subject: [PATCH 1/2] feat: add Copy for LLM button to all doc pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a "Copy page" button to the footer of every documentation page that lets users copy the page's raw Markdown to the clipboard — ideal for pasting into LLMs like Claude or ChatGPT. Also includes a "View as Markdown" option that links directly to the raw GitHub file for the current page. The button is placed in content.tsx alongside the existing "Edit on GitHub" link, so it automatically appears on all doc pages without touching individual MDX files. --- website/src/components/CopyForLLM.tsx | 182 ++++++++++++++++++ website/src/components/index.ts | 1 + .../src/layout/templates/default/content.tsx | 35 +++- 3 files changed, 209 insertions(+), 9 deletions(-) create mode 100644 website/src/components/CopyForLLM.tsx diff --git a/website/src/components/CopyForLLM.tsx b/website/src/components/CopyForLLM.tsx new file mode 100644 index 000000000000..1b27a5ac32fc --- /dev/null +++ b/website/src/components/CopyForLLM.tsx @@ -0,0 +1,182 @@ +import { useEffect, useRef, useState } from 'react' + +interface CopyForLLMProps { + /** URL to the raw Markdown source of this page */ + rawUrl: string +} + +/** + * A "Copy page" button with a dropdown that lets users: + * 1. Copy the page's raw Markdown to the clipboard (great for LLMs) + * 2. Open the raw Markdown in a new tab + */ +export function CopyForLLM({ rawUrl }: CopyForLLMProps) { + const [open, setOpen] = useState(false) + const [status, setStatus] = useState<'idle' | 'copying' | 'copied'>('idle') + const menuRef = useRef(null) + + // Close the dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setOpen(false) + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, []) + + const copyMarkdown = async () => { + setOpen(false) + setStatus('copying') + try { + const res = await fetch(rawUrl) + const text = await res.text() + await navigator.clipboard.writeText(text) + setStatus('copied') + setTimeout(() => setStatus('idle'), 2000) + } catch { + setStatus('idle') + } + } + + const label = + status === 'copied' ? 'Copied!' : status === 'copying' ? 'Copying…' : 'Copy page' + + return ( +
+ {/* Primary button */} + + + {/* Chevron toggle */} + + + {/* Dropdown */} + {open && ( +
+ + + setOpen(false)} + className="flex items-start gap-3 px-4 py-3 transition hover:bg-space-1600" + > + +
+
View as Markdown ↗
+
View this page as plain text
+
+
+
+ )} +
+ ) +} + +// --------------------------------------------------------------------------- +// Inline SVG icons (avoids adding an icon library dependency) +// --------------------------------------------------------------------------- + +function ClipboardIcon({ + className, + size = 14, +}: { + className?: string + size?: number +}) { + return ( + + ) +} + +function ChevronIcon({ open }: { open: boolean }) { + return ( + + ) +} + +function MarkdownIcon({ className }: { className?: string }) { + return ( + + ) +} diff --git a/website/src/components/index.ts b/website/src/components/index.ts index 6b48b4900732..35e2632293ff 100644 --- a/website/src/components/index.ts +++ b/website/src/components/index.ts @@ -1,6 +1,7 @@ export * from './Callout' export * from './Card' export * from './CodeBlock' +export * from './CopyForLLM' export * from './DocSearch' export * from './Heading' export * from './Image' diff --git a/website/src/layout/templates/default/content.tsx b/website/src/layout/templates/default/content.tsx index d8397b04f9ee..38bb05816700 100644 --- a/website/src/layout/templates/default/content.tsx +++ b/website/src/layout/templates/default/content.tsx @@ -3,6 +3,7 @@ import { type ComponentProps, useContext } from 'react' import { ButtonOrLink, classNames, ExperimentalDivider, ExperimentalLink } from '@edgeandnode/gds' import { ArrowLeft, ArrowRight, CalendarDynamic, HourglassDynamic, SocialGitHub } from '@edgeandnode/gds/icons' +import { CopyForLLM } from '@/components' import { useI18n } from '@/i18n' import { LayoutContext } from '../../shared' @@ -22,6 +23,19 @@ export default function TemplateDefaultContent({ className, children, ...props } return `https://github.com/graphprotocol/docs/blob/main/website/src/pages/${segments.map(encodeURIComponent).join('/')}` })() + // Derive the raw Markdown URL so users can copy it directly into an LLM. + // For remote pages (e.g. sourced from another repo), transform the GitHub + // blob URL into a raw.githubusercontent.com URL. For local pages, build + // the raw URL from the file path. + const rawMarkdownUrl = remotePageUrl + ? remotePageUrl + .replace('https://github.com/', 'https://raw.githubusercontent.com/') + .replace('/blob/', '/') + : (() => { + const [_src, _pages, ...segments] = filePath.split('/') + return `https://raw.githubusercontent.com/graphprotocol/docs/main/website/src/pages/${segments.join('/')}` + })() + return (
) : null} - } - className="ms-auto text-space-700" - > - {t('global.page.edit')} - +
+ + } + className="text-space-700" + > + {t('global.page.edit')} + +
Date: Tue, 31 Mar 2026 22:49:27 -0400 Subject: [PATCH 2/2] fix: resolve lint and format failures - Wrap async copyMarkdown in void to fix @typescript-eslint/no-misused-promises - Run Prettier to fix class ordering and formatting in CopyForLLM.tsx and content.tsx" --- website/src/components/CopyForLLM.tsx | 31 +++++++------------ .../src/layout/templates/default/content.tsx | 4 +-- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/website/src/components/CopyForLLM.tsx b/website/src/components/CopyForLLM.tsx index 1b27a5ac32fc..6234d6eb13b9 100644 --- a/website/src/components/CopyForLLM.tsx +++ b/website/src/components/CopyForLLM.tsx @@ -40,19 +40,18 @@ export function CopyForLLM({ rawUrl }: CopyForLLMProps) { } } - const label = - status === 'copied' ? 'Copied!' : status === 'copying' ? 'Copying…' : 'Copy page' + const label = status === 'copied' ? 'Copied!' : status === 'copying' ? 'Copying…' : 'Copy page' return (
{/* Primary button */}