From 2e185a1834bf1d3b26242be9caa69c6e619d457a Mon Sep 17 00:00:00 2001 From: n3-rd Date: Tue, 10 Feb 2026 01:30:23 +0100 Subject: [PATCH 1/2] feat: add CopyAsMarkdown component for easy markdown copying Introduced a new CopyAsMarkdown component that allows users to copy the current page's content as markdown. This helps users quicky paste docs to LLMs --- src/components/copy-as-markdown.tsx | 77 +++++++++++++++++++++++++++++ src/theme.config.jsx | 7 +++ 2 files changed, 84 insertions(+) create mode 100644 src/components/copy-as-markdown.tsx diff --git a/src/components/copy-as-markdown.tsx b/src/components/copy-as-markdown.tsx new file mode 100644 index 0000000..3e72b41 --- /dev/null +++ b/src/components/copy-as-markdown.tsx @@ -0,0 +1,77 @@ +'use client' + +import { useRouter } from 'next/router' +import { useCallback, useState } from 'react' +import { useConfig } from 'nextra-theme-docs' + +function getRawBase(repo: string) { + const m = repo.match(/github\.com\/([^/]+)\/([^/]+)(?:\/tree\/([^/]+))?/) + return m ? `https://raw.githubusercontent.com/${m[1]}/${m[2]}/${m[3] || 'main'}/src/pages` : null +} + +async function fetchMarkdown(path: string, base: string) { + const p = (path.split('?')[0].replace(/\/+$/, '') || '/index') + const urls = p === '/index' || !p + ? [`${base}/index.mdx`, `${base}/index.md`] + : [`${base}${p}.mdx`, `${base}${p}.md`, `${base}${p}/index.mdx`, `${base}${p}/index.md`] + for (const url of urls) { + const r = await fetch(url) + if (r.ok) return r.text() + } + throw new Error('Not found') +} + +function strip(raw: string) { + return raw + .replace(/^---[\s\S]*?---\s*\n?/, '') + .replace(/^import\s+[\s\S]*?from\s+['"][^'"]*['"]\s*;?\s*$/gm, '') + .replace(/^export\s+\w+\s+.*$/gm, '') + .trim() +} + +export function CopyAsMarkdown() { + const router = useRouter() + const config = useConfig() + const [s, setS] = useState<'idle' | 'loading' | 'copied' | 'error'>('idle') + + const onClick = useCallback(async () => { + const base = getRawBase(config.docsRepositoryBase || 'https://github.com/storacha/docs/tree/main') + if (!base) return setS('error') + setS('loading') + try { + const raw = await fetchMarkdown(router.asPath, base) + await navigator.clipboard.writeText(strip(raw)) + setS('copied') + } catch { + setS('error') + } + setTimeout(() => setS('idle'), 2000) + }, [router.asPath, config.docsRepositoryBase]) + + const label = { idle: 'Copy as Markdown', loading: 'Loading...', copied: 'Copied', error: 'Failed' }[s] + + const CopyIcon = () => ( + + + + ) + + const CheckIcon = () => ( + + + + ) + + return ( + + ) +} diff --git a/src/theme.config.jsx b/src/theme.config.jsx index c7191c9..d23c39d 100644 --- a/src/theme.config.jsx +++ b/src/theme.config.jsx @@ -1,5 +1,6 @@ import { useConfig } from 'nextra-theme-docs' import { DocsLogo } from './components/brand' +import { CopyAsMarkdown } from './components/copy-as-markdown' /** * @type {import('nextra-theme-docs').DocsThemeConfig} @@ -15,6 +16,12 @@ const config = { link: 'https://github.com/storacha/upload-service' }, docsRepositoryBase: 'https://github.com/storacha/docs/tree/main', + main: ({ children }) => ( + <> + {children} + + + ), footer: { component:
}, From bac49c80e1971c1d22fea77f2487ba2fb473eb4e Mon Sep 17 00:00:00 2001 From: n3-rd Date: Tue, 17 Feb 2026 13:25:16 +0100 Subject: [PATCH 2/2] refactor: enhance CopyAsMarkdown component with improved error handling and loading states Updated the CopyAsMarkdown component to include better state management using useRef and useEffect for handling loading and error states. Introduced a new regex for repository parsing and optimized the fetchMarkdown function to handle multiple URL candidates. Improved user feedback with status labels during the copy process. --- src/components/copy-as-markdown.tsx | 116 ++++++++++++++++++---------- 1 file changed, 77 insertions(+), 39 deletions(-) diff --git a/src/components/copy-as-markdown.tsx b/src/components/copy-as-markdown.tsx index 3e72b41..27e732c 100644 --- a/src/components/copy-as-markdown.tsx +++ b/src/components/copy-as-markdown.tsx @@ -1,77 +1,115 @@ 'use client' import { useRouter } from 'next/router' -import { useCallback, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { useConfig } from 'nextra-theme-docs' +const REPO_RE = /github\.com\/([^/]+)\/([^/]+)(?:\/tree\/([^/]+))?/ +const DEFAULT_REPO = 'https://github.com/storacha/docs/tree/main' +const RESET_MS = 2000 + +type Status = 'idle' | 'loading' | 'copied' | 'error' + +const LABELS: Record = { + idle: 'Copy as Markdown', + loading: 'Loading...', + copied: 'Copied', + error: 'Failed', +} + function getRawBase(repo: string) { - const m = repo.match(/github\.com\/([^/]+)\/([^/]+)(?:\/tree\/([^/]+))?/) - return m ? `https://raw.githubusercontent.com/${m[1]}/${m[2]}/${m[3] || 'main'}/src/pages` : null + const m = repo.match(REPO_RE) + return m + ? `https://raw.githubusercontent.com/${m[1]}/${m[2]}/${m[3] || 'main'}/src/pages` + : null } -async function fetchMarkdown(path: string, base: string) { - const p = (path.split('?')[0].replace(/\/+$/, '') || '/index') - const urls = p === '/index' || !p +function candidateUrls(path: string, base: string) { + const p = path.split('?')[0].replace(/\/+$/, '') || '/index' + return p === '/index' ? [`${base}/index.mdx`, `${base}/index.md`] : [`${base}${p}.mdx`, `${base}${p}.md`, `${base}${p}/index.mdx`, `${base}${p}/index.md`] - for (const url of urls) { - const r = await fetch(url) - if (r.ok) return r.text() - } - throw new Error('Not found') +} + +async function fetchMarkdown(path: string, base: string, signal?: AbortSignal) { + const results = await Promise.allSettled( + candidateUrls(path, base).map((url) => fetch(url, { signal }).then((r) => (r.ok ? r.text() : Promise.reject()))) + ) + const hit = results.find((r): r is PromiseFulfilledResult => r.status === 'fulfilled') + if (!hit) throw new Error('Not found') + return hit.value } function strip(raw: string) { return raw .replace(/^---[\s\S]*?---\s*\n?/, '') - .replace(/^import\s+[\s\S]*?from\s+['"][^'"]*['"]\s*;?\s*$/gm, '') + .replace(/^import\s[\s\S]*?from\s+['"][^'"]*['"];?\s*$/gm, '') .replace(/^export\s+\w+\s+.*$/gm, '') .trim() } +const iconClass = 'nx-w-4 nx-h-4 nx-shrink-0' + +const CopyIcon = () => ( + + + +) + +const CheckIcon = () => ( + + + +) + export function CopyAsMarkdown() { const router = useRouter() - const config = useConfig() - const [s, setS] = useState<'idle' | 'loading' | 'copied' | 'error'>('idle') + const { docsRepositoryBase } = useConfig() + const [status, setStatus] = useState('idle') + const timerRef = useRef>() + const abortRef = useRef() + + useEffect(() => { + return () => { + clearTimeout(timerRef.current) + abortRef.current?.abort() + } + }, []) + + const resetAfterDelay = useCallback((s: Status) => { + setStatus(s) + clearTimeout(timerRef.current) + timerRef.current = setTimeout(() => setStatus('idle'), RESET_MS) + }, []) const onClick = useCallback(async () => { - const base = getRawBase(config.docsRepositoryBase || 'https://github.com/storacha/docs/tree/main') - if (!base) return setS('error') - setS('loading') + const base = getRawBase(docsRepositoryBase || DEFAULT_REPO) + if (!base) return resetAfterDelay('error') + + abortRef.current?.abort() + const ac = new AbortController() + abortRef.current = ac + + setStatus('loading') try { - const raw = await fetchMarkdown(router.asPath, base) + const raw = await fetchMarkdown(router.asPath, base, ac.signal) await navigator.clipboard.writeText(strip(raw)) - setS('copied') + resetAfterDelay('copied') } catch { - setS('error') + if (!ac.signal.aborted) resetAfterDelay('error') } - setTimeout(() => setS('idle'), 2000) - }, [router.asPath, config.docsRepositoryBase]) - - const label = { idle: 'Copy as Markdown', loading: 'Loading...', copied: 'Copied', error: 'Failed' }[s] - - const CopyIcon = () => ( - - - - ) - - const CheckIcon = () => ( - - - - ) + }, [router.asPath, docsRepositoryBase, resetAfterDelay]) return ( ) }