diff --git a/apify-docs-theme/src/theme/MDXComponents/index.js b/apify-docs-theme/src/theme/MDXComponents/index.js index c70b80412e..dd24cd8770 100644 --- a/apify-docs-theme/src/theme/MDXComponents/index.js +++ b/apify-docs-theme/src/theme/MDXComponents/index.js @@ -1,3 +1,4 @@ +import LLMButtons from '@site/src/components/LLMButtons'; import Admonition from '@theme/Admonition'; import MDXA from '@theme/MDXComponents/A'; import MDXCode from '@theme/MDXComponents/Code'; @@ -29,5 +30,6 @@ const MDXComponents = { admonition: Admonition, mermaid: Mermaid, RunnableCodeBlock, + LLMButtons, }; export default MDXComponents; diff --git a/docusaurus.config.js b/docusaurus.config.js index b5d81a2b4b..1cf94cc3a5 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -1,11 +1,12 @@ const { join, resolve } = require('node:path'); const clsx = require('clsx'); -const { createApiPageMD } = require('docusaurus-plugin-openapi-docs/lib/markdown'); +const { createApiPageMD, createInfoPageMD } = require('docusaurus-plugin-openapi-docs/lib/markdown'); const { config } = require('./apify-docs-theme'); const { collectSlugs } = require('./tools/utils/collectSlugs'); const { externalLinkProcessor } = require('./tools/utils/externalLink'); +const { removeLlmButtons } = require('./tools/utils/removeLlmButtons'); /** @type {Partial} */ module.exports = { @@ -196,6 +197,21 @@ module.exports = { md = md.replace('-->', '-->'); } + // Find the first Heading h1 and add LLMButtons after it + // eslint-disable-next-line max-len + const headingRegex = /(]*as=\{"h1"\}[^>]*className=\{"openapi__heading"\}[^>]*children=\{[^}]*\}[^>]*>\s*<\/Heading>)/; + md = md.replace(headingRegex, '$1\n\n\n'); + + return md; + }, + createInfoPageMD: (pageData) => { + let md = createInfoPageMD(pageData); + + // Find the first Heading h1 and add LLMButtons after it + // eslint-disable-next-line max-len + const headingRegex = /(]*as=\{"h1"\}[^>]*className=\{"openapi__heading"\}[^>]*children=\{[^}]*\}[^>]*>\s*<\/Heading>)/; + md = md.replace(headingRegex, '$1\n\n\n'); + return md; }, }, @@ -293,6 +309,8 @@ module.exports = { categoryName: 'Platform documentation', }, ], + // Add custom remark processing to remove LLM button text + remarkPlugins: [removeLlmButtons], }, }, ], diff --git a/package-lock.json b/package-lock.json index 24313c3fbc..f5ef6ded04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,7 +74,7 @@ }, "apify-docs-theme": { "name": "@apify/docs-theme", - "version": "1.0.197", + "version": "1.0.198", "license": "ISC", "dependencies": { "@apify/docs-search-modal": "^1.2.2", diff --git a/sources/legal/index.mdx b/sources/legal/index.mdx index 1a8788870f..78ffdf6be3 100644 --- a/sources/legal/index.mdx +++ b/sources/legal/index.mdx @@ -7,7 +7,6 @@ slug: / hide_table_of_contents: true --- -# Apify Legal ## Company details (Impressum) diff --git a/sources/platform/actors/running/index.md b/sources/platform/actors/running/index.md index 44bae2c156..56541d822d 100644 --- a/sources/platform/actors/running/index.md +++ b/sources/platform/actors/running/index.md @@ -5,8 +5,6 @@ sidebar_position: 7.1 slug: /actors/running --- -# Running Actors - **In this section, you learn how to run Apify Actors using Apify Console or programmatically. You will learn about their configuration, versioning, data retention, usage, and pricing.** import Tabs from '@theme/Tabs'; diff --git a/sources/platform/index.mdx b/sources/platform/index.mdx index 87d6993c11..50204a168f 100644 --- a/sources/platform/index.mdx +++ b/sources/platform/index.mdx @@ -1,16 +1,15 @@ --- -title: Home +title: Apify platform description: Apify is your one-stop shop for web scraping, data extraction, and RPA. Automate anything you can do manually in a browser. slug: / hide_table_of_contents: true sidebar_position: 0 +sidebar_label: Home --- import Card from "@site/src/components/Card"; import CardGrid from "@site/src/components/CardGrid"; import homepageContent from "./homepage_content.json"; -# Apify platform - > **Apify** is a cloud platform that helps you build reliable web scrapers, fast, and automate anything you can do manually in a web browser. > > **Actors** are serverless cloud programs running on the Apify platform that can easily crawl websites with millions of pages, but also perform arbitrary computing jobs such as sending emails or data transformations. They can be started manually, using our API or scheduler, and they can be easily integrated with other apps. diff --git a/sources/platform/proxy/index.md b/sources/platform/proxy/index.md index 787659a405..7397d12728 100644 --- a/sources/platform/proxy/index.md +++ b/sources/platform/proxy/index.md @@ -11,8 +11,6 @@ import TabItem from '@theme/TabItem'; import Card from "@site/src/components/Card"; import CardGrid from "@site/src/components/CardGrid"; -# Proxy - **Learn to anonymously access websites in scraping/automation jobs. Improve data outputs and efficiency of bots, and access websites from various geographies.** --- diff --git a/sources/platform/security.md b/sources/platform/security.md index 8cc96b013f..ec13e53011 100644 --- a/sources/platform/security.md +++ b/sources/platform/security.md @@ -6,8 +6,6 @@ category: platform slug: /security --- -# Security - **Learn more about Apify's security practices and data protection measures that are used to protect your Actors, their data, and the Apify platform in general.** --- diff --git a/sources/platform/storage/dataset.md b/sources/platform/storage/dataset.md index 01831adaeb..4178fdfec7 100644 --- a/sources/platform/storage/dataset.md +++ b/sources/platform/storage/dataset.md @@ -6,8 +6,6 @@ toc_max_heading_level: 4 slug: /storage/dataset --- -# Dataset - **Store and export web scraping, crawling or data processing job results. Learn how to access and manage datasets in Apify Console or via API.** import Tabs from '@theme/Tabs'; diff --git a/sources/platform/storage/key_value_store.md b/sources/platform/storage/key_value_store.md index 95941d9a79..ffe40d484c 100644 --- a/sources/platform/storage/key_value_store.md +++ b/sources/platform/storage/key_value_store.md @@ -6,8 +6,6 @@ sidebar_position: 9.3 slug: /storage/key-value-store --- -# Key-value store - **Store anything from Actor or task run results, JSON documents, or images. Learn how to access and manage key-value stores from Apify Console or via API.** import Tabs from '@theme/Tabs'; diff --git a/src/components/LLMButtons/CopyForLLM/index.tsx b/src/components/LLMButtons/CopyForLLM/index.tsx new file mode 100644 index 0000000000..94dbc578c2 --- /dev/null +++ b/src/components/LLMButtons/CopyForLLM/index.tsx @@ -0,0 +1,71 @@ +import React, { useState } from 'react'; + +import styles from '../styles.module.css'; + +// Custom component for button text +function ButtonText({ isLoading, isCopied }: { isLoading: boolean; isCopied: boolean }) { + if (isLoading) { + return <>Copying...; + } + if (isCopied) { + return <>Copied!; + } + return <>Copy for LLM; +} + +export default function CopyForLLM() { + const [isLoading, setIsLoading] = useState(false); + const [isCopied, setIsCopied] = useState(false); + + const handleCopy = async () => { + if ((window as any).analytics) { + (window as any).analytics.track('Clicked', { + app: 'docs', + button_text: 'Copy for LLM', + element: 'llm-buttons.copyForLLM', + }); + } + + try { + setIsLoading(true); + + const currentUrl = window.location.href; + const markdownUrl = `${currentUrl}.md`; + + // Fetch the markdown content + const response = await fetch(markdownUrl); + + if (!response.ok) { + throw new Error(`Failed to fetch markdown: ${response.status}`); + } + + const markdownContent = await response.text(); + + // Copy to clipboard + await navigator.clipboard.writeText(markdownContent); + + // Show success feedback + setIsCopied(true); + setTimeout(() => setIsCopied(false), 2000); + } catch (error) { + console.error('Failed to copy markdown content:', error); + } finally { + setIsLoading(false); + } + }; + + return ( + + ); +} diff --git a/src/components/LLMButtons/ViewAsMarkdown/index.tsx b/src/components/LLMButtons/ViewAsMarkdown/index.tsx new file mode 100644 index 0000000000..0f6e751fda --- /dev/null +++ b/src/components/LLMButtons/ViewAsMarkdown/index.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import styles from '../styles.module.css'; + +export default function ViewAsMarkdown() { + const handleClick = () => { + if ((window as any).analytics) { + (window as any).analytics.track('Clicked', { + app: 'docs', + button_text: 'View as Markdown', + element: 'llm-buttons.viewAsMarkdown', + }); + } + + try { + const currentUrl = window.location.href; + const markdownUrl = `${currentUrl}.md`; + window.open(markdownUrl, '_blank'); + } catch (error) { + console.error('Error opening markdown file:', error); + } + }; + + return ( + + ); +} diff --git a/src/components/LLMButtons/index.tsx b/src/components/LLMButtons/index.tsx new file mode 100644 index 0000000000..6ab85fa376 --- /dev/null +++ b/src/components/LLMButtons/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import CopyForLLM from './CopyForLLM'; +import styles from './styles.module.css'; +import ViewAsMarkdown from './ViewAsMarkdown'; + +export default function LLMButtons() { + return ( +
+ +
+ +
+ ); +} diff --git a/src/components/LLMButtons/styles.module.css b/src/components/LLMButtons/styles.module.css new file mode 100644 index 0000000000..d517b5c9c9 --- /dev/null +++ b/src/components/LLMButtons/styles.module.css @@ -0,0 +1,57 @@ +.llmButtonsContainer { + display: flex; + align-items: center; + gap: 12px; + margin-top: -8px; + margin-bottom: calc(var(--ifm-h1-vertical-rhythm-bottom) * var(--ifm-leading)); +} + +.llmButtonsSeparator { + width: 1px; + height: 16px; + background-color: var(--ifm-hr-background-color); +} + +.llmButton { + display: flex; + align-items: center; + background-color: transparent; + border: none; + height: 16px; + cursor: pointer; + padding: 0; + gap: 4px; +} + +.llmButtonIcon { + width: 16px; + height: 16px; + margin: 0 !important; + cursor: pointer; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + display: inline-block; +} + +.llmButtonIconBackgroundMarkdown { + background-image: url('/img/markdown.svg'); + +} + +.llmButtonIconBackgroundCopy { + background-image: url('/img/copy.svg'); +} + +/* Dark theme adjustments */ +[data-theme='dark'] .llmButtonIcon { + color: #e0e0e0; +} + +[data-theme='dark'] .llmButtonIconBackgroundMarkdown { + background-image: url('/img/markdown-dark-theme.svg'); +} + +[data-theme='dark'] .llmButtonIconBackgroundCopy { + background-image: url('/img/copy-dark-theme.svg'); +} \ No newline at end of file diff --git a/src/theme/DocItem/Content/index.js b/src/theme/DocItem/Content/index.js new file mode 100644 index 0000000000..14855855b6 --- /dev/null +++ b/src/theme/DocItem/Content/index.js @@ -0,0 +1,61 @@ +import { useDoc } from '@docusaurus/plugin-content-docs/client'; +import { useLocation } from '@docusaurus/router'; +import { ThemeClassNames } from '@docusaurus/theme-common'; +import Heading from '@theme/Heading'; +import MDXContent from '@theme/MDXContent'; +import clsx from 'clsx'; +import React from 'react'; + +import LLMButtons from '../../../components/LLMButtons'; + +function useSyntheticTitle() { + const { metadata, frontMatter, contentTitle } = useDoc(); + const shouldRender = !frontMatter.hide_title && typeof contentTitle === 'undefined'; + + if (!shouldRender) { + return null; + } + + return metadata.title; +} + +export default function DocItemContent({ children }) { + const syntheticTitle = useSyntheticTitle(); + const location = useLocation(); + + // Define the allowed paths that should show LLMButtons (tag/info pages) + // The logic is handled here, and also in docusaurus.config.js (see docusaurus-plugin-openapi-docs) + const allowedPaths = [ + '/api/v2/getting-started', + '/api/v2/actors', + '/api/v2/actors-actor-versions', + '/api/v2/actors-actor-builds', + '/api/v2/actors-actor-runs', + '/api/v2/actors-webhook-collection', + '/api/v2/actor-builds', + '/api/v2/actor-runs', + '/api/v2/actor-tasks', + '/api/v2/storage-datasets', + '/api/v2/storage-key-value-stores', + '/api/v2/storage-request-queues', + '/api/v2/storage-request-queues-requests', + '/api/v2/storage-request-queues-requests-locks', + '/api/v2/webhooks-webhooks', + '/api/v2/webhooks-webhook-dispatches', + '/api/v2/schedules', + '/api/v2/store', + '/api/v2/logs', + '/api/v2/users', + '/platform', + ]; + + const shouldShowLLMButtons = allowedPaths.some((path) => location.pathname.startsWith(path)); + + return ( +
+ {syntheticTitle && {syntheticTitle}} + {shouldShowLLMButtons && } + {children} +
+ ); +} diff --git a/src/theme/DocItem/Layout/index.js b/src/theme/DocItem/Layout/index.js index 1c69bfc949..25e007163c 100644 --- a/src/theme/DocItem/Layout/index.js +++ b/src/theme/DocItem/Layout/index.js @@ -5,7 +5,6 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import Giscus from '@giscus/react'; import ContentVisibility from '@theme/ContentVisibility'; import DocBreadcrumbs from '@theme/DocBreadcrumbs'; -import DocItemContent from '@theme/DocItem/Content'; import DocItemFooter from '@theme/DocItem/Footer'; import DocItemPaginator from '@theme/DocItem/Paginator'; import DocItemTOCDesktop from '@theme/DocItem/TOC/Desktop'; @@ -15,6 +14,7 @@ import DocVersionBanner from '@theme/DocVersionBanner'; import clsx from 'clsx'; import React, { useCallback } from 'react'; +import DocItemContent from '../Content'; import styles from './styles.module.css'; /** diff --git a/static/img/copy-dark-theme.svg b/static/img/copy-dark-theme.svg new file mode 100644 index 0000000000..1b1e48450c --- /dev/null +++ b/static/img/copy-dark-theme.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/img/copy.svg b/static/img/copy.svg new file mode 100644 index 0000000000..1da744647f --- /dev/null +++ b/static/img/copy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/img/markdown-dark-theme.svg b/static/img/markdown-dark-theme.svg new file mode 100644 index 0000000000..90f88d48a6 --- /dev/null +++ b/static/img/markdown-dark-theme.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/static/img/markdown.svg b/static/img/markdown.svg new file mode 100644 index 0000000000..b5599a0f78 --- /dev/null +++ b/static/img/markdown.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/tools/utils/removeLlmButtons.js b/tools/utils/removeLlmButtons.js new file mode 100644 index 0000000000..9a5bf8d598 --- /dev/null +++ b/tools/utils/removeLlmButtons.js @@ -0,0 +1,48 @@ +const { visit } = require('unist-util-visit'); + +/** + * Remark plugin to remove LLM button text and related elements from markdown content. + * This is used by the @signalwire/docusaurus-plugin-llms-txt plugin to clean up + * the generated markdown files. + */ +function removeLlmButtons() { + return (tree) => { + // Remove text nodes that contain LLM button text + visit(tree, 'text', (node, index, parent) => { + if (node.value && ( + node.value.includes('View as Markdown') + || node.value.includes('Copy for LLM') + || node.value.includes('View as MarkdownCopy for LLM') + || node.value.trim() === 'View as Markdown' + || node.value.trim() === 'Copy for LLM' + )) { + // Remove the text node + parent.children.splice(index, 1); + return index; // Adjust index after removal + } + return undefined; // Explicit return for consistency + }); + + // Clean up empty paragraphs that resulted from text removal + visit(tree, 'paragraph', (node, index, parent) => { + // Check if paragraph is empty or only contains whitespace + const hasContent = node.children && node.children.some((child) => { + if (child.type === 'text') { + return child.value && child.value.trim().length > 0; + } + return true; // Keep non-text nodes + }); + + if (!hasContent) { + // Remove empty paragraph + parent.children.splice(index, 1); + return index; + } + return undefined; // Explicit return for consistency + }); + }; +} + +module.exports = { + removeLlmButtons, +};