11'use client' ;
22
3- import { Fragment , useEffect , useRef , useState } from 'react' ;
3+ import { Fragment , useCallback , useEffect , useRef , useState } from 'react' ;
44import { createPortal } from 'react-dom' ;
55import { Clipboard } from 'react-feather' ;
66import Link from 'next/link' ;
@@ -19,10 +19,21 @@ export function CopyMarkdownButton({pathname}: CopyMarkdownButtonProps) {
1919 const [ error , setError ] = useState ( false ) ;
2020 const [ isOpen , setIsOpen ] = useState ( false ) ;
2121 const [ isMounted , setIsMounted ] = useState ( false ) ;
22+ const [ prefetchedContent , setPrefetchedContent ] = useState < string | null > ( null ) ;
2223 const buttonRef = useRef < HTMLDivElement > ( null ) ;
2324 const dropdownRef = useRef < HTMLDivElement > ( null ) ;
2425 const { emit} = usePlausibleEvent ( ) ;
2526
27+ const fetchMarkdownContent = useCallback ( async ( ) : Promise < string > => {
28+ // PSA: It's expected that this doesn't work on local development since we need
29+ // the generated markdown files, which only are generated in the deploy pipeline.
30+ const response = await fetch ( `${ window . location . origin } /${ pathname } .md` ) ;
31+ if ( ! response . ok ) {
32+ throw new Error ( `Failed to fetch markdown content: ${ response . status } ` ) ;
33+ }
34+ return await response . text ( ) ;
35+ } , [ pathname ] ) ;
36+
2637 const copyMarkdownToClipboard = async ( ) => {
2738 setIsLoading ( true ) ;
2839 setCopied ( false ) ;
@@ -32,14 +43,14 @@ export function CopyMarkdownButton({pathname}: CopyMarkdownButtonProps) {
3243 emit ( 'Copy Page' , { props : { page : pathname , source : 'copy_button' } } ) ;
3344
3445 try {
35- // This doesn't work on local development since we need the generated markdown
36- // files, and we need to be aware of the origin since we have two different origins.
37- const response = await fetch ( ` ${ window . location . origin } / ${ pathname } .md` ) ;
38- if ( ! response . ok ) {
39- throw new Error ( `Failed to fetch markdown content: ${ response . status } ` ) ;
46+ let content : string ;
47+ if ( prefetchedContent ) {
48+ content = prefetchedContent ;
49+ } else {
50+ content = await fetchMarkdownContent ( ) ;
4051 }
4152
42- await navigator . clipboard . writeText ( await response . text ( ) ) ;
53+ await navigator . clipboard . writeText ( content ) ;
4354 setCopied ( true ) ;
4455 setTimeout ( ( ) => setCopied ( false ) , 2000 ) ;
4556 } catch ( err ) {
@@ -82,6 +93,22 @@ export function CopyMarkdownButton({pathname}: CopyMarkdownButtonProps) {
8293 } ;
8394 } , [ ] ) ;
8495
96+ // Pre-fetch markdown content to avoid losing user gesture context. On iOS we can't async
97+ // fetch on tap because the user gesture is lost by the time we try to update the clipboard.
98+ useEffect ( ( ) => {
99+ if ( ! prefetchedContent ) {
100+ const prefetchContent = async ( ) => {
101+ try {
102+ const content = await fetchMarkdownContent ( ) ;
103+ setPrefetchedContent ( content ) ;
104+ } catch ( err ) {
105+ // Silently fail - we'll fall back to regular fetch on click
106+ }
107+ } ;
108+ prefetchContent ( ) ;
109+ }
110+ } , [ pathname , prefetchedContent , fetchMarkdownContent ] ) ;
111+
85112 const getDropdownPosition = ( ) => {
86113 if ( ! buttonRef . current ) return { top : 0 , left : 0 } ;
87114 const rect = buttonRef . current . getBoundingClientRect ( ) ;
0 commit comments