From e7698fcf7176da39085bb2d54d14df78074305dd Mon Sep 17 00:00:00 2001 From: Devon White Date: Thu, 2 Oct 2025 04:59:30 -0400 Subject: [PATCH 01/22] Theme Styling updates --- README.md | 40 +- packages/docusaurus-plugin-llms-txt/README.md | 4 + .../src/plugin-llms-txt.d.ts | 23 +- .../src/getSwizzleConfig.ts | 16 +- .../src/hooks/useCopyButtonConfig.ts | 4 - .../src/hooks/useCopyContentData.ts | 105 +- .../CopyPageContent/CopyButton/index.tsx | 4 +- .../CopyPageContent/DropdownMenu/index.tsx | 10 +- .../DropdownMenu/styles.module.css | 6 + .../src/theme/CopyPageContent/index.tsx | 45 +- .../theme/CopyPageContent/styles.module.css | 12 +- .../src/theme/DocItem/Content/index.tsx | 29 - .../src/theme/DocItem/Layout/index.tsx | 85 + .../theme/DocItem/Layout/styles.module.css | 18 + .../docusaurus-theme-llms-txt/src/types.ts | 1 + website/docs/intro.md | 3 +- website/docs/test/index.md | 8 + website/docs/test/level1/index.md | 8 + website/docs/test/level1/level2/index.md | 8 + .../docs/test/level1/level2/level3/index.md | 8 + .../level3/level4/deeply-nested-page.md | 35 + website/package.json | 4 +- yarn.lock | 1957 ++++++++++------- 23 files changed, 1460 insertions(+), 973 deletions(-) delete mode 100644 packages/docusaurus-theme-llms-txt/src/theme/DocItem/Content/index.tsx create mode 100644 packages/docusaurus-theme-llms-txt/src/theme/DocItem/Layout/index.tsx create mode 100644 packages/docusaurus-theme-llms-txt/src/theme/DocItem/Layout/styles.module.css create mode 100644 website/docs/test/index.md create mode 100644 website/docs/test/level1/index.md create mode 100644 website/docs/test/level1/level2/index.md create mode 100644 website/docs/test/level1/level2/level3/index.md create mode 100644 website/docs/test/level1/level2/level3/level4/deeply-nested-page.md diff --git a/README.md b/README.md index 26a004d..45cfad9 100644 --- a/README.md +++ b/README.md @@ -8,23 +8,26 @@ structure. | Package | Version | Description | | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | [`@signalwire/docusaurus-plugin-llms-txt`](./packages/docusaurus-plugin-llms-txt) | ![npm](https://img.shields.io/npm/v/@signalwire/docusaurus-plugin-llms-txt) | Generate Markdown versions of Docusaurus pages and an llms.txt index file | +| [`@signalwire/docusaurus-theme-llms-txt`](./packages/docusaurus-theme-llms-txt) | ![npm](https://img.shields.io/npm/v/@signalwire/docusaurus-theme-llms-txt) | Theme components for llms.txt plugin with copy-to-clipboard functionality | ## 🚀 Quick Start ### Installation ```bash -npm install @signalwire/docusaurus-plugin-llms-txt +npm install @signalwire/docusaurus-plugin-llms-txt @signalwire/docusaurus-theme-llms-txt # or -yarn add @signalwire/docusaurus-plugin-llms-txt +yarn add @signalwire/docusaurus-plugin-llms-txt @signalwire/docusaurus-theme-llms-txt ``` ### Usage -Add to your `docusaurus.config.js`: +Add to your `docusaurus.config.ts`: -```javascript -module.exports = { +```typescript +import type { Config } from '@docusaurus/types'; + +const config: Config = { plugins: [ [ '@signalwire/docusaurus-plugin-llms-txt', @@ -33,7 +36,10 @@ module.exports = { }, ], ], + themes: ['@signalwire/docusaurus-theme-llms-txt'], }; + +export default config; ``` ## 🏗 Development @@ -121,7 +127,8 @@ For detailed publishing instructions, see [PUBLISHING.md](./PUBLISHING.md). ``` docusaurus-plugins/ ├── packages/ # Published packages -│ └── docusaurus-plugin-llms-txt/ +│ ├── docusaurus-plugin-llms-txt/ +│ └── docusaurus-theme-llms-txt/ ├── website/ # Demo/documentation site ├── .changeset/ # Changeset configuration ├── lerna.json # Lerna configuration @@ -194,3 +201,24 @@ consumption. - 🎯 Content filtering [View Package →](./packages/docusaurus-plugin-llms-txt) + +### [@signalwire/docusaurus-theme-llms-txt](./packages/docusaurus-theme-llms-txt) + +Theme package providing UI components for the llms.txt plugin, including a copy-to-clipboard button +for page content. + +**Key Features:** + +- 📋 Copy page content as Markdown +- 🤖 Format for ChatGPT and Claude +- 🎯 Smart detection of page title +- 📱 Responsive mobile/desktop layouts +- 🔧 Fully swizzlable components + +**Architecture:** + +- Uses DOM-based detection to identify page titles (H1 in `
` tags) +- Global data fetching with shared cache for performance +- Integrates seamlessly with Docusaurus theme system + +[View Package →](./packages/docusaurus-theme-llms-txt) diff --git a/packages/docusaurus-plugin-llms-txt/README.md b/packages/docusaurus-plugin-llms-txt/README.md index 4e840b1..a6be8f1 100644 --- a/packages/docusaurus-plugin-llms-txt/README.md +++ b/packages/docusaurus-plugin-llms-txt/README.md @@ -1,5 +1,9 @@ # @signalwire/docusaurus-plugin-llms-txt +> **📣 Version 2.0 Documentation** This documentation is for version 2.0, which includes breaking +> API changes. If you're using version 1.x, please refer to the +> [v1.2.2 documentation on npm](https://www.npmjs.com/package/@signalwire/docusaurus-plugin-llms-txt/v/1.2.2). + A Docusaurus plugin that transforms your documentation into AI-friendly formats. It automatically converts your site's rendered HTML pages into clean markdown files and generates an `llms.txt` index file, making your documentation easily consumable by Large Language Models while preserving the diff --git a/packages/docusaurus-plugin-llms-txt/src/plugin-llms-txt.d.ts b/packages/docusaurus-plugin-llms-txt/src/plugin-llms-txt.d.ts index 7c60c87..fe51373 100644 --- a/packages/docusaurus-plugin-llms-txt/src/plugin-llms-txt.d.ts +++ b/packages/docusaurus-plugin-llms-txt/src/plugin-llms-txt.d.ts @@ -26,11 +26,22 @@ declare module '@theme/CopyPageContent' { export interface Props { readonly className?: string; + readonly isMobile?: boolean; } export default function CopyPageContent(props: Props): ReactNode; } +// DocItem/Layout - wrapper that positions CopyPageContent button +declare module '@theme/DocItem/Layout' { + import type { ReactNode } from 'react'; + import type { Props as LayoutProps } from '@theme-original/DocItem/Layout'; + + export type Props = LayoutProps; + + export default function DocItemLayout(props: Props): ReactNode; +} + // CopyButton subcomponent declare module '@theme/CopyPageContent/CopyButton' { import type { ReactNode } from 'react'; @@ -54,6 +65,7 @@ declare module '@theme/CopyPageContent/DropdownMenu' { readonly isOpen: boolean; readonly finalConfig: any; readonly onAction: (action: string) => void; + readonly isMobile?: boolean; } export default function DropdownMenu(props: Props): ReactNode; @@ -102,14 +114,3 @@ declare module '@theme/CopyPageContent/Icons/ClaudeIcon' { import type { ReactNode } from 'react'; export default function ClaudeIcon(): ReactNode; } - -// DocItem Content component (existing) -declare module '@theme/DocItem/Content' { - import type { ReactNode } from 'react'; - import type { WrapperProps } from '@docusaurus/types'; - import type ContentType from '@theme-init/DocItem/Content'; - - export type Props = WrapperProps; - - export default function DocItemContent(props: Props): ReactNode; -} diff --git a/packages/docusaurus-theme-llms-txt/src/getSwizzleConfig.ts b/packages/docusaurus-theme-llms-txt/src/getSwizzleConfig.ts index b2f7ec1..7f0c634 100644 --- a/packages/docusaurus-theme-llms-txt/src/getSwizzleConfig.ts +++ b/packages/docusaurus-theme-llms-txt/src/getSwizzleConfig.ts @@ -15,6 +15,14 @@ import type { SwizzleConfig } from '@docusaurus/types'; export default function getSwizzleConfig(): SwizzleConfig { return { components: { + 'DocItem/Layout': { + actions: { + eject: 'safe', + wrap: 'safe', + }, + description: + 'Doc item layout wrapper that positions the CopyPageContent button. Safe to customize for different button placements or layout modifications.', + }, CopyPageContent: { actions: { eject: 'unsafe', @@ -87,14 +95,6 @@ export default function getSwizzleConfig(): SwizzleConfig { description: 'Markdown file icon component. Safe to replace with custom icon.', }, - 'DocItem/Content': { - actions: { - eject: 'safe', - wrap: 'safe', - }, - description: - 'Documentation content wrapper that integrates the copy page button with Docusaurus content. Safe to customize for layout modifications and additional content integration.', - }, }, }; } diff --git a/packages/docusaurus-theme-llms-txt/src/hooks/useCopyButtonConfig.ts b/packages/docusaurus-theme-llms-txt/src/hooks/useCopyButtonConfig.ts index 272c485..373cec8 100644 --- a/packages/docusaurus-theme-llms-txt/src/hooks/useCopyButtonConfig.ts +++ b/packages/docusaurus-theme-llms-txt/src/hooks/useCopyButtonConfig.ts @@ -36,10 +36,6 @@ const DEFAULT_CONFIG: ResolvedCopyPageContentOptions = { export default function useCopyButtonConfig( pluginConfig: boolean | CopyPageContentOptions | undefined ): ResolvedCopyPageContentOptions { - if (pluginConfig === false) { - throw new Error('Component should not render when disabled'); - } - // Memoize configuration merging to prevent unnecessary recalculations return useMemo(() => { let baseConfig = { ...DEFAULT_CONFIG }; diff --git a/packages/docusaurus-theme-llms-txt/src/hooks/useCopyContentData.ts b/packages/docusaurus-theme-llms-txt/src/hooks/useCopyContentData.ts index 6f17589..c2aba79 100644 --- a/packages/docusaurus-theme-llms-txt/src/hooks/useCopyContentData.ts +++ b/packages/docusaurus-theme-llms-txt/src/hooks/useCopyContentData.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ -import { useState, useEffect, useRef, useMemo } from 'react'; +import { useState, useEffect } from 'react'; // JSON data structure interface CopyContentData { @@ -13,58 +13,51 @@ interface CopyContentData { interface CacheEntry { url: string; - data: CopyContentData; + data: CopyContentData | null; promise?: Promise; } +// Global module-level cache shared across all component instances +// This prevents multiple instances from fetching the same data simultaneously +const globalCache = new Map(); + export default function useCopyContentData(dataUrl: string | undefined): { copyContentData: CopyContentData | null; isLoading: boolean; } { - // Component-managed cache using useRef for persistence across re-renders - // This cache resets when component unmounts or dataUrl changes - const cacheRef = useRef(null); + const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(true); - // Use useMemo to determine current data state without side effects - const _currentData = useMemo(() => { - if (!dataUrl || typeof window === 'undefined') { - return { data: null, needsFetch: false, shouldLoad: false }; + useEffect(() => { + if (!dataUrl) { + setIsLoading(false); + setData(null); + return undefined; } - // Check if we have cached data for the exact same URL - if (cacheRef.current?.url === dataUrl) { - return { - data: cacheRef.current.data, - needsFetch: false, - shouldLoad: false, - }; - } + const cached = globalCache.get(dataUrl); - // If we have a pending promise for the same URL, wait for it - if (cacheRef.current?.promise && cacheRef.current.url === dataUrl) { - return { data: null, needsFetch: false, shouldLoad: true }; + // If we already have data in cache, use it immediately + if (cached?.data) { + setData(cached.data); + setIsLoading(false); + return undefined; } - // Clear old cache since URL has changed and need to fetch - return { data: null, needsFetch: true, shouldLoad: true }; - }, [dataUrl]); - - useEffect(() => { - // Update loading state based on current data state - setIsLoading(_currentData.shouldLoad); - - // Early return if no fetch is needed or dataUrl is undefined - if (!_currentData.needsFetch || !dataUrl) { + // If there's a pending fetch, wait for it + if (cached?.promise) { + setIsLoading(true); + void cached.promise.then((fetchedData) => { + setData(fetchedData); + setIsLoading(false); + return undefined; + }); return undefined; } - // Clear old cache since URL has changed - cacheRef.current = null; - - let isCancelled = false; + // Need to start a new fetch + setIsLoading(true); - // Create fetch function const fetchData = async (): Promise => { const response = await fetch(dataUrl); if (!response.ok) { @@ -75,44 +68,38 @@ export default function useCopyContentData(dataUrl: string | undefined): { return (await response.json()) as CopyContentData; }; - // Start the fetch and store promise in cache const promise = fetchData(); - cacheRef.current = { + globalCache.set(dataUrl, { url: dataUrl, - data: {} as CopyContentData, // Temporary, will be replaced + data: null, promise, - }; + }); - // Handle the promise void promise - .then((data) => { - if (!isCancelled && cacheRef.current?.url === dataUrl) { - // Update cache with successful result - cacheRef.current = { - url: dataUrl, - data, - }; - setIsLoading(false); - } + .then((fetchedData) => { + // Update global cache + globalCache.set(dataUrl, { + url: dataUrl, + data: fetchedData, + }); + // Update local state + setData(fetchedData); + setIsLoading(false); return undefined; }) .catch((error) => { console.error('Failed to load copy content data:', error); - if (!isCancelled) { - // Clear cache on error - cacheRef.current = null; - setIsLoading(false); - } + globalCache.delete(dataUrl); + setData(null); + setIsLoading(false); return undefined; }); - return () => { - isCancelled = true; - }; - }, [dataUrl, _currentData]); + return undefined; + }, [dataUrl]); return { - copyContentData: _currentData.data || cacheRef.current?.data || null, + copyContentData: data, isLoading, }; } diff --git a/packages/docusaurus-theme-llms-txt/src/theme/CopyPageContent/CopyButton/index.tsx b/packages/docusaurus-theme-llms-txt/src/theme/CopyPageContent/CopyButton/index.tsx index 80fbadb..2b9e3d7 100644 --- a/packages/docusaurus-theme-llms-txt/src/theme/CopyPageContent/CopyButton/index.tsx +++ b/packages/docusaurus-theme-llms-txt/src/theme/CopyPageContent/CopyButton/index.tsx @@ -43,7 +43,7 @@ export default function CopyButton({ className={styles.mainButton} onClick={onMainAction} aria-label={ - copyStatus === 'success' ? 'Copied!' : 'Copy page as Markdown' + copyStatus === 'success' ? 'Copied' : 'Copy page as Markdown' } > {copyStatus === 'success' ? ( @@ -52,7 +52,7 @@ export default function CopyButton({ )} - {copyStatus === 'success' ? 'Copied!' : finalConfig.buttonLabel} + {copyStatus === 'success' ? 'Copied' : finalConfig.buttonLabel}