From 3bb998f26aed4f04b814fa728fb1da7bf3b6dbf0 Mon Sep 17 00:00:00 2001 From: Bilal Karim <4129613+bilal-karim@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:15:52 -0400 Subject: [PATCH 1/3] Add Open in ChatGPT and Open in Claude actions Adds two external-link actions alongside the existing Copy for LLM and View as Markdown CTAs under the page title. Each opens the respective assistant in a new tab with a prompt prefilled to read the current page. - Capture the page URL client-side (window unavailable during SSR) - Render ChatGPT/Claude as anchors with OpenAI/Claude brand icons plus a trailing external-link icon - Normalize button vs. anchor height (line-height, box-sizing, font-family) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../LLMActions/LLMActions.module.css | 12 ++++++ src/components/LLMActions/LLMActions.tsx | 42 ++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/components/LLMActions/LLMActions.module.css b/src/components/LLMActions/LLMActions.module.css index 9769ccff7f..812a57243b 100644 --- a/src/components/LLMActions/LLMActions.module.css +++ b/src/components/LLMActions/LLMActions.module.css @@ -15,9 +15,13 @@ border: 1px solid var(--ifm-color-emphasis-300); background: transparent; color: var(--ifm-color-emphasis-700); + font-family: inherit; font-size: 0.8rem; font-weight: 500; + line-height: 1.5; + box-sizing: border-box; cursor: pointer; + text-decoration: none; transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease; } @@ -25,6 +29,7 @@ background: var(--ifm-color-emphasis-100); border-color: var(--ifm-color-emphasis-400); color: var(--ifm-color-emphasis-800); + text-decoration: none; } .actionButton:focus-visible { @@ -43,6 +48,13 @@ flex-shrink: 0; } +.externalIcon { + width: 0.7em; + height: 0.7em; + flex-shrink: 0; + opacity: 0.6; +} + /* Dark mode adjustments */ :global([data-theme='dark']) .actionButton { border-color: var(--ifm-color-emphasis-400); diff --git a/src/components/LLMActions/LLMActions.tsx b/src/components/LLMActions/LLMActions.tsx index fe8bd44d7b..8046a05836 100644 --- a/src/components/LLMActions/LLMActions.tsx +++ b/src/components/LLMActions/LLMActions.tsx @@ -1,6 +1,7 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { useDoc } from '@docusaurus/plugin-content-docs/client'; -import { FaRegCopy, FaCheck, FaMarkdown } from 'react-icons/fa'; +import { FaRegCopy, FaCheck, FaMarkdown, FaExternalLinkAlt } from 'react-icons/fa'; +import { SiOpenai, SiClaude } from 'react-icons/si'; import styles from './LLMActions.module.css'; /** @@ -36,10 +37,21 @@ function buildRawUrlFromSlug(slug: string): string { export default function LLMActions() { const [copied, setCopied] = useState(false); const [loading, setLoading] = useState(false); + const [pageUrl, setPageUrl] = useState(''); const { metadata, frontMatter } = useDoc(); const { editUrl, slug } = metadata; + // window is not available during SSR, so capture the page URL on the client. + useEffect(() => { + setPageUrl(window.location.href); + }, []); + + // Prefilled prompts that point the assistant at this page. + const prompt = `Read ${pageUrl} and answer questions about the content.`; + const chatGptUrl = `https://chatgpt.com/?prompt=${encodeURIComponent(prompt)}`; + const claudeUrl = `https://claude.ai/new?q=${encodeURIComponent(prompt)}`; + // Try to get raw URL from editUrl first, then fall back to slug-based construction let rawUrl = editUrl ? getGitHubRawUrl(editUrl) : null; if (!rawUrl && slug) { @@ -109,6 +121,32 @@ export default function LLMActions() { View as Markdown + + + Open in ChatGPT + + + + + Open in Claude + + ); } From 57fba60306da3e461f3e253294d4a6c5dfeabcb0 Mon Sep 17 00:00:00 2001 From: Bilal Karim <4129613+bilal-karim@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:41:36 -0400 Subject: [PATCH 2/3] Reuse pageUrl state in Copy for LLM to avoid shadowing The new pageUrl state shadowed a local of the same name inside handleCopyForLLM. Reuse the state value (added to the callback deps) instead of re-reading window.location.href. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components/LLMActions/LLMActions.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/LLMActions/LLMActions.tsx b/src/components/LLMActions/LLMActions.tsx index 8046a05836..740e3e5146 100644 --- a/src/components/LLMActions/LLMActions.tsx +++ b/src/components/LLMActions/LLMActions.tsx @@ -71,7 +71,6 @@ export default function LLMActions() { throw new Error(`Failed to fetch: ${response.status}`); } const markdown = await response.text(); - const pageUrl = window.location.href; const content = `Source: ${pageUrl}\n\n${markdown}`; await navigator.clipboard.writeText(content); @@ -82,7 +81,7 @@ export default function LLMActions() { } finally { setLoading(false); } - }, [rawUrl]); + }, [rawUrl, pageUrl]); const handleViewMarkdown = useCallback(() => { if (rawUrl) { From 3e09dfc40e526b4c8ee9b8d085f0acab83f46720 Mon Sep 17 00:00:00 2001 From: Bilal Karim <4129613+bilal-karim@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:52:47 -0400 Subject: [PATCH 3/3] Derive page URL from Docusaurus config instead of window Addresses review feedback: the ChatGPT/Claude prompts were built from a client-side window.location.href captured in useEffect, which left the URL empty during SSR and the first render. Derive a canonical absolute URL from siteConfig.url + metadata.permalink instead, so the prompt is always valid with no effect, state, or empty-URL window. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components/LLMActions/LLMActions.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/LLMActions/LLMActions.tsx b/src/components/LLMActions/LLMActions.tsx index 740e3e5146..38117c6f2d 100644 --- a/src/components/LLMActions/LLMActions.tsx +++ b/src/components/LLMActions/LLMActions.tsx @@ -1,5 +1,6 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useCallback } from 'react'; import { useDoc } from '@docusaurus/plugin-content-docs/client'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import { FaRegCopy, FaCheck, FaMarkdown, FaExternalLinkAlt } from 'react-icons/fa'; import { SiOpenai, SiClaude } from 'react-icons/si'; import styles from './LLMActions.module.css'; @@ -37,15 +38,14 @@ function buildRawUrlFromSlug(slug: string): string { export default function LLMActions() { const [copied, setCopied] = useState(false); const [loading, setLoading] = useState(false); - const [pageUrl, setPageUrl] = useState(''); const { metadata, frontMatter } = useDoc(); - const { editUrl, slug } = metadata; + const { editUrl, slug, permalink } = metadata; + const { siteConfig } = useDocusaurusContext(); - // window is not available during SSR, so capture the page URL on the client. - useEffect(() => { - setPageUrl(window.location.href); - }, []); + // Canonical, absolute page URL derived from Docusaurus config + permalink. + // Available during SSR and the first render, so the prompts are always valid. + const pageUrl = `${siteConfig.url}${permalink}`; // Prefilled prompts that point the assistant at this page. const prompt = `Read ${pageUrl} and answer questions about the content.`;