diff --git a/api-docs/scripts/generate-openapi-articles.ts b/api-docs/scripts/generate-openapi-articles.ts index ceb6d8ee7..fcf8bc2a5 100644 --- a/api-docs/scripts/generate-openapi-articles.ts +++ b/api-docs/scripts/generate-openapi-articles.ts @@ -213,7 +213,7 @@ interface GeneratePagesOptions { * Generate Hugo content pages from article data * * Creates markdown files with frontmatter from article metadata. - * Each article becomes a page with type: api that renders via RapiDoc. + * Each article becomes a page with type: api that renders via Hugo-native templates. * * @param options - Generation options */ @@ -399,7 +399,7 @@ interface GenerateTagPagesOptions { * Generate Hugo content pages from tag-based article data * * Creates markdown files with frontmatter from article metadata. - * Each article becomes a page with type: api that renders via RapiDoc. + * Each article becomes a page with type: api that renders via Hugo-native templates. * Includes operation metadata for TOC generation. * * @param options - Generation options @@ -642,223 +642,9 @@ ${yaml.dump(frontmatter)}--- ); // NOTE: Path page generation is disabled - all operations are now displayed - // inline on tag pages using RapiDoc with hash-based navigation for deep linking. - // The tag pages render all operations in a single scrollable view with a - // server-side generated TOC for quick navigation. - // - // Previously this generated individual pages per API path: - // generatePathPages({ articlesPath, contentPath, pathSpecFiles }); -} - -/** - * Options for generating path pages - */ -interface GeneratePathPagesOptions { - /** Path to the articles data directory */ - articlesPath: string; - /** Output path for generated content pages */ - contentPath: string; - /** Map of API path to path-specific spec file (for single-path rendering) */ - pathSpecFiles?: Map; -} - -/** - * Convert API path to URL-safe slug with normalized version prefix - * - * Transforms an API path to a URL-friendly format: - * - Removes leading "/api" prefix (added by parent directory structure) - * - Ensures all paths have a version prefix (defaults to v1 if none) - * - Removes leading slash - * - Removes curly braces from path parameters (e.g., {db} → db) - * - * Examples: - * - "/write" → "v1/write" - * - "/api/v3/configure/database" → "v3/configure/database" - * - "/api/v3/configure/database/{db}" → "v3/configure/database/db" - * - "/api/v2/write" → "v2/write" - * - "/health" → "v1/health" - * - * @param apiPath - The API path (e.g., "/write", "/api/v3/write_lp") - * @returns URL-safe path slug with version prefix (e.g., "v1/write", "v3/configure/database") - */ -function apiPathToSlug(apiPath: string): string { - // Remove leading "/api" prefix if present - let normalizedPath = apiPath.replace(/^\/api/, ''); - // Remove leading slash - normalizedPath = normalizedPath.replace(/^\//, ''); - - // If path doesn't start with version prefix, add v1/ - if (!/^v\d+\//.test(normalizedPath)) { - normalizedPath = `v1/${normalizedPath}`; - } - - // Remove curly braces from path parameters (e.g., {db} → db) - // to avoid URL encoding issues in Hugo - normalizedPath = normalizedPath.replace(/[{}]/g, ''); - - return normalizedPath; -} - -/** Method sort order for consistent display */ -const METHOD_ORDER = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']; - -/** - * Generate standalone Hugo content pages for each API path - * - * Creates individual pages at path-based URLs like /api/v3/configure/database/ - * for each unique API path. Each page includes all HTTP methods (operations) - * for that path, rendered using RapiDoc with match-type='includes'. - * - * When pathSpecFiles is provided, uses path-specific specs for isolated rendering. - * Falls back to tag-based specs when pathSpecFiles is not available. - * - * @param options - Generation options - */ -function generatePathPages(options: GeneratePathPagesOptions): void { - const { articlesPath, contentPath, pathSpecFiles } = options; - const yaml = require('js-yaml'); - const articlesFile = path.join(articlesPath, 'articles.yml'); - - if (!fs.existsSync(articlesFile)) { - console.warn(`⚠️ Articles file not found: ${articlesFile}`); - return; - } - - // Read articles data - const articlesContent = fs.readFileSync(articlesFile, 'utf8'); - const data = yaml.load(articlesContent) as { - articles: Array<{ - path: string; - fields: { - name?: string; - title?: string; - tag?: string; - isConceptual?: boolean; - showSecuritySchemes?: boolean; - staticFilePath?: string; - operations?: OperationMeta[]; - related?: string[]; - weight?: number; - }; - }>; - }; - - if (!data.articles || !Array.isArray(data.articles)) { - console.warn(`⚠️ No articles found in ${articlesFile}`); - return; - } - - // Collect all operations and group by API path - const pathOperations = new Map< - string, - { - operations: OperationMeta[]; - tagSpecFile?: string; - tagName: string; - } - >(); - - // Process each article (tag) and collect operations by path - for (const article of data.articles) { - // Skip conceptual articles (they don't have operations) - if (article.fields.isConceptual) { - continue; - } - - const operations = article.fields.operations || []; - const tagSpecFile = article.fields.staticFilePath; - const tagName = article.fields.tag || article.fields.name || ''; - - for (const op of operations) { - const existing = pathOperations.get(op.path); - if (existing) { - // Add operation to existing path group - existing.operations.push(op); - } else { - // Create new path group - pathOperations.set(op.path, { - operations: [op], - tagSpecFile, - tagName, - }); - } - } - } - - let pathCount = 0; - - // Generate a page for each unique API path - for (const [apiPath, pathData] of pathOperations) { - // Build page path: api/{path}/ - // e.g., /api/v3/configure/database -> api/v3/configure/database/ - const pathSlug = apiPathToSlug(apiPath); - // Only add 'api/' prefix if the path doesn't already start with 'api/' - const basePath = pathSlug.startsWith('api/') ? pathSlug : `api/${pathSlug}`; - const pathDir = path.join(contentPath, basePath); - const pathFile = path.join(pathDir, '_index.md'); - - // Create directory if needed - if (!fs.existsSync(pathDir)) { - fs.mkdirSync(pathDir, { recursive: true }); - } - - // Sort operations by method order - const sortedOperations = [...pathData.operations].sort((a, b) => { - const aIndex = METHOD_ORDER.indexOf(a.method.toUpperCase()); - const bIndex = METHOD_ORDER.indexOf(b.method.toUpperCase()); - return (aIndex === -1 ? 999 : aIndex) - (bIndex === -1 ? 999 : bIndex); - }); - - // Use first operation's summary or construct from methods - const methods = sortedOperations.map((op) => op.method.toUpperCase()); - const title = - sortedOperations.length === 1 && sortedOperations[0].summary - ? sortedOperations[0].summary - : `${apiPath}`; - - // Determine spec file - use path-specific spec if available - const pathSpecFile = pathSpecFiles?.get(apiPath); - const specFile = pathSpecFile || pathData.tagSpecFile; - - const frontmatter: Record = { - title, - description: `API reference for ${apiPath} - ${methods.join(', ')}`, - type: 'api-path', - layout: 'path', - // RapiDoc configuration - specFile, - apiPath, - // Include all operations for TOC generation - operations: sortedOperations.map((op) => ({ - operationId: op.operationId, - method: op.method, - path: op.path, - summary: op.summary, - ...(op.compatVersion && { compatVersion: op.compatVersion }), - })), - tag: pathData.tagName, - }; - - // Collect related links from all operations - const relatedLinks: string[] = []; - for (const op of sortedOperations) { - if (op.externalDocs?.url && !relatedLinks.includes(op.externalDocs.url)) { - relatedLinks.push(op.externalDocs.url); - } - } - if (relatedLinks.length > 0) { - frontmatter.related = relatedLinks; - } - - const pageContent = `--- -${yaml.dump(frontmatter)}--- -`; - - fs.writeFileSync(pathFile, pageContent); - pathCount++; - } - - console.log(`✓ Generated ${pathCount} path pages in ${contentPath}/api/`); + // inline on tag pages using Hugo-native templates with hash-based navigation + // for deep linking. The tag pages render all operations in a single scrollable + // view with a server-side generated TOC for quick navigation. } /** diff --git a/api-docs/scripts/openapi-paths-to-hugo-data/index.ts b/api-docs/scripts/openapi-paths-to-hugo-data/index.ts index abb4580c6..e6ea57d64 100644 --- a/api-docs/scripts/openapi-paths-to-hugo-data/index.ts +++ b/api-docs/scripts/openapi-paths-to-hugo-data/index.ts @@ -517,7 +517,7 @@ function writeTagOpenapis( const operation = pathItem[method] as Operation | undefined; if (operation?.tags?.includes(tagName)) { // Clone the operation and restrict tags to only this tag - // This prevents RapiDoc from rendering the operation multiple times + // This prevents the operation from being rendered multiple times // (once per tag) when an operation belongs to multiple tags const filteredOperation = { ...operation, tags: [tagName] }; filteredPathItem[method] = filteredOperation; @@ -636,8 +636,8 @@ export function writePathSpecificSpecs( // Deep clone pathItem to avoid mutating original const clonedPathItem: PathItem = JSON.parse(JSON.stringify(pathItem)); - // Limit each operation to a single tag to prevent duplicate rendering in RapiDoc - // RapiDoc renders operations once per tag, so multiple tags cause duplicates + // Limit each operation to a single tag to prevent duplicate rendering + // Operations with multiple tags would be rendered once per tag const usedTags = new Set(); HTTP_METHODS.forEach((method) => { const operation = clonedPathItem[method] as Operation | undefined; @@ -654,7 +654,7 @@ export function writePathSpecificSpecs( }); // Create spec with just this path (all its methods) - // Include global security requirements so RapiDoc displays auth correctly + // Include global security requirements so auth info displays correctly const pathSpec: OpenAPIDocument = { openapi: openapi.openapi, info: { @@ -1001,7 +1001,7 @@ function writeOpenapiArticleData( * Sanitize markdown description by removing fragment links and ReDoc directives * * Handles three cases: - * 1. RapiDoc fragment links: [text](#section/...) -> text (removes the link entirely) + * 1. OpenAPI fragment links: [text](#section/...) -> text (removes the link entirely) * 2. Relative links with fragments: [text](/path/#anchor) -> [text](/path/) (keeps link, removes fragment) * 3. ReDoc injection directives: (removes entirely) * @@ -1022,12 +1022,12 @@ function sanitizeDescription(description: string | undefined): string { sanitized = sanitized.replace(//g, ''); // Handle markdown links: - // 1. RapiDoc fragment links (#section/..., #operation/..., #tag/...) -> replace with just the text + // 1. OpenAPI fragment links (#section/..., #operation/..., #tag/...) -> replace with just the text // 2. Relative links with fragments (/path/#anchor) -> keep link but remove fragment sanitized = sanitized.replace( /\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => { - // Case 1: RapiDoc fragment links (starts with #section/, #operation/, #tag/) + // Case 1: OpenAPI fragment links (starts with #section/, #operation/, #tag/) if (url.match(/^#(section|operation|tag)\//)) { return text; // Just return the link text, no markdown link } diff --git a/assets/js/components/api-rapidoc.ts b/assets/js/components/api-rapidoc.ts deleted file mode 100644 index 8807c46c4..000000000 --- a/assets/js/components/api-rapidoc.ts +++ /dev/null @@ -1,259 +0,0 @@ -/** - * RapiDoc API Documentation Component - * - * Initializes the full RapiDoc renderer with theme synchronization. - * This is the component version of the inline JavaScript from rapidoc.html. - * - * Features: - * - Theme detection from Hugo's stylesheet toggle system - * - Automatic theme synchronization when user toggles dark/light mode - * - Shadow DOM manipulation to hide unwanted UI elements - * - CSS custom property injection for styling - * - * Usage: - *
- * - * The component expects a element to already exist in the container - * (created by Hugo template) or will wait for it to be added. - */ - -import { getPreference } from '../services/local-storage.js'; - -interface ComponentOptions { - component: HTMLElement; -} - -interface ThemeColors { - theme: 'light' | 'dark'; - bgColor: string; - textColor: string; - headerColor: string; - primaryColor: string; - navBgColor: string; - navTextColor: string; - navHoverBgColor: string; - navHoverTextColor: string; - navAccentColor: string; - codeTheme: string; -} - -type CleanupFn = () => void; - -/** - * Get current theme from localStorage (source of truth for Hugo theme system) - */ -function getTheme(): 'dark' | 'light' { - const theme = getPreference('theme'); - return theme === 'dark' ? 'dark' : 'light'; -} - -/** - * Get theme colors matching Hugo SCSS variables - */ -function getThemeColors(isDark: boolean): ThemeColors { - if (isDark) { - return { - theme: 'dark', - bgColor: '#14141F', // $grey10 ($article-bg in dark theme) - textColor: '#D4D7DD', // $g15-platinum - headerColor: '#D4D7DD', - primaryColor: '#a0a0ff', - navBgColor: '#1a1a2a', - navTextColor: '#D4D7DD', - navHoverBgColor: '#252535', - navHoverTextColor: '#ffffff', - navAccentColor: '#a0a0ff', - codeTheme: 'monokai', - }; - } - - return { - theme: 'light', - bgColor: '#ffffff', // $g20-white - textColor: '#2b2b2b', - headerColor: '#020a47', // $br-dark-blue - primaryColor: '#020a47', - navBgColor: '#f7f8fa', - navTextColor: '#2b2b2b', - navHoverBgColor: '#e8e8f0', - navHoverTextColor: '#020a47', - navAccentColor: '#020a47', - codeTheme: 'prism', - }; -} - -/** - * Apply theme to RapiDoc element - */ -function applyTheme(rapiDoc: HTMLElement): void { - const isDark = getTheme() === 'dark'; - const colors = getThemeColors(isDark); - - rapiDoc.setAttribute('theme', colors.theme); - rapiDoc.setAttribute('bg-color', colors.bgColor); - rapiDoc.setAttribute('text-color', colors.textColor); - rapiDoc.setAttribute('header-color', colors.headerColor); - rapiDoc.setAttribute('primary-color', colors.primaryColor); - rapiDoc.setAttribute('nav-bg-color', colors.navBgColor); - rapiDoc.setAttribute('nav-text-color', colors.navTextColor); - rapiDoc.setAttribute('nav-hover-bg-color', colors.navHoverBgColor); - rapiDoc.setAttribute('nav-hover-text-color', colors.navHoverTextColor); - rapiDoc.setAttribute('nav-accent-color', colors.navAccentColor); - rapiDoc.setAttribute('code-theme', colors.codeTheme); -} - -/** - * Set custom CSS properties on RapiDoc element - */ -function setInputBorderStyles(rapiDoc: HTMLElement): void { - rapiDoc.style.setProperty('--border-color', '#00A3FF'); -} - -/** - * Hide unwanted elements in RapiDoc shadow DOM - */ -function hideExpandCollapseControls(rapiDoc: HTMLElement): void { - const maxAttempts = 10; - let attempts = 0; - - const tryHide = (): void => { - attempts++; - - try { - const shadowRoot = rapiDoc.shadowRoot; - if (!shadowRoot) { - if (attempts < maxAttempts) { - setTimeout(tryHide, 500); - } - return; - } - - // Find elements containing "Expand all" / "Collapse all" and hide them - const allElements = shadowRoot.querySelectorAll('*'); - let hiddenCount = 0; - - allElements.forEach((element) => { - const text = element.textContent || ''; - - if (text.includes('Expand all') || text.includes('Collapse all')) { - (element as HTMLElement).style.display = 'none'; - if (element.parentElement) { - element.parentElement.style.display = 'none'; - } - hiddenCount++; - } - }); - - // Hide "Overview" headings - const headings = shadowRoot.querySelectorAll('h1, h2, h3, h4'); - headings.forEach((heading) => { - const text = (heading.textContent || '').trim(); - if (text.includes('Overview')) { - (heading as HTMLElement).style.display = 'none'; - hiddenCount++; - } - }); - - // Inject CSS as backup - const style = document.createElement('style'); - style.textContent = ` - .section-gap.section-tag, - [id*="overview"], - .regular-font.section-gap:empty, - h1:empty, h2:empty, h3:empty { - display: none !important; - } - `; - shadowRoot.appendChild(style); - - if (hiddenCount === 0 && attempts < maxAttempts) { - setTimeout(tryHide, 500); - } - } catch { - if (attempts < maxAttempts) { - setTimeout(tryHide, 500); - } - } - }; - - setTimeout(tryHide, 500); -} - -/** - * Watch for theme changes via stylesheet toggle - */ -function watchThemeChanges(rapiDoc: HTMLElement): CleanupFn { - const handleThemeChange = (): void => { - applyTheme(rapiDoc); - }; - - // Watch stylesheet disabled attribute changes (Hugo theme.js toggles this) - const observer = new MutationObserver((mutations) => { - for (const mutation of mutations) { - if ( - mutation.type === 'attributes' && - mutation.target instanceof HTMLLinkElement && - mutation.target.title?.includes('theme') - ) { - handleThemeChange(); - break; - } - // Also watch data-theme changes as fallback - if (mutation.attributeName === 'data-theme') { - handleThemeChange(); - } - } - }); - - // Observe head for stylesheet changes - observer.observe(document.head, { - attributes: true, - attributeFilter: ['disabled'], - subtree: true, - }); - - // Observe documentElement for data-theme changes - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ['data-theme'], - }); - - return (): void => { - observer.disconnect(); - }; -} - -/** - * Initialize RapiDoc component - */ -export default function ApiRapiDoc({ - component, -}: ComponentOptions): CleanupFn | void { - // Find the rapi-doc element inside the container - const rapiDoc = component.querySelector('rapi-doc') as HTMLElement | null; - - if (!rapiDoc) { - console.warn('[API RapiDoc] No rapi-doc element found in container'); - return; - } - - // Apply initial theme - applyTheme(rapiDoc); - - // Set custom CSS properties - if (customElements && customElements.whenDefined) { - customElements.whenDefined('rapi-doc').then(() => { - setInputBorderStyles(rapiDoc); - setTimeout(() => setInputBorderStyles(rapiDoc), 500); - }); - } else { - setInputBorderStyles(rapiDoc); - setTimeout(() => setInputBorderStyles(rapiDoc), 500); - } - - // Hide unwanted UI elements - hideExpandCollapseControls(rapiDoc); - - // Watch for theme changes - return watchThemeChanges(rapiDoc); -} diff --git a/assets/js/components/rapidoc-mini.ts b/assets/js/components/rapidoc-mini.ts deleted file mode 100644 index 4a79da30e..000000000 --- a/assets/js/components/rapidoc-mini.ts +++ /dev/null @@ -1,791 +0,0 @@ -/** - * RapiDoc Mini Component - * - * Initializes RapiDoc Mini web component for single API operation rendering. - * Features: - * - Dynamic CDN loading (memoized across instances) - * - Theme synchronization with Hugo theme system - * - Multiple instance support (no hardcoded IDs) - * - Cleanup function for proper teardown - * - * Usage: - *
- *
- */ - -import { getPreference } from '../services/local-storage.js'; - -interface ComponentOptions { - component: HTMLElement; -} - -interface ThemeConfig { - theme: 'light' | 'dark'; - bgColor: string; - textColor: string; - primaryColor: string; - navBgColor: string; - navTextColor: string; - navHoverBgColor: string; - navHoverTextColor: string; -} - -type CleanupFn = () => void; - -// Use full RapiDoc for proper auth tooltip behavior -// (mini version has limited features) -const RAPIDOC_CDN = 'https://unpkg.com/rapidoc/dist/rapidoc-min.js'; -const RAPIDOC_ELEMENT = 'rapi-doc'; - -// Memoization: track script loading state -let scriptLoadPromise: Promise | null = null; - -/** - * Load RapiDoc Mini script from CDN (memoized) - */ -function loadRapiDocScript(timeout = 10000): Promise { - // Return existing promise if already loading - if (scriptLoadPromise) { - return scriptLoadPromise; - } - - // Check if custom element already registered - if (customElements.get(RAPIDOC_ELEMENT)) { - return Promise.resolve(); - } - - scriptLoadPromise = new Promise((resolve, reject) => { - // Check if script tag already exists - const existing = Array.from(document.scripts).find( - (s) => s.src && s.src.includes('rapidoc') - ); - - if (existing && customElements.get(RAPIDOC_ELEMENT)) { - return resolve(); - } - - const script = document.createElement('script'); - script.type = 'module'; - script.src = RAPIDOC_CDN; - - script.onload = () => { - // Poll for custom element registration - const startTime = Date.now(); - const pollInterval = setInterval(() => { - if (customElements.get(RAPIDOC_ELEMENT)) { - clearInterval(pollInterval); - resolve(); - } else if (Date.now() - startTime > timeout) { - clearInterval(pollInterval); - reject(new Error('RapiDoc Mini custom element not registered')); - } - }, 50); - }; - - script.onerror = () => { - scriptLoadPromise = null; // Reset on error for retry - reject(new Error(`Failed to load RapiDoc Mini from ${RAPIDOC_CDN}`)); - }; - - document.head.appendChild(script); - }); - - return scriptLoadPromise; -} - -/** - * Get current theme from localStorage - */ -function getTheme(): 'dark' | 'light' { - const theme = getPreference('theme'); - return theme === 'dark' ? 'dark' : 'light'; -} - -/** - * Get theme configuration for RapiDoc Mini - * Colors matched to Hugo theme SCSS variables: - * - Dark: _theme-dark.scss - * - Light: _theme-light.scss - */ -function getThemeConfig(isDark: boolean): ThemeConfig { - return isDark - ? { - theme: 'dark', - bgColor: '#14141F', // $grey10 ($article-bg in dark theme) - textColor: '#D4D7DD', // $g15-platinum ($article-text in dark theme) - primaryColor: '#00A3FF', // $b-pool ($article-link) - navBgColor: '#07070E', // $grey5 ($body-bg in dark theme) - navTextColor: '#D4D7DD', // $g15-platinum ($nav-item) - navHoverBgColor: '#00A3FF', // $b-pool - navHoverTextColor: '#FFFFFF', // $g20-white - } - : { - theme: 'light', - bgColor: '#FFFFFF', // $g20-white ($article-bg in light theme) - textColor: '#020a47', // $br-dark-blue ($article-text in light theme) - primaryColor: '#00A3FF', // $b-pool ($article-link) - navBgColor: '#f3f4fb', // $body-bg in light theme - navTextColor: '#757888', // $g9-mountain ($nav-item) - navHoverBgColor: '#BF2FE5', // $br-magenta ($nav-item-hover) - navHoverTextColor: '#FFFFFF', // $g20-white - }; -} - -/** - * Apply theme attributes to RapiDoc Mini element - */ -function applyTheme(element: HTMLElement): void { - const isDark = getTheme() === 'dark'; - const config = getThemeConfig(isDark); - - // Core theme colors - element.setAttribute('theme', config.theme); - element.setAttribute('bg-color', config.bgColor); - element.setAttribute('text-color', config.textColor); - element.setAttribute('primary-color', config.primaryColor); - - // Navigation colors (for any internal nav elements) - element.setAttribute('nav-bg-color', config.navBgColor); - element.setAttribute('nav-text-color', config.navTextColor); - element.setAttribute('nav-hover-bg-color', config.navHoverBgColor); - element.setAttribute('nav-hover-text-color', config.navHoverTextColor); - - // Accent color - prevent green defaults - element.setAttribute('nav-accent-color', config.primaryColor); -} - -/** - * Build match pattern that identifies operations within a spec. - * - * When using path-specific specs (recommended), the spec contains only one path, - * so matchPaths is just the HTTP method (e.g., "post"). The path isolation at the - * file level prevents substring matching issues - no title needed. - * - * When using tag-based specs (fallback), matchPaths includes the full path - * (e.g., "post /api/v3/configure/token/admin"). Adding the title helps differentiate - * operations whose paths are prefixes of each other. - * - * RapiDoc's search string format: - * `${method} ${path} ${summary} ${description} ${operationId} ${tagName}`.toLowerCase() - * - * @param matchPaths - The match pattern: just method for path-specific specs, - * or "method /path" for tag-based specs - * @param title - Optional page title to append (only used for tag-based specs) - * @returns Pattern for RapiDoc's match-paths attribute - */ -function buildMatchPattern(matchPaths: string, title?: string): string { - // Detect path-specific spec mode: matchPaths is just an HTTP method (no path) - const isMethodOnly = /^(get|post|put|patch|delete|options|head|trace)$/i.test( - matchPaths.trim() - ); - - // For path-specific specs: use method only, title not needed (path isolated at file level) - // For tag-based specs: append title to differentiate prefix conflicts - if (title && !isMethodOnly) { - return `${matchPaths} ${title.toLowerCase()}`; - } - return matchPaths; -} - -/** - * Create RapiDoc Mini element with configuration - */ -function createRapiDocElement( - specUrl: string, - matchPaths?: string, - matchType?: string, - title?: string, - isTagPage?: boolean, - renderStyle?: string -): HTMLElement { - const element = document.createElement(RAPIDOC_ELEMENT); - - // Core attributes - element.setAttribute('spec-url', specUrl); - - // Set match-paths filter. With path-specific specs, this is just the method. - // With tag-based specs, includes path + optional title for uniqueness. - if (matchPaths) { - element.setAttribute('match-paths', buildMatchPattern(matchPaths, title)); - } - - // Set match-type for filtering mode ('includes' shows all methods matching path) - element.setAttribute('match-type', matchType || 'includes'); - - // Typography - match docs theme fonts - element.setAttribute( - 'regular-font', - 'Proxima Nova, -apple-system, BlinkMacSystemFont, sans-serif' - ); - element.setAttribute( - 'mono-font', - 'IBM Plex Mono, Monaco, Consolas, monospace' - ); - element.setAttribute('font-size', 'default'); // Match surrounding content size - - // Layout - use 'read' style for compact, single-operation display - // - // EXPERIMENTAL FINDINGS (Task 4 - API Security Schemes): - // ----------------------------------------------------- - // RapiDoc's `allow-authentication="true"` DOES NOT show auth input - // on operation pages when using `match-paths` to filter to a single - // operation. Here's what was tested: - // - // 1. render-style="read" + allow-authentication="true": - // - Auth section (#auth) exists in shadow DOM with input fields - // - BUT it's not visible (filtered out by match-paths) - // - Only shows the matched operation, not the full spec - // - Found: username/password inputs for Basic auth in shadow DOM - // - Result: NO visible auth UI for users - // - // 2. render-style="focused" + allow-authentication="true": - // - Auth section completely removed from shadow DOM - // - Shows links to #auth section that don't exist (broken links) - // - Lists security schemes but no input fields - // - Result: NO auth section at all - // - // CONCLUSION: - // RapiDoc's built-in authentication UI is incompatible with - // match-paths filtering. The auth section is either hidden or - // completely removed when filtering to single operations. - // For credential input on operation pages, we need a custom - // component (Task 5). - // - // Layout and render style for compact operation display - element.setAttribute('layout', 'column'); - element.setAttribute('render-style', renderStyle || 'read'); - element.setAttribute('show-header', 'false'); - element.setAttribute('allow-server-selection', 'false'); - - // Tag page specific settings - if (isTagPage) { - // Enable URL hash updates as user scrolls/clicks operations - element.setAttribute('update-route', 'true'); - - // If URL has a hash, navigate to that operation on load - // Hash format: #{method}-{path} (e.g., #post-/api/v3/configure/distinct_cache) - const hash = window.location.hash; - if (hash && hash.length > 1) { - const gotoPath = hash.substring(1); // Remove the '#' - element.setAttribute('goto-path', gotoPath); - } - } - - // Schema display - use 'table' style to reduce parameter indentation - element.setAttribute('schema-style', 'table'); - element.setAttribute('default-schema-tab', 'schema'); - element.setAttribute('paths-expanded', 'true'); - element.setAttribute('schema-expand-level', '1'); - - // Interactivity - element.setAttribute('allow-try', 'true'); - element.setAttribute('fill-request-fields-with-example', 'true'); - - // Reduce excessive spacing - element.setAttribute('use-path-in-nav-bar', 'false'); - element.setAttribute('show-info', 'false'); - - // Authentication display - disabled because RapiDoc's auth UI doesn't work - // with match-paths filtering. We show a separate auth info banner instead. - element.setAttribute('allow-authentication', 'false'); - element.setAttribute('show-components', 'false'); - - // Custom CSS for internal style overrides (table layout, etc.) - element.setAttribute('css-file', '/css/rapidoc-custom.css'); - - // Override method colors to use theme primary color instead of green - element.setAttribute('post-color', '#00A3FF'); // $b-pool instead of green - element.setAttribute('get-color', '#00A3FF'); - element.setAttribute('put-color', '#9394FF'); // $br-galaxy - element.setAttribute('delete-color', '#BF3D5E'); // $r-ruby - element.setAttribute('patch-color', '#9394FF'); - - // Apply initial theme - applyTheme(element); - - return element; -} - -/** - * Inject custom styles into RapiDoc's shadow DOM - * Removes the top border and reduces whitespace above operations - */ -function injectShadowStyles(element: HTMLElement): void { - const tryInject = (): boolean => { - const shadowRoot = (element as unknown as { shadowRoot: ShadowRoot | null }) - .shadowRoot; - if (!shadowRoot) return false; - - // Check if styles already injected - if (shadowRoot.querySelector('#rapidoc-custom-styles')) return true; - - const style = document.createElement('style'); - style.id = 'rapidoc-custom-styles'; - style.textContent = ` - /* Hide the operation divider line */ - .divider[part="operation-divider"] { - display: none !important; - } - - /* Reduce spacing above operation sections */ - .section-gap { - padding-top: 0 !important; - } - - /* Fix text cutoff - ensure content flows responsively */ - .req-res-title, - .resp-head, - .api-request, - .api-response, - .param-name, - .param-type, - .descr { - word-wrap: break-word; - overflow-wrap: break-word; - } - - /* Allow wrapping in tables and flex containers */ - table { - table-layout: auto !important; - width: 100% !important; - } - - td, th { - word-wrap: break-word; - overflow-wrap: break-word; - white-space: normal !important; - } - - /* Prevent horizontal overflow */ - .section-gap, - .expanded-req-resp-container { - max-width: 100%; - overflow-x: auto; - } - - /* Fix Copy button overlapping code content in Try It section */ - /* The Copy button is absolutely positioned, so add padding to prevent overlap */ - .curl-request pre, - .curl-request code, - .request-body-container pre, - .response-panel pre { - padding-right: 4.5rem !important; /* Space for Copy button */ - } - - /* Ensure Copy button stays visible and accessible */ - .copy-btn, - button[title="Copy"] { - z-index: 1; - background: rgba(0, 163, 255, 0.9) !important; - border-radius: 4px; - } - - /* Make code blocks wrap long URLs instead of requiring horizontal scroll */ - .curl-request code, - .request-body-container code { - white-space: pre-wrap !important; - word-break: break-all; - } - - /* ============================================ - RESPONSIVE STYLES FOR MOBILE (max-width: 600px) - ============================================ */ - @media (max-width: 600px) { - /* CRITICAL FIX: The button row container uses inline display:flex - and the hide-in-small-screen div has width:calc(100% - 60px) - which only leaves 60px for all buttons - not enough! - - Structure: - div[part="wrap-request-btn"] style="display:flex" - > div.hide-in-small-screen style="width:calc(100% - 60px)" <- PROBLEM - > button.m-btn (FILL EXAMPLE) - > button.m-btn (CLEAR) - > button.m-btn (TRY) - */ - - /* Target the outer button wrapper - make it wrap */ - [part="wrap-request-btn"] { - flex-wrap: wrap !important; - gap: 0.5rem !important; - } - - /* The hide-in-small-screen div steals width - override its calc() */ - .hide-in-small-screen { - width: 100% !important; - flex-basis: 100% !important; - margin-bottom: 0.5rem !important; - } - - /* Make buttons smaller on mobile and ensure they fit */ - button.m-btn, .m-btn { - padding: 0.35rem 0.5rem !important; - font-size: 0.7rem !important; - margin-right: 4px !important; - flex-shrink: 0 !important; - } - - /* Full width for nested elements */ - .request-body-container, - .response-panel, - .req-resp-container { - width: 100% !important; - max-width: 100% !important; - } - - /* Make tables scroll horizontally if needed */ - .table-wrapper, - table { - display: block !important; - overflow-x: auto !important; - -webkit-overflow-scrolling: touch; - } - - /* Tab navigation - scroll horizontally */ - .tab-buttons, - [role="tablist"] { - overflow-x: auto !important; - flex-wrap: nowrap !important; - -webkit-overflow-scrolling: touch; - } - } - `; - shadowRoot.appendChild(style); - - return true; - }; - - // Try immediately - if (tryInject()) return; - - // Retry a few times as shadow DOM may not be ready - let attempts = 0; - const maxAttempts = 10; - const interval = setInterval(() => { - attempts++; - if (tryInject() || attempts >= maxAttempts) { - clearInterval(interval); - } - }, 100); -} - -/** - * Apply responsive fixes directly to element styles via JavaScript - * This is more reliable than CSS for overriding inline styles - * - * IMPORTANT: RapiDoc has NESTED shadow DOMs: - * rapi-doc.shadowRoot > api-request.shadowRoot - * The buttons (FILL EXAMPLE, CLEAR, TRY) are in the api-request shadow root. - */ -function applyResponsiveFixes(element: HTMLElement): void { - /** - * Recursively collect all shadow roots (including nested ones) - */ - const getAllShadowRoots = (root: Document | ShadowRoot): ShadowRoot[] => { - const shadowRoots: ShadowRoot[] = []; - const elements = Array.from(root.querySelectorAll('*')); - for (const el of elements) { - const htmlEl = el as HTMLElement; - if (htmlEl.shadowRoot) { - shadowRoots.push(htmlEl.shadowRoot); - shadowRoots.push(...getAllShadowRoots(htmlEl.shadowRoot)); - } - } - return shadowRoots; - }; - - /** - * Apply fixes to a single shadow root - */ - const applyFixes = (shadowRoot: ShadowRoot): number => { - let fixCount = 0; - - // Find the button wrapper by its part attribute - const btnWrapper = shadowRoot.querySelector( - '[part="wrap-request-btn"]' - ) as HTMLElement; - - if (btnWrapper) { - btnWrapper.style.flexWrap = 'wrap'; - btnWrapper.style.gap = '0.5rem'; - fixCount++; - } - - // Find hide-in-small-screen divs and fix their width - const hideInSmall = shadowRoot.querySelectorAll( - '.hide-in-small-screen' - ) as NodeListOf; - hideInSmall.forEach((el) => { - el.style.width = '100%'; - el.style.flexBasis = '100%'; - el.style.marginBottom = '0.5rem'; - fixCount++; - }); - - // Make buttons smaller - const buttons = shadowRoot.querySelectorAll( - '.m-btn' - ) as NodeListOf; - buttons.forEach((btn) => { - btn.style.padding = '0.35rem 0.5rem'; - btn.style.fontSize = '0.7rem'; - btn.style.marginRight = '4px'; - }); - if (buttons.length > 0) { - fixCount += buttons.length; - } - - return fixCount; - }; - - /** - * Apply fixes to ALL shadow roots (including nested api-request) - * Returns the number of buttons found (used to determine if RapiDoc fully loaded) - */ - const applyToAllShadowRoots = (): number => { - const topShadowRoot = ( - element as unknown as { shadowRoot: ShadowRoot | null } - ).shadowRoot; - if (!topShadowRoot) { - return 0; - } - - // Get all shadow roots including nested ones (e.g., api-request inside rapi-doc) - const allShadowRoots = [topShadowRoot, ...getAllShadowRoots(topShadowRoot)]; - - let buttonCount = 0; - - for (const sr of allShadowRoots) { - applyFixes(sr); - buttonCount += sr.querySelectorAll('.m-btn').length; - } - - return buttonCount; - }; - - const tryApplyFixes = (): boolean => { - const isMobile = window.matchMedia('(max-width: 600px)').matches; - if (!isMobile) { - return true; // Not mobile, no fixes needed - } - - const topShadowRoot = ( - element as unknown as { shadowRoot: ShadowRoot | null } - ).shadowRoot; - if (!topShadowRoot) { - return false; // Shadow root not ready - } - - const buttonCount = applyToAllShadowRoots(); - - // Set up observer on top-level shadow root for future changes - const existingObserver = ( - topShadowRoot as unknown as { _mobileObserver?: MutationObserver } - )._mobileObserver; - if (!existingObserver) { - const observer = new MutationObserver(() => { - applyToAllShadowRoots(); - }); - observer.observe(topShadowRoot, { - childList: true, - subtree: true, - }); - ( - topShadowRoot as unknown as { _mobileObserver?: MutationObserver } - )._mobileObserver = observer; - } - - // Need at least 3 buttons (FILL EXAMPLE, CLEAR, TRY) to consider complete - return buttonCount >= 3; - }; - - // Always apply fixes regardless of viewport (let CSS handle visibility) - // This ensures fixes are ready when user resizes to mobile - const applyFixesUnconditionally = (): boolean => { - const topShadowRoot = ( - element as unknown as { shadowRoot: ShadowRoot | null } - ).shadowRoot; - if (!topShadowRoot) { - return false; // Shadow root not ready - } - - const buttonCount = applyToAllShadowRoots(); - - // Set up observer on top-level shadow root for future changes - const existingObserver = ( - topShadowRoot as unknown as { _mobileObserver?: MutationObserver } - )._mobileObserver; - if (!existingObserver) { - const observer = new MutationObserver(() => { - applyToAllShadowRoots(); - }); - observer.observe(topShadowRoot, { - childList: true, - subtree: true, - }); - ( - topShadowRoot as unknown as { _mobileObserver?: MutationObserver } - )._mobileObserver = observer; - } - - // Need at least 3 buttons (FILL EXAMPLE, CLEAR, TRY) to consider complete - return buttonCount >= 3; - }; - - // Try immediately - if (applyFixesUnconditionally()) return; - - // Retry with increasing delays (RapiDoc loads spec asynchronously) - // Need longer delays - RapiDoc fully renders after spec is loaded - const delays = [100, 300, 500, 1000, 1500, 2000, 3000, 5000]; - let attempt = 0; - - const retry = (): void => { - if (attempt >= delays.length) { - return; - } - setTimeout(() => { - if (!applyFixesUnconditionally()) { - attempt++; - retry(); - } - }, delays[attempt]); - }; - - retry(); - - // Also reapply on resize - window.addEventListener('resize', () => { - tryApplyFixes(); - }); -} - -/** - * Watch for theme changes and update RapiDoc element - */ -function watchThemeChanges(container: HTMLElement): CleanupFn { - let currentElement: HTMLElement | null = - container.querySelector(RAPIDOC_ELEMENT); - - const handleThemeChange = (): void => { - if (currentElement) { - applyTheme(currentElement); - } - }; - - // Watch stylesheet changes (Hugo theme.js enables/disables stylesheets) - const styleObserver = new MutationObserver((mutations) => { - for (const mutation of mutations) { - if ( - mutation.type === 'attributes' && - mutation.target instanceof HTMLLinkElement && - mutation.target.title?.includes('theme') - ) { - handleThemeChange(); - break; - } - } - }); - - const head = document.querySelector('head'); - if (head) { - styleObserver.observe(head, { - attributes: true, - attributeFilter: ['disabled'], - subtree: true, - }); - } - - // Watch localStorage changes from other tabs - const storageHandler = (event: StorageEvent): void => { - if (event.key === 'influxdata_docs_preferences' && event.newValue) { - try { - const prefs = JSON.parse(event.newValue); - if (prefs.theme) { - handleThemeChange(); - } - } catch (error) { - console.error('[RapiDoc Mini] Failed to parse preferences:', error); - } - } - }; - - window.addEventListener('storage', storageHandler); - - // Return cleanup function - return (): void => { - styleObserver.disconnect(); - window.removeEventListener('storage', storageHandler); - }; -} - -/** - * Show error message in container - */ -function showError(container: HTMLElement, message: string): void { - container.innerHTML = ` -
-

Error loading API documentation

-

${message}

-
- `; -} - -/** - * Initialize RapiDoc Mini component - */ -export default async function RapiDocMini({ - component, -}: ComponentOptions): Promise { - try { - // Get configuration from data attributes - const specUrl = component.dataset.specUrl; - const matchPaths = component.dataset.matchPaths; - const matchType = component.dataset.matchType; - const title = component.dataset.title; - const isTagPage = component.dataset.tagPage === 'true'; - const renderStyle = component.dataset.renderStyle; - - if (!specUrl) { - console.error('[RapiDoc Mini] No data-spec-url attribute provided'); - showError(component, 'No API specification configured.'); - return; - } - - // Load RapiDoc Mini from CDN (memoized) - try { - await loadRapiDocScript(); - } catch (error) { - console.error('[RapiDoc Mini] Failed to load from CDN:', error); - showError( - component, - 'Failed to load API viewer. Please refresh the page.' - ); - return; - } - - // Create and append RapiDoc Mini element - const rapiDocElement = createRapiDocElement( - specUrl, - matchPaths, - matchType, - title, - isTagPage, - renderStyle - ); - component.appendChild(rapiDocElement); - - // Inject custom styles into shadow DOM to remove borders/spacing - injectShadowStyles(rapiDocElement); - - // Apply responsive fixes for mobile (modifies inline styles directly) - applyResponsiveFixes(rapiDocElement); - - // Watch for theme changes and return cleanup function - return watchThemeChanges(component); - } catch (error) { - console.error('[RapiDoc Mini] Component initialization error:', error); - showError(component, 'API viewer failed to initialize.'); - } -} diff --git a/assets/js/main.js b/assets/js/main.js index 5cfb75098..e8cba67f3 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -47,9 +47,7 @@ import { SidebarToggle } from './sidebar-toggle.js'; import Theme from './theme.js'; import ThemeSwitch from './theme-switch.js'; import ApiAuthInput from './components/api-auth-input.ts'; -import ApiRapiDoc from './components/api-rapidoc.ts'; import ApiToc from './components/api-toc.ts'; -import RapiDocMini from './components/rapidoc-mini.ts'; /** * Component Registry @@ -82,9 +80,7 @@ const componentRegistry = { theme: Theme, 'theme-switch': ThemeSwitch, 'api-auth-input': ApiAuthInput, - 'api-rapidoc': ApiRapiDoc, 'api-toc': ApiToc, - 'rapidoc-mini': RapiDocMini, }; /** diff --git a/cypress/e2e/content/api-reference.cy.js b/cypress/e2e/content/api-reference.cy.js index 913c3afab..2c7564485 100644 --- a/cypress/e2e/content/api-reference.cy.js +++ b/cypress/e2e/content/api-reference.cy.js @@ -3,9 +3,10 @@ /** * API Reference Documentation E2E Tests * - * Tests both: - * 1. Legacy API reference pages (link validation, content structure) - * 2. New 3-column layout with tabs and TOC (for InfluxDB 3 Core/Enterprise) + * Tests: + * 1. API reference pages (link validation, content structure) + * 2. 3-column layout with TOC (for InfluxDB 3 Core/Enterprise) + * 3. Hugo-native tag page rendering * * Run with: * node cypress/support/run-e2e-specs.js --spec "cypress/e2e/content/api-reference.cy.js" content/influxdb3/core/reference/api/_index.md @@ -128,13 +129,12 @@ describe('API reference content', () => { /** * API Reference Layout Tests - * Tests layout and renderer for InfluxDB 3 Core/Enterprise API documentation + * Tests layout for InfluxDB 3 Core/Enterprise API documentation */ describe('API reference layout', () => { - // API tag pages have RapiDoc renderer const layoutSubjects = [ - '/influxdb3/core/api/v3/engine/', - '/influxdb3/enterprise/api/v3/engine/', + '/influxdb3/core/api/write-data/', + '/influxdb3/enterprise/api/write-data/', ]; layoutSubjects.forEach((subject) => { @@ -163,13 +163,26 @@ describe('API reference layout', () => { 'exist' ); }); + + it('displays TOC on page', () => { + cy.get('.api-toc').should('exist'); + }); }); - describe('API Renderer', () => { - it('loads API documentation renderer', () => { - cy.get( - '.api-reference-container, rapi-doc, .api-reference-wrapper' - ).should('exist'); + describe('Hugo-native renderer', () => { + it('renders API operations container', () => { + cy.get('.api-hugo-native, .api-operations-section').should('exist'); + }); + + it('renders operation elements', () => { + cy.get('.api-operation').should('have.length.at.least', 1); + }); + + it('operation has method badge and path', () => { + cy.get('.api-operation').first().within(() => { + cy.get('.api-method').should('exist'); + cy.get('.api-path').should('exist'); + }); }); }); }); @@ -177,18 +190,18 @@ describe('API reference layout', () => { }); /** - * RapiDoc Mini Component Tests - * Tests the RapiDoc Mini component behavior on path pages + * API Tag Page Tests + * Tests Hugo-native tag pages render operations correctly */ -describe('RapiDoc Mini component', () => { - // Path pages use RapiDoc Mini with match-type="includes" to show all methods - const pathPages = [ - '/influxdb3/core/api/v1/write/', - '/influxdb3/core/api/v3/write_lp/', +describe('API tag pages', () => { + const tagPages = [ + '/influxdb3/core/api/write-data/', + '/influxdb3/core/api/query-data/', + '/influxdb3/enterprise/api/write-data/', ]; - pathPages.forEach((page) => { - describe(`Path page ${page}`, () => { + tagPages.forEach((page) => { + describe(`Tag page ${page}`, () => { beforeEach(() => { cy.intercept('GET', '**', (req) => { req.continue((res) => { @@ -203,139 +216,33 @@ describe('RapiDoc Mini component', () => { cy.visit(page); }); - describe('Component initialization', () => { - it('renders rapidoc-mini component container', () => { - cy.get('[data-component="rapidoc-mini"]').should('exist'); - }); + it('displays page title', () => { + cy.get('h1').should('exist'); + }); - it('has data-spec-url attribute', () => { - cy.get('[data-component="rapidoc-mini"]') - .should('have.attr', 'data-spec-url') - .and('match', /\.ya?ml$/); - }); + it('renders operations section', () => { + cy.get('.api-operations, .api-operations-section').should('exist'); + }); - it('has data-match-paths attribute with API path', () => { - cy.get('[data-component="rapidoc-mini"]') - .should('have.attr', 'data-match-paths') - .and('match', /^\//); - }); - - it('has data-match-type attribute set to includes', () => { - cy.get('[data-component="rapidoc-mini"]').should( - 'have.attr', - 'data-match-type', - 'includes' - ); - }); - - it('includes machine-readable spec links', () => { - cy.get('link[rel="alternate"][type="application/x-yaml"]').should( - 'exist' - ); - cy.get('link[rel="alternate"][type="application/json"]').should( + it('operations have proper structure', () => { + cy.get('.api-operation').first().within(() => { + // Check for operation header with method and path + cy.get('.api-operation-header, .api-operation-endpoint').should( 'exist' ); + cy.get('.api-method').should('exist'); + cy.get('.api-path').should('exist'); }); }); - describe('RapiDoc element creation', () => { - it('creates rapi-doc-mini custom element', () => { - // Wait for component to initialize and create the element - cy.get('rapi-doc-mini', { timeout: 10000 }).should('exist'); - }); - - it('rapi-doc-mini has spec-url attribute', () => { - cy.get('rapi-doc-mini', { timeout: 10000 }) - .should('have.attr', 'spec-url') - .and('match', /\.ya?ml$/); - }); - - it('rapi-doc-mini has theme attributes', () => { - cy.get('rapi-doc-mini', { timeout: 10000 }).should( - 'have.attr', - 'theme' - ); - }); - }); - }); - }); - - describe('api-operation shortcode on example page', () => { - beforeEach(() => { - cy.intercept('GET', '**', (req) => { - req.continue((res) => { - if (res.headers['content-type']?.includes('text/html')) { - res.body = res.body.replace( - /data-user-analytics-fingerprint-enabled="true"/, - 'data-user-analytics-fingerprint-enabled="false"' - ); - } - }); - }); - cy.visit('/example/'); - }); - - describe('Multiple instances', () => { - it('renders multiple rapidoc-mini containers', () => { - cy.get('[data-component="rapidoc-mini"]').should( - 'have.length.at.least', - 2 - ); + it('TOC contains operation links', () => { + cy.get('.api-toc-nav').should('exist'); + cy.get('.api-toc-link').should('have.length.at.least', 1); }); - it('each instance has unique match-paths', () => { - cy.get('[data-component="rapidoc-mini"]').then(($containers) => { - const matchPaths = []; - $containers.each((_, el) => { - const path = el.getAttribute('data-match-paths'); - expect(matchPaths).to.not.include(path); - matchPaths.push(path); - }); - }); + it('TOC links have method badges', () => { + cy.get('.api-toc-link .api-method').should('have.length.at.least', 1); }); - - it('each instance creates its own rapi-doc-mini element', () => { - cy.get('rapi-doc-mini', { timeout: 10000 }).should( - 'have.length.at.least', - 2 - ); - }); - }); - }); - - describe('Theme synchronization', () => { - beforeEach(() => { - cy.intercept('GET', '**', (req) => { - req.continue((res) => { - if (res.headers['content-type']?.includes('text/html')) { - res.body = res.body.replace( - /data-user-analytics-fingerprint-enabled="true"/, - 'data-user-analytics-fingerprint-enabled="false"' - ); - } - }); - }); - cy.visit('/influxdb3/core/api/v1/write/'); - }); - - it('applies light theme by default', () => { - cy.get('rapi-doc-mini', { timeout: 10000 }) - .should('have.attr', 'theme') - .and('match', /light|dark/); - }); - - it('rapi-doc-mini has background color attribute', () => { - cy.get('rapi-doc-mini', { timeout: 10000 }).should( - 'have.attr', - 'bg-color' - ); - }); - - it('rapi-doc-mini has text color attribute', () => { - cy.get('rapi-doc-mini', { timeout: 10000 }).should( - 'have.attr', - 'text-color' - ); }); }); }); @@ -435,11 +342,11 @@ describe('All endpoints page', () => { ); }); - it('operation cards link to path pages with method anchors', () => { + it('operation cards link to tag pages with method anchors', () => { cy.get('.api-operation-card') .first() .should('have.attr', 'href') - .and('match', /\/api\/.*\/#(get|post|put|patch|delete)$/i); + .and('match', /\/api\/.*\/#(get|post|put|patch|delete)-/i); }); it('is accessible from navigation', () => { diff --git a/layouts/_default/api.html b/layouts/_default/api.html index 39e10cb2c..42987cb05 100644 --- a/layouts/_default/api.html +++ b/layouts/_default/api.html @@ -1,7 +1,7 @@ {{/* API Documentation Default Layout Fallback layout for API documentation pages. Delegates to appropriate templates based on page type: - Section pages: Use section.html logic (children listing) - Pages with staticFilePath: Use -RapiDoc renderer Note: This template exists as a catch-all but specific +Hugo-native renderer Note: This template exists as a catch-all but specific templates (api/section.html, api/list.html, api/single.html) should be preferred. */}} @@ -13,8 +13,7 @@ preferred. */}} {{ $version = index $pathParts 2 }} {{ end }} -{{/* Section pages without staticFilePath should render content -directly, not use RapiDoc */}} {{ if and .IsSection (not .Params.staticFilePath) +{{/* Section pages without staticFilePath render content directly */}} {{ if and .IsSection (not .Params.staticFilePath) }} {{ partial "header.html" . }} {{ partial "topnav.html" . }}
@@ -66,7 +65,7 @@ directly, not use RapiDoc */}} {{ if and .IsSection (not .Params.staticFilePath)
{{ partial "footer.html" . }} {{ else }} {{/* Pages with staticFilePath -(operation pages) use RapiDoc renderer */}} {{ partial "header.html" . }} {{ +(operation pages) use Hugo-native renderer */}} {{ partial "header.html" . }} {{ partial "topnav.html" . }}
diff --git a/layouts/api/list.html b/layouts/api/list.html index b12dbc965..9169e312d 100644 --- a/layouts/api/list.html +++ b/layouts/api/list.html @@ -1,7 +1,7 @@ {{/* API Documentation Layout Handles two cases: 1. Section index (no 'tag' param) - shows list of tag pages from article data 2. Tag pages (has 'tag' -param) - shows RapiDoc with all operations for the tag For conceptual pages -(isConceptual: true), shows content without operations. */}} +param) - shows all operations for the tag using Hugo-native templates. +For conceptual pages (isConceptual: true), shows content without operations. */}} {{/* Extract product and version from URL path for download buttons */}} {{/* Example: /influxdb3/clustered/api/ → ["", "influxdb3", "clustered", "api", ""] */}} @@ -68,9 +68,7 @@ param) - shows RapiDoc with all operations for the tag For conceptual pages }} {{ . | markdownify }} {{ end }} {{ end }} - {{ else }} {{/* Operational Page - Show RapiDoc with all operations */}} - {{/* Note: Description is shown in the RapiDoc info section from the - spec */}} + {{ else }} {{/* Operational Page - Show all operations */}} {{/* Download OpenAPI spec button - context-aware for Clustered/Cloud Dedicated */}} {{ with .Params.staticFilePath }} @@ -126,18 +124,10 @@ param) - shows RapiDoc with all operations for the tag For conceptual pages
{{ . }}
{{ end }} - {{/* Choose rendering mode: Hugo-native (POC) or RapiDoc (default) */}} - {{ $useHugoNative := .Params.useHugoNative | default false }} - + {{/* Render operations using Hugo-native templates */}} {{ with .Params.staticFilePath }}
- {{ if $useHugoNative }} - {{/* Hugo-Native rendering (docusaurus-openapi style) */}} - {{ partial "api/hugo-native/tag-renderer.html" $ }} - {{ else }} - {{/* RapiDoc rendering (current default) */}} - {{ partial "api/rapidoc-tag.html" $ }} - {{ end }} + {{ partial "api/tag-renderer.html" $ }}
{{ end }} {{ end }} {{ end }} {{ partial "article/related.html" . }} @@ -150,9 +140,9 @@ param) - shows RapiDoc with all operations for the tag For conceptual pages
diff --git a/layouts/partials/api/hugo-native/parameter-row.html b/layouts/partials/api/parameter-row.html similarity index 100% rename from layouts/partials/api/hugo-native/parameter-row.html rename to layouts/partials/api/parameter-row.html diff --git a/layouts/partials/api/hugo-native/parameters.html b/layouts/partials/api/parameters.html similarity index 89% rename from layouts/partials/api/hugo-native/parameters.html rename to layouts/partials/api/parameters.html index 43ad2efff..4285feafd 100644 --- a/layouts/partials/api/hugo-native/parameters.html +++ b/layouts/partials/api/parameters.html @@ -53,7 +53,7 @@
Path parameters
{{ range $pathParams }} - {{ partial "api/hugo-native/parameter-row.html" (dict "param" . "spec" $spec) }} + {{ partial "api/parameter-row.html" (dict "param" . "spec" $spec) }} {{ end }}
@@ -65,7 +65,7 @@
Query parameters
{{ range $queryParams }} - {{ partial "api/hugo-native/parameter-row.html" (dict "param" . "spec" $spec) }} + {{ partial "api/parameter-row.html" (dict "param" . "spec" $spec) }} {{ end }}
@@ -77,7 +77,7 @@
Header parameters
{{ range $headerParams }} - {{ partial "api/hugo-native/parameter-row.html" (dict "param" . "spec" $spec) }} + {{ partial "api/parameter-row.html" (dict "param" . "spec" $spec) }} {{ end }}
diff --git a/layouts/partials/api/rapidoc-mini.html b/layouts/partials/api/rapidoc-mini.html deleted file mode 100644 index 5e133ee0a..000000000 --- a/layouts/partials/api/rapidoc-mini.html +++ /dev/null @@ -1,213 +0,0 @@ -{{/* - RapiDoc Mini - Lightweight Single Operation Renderer - - Renders a single API operation using RapiDoc Mini web component. - Uses TypeScript component for initialization and theme management. - - Required page params: - - specFile: Path to the OpenAPI specification file - - matchPaths: Filter expression (e.g., "post /write" or "get /api/v3/query_sql") - - Optional page params: - - operationId: Operation ID for linking/navigation purposes - - apiPath: The API path (used to determine auth schemes) -*/}} - -{{ $specPath := .Params.specFile }} -{{ $specPathJSON := replace $specPath ".yaml" ".json" | replace ".yml" ".json" }} -{{ $matchPaths := .Params.matchPaths | default "" }} -{{ $operationId := .Params.operationId | default "" }} -{{ $title := .Title | default "" }} -{{ $apiPath := .Params.apiPath | default "" }} - -{{/* - Determine supported auth schemes based on API path: - - /api/v3/* endpoints: BearerAuthentication only - - /api/v2/* endpoints: BearerAuthentication + TokenAuthentication - - /write, /query (v1): All 4 schemes (Bearer, Token, Basic, Querystring) -*/}} -{{ $authSchemes := "bearer" }} -{{ if hasPrefix $apiPath "/api/v3" }} - {{ $authSchemes = "bearer" }} -{{ else if hasPrefix $apiPath "/api/v2" }} - {{ $authSchemes = "bearer,token" }} -{{ else if or (eq $apiPath "/write") (eq $apiPath "/query") }} - {{ $authSchemes = "bearer,token,basic,querystring" }} -{{ end }} - -{{/* Machine-readable links for AI agent discovery */}} -{{ if $specPath }} - - -{{ end }} - -{{/* Auth credentials modal */}} - - - -{{/* Authentication info banner with trigger for credentials modal */}} -
- - - - This endpoint requires authentication. - -
- -{{/* Component container - TypeScript handles initialization */}} -
- {{/* RapiDoc Mini element created by TypeScript component */}} -
- -{{/* Custom slot content if needed */}} -{{ with .Content }} -
- {{ . }} -
-{{ end }} - - diff --git a/layouts/partials/api/rapidoc-tag.html b/layouts/partials/api/rapidoc-tag.html deleted file mode 100644 index 3ece1f4ba..000000000 --- a/layouts/partials/api/rapidoc-tag.html +++ /dev/null @@ -1,170 +0,0 @@ -{{/* - RapiDoc Tag - Full Tag Operations Renderer - - Renders all API operations for a tag using RapiDoc. - Uses tag-specific spec file containing all operations. - Supports hash-based navigation to specific operations. - - Required page params: - - staticFilePath: Path to the tag-specific OpenAPI spec file - - Optional page params: - - operations: Array of operations for TOC generation -*/}} - -{{ $specPath := .Params.staticFilePath }} -{{ $specPathJSON := replace $specPath ".yaml" ".json" | replace ".yml" ".json" }} -{{ $operations := .Params.operations | default slice }} - -{{/* Determine auth schemes - tag pages may have mixed v1/v2/v3 endpoints */}} -{{/* Default to bearer, but could be enhanced to detect from operations */}} -{{ $authSchemes := "bearer" }} - -{{/* Machine-readable links for AI agent discovery */}} -{{ if $specPath }} - - -{{ end }} - -{{/* Auth credentials modal */}} - - - -{{/* Authentication info banner */}} -
- - - - These endpoints require authentication. - -
- -{{/* - RapiDoc component for tag - renders all operations in the spec. - No match-paths filter needed since tag spec contains only relevant operations. - - Key attributes: - - render-style="read": Linear document flow (no internal scrolling) - - update-route="true": Updates URL hash as user navigates (default) - - data-tag-page="true": Signals this is a tag page for JS initialization -*/}} -
- {{/* RapiDoc element created by TypeScript component */}} -
- - diff --git a/layouts/partials/api/rapidoc.html b/layouts/partials/api/rapidoc.html deleted file mode 100644 index dedb620ed..000000000 --- a/layouts/partials/api/rapidoc.html +++ /dev/null @@ -1,179 +0,0 @@ -{{/* - RapiDoc API Documentation Renderer - - Primary API documentation renderer using RapiDoc with "Mix your own HTML" slots. - See: https://rapidocweb.com/examples.html - - Required page params: - - staticFilePath: Path to the OpenAPI specification file - - Optional page params: - - operationId: Specific operation to display (renders only that operation) - - tag: Tag to filter operations by - - RapiDoc slots available for custom content: - - slot="header" - Custom header - - slot="footer" - Custom footer - - slot="overview" - Custom overview content - - slot="auth" - Custom authentication section - - slot="nav-logo" - Custom navigation logo -*/}} - -{{ $specPath := .Params.staticFilePath }} -{{ $specPathJSON := replace $specPath ".yaml" ".json" | replace ".yml" ".json" }} -{{ $operationId := .Params.operationId | default "" }} -{{ $tag := .Params.tag | default "" }} - -{{/* Machine-readable links for AI agent discovery */}} -{{ if $specPath }} - - -{{ end }} - -
- {{/* RapiDoc component with slot-based customization */}} - - {{/* Custom overview slot - Hugo page content */}} - {{ with .Content }} -
- {{ . }} -
- {{ end }} - - {{/* Custom examples from frontmatter */}} - {{ with .Params.examples }} -
-

Examples

- {{ range . }} -
-

{{ .title }}

- {{ with .description }}

{{ . | markdownify }}

{{ end }} -
{{ .code }}
-
- {{ end }} -
- {{ end }} -
-
- -{{/* Load RapiDoc from CDN */}} - - - diff --git a/layouts/partials/api/renderer.html b/layouts/partials/api/renderer.html index de3dfe938..d2b32f308 100644 --- a/layouts/partials/api/renderer.html +++ b/layouts/partials/api/renderer.html @@ -1,10 +1,12 @@ {{/* API Renderer - Renders API documentation using RapiDoc. + Renders API documentation using Hugo-native templates. + This partial is maintained for backward compatibility. Required page params: - staticFilePath: Path to the OpenAPI specification file + - operations: Array of operation metadata */}} -{{ partial "api/rapidoc.html" . }} +{{ partial "api/tag-renderer.html" . }} diff --git a/layouts/partials/api/hugo-native/request-body.html b/layouts/partials/api/request-body.html similarity index 94% rename from layouts/partials/api/hugo-native/request-body.html rename to layouts/partials/api/request-body.html index f90df93f1..9663c8d56 100644 --- a/layouts/partials/api/hugo-native/request-body.html +++ b/layouts/partials/api/request-body.html @@ -55,6 +55,6 @@ {{/* Render schema properties */}} {{ with $resolvedSchema }} - {{ partial "api/hugo-native/schema.html" (dict "schema" . "spec" $spec "level" 0) }} + {{ partial "api/schema.html" (dict "schema" . "spec" $spec "level" 0) }} {{ end }} diff --git a/layouts/partials/api/hugo-native/responses.html b/layouts/partials/api/responses.html similarity index 96% rename from layouts/partials/api/hugo-native/responses.html rename to layouts/partials/api/responses.html index 8f66a7cd8..3973a431d 100644 --- a/layouts/partials/api/hugo-native/responses.html +++ b/layouts/partials/api/responses.html @@ -69,7 +69,7 @@ {{ end }}
- {{ partial "api/hugo-native/schema.html" (dict "schema" $resolvedSchema "spec" $spec "level" 0) }} + {{ partial "api/schema.html" (dict "schema" $resolvedSchema "spec" $spec "level" 0) }}
{{ end }} {{ end }} diff --git a/layouts/partials/api/hugo-native/schema.html b/layouts/partials/api/schema.html similarity index 94% rename from layouts/partials/api/hugo-native/schema.html rename to layouts/partials/api/schema.html index 6d2f2c06d..e820c1c0c 100644 --- a/layouts/partials/api/hugo-native/schema.html +++ b/layouts/partials/api/schema.html @@ -93,12 +93,12 @@ {{/* Nested object/array rendering (limit depth to prevent infinite loops) */}} {{ if and (eq $propType "object") (lt $level 2) }} {{ with $propSchema.properties }} - {{ partial "api/hugo-native/schema.html" (dict "schema" $propSchema "spec" $spec "level" (add $level 1)) }} + {{ partial "api/schema.html" (dict "schema" $propSchema "spec" $spec "level" (add $level 1)) }} {{ end }} {{ else if and (eq $propType "array") (lt $level 2) }} {{ with $propSchema.items }} {{ if isset . "properties" }} - {{ partial "api/hugo-native/schema.html" (dict "schema" . "spec" $spec "level" (add $level 1)) }} + {{ partial "api/schema.html" (dict "schema" . "spec" $spec "level" (add $level 1)) }} {{ end }} {{ end }} {{ end }} diff --git a/layouts/partials/api/hugo-native/tag-renderer.html b/layouts/partials/api/tag-renderer.html similarity index 91% rename from layouts/partials/api/hugo-native/tag-renderer.html rename to layouts/partials/api/tag-renderer.html index ebd4a94e2..ad3f3d61e 100644 --- a/layouts/partials/api/hugo-native/tag-renderer.html +++ b/layouts/partials/api/tag-renderer.html @@ -1,7 +1,7 @@ {{/* - Hugo-Native Tag Page Renderer + Tag Page Renderer - Renders all operations for a tag page using Hugo templates instead of RapiDoc. + Renders all operations for a tag page using Hugo templates. Parses the OpenAPI spec file and renders each operation natively. Required page params: @@ -9,7 +9,7 @@ - operations: Array of operation metadata from frontmatter Usage: - {{ partial "api/hugo-native/tag-renderer.html" . }} + {{ partial "api/tag-renderer.html" . }} */}} {{ $page := . }} @@ -61,7 +61,7 @@ {{/* Operations List */}}
{{ range $operations }} - {{ partial "api/hugo-native/operation.html" (dict + {{ partial "api/operation.html" (dict "operation" . "spec" $spec "context" $page diff --git a/plans/2026-02-13-hugo-native-api-migration.md b/plans/2026-02-13-hugo-native-api-migration.md new file mode 100644 index 000000000..b781b92ec --- /dev/null +++ b/plans/2026-02-13-hugo-native-api-migration.md @@ -0,0 +1,319 @@ +# Hugo-Native API Reference Migration Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Complete migration to Hugo-native API reference rendering for all InfluxDB products, removing RapiDoc and simplifying the codebase. + +**Architecture:** The Hugo-native approach renders OpenAPI specs using Hugo templates instead of RapiDoc web components. This provides faster page loads, better SEO, consistent styling, and easier customization. Users access operations only through tag pages (no individual operation URLs). + +**Tech Stack:** TypeScript (generation scripts), Hugo templates, SCSS, OpenAPI 3.0 specs + +*** + +## Overview + +### Design Principles + +- **Consistency:** Unified look and feel across all API reference pages +- **Performance:** Fast page loads, full SEO indexability (no shadow DOM) +- **Simplicity:** No web components, no client-side rendering +- **Tag-based navigation:** Operations grouped by tag, accessed via tag pages only + +### URL Structure + +- **API index:** `/influxdb3/core/api/` +- **Tag page:** `/influxdb3/core/api/cache-distinct-values/` +- **All endpoints:** `/influxdb3/core/api/all-endpoints/` + +**Note:** Individual operation pages (e.g., `/influxdb3/core/api/v1/write/`) are being removed. Operations are accessed only through tag pages. + +*** + +## Migration Tasks + +### Task 1: Promote Hugo-native templates to default ✅ COMPLETED + +**Priority:** High | **Status:** Completed 2026-02-13 + +Move Hugo-native templates from POC location to production location. + +**Files moved:** + +- `layouts/partials/api/hugo-native/tag-renderer.html` → `layouts/partials/api/tag-renderer.html` +- `layouts/partials/api/hugo-native/operation.html` → `layouts/partials/api/operation.html` +- `layouts/partials/api/hugo-native/parameters.html` → `layouts/partials/api/parameters.html` +- `layouts/partials/api/hugo-native/parameter-row.html` → `layouts/partials/api/parameter-row.html` +- `layouts/partials/api/hugo-native/request-body.html` → `layouts/partials/api/request-body.html` +- `layouts/partials/api/hugo-native/schema.html` → `layouts/partials/api/schema.html` +- `layouts/partials/api/hugo-native/responses.html` → `layouts/partials/api/responses.html` + +**Completed steps:** + +1. ✅ Moved 7 files from `hugo-native/` subdirectory to parent directory +2. ✅ Updated `layouts/api/list.html` to use new locations (removed `hugo-native/` prefix) +3. ✅ Removed `$useHugoNative` conditional logic from `layouts/api/list.html` +4. ✅ Deleted `layouts/partials/api/hugo-native/` directory + +**Verification:** Hugo build passes, pages render correctly at `/influxdb3/core/api/` + +*** + +### Task 2: Remove RapiDoc templates and partials ✅ COMPLETED + +**Priority:** High | **Status:** Completed 2026-02-13 + +Delete RapiDoc-specific templates now that Hugo-native is the default. + +**Files deleted:** + +- `layouts/partials/api/rapidoc.html` +- `layouts/partials/api/rapidoc-tag.html` +- `layouts/partials/api/rapidoc-mini.html` + +**Verification:** `grep -r "rapidoc" layouts/` returns no results + +*** + +### Task 3: Remove RapiDoc JavaScript components ✅ COMPLETED + +**Priority:** High | **Status:** Completed 2026-02-13 + +Delete RapiDoc-specific TypeScript components. + +**Files deleted:** + +- `assets/js/components/api-rapidoc.ts` +- `assets/js/components/rapidoc-mini.ts` + +**Files updated:** + +- `assets/js/main.js` - Removed RapiDoc component imports and registrations + +**Verification:** `yarn build:ts` completes without errors + +*** + +### Task 4: Remove operation page generation ✅ COMPLETED + +**Priority:** High | **Status:** Completed 2026-02-13 + +Update generation scripts to remove dead code and RapiDoc references. + +**Files modified:** + +- `api-docs/scripts/generate-openapi-articles.ts` - Removed \~200 lines of dead `generatePathPages` function +- `api-docs/scripts/openapi-paths-to-hugo-data/index.ts` - Updated comments to remove RapiDoc references + +**Changes:** + +1. ✅ Removed dead `generatePathPages` function (operation page generation was already disabled) +2. ✅ Updated comments from "RapiDoc" to "Hugo-native templates" +3. ✅ Updated "RapiDoc fragment links" to "OpenAPI fragment links" + +**Note:** The `useHugoNative` flag was not found in the codebase - operation page generation was already disabled with a comment noting operations are rendered inline on tag pages. + +*** + +### Task 5: Update Cypress tests for Hugo-native ✅ COMPLETED + +**Priority:** High | **Status:** Completed 2026-02-13 + +Simplified Cypress tests now that we use standard HTML instead of shadow DOM. + +**Files modified:** + +- `cypress/e2e/content/api-reference.cy.js` - Rewrote test file + +**Changes:** + +1. ✅ Removed entire "RapiDoc Mini component" describe block (\~160 lines of shadow DOM tests) +2. ✅ Added "API tag pages" tests with Hugo-native selectors (`.api-operation`, `.api-method`, `.api-path`) +3. ✅ Added "API section page structure" tests +4. ✅ Added "All endpoints page" tests +5. ✅ Updated "API reference layout" tests to use Hugo-native selectors + +**New test structure implemented:** + +- `API reference content` - Tests API index pages load with valid links +- `API reference layout` - Tests 3-column layout (sidebar, content, TOC) +- `API tag pages` - Tests operation rendering, method badges, TOC links +- `API section page structure` - Tests tag listing on section pages +- `All endpoints page` - Tests operation cards with links to tag pages + +*** + +### Task 6: Clean up styles + +**Priority:** Medium + +Remove RapiDoc-specific styles and consolidate Hugo-native styles. + +**Files to review:** + +- `assets/styles/layouts/_api-layout.scss` +- `assets/styles/layouts/_api-overrides.scss` +- `assets/styles/layouts/_api-hugo-native.scss` + +**Changes:** + +1. Remove RapiDoc-specific CSS variables and selectors +2. Merge `_api-hugo-native.scss` into `_api-layout.scss` +3. Remove `_api-overrides.scss` if only contains RapiDoc overrides +4. Update SCSS imports in the main stylesheet + +*** + +### Task 7: Fix Generation Script for Clean Regeneration + +**Priority:** Medium + +Update the generation script to properly clean directories before regenerating. + +**Files to modify:** + +- `api-docs/scripts/generate-openapi-articles.ts` +- `api-docs/scripts/openapi-paths-to-hugo-data/index.ts` + +**Requirements:** + +1. Add `--clean` flag to remove target directories before generating +2. Track expected output files and warn about stale entries +3. Clear Hugo resource cache when structural changes detected + +*** + +### Task 8: Apply Cache Data tag split to InfluxDB 3 Enterprise + +**Priority:** Medium + +Apply the same tag split done for Core. + +**Files to modify:** + +- `api-docs/influxdb3/enterprise/v3/ref.yml` + +**Changes:** + +1. Replace "Cache data" tag with "Cache distinct values" and "Cache last value" tags +2. Update operation tag references +3. Update x-tagGroups references +4. Regenerate: `sh api-docs/generate-api-docs.sh` + +*** + +### Task 9: Migrate remaining products to Hugo-native + +**Priority:** Medium + +After the infrastructure is in place, migrate remaining products. + +**Products:** + +- [ ] cloud-dedicated (management API) +- [ ] cloud-serverless +- [ ] clustered (management API) +- [ ] cloud-v2 +- [ ] oss-v2 +- [ ] oss-v1 + +**For each product:** + +1. Review tag structure in OpenAPI spec +2. Add `x-influxdata-related` links where appropriate +3. Clean and regenerate +4. Verify all tag pages render correctly + +*** + +## Key Files Reference + +**Hugo-Native Templates (after migration):** + +- `layouts/partials/api/tag-renderer.html` - Main tag page renderer +- `layouts/partials/api/operation.html` - Individual operation renderer +- `layouts/partials/api/parameters.html` - Parameters section +- `layouts/partials/api/parameter-row.html` - Single parameter row +- `layouts/partials/api/request-body.html` - Request body section +- `layouts/partials/api/schema.html` - JSON schema renderer +- `layouts/partials/api/responses.html` - Response section + +**Layouts:** + +- `layouts/api/list.html` - Tag page layout (Hugo-native only) +- `layouts/api/section.html` - API section page layout +- `layouts/api/all-endpoints.html` - All endpoints page layout + +**Styles:** + +- `assets/styles/layouts/_api-layout.scss` - Consolidated API styles + +**Generation:** + +- `api-docs/scripts/generate-openapi-articles.ts` - Main generation script +- `api-docs/scripts/openapi-paths-to-hugo-data/index.ts` - OpenAPI processing + +*** + +## Verification Checklist + +Before considering migration complete for each product: + +- [ ] All tag pages render without errors +- [ ] Operation details (parameters, request body, responses) display correctly +- [ ] Schema references resolve and render +- [ ] `x-influxdata-related` links appear at page bottom +- [ ] Navigation shows correct tag structure +- [ ] Mobile responsive layout works +- [ ] No console errors in browser DevTools +- [ ] "On this page" TOC links work correctly +- [ ] Cypress tests pass +- [ ] No RapiDoc references remain in codebase + +## Files to Delete (Summary) + +**Already deleted (Tasks 1-3):** + +- ✅ `layouts/partials/api/rapidoc.html` +- ✅ `layouts/partials/api/rapidoc-tag.html` +- ✅ `layouts/partials/api/rapidoc-mini.html` +- ✅ `layouts/partials/api/hugo-native/` (entire directory - 7 files moved to parent) +- ✅ `assets/js/components/api-rapidoc.ts` +- ✅ `assets/js/components/rapidoc-mini.ts` + +**Still to review (Task 6):** + +- `assets/styles/layouts/_api-overrides.scss` (if RapiDoc-only) + +*** + +## Migration Findings + +### Completed Work Summary (Tasks 1-5) + +**Infrastructure changes:** + +- Hugo-native templates are now the default (no feature flag required) +- All RapiDoc code removed from layouts and JavaScript +- Generation scripts cleaned up (\~200 lines of dead code removed) +- Cypress tests simplified (no more shadow DOM piercing) + +**Key discoveries:** + +1. The `useHugoNative` flag did not exist in the codebase - operation page generation was already disabled +2. The `generatePathPages` function was dead code that could be safely removed +3. RapiDoc Mini tests were \~160 lines that are no longer needed +4. Hugo build and TypeScript compilation both pass after all changes + +**Verification status:** + +- ✅ Hugo build: `npx hugo --quiet` passes +- ✅ TypeScript: `yarn build:ts` passes +- ⏳ Cypress tests: Need to run `yarn test:e2e` to verify new tests pass +- ⏳ Visual review: Need to check pages render correctly in browser + +### Remaining Work (Tasks 6-9) + +1. **Task 6 (styles)**: Review and consolidate SCSS files +2. **Task 7 (clean regeneration)**: Add `--clean` flag to generation scripts +3. **Task 8 (Enterprise tags)**: Split Cache Data tag in Enterprise spec +4. **Task 9 (product migration)**: Apply to remaining 6 products diff --git a/plans/TESTING.md b/plans/TESTING.md new file mode 100644 index 000000000..7b2888650 --- /dev/null +++ b/plans/TESTING.md @@ -0,0 +1,77 @@ +# API Reference Testing Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Validate Hugo-native API reference pages render correctly and all tests pass. + +**Architecture:** Hugo-native rendering uses standard HTML without shadow DOM, making tests simpler. No RapiDoc web components - operations are rendered server-side by Hugo templates. + +**Tech Stack:** Hugo templates, SCSS, Cypress + +*** + +## Test Structure + +The API reference tests validate: + +1. **API index pages** - Main API landing pages load correctly +2. **API tag pages** - Tag pages render operations with parameters/responses +3. **Section structure** - Section pages list tag children correctly +4. **All endpoints** - All endpoints page shows all operations +5. **Layout** - 3-column layout with sidebar, content, and TOC + +## Running Tests + +### Quick validation + +```bash +# Build site +yarn hugo --quiet + +# Start server +yarn hugo server & + +# Test pages load +curl -s -o /dev/null -w "%{http_code}" http://localhost:1313/influxdb3/core/api/ +# Expected: 200 + +# Run Cypress tests (for example, for InfluxDB 3 Core) +node cypress/support/run-e2e-specs.js --spec "cypress/e2e/content/api-reference.cy.js" content/influxdb3/core/api/_index.md + +# Stop server +pkill -f "hugo server" +``` + +### Full test suite + +```bash +node cypress/support/run-e2e-specs.js --spec "cypress/e2e/content/api-reference.cy.js" +``` + +## Test Selectors (Hugo-Native) + +Since Hugo-native uses standard HTML, tests use simple CSS selectors: + +| Element | Selector | +| ---------------- | ------------------- | +| Page title | `h1` | +| Operation | `.api-operation` | +| Method badge | `.api-method` | +| Path | `.api-path` | +| Parameters table | `.api-parameters` | +| Request body | `.api-request-body` | +| Responses | `.api-responses` | +| TOC | `.api-toc` | +| Related links | `.article--related` | + +## Expected Test Coverage + +- [ ] API index pages (Core, Enterprise, Cloud Dedicated, Clustered, Cloud Serverless) +- [ ] Tag pages render operations +- [ ] Parameters display correctly +- [ ] Request body sections display +- [ ] Response sections display +- [ ] TOC links work +- [ ] All endpoints page lists operations +- [ ] Section pages list tags +- [ ] Links are valid