refactor(api): complete Hugo-native migration, remove RapiDoc
Complete Tasks 1-5 of the Hugo-native API migration: - Promote Hugo-native templates from POC to production location - Remove RapiDoc templates (rapidoc.html, rapidoc-tag.html, rapidoc-mini.html) - Remove RapiDoc JS components (api-rapidoc.ts, rapidoc-mini.ts) - Clean up generation scripts (~200 lines of dead code removed) - Rewrite Cypress tests for standard HTML (no shadow DOM) Templates moved from layouts/partials/api/hugo-native/ to parent. All verifications pass (Hugo build, TypeScript compilation).claude/api-code-samples-plan-MEkQO
parent
3c580d33dd
commit
2b3ed56218
|
|
@ -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<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<string, unknown> = {
|
||||
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.
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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<string>();
|
||||
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: <!-- ReDoc-Inject: ... --> (removes entirely)
|
||||
*
|
||||
|
|
@ -1022,12 +1022,12 @@ function sanitizeDescription(description: string | undefined): string {
|
|||
sanitized = sanitized.replace(/<!--\s*ReDoc-Inject:.*?-->/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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
* <div data-component="api-rapidoc" data-spec-url="/path/to/spec.yml"></div>
|
||||
*
|
||||
* The component expects a <rapi-doc> 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);
|
||||
}
|
||||
|
|
@ -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:
|
||||
* <div data-component="rapidoc-mini"
|
||||
* data-spec-url="/path/to/spec.yml"
|
||||
* data-match-paths="post /write">
|
||||
* </div>
|
||||
*/
|
||||
|
||||
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<void> | null = null;
|
||||
|
||||
/**
|
||||
* Load RapiDoc Mini script from CDN (memoized)
|
||||
*/
|
||||
function loadRapiDocScript(timeout = 10000): Promise<void> {
|
||||
// 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<HTMLElement>;
|
||||
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<HTMLElement>;
|
||||
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 = `
|
||||
<div class="api-operation-error">
|
||||
<p><strong>Error loading API documentation</strong></p>
|
||||
<p>${message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize RapiDoc Mini component
|
||||
*/
|
||||
export default async function RapiDocMini({
|
||||
component,
|
||||
}: ComponentOptions): Promise<CleanupFn | void> {
|
||||
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.');
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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" . }}
|
||||
|
||||
<div class="page-wrapper">
|
||||
|
|
@ -66,7 +65,7 @@ directly, not use RapiDoc */}} {{ if and .IsSection (not .Params.staticFilePath)
|
|||
</div>
|
||||
|
||||
{{ 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" . }}
|
||||
|
||||
<div class="page-wrapper">
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
</section>
|
||||
|
||||
{{ 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
|
|||
<section class="api-content-body">{{ . }}</section>
|
||||
{{ end }}
|
||||
|
||||
{{/* Choose rendering mode: Hugo-native (POC) or RapiDoc (default) */}}
|
||||
{{ $useHugoNative := .Params.useHugoNative | default false }}
|
||||
|
||||
{{/* Render operations using Hugo-native templates */}}
|
||||
{{ with .Params.staticFilePath }}
|
||||
<section class="api-operations-section">
|
||||
{{ 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" $ }}
|
||||
</section>
|
||||
{{ end }} {{ end }} {{ end }} {{ partial "article/related.html" . }}
|
||||
</article>
|
||||
|
|
@ -150,9 +140,9 @@ param) - shows RapiDoc with all operations for the tag For conceptual pages
|
|||
<aside class="api-toc" data-component="api-toc">
|
||||
<h4 class="api-toc-header">ON THIS PAGE</h4>
|
||||
<nav class="api-toc-nav">
|
||||
{{/* Operation links - use RapiDoc anchor format: {method}-{path} Use
|
||||
safeURL to prevent URL-encoding of slashes Show method badge +
|
||||
human-readable summary (not path) */}} {{ range $operations }} {{
|
||||
{{/* Operation links - use anchor format: {method}-{path}
|
||||
Use safeURL to prevent URL-encoding of slashes
|
||||
Show method badge + human-readable summary (not path) */}} {{ range $operations }} {{
|
||||
$anchor := printf "#%s-%s" (lower .method) .path }}
|
||||
<a
|
||||
href="{{ $anchor | safeURL }}"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{{/* API Documentation Section Layout Used for API section index pages (e.g.,
|
||||
/influxdb3/core/api/). Shows page content with children listing instead of
|
||||
RapiDoc renderer. For tag pages (with 'tag' param), Hugo uses list.html instead.
|
||||
/influxdb3/core/api/). Shows page content with children listing.
|
||||
For tag pages (with 'tag' param), Hugo uses list.html instead.
|
||||
*/}}
|
||||
|
||||
{{/* Extract product and version from URL path for download buttons */}}
|
||||
|
|
|
|||
|
|
@ -2,20 +2,14 @@
|
|||
API Documentation Single Page Layout
|
||||
|
||||
Used for:
|
||||
- Individual operation pages (nested under tag pages)
|
||||
- Conceptual pages (isConceptual: true) like Authentication, Quick start
|
||||
|
||||
For operation pages:
|
||||
- Renders RapiDoc with "Mix your own HTML" slots for custom content
|
||||
- Shows method badge, path, and operation details
|
||||
- Individual operation pages (legacy - being phased out)
|
||||
|
||||
For conceptual pages:
|
||||
- Shows Hugo content or tagDescription markdown
|
||||
|
||||
Required frontmatter:
|
||||
- title: Page title
|
||||
- staticFilePath: Path to OpenAPI spec (for operation pages)
|
||||
- operationId: Operation ID to render (for operation pages)
|
||||
- isConceptual: true (for conceptual pages)
|
||||
*/}}
|
||||
|
||||
|
|
@ -128,7 +122,9 @@
|
|||
{{ partial "api/security-schemes.html" . }}
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
{{/* Operation Page - RapiDoc with custom slots */}}
|
||||
{{/* Operation Page - Hugo-native rendering */}}
|
||||
{{/* Note: Individual operation pages are being phased out. */}}
|
||||
{{/* Operations are now accessed via tag pages only. */}}
|
||||
|
||||
{{/* Hugo page content shown as overview */}}
|
||||
{{ with .Content }}
|
||||
|
|
@ -137,13 +133,6 @@
|
|||
</section>
|
||||
{{ end }}
|
||||
|
||||
{{/* RapiDoc renderer with slot-based customization */}}
|
||||
{{ with .Params.staticFilePath }}
|
||||
<section class="api-renderer-section">
|
||||
{{ partial "api/rapidoc.html" $ }}
|
||||
</section>
|
||||
{{ end }}
|
||||
|
||||
{{ end }}
|
||||
|
||||
{{/* Related documentation links */}}
|
||||
|
|
|
|||
|
|
@ -46,16 +46,16 @@
|
|||
{{/* Parameters Section */}}
|
||||
{{ $params := $opDef.parameters | default slice }}
|
||||
{{ if gt (len $params) 0 }}
|
||||
{{ partial "api/hugo-native/parameters.html" (dict "parameters" $params "spec" $spec) }}
|
||||
{{ partial "api/parameters.html" (dict "parameters" $params "spec" $spec) }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Request Body Section */}}
|
||||
{{ with $opDef.requestBody }}
|
||||
{{ partial "api/hugo-native/request-body.html" (dict "requestBody" . "spec" $spec) }}
|
||||
{{ partial "api/request-body.html" (dict "requestBody" . "spec" $spec) }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Responses Section */}}
|
||||
{{ with $opDef.responses }}
|
||||
{{ partial "api/hugo-native/responses.html" (dict "responses" . "spec" $spec) }}
|
||||
{{ partial "api/responses.html" (dict "responses" . "spec" $spec) }}
|
||||
{{ end }}
|
||||
</div>
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
<h5 class="api-param-group-title">Path parameters</h5>
|
||||
<div class="api-param-list">
|
||||
{{ range $pathParams }}
|
||||
{{ partial "api/hugo-native/parameter-row.html" (dict "param" . "spec" $spec) }}
|
||||
{{ partial "api/parameter-row.html" (dict "param" . "spec" $spec) }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
<h5 class="api-param-group-title">Query parameters</h5>
|
||||
<div class="api-param-list">
|
||||
{{ range $queryParams }}
|
||||
{{ partial "api/hugo-native/parameter-row.html" (dict "param" . "spec" $spec) }}
|
||||
{{ partial "api/parameter-row.html" (dict "param" . "spec" $spec) }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
<h5 class="api-param-group-title">Header parameters</h5>
|
||||
<div class="api-param-list">
|
||||
{{ range $headerParams }}
|
||||
{{ partial "api/hugo-native/parameter-row.html" (dict "param" . "spec" $spec) }}
|
||||
{{ partial "api/parameter-row.html" (dict "param" . "spec" $spec) }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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 }}
|
||||
<link rel="alternate" type="application/x-yaml" href="{{ $specPath | absURL }}" title="OpenAPI Specification (YAML)" />
|
||||
<link rel="alternate" type="application/json" href="{{ $specPathJSON | absURL }}" title="OpenAPI Specification (JSON)" />
|
||||
{{ end }}
|
||||
|
||||
{{/* Auth credentials modal */}}
|
||||
<div class="api-auth-backdrop" hidden></div>
|
||||
<div class="api-auth-popover" role="dialog" aria-label="API credentials" hidden>
|
||||
{{/* Content rendered by TypeScript component */}}
|
||||
</div>
|
||||
|
||||
{{/* Authentication info banner with trigger for credentials modal */}}
|
||||
<div class="api-auth-info"
|
||||
data-component="api-auth-input"
|
||||
data-schemes="{{ $authSchemes }}">
|
||||
<svg class="api-auth-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
||||
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>
|
||||
</svg>
|
||||
<span class="api-auth-status">This endpoint requires authentication.</span>
|
||||
<button type="button" class="api-auth-trigger">Set credentials</button>
|
||||
</div>
|
||||
|
||||
{{/* Component container - TypeScript handles initialization */}}
|
||||
<div class="api-reference-wrapper api-reference-mini"
|
||||
data-component="rapidoc-mini"
|
||||
data-spec-url="{{ $specPath }}"
|
||||
{{ with $matchPaths }}data-match-paths="{{ . }}"{{ end }}
|
||||
{{ with $operationId }}data-operation-id="{{ . }}"{{ end }}
|
||||
{{ with $title }}data-title="{{ . }}"{{ end }}>
|
||||
{{/* RapiDoc Mini element created by TypeScript component */}}
|
||||
</div>
|
||||
|
||||
{{/* Custom slot content if needed */}}
|
||||
{{ with .Content }}
|
||||
<div class="api-overview-content">
|
||||
{{ . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<style>
|
||||
/* Screen reader only - visually hidden but accessible */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.api-reference-mini {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
position: relative; /* Contains absolutely positioned auth elements */
|
||||
}
|
||||
|
||||
rapi-doc-mini,
|
||||
rapi-doc {
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
display: block;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
|
||||
/* Override RapiDoc internal CSS variables */
|
||||
--light-border-color: rgba(0, 163, 255, 0.25);
|
||||
--border-color: rgba(0, 163, 255, 0.25);
|
||||
--font-size-regular: 16px;
|
||||
|
||||
/* Use green for links/status text so they don't look like clickable links */
|
||||
/* HTTP method colors are set via element attributes, not these variables */
|
||||
--blue: #34BB55; /* $gr-rainforest - green for status/links */
|
||||
--green: #34BB55; /* $gr-rainforest - POST */
|
||||
--orange: #FFB94A; /* $y-pineapple - PUT (distinct from red) */
|
||||
--red: #F95F53; /* $r-curacao - DELETE */
|
||||
--purple: #9b2aff; /* $br-new-purple - PATCH */
|
||||
}
|
||||
|
||||
/* Dark mode overrides */
|
||||
[data-theme="dark"] rapi-doc-mini,
|
||||
[data-theme="dark"] rapi-doc,
|
||||
html:has(link[title="dark-theme"]:not([disabled])) rapi-doc-mini,
|
||||
html:has(link[title="dark-theme"]:not([disabled])) rapi-doc {
|
||||
/* Use green for links/status text in dark mode */
|
||||
--blue: #009F5F; /* $gr-viridian - green for status/links */
|
||||
--green: #009F5F; /* $gr-viridian - POST */
|
||||
--orange: #FFC800; /* $y-thunder - PUT (brighter for dark mode) */
|
||||
--red: #DC4E58; /* $r-fire - DELETE */
|
||||
--purple: #8E1FC3; /* $p-amethyst - PATCH */
|
||||
}
|
||||
|
||||
/* Hide tag section on operation pages - only show on tag landing pages */
|
||||
rapi-doc::part(section-tag) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Responsive layout - allow content to flow naturally */
|
||||
.api-reference-mini {
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Authentication info banner */
|
||||
.api-auth-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
background: #F0F4FF;
|
||||
border: 1px solid rgba(0, 163, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
color: #545667;
|
||||
}
|
||||
|
||||
.api-auth-info .api-auth-icon {
|
||||
flex-shrink: 0;
|
||||
color: #00A3FF;
|
||||
}
|
||||
|
||||
.api-auth-info .api-auth-status {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.api-auth-info .api-auth-trigger {
|
||||
background: none;
|
||||
border: 1px solid #00A3FF;
|
||||
color: #00A3FF;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.api-auth-info .api-auth-trigger:hover {
|
||||
background: #00A3FF;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
[data-theme="dark"] .api-auth-info,
|
||||
html:has(link[title="dark-theme"]:not([disabled])) .api-auth-info {
|
||||
background: #1a1a2e;
|
||||
border-color: rgba(0, 163, 255, 0.2);
|
||||
color: #D4D7DD;
|
||||
}
|
||||
|
||||
|
||||
/* Error state styling */
|
||||
.api-operation-error {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: #BF3D5E; /* $r-ruby */
|
||||
background: #FFF7F4; /* $r-flan */
|
||||
border: 1px solid #C6CAD3;
|
||||
border-radius: 4px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.api-operation-error p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .api-operation-error,
|
||||
html:has(link[title="dark-theme"]:not([disabled])) .api-operation-error {
|
||||
background: #2F1F29; /* $r-basalt */
|
||||
color: #FFB6A0; /* $r-tungsten */
|
||||
border-color: #333346;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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 }}
|
||||
<link rel="alternate" type="application/x-yaml" href="{{ $specPath | absURL }}" title="OpenAPI Specification (YAML)" />
|
||||
<link rel="alternate" type="application/json" href="{{ $specPathJSON | absURL }}" title="OpenAPI Specification (JSON)" />
|
||||
{{ end }}
|
||||
|
||||
{{/* Auth credentials modal */}}
|
||||
<div class="api-auth-backdrop" hidden></div>
|
||||
<div class="api-auth-popover" role="dialog" aria-label="API credentials" hidden>
|
||||
{{/* Content rendered by TypeScript component */}}
|
||||
</div>
|
||||
|
||||
{{/* Authentication info banner */}}
|
||||
<div class="api-auth-info"
|
||||
data-component="api-auth-input"
|
||||
data-schemes="{{ $authSchemes }}">
|
||||
<svg class="api-auth-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
||||
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>
|
||||
</svg>
|
||||
<span class="api-auth-status">These endpoints require authentication.</span>
|
||||
<button type="button" class="api-auth-trigger">Set credentials</button>
|
||||
</div>
|
||||
|
||||
{{/*
|
||||
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
|
||||
*/}}
|
||||
<div class="api-reference-wrapper api-reference-tag"
|
||||
data-component="rapidoc-mini"
|
||||
data-spec-url="{{ $specPath }}"
|
||||
data-tag-page="true"
|
||||
data-render-style="read">
|
||||
{{/* RapiDoc element created by TypeScript component */}}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.api-reference-tag {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Ensure RapiDoc expands to fit content - no internal scrolling */
|
||||
.api-reference-tag rapi-doc-mini,
|
||||
.api-reference-tag rapi-doc {
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
display: block;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
|
||||
/* No fixed height - content flows naturally */
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
|
||||
/* CSS variables for theming */
|
||||
--light-border-color: rgba(0, 163, 255, 0.25);
|
||||
--border-color: rgba(0, 163, 255, 0.25);
|
||||
--font-size-regular: 16px;
|
||||
|
||||
/* Method colors */
|
||||
--blue: #34BB55; /* $gr-rainforest - green for status/links */
|
||||
--green: #34BB55; /* $gr-rainforest - POST */
|
||||
--orange: #FFB94A; /* $y-pineapple - PUT */
|
||||
--red: #F95F53; /* $r-curacao - DELETE */
|
||||
--purple: #9b2aff; /* $br-new-purple - PATCH */
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
[data-theme="dark"] .api-reference-tag rapi-doc-mini,
|
||||
[data-theme="dark"] .api-reference-tag rapi-doc,
|
||||
html:has(link[title="dark-theme"]:not([disabled])) .api-reference-tag rapi-doc-mini,
|
||||
html:has(link[title="dark-theme"]:not([disabled])) .api-reference-tag rapi-doc {
|
||||
--blue: #009F5F;
|
||||
--green: #009F5F;
|
||||
--orange: #FFC800;
|
||||
--red: #DC4E58;
|
||||
--purple: #8E1FC3;
|
||||
}
|
||||
|
||||
/* Visual separation between operations */
|
||||
.api-reference-tag rapi-doc-mini::part(section-operation-summary),
|
||||
.api-reference-tag rapi-doc::part(section-operation-summary) {
|
||||
border-top: 2px solid rgba(0, 163, 255, 0.15);
|
||||
padding-top: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
/* First operation doesn't need top border */
|
||||
.api-reference-tag rapi-doc-mini::part(section-operation-summary):first-of-type,
|
||||
.api-reference-tag rapi-doc::part(section-operation-summary):first-of-type {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Authentication info banner */
|
||||
.api-auth-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
background: #F0F4FF;
|
||||
border: 1px solid rgba(0, 163, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
color: #545667;
|
||||
}
|
||||
|
||||
.api-auth-info .api-auth-icon {
|
||||
flex-shrink: 0;
|
||||
color: #00A3FF;
|
||||
}
|
||||
|
||||
.api-auth-info .api-auth-status {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.api-auth-info .api-auth-trigger {
|
||||
background: none;
|
||||
border: 1px solid #00A3FF;
|
||||
color: #00A3FF;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.api-auth-info .api-auth-trigger:hover {
|
||||
background: #00A3FF;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Dark mode auth banner */
|
||||
[data-theme="dark"] .api-auth-info,
|
||||
html:has(link[title="dark-theme"]:not([disabled])) .api-auth-info {
|
||||
background: #1a1a2e;
|
||||
border-color: rgba(0, 163, 255, 0.2);
|
||||
color: #D4D7DD;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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 }}
|
||||
<link rel="alternate" type="application/x-yaml" href="{{ $specPath | absURL }}" title="OpenAPI Specification (YAML)" />
|
||||
<link rel="alternate" type="application/json" href="{{ $specPathJSON | absURL }}" title="OpenAPI Specification (JSON)" />
|
||||
{{ end }}
|
||||
|
||||
<div class="api-reference-wrapper" data-component="api-rapidoc">
|
||||
{{/* RapiDoc component with slot-based customization */}}
|
||||
<rapi-doc
|
||||
id="api-doc"
|
||||
spec-url="{{ $specPath }}"
|
||||
theme="light"
|
||||
bg-color="#ffffff"
|
||||
text-color="#2b2b2b"
|
||||
header-color="#020a47"
|
||||
primary-color="#00A3FF"
|
||||
nav-bg-color="#f7f8fa"
|
||||
nav-text-color="#2b2b2b"
|
||||
nav-hover-bg-color="#e8e8f0"
|
||||
nav-hover-text-color="#020a47"
|
||||
nav-accent-color="#020a47"
|
||||
regular-font="Proxima Nova, -apple-system, BlinkMacSystemFont, sans-serif"
|
||||
mono-font="IBM Plex Mono, Monaco, Consolas, monospace"
|
||||
font-size="large"
|
||||
render-style="view"
|
||||
layout="column"
|
||||
schema-style="table"
|
||||
default-schema-tab="model"
|
||||
response-area-height="400px"
|
||||
show-header="false"
|
||||
show-info="false"
|
||||
show-side-nav="false"
|
||||
show-components="false"
|
||||
allow-authentication="true"
|
||||
allow-try="false"
|
||||
allow-spec-url-load="false"
|
||||
allow-spec-file-load="false"
|
||||
allow-server-selection="false"
|
||||
allow-search="false"
|
||||
fill-request-fields-with-example="true"
|
||||
persist-auth="false"
|
||||
{{ if $operationId }}goto-path="op/{{ $operationId }}"{{ end }}
|
||||
{{ if $tag }}match-paths="tag/{{ $tag }}"{{ end }}
|
||||
>
|
||||
{{/* Custom overview slot - Hugo page content */}}
|
||||
{{ with .Content }}
|
||||
<div slot="overview">
|
||||
{{ . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{/* Custom examples from frontmatter */}}
|
||||
{{ with .Params.examples }}
|
||||
<div slot="footer" class="api-custom-examples">
|
||||
<h3>Examples</h3>
|
||||
{{ range . }}
|
||||
<div class="api-example">
|
||||
<h4>{{ .title }}</h4>
|
||||
{{ with .description }}<p>{{ . | markdownify }}</p>{{ end }}
|
||||
<pre><code class="language-{{ .lang | default "bash" }}">{{ .code }}</code></pre>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</rapi-doc>
|
||||
</div>
|
||||
|
||||
{{/* Load RapiDoc from CDN */}}
|
||||
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
|
||||
|
||||
<style>
|
||||
.api-reference-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
rapi-doc {
|
||||
width: 100%;
|
||||
min-height: 600px;
|
||||
display: block;
|
||||
/* Override RapiDoc's internal font sizes to match Hugo docs */
|
||||
--font-size-small: 15px;
|
||||
--font-size-mono: 15px;
|
||||
--font-size-regular: 17px;
|
||||
/* Match Hugo theme backgrounds - light mode default */
|
||||
--bg: #ffffff;
|
||||
--bg2: #f7f8fa;
|
||||
--bg3: #eef0f3;
|
||||
/* Input field border styling - subtle with transparency */
|
||||
--border-color: rgba(0, 163, 255, 0.25);
|
||||
--light-border-color: rgba(0, 163, 255, 0.15);
|
||||
/* HTTP method colors - lighter palette for light theme */
|
||||
--blue: #00A3FF; /* $b-pool - GET */
|
||||
--green: #34BB55; /* $gr-rainforest - POST */
|
||||
--orange: #FFB94A; /* $y-pineapple - PUT (distinct from red) */
|
||||
--red: #F95F53; /* $r-curacao - DELETE */
|
||||
--purple: #9b2aff; /* $br-new-purple - PATCH */
|
||||
}
|
||||
|
||||
/* Dark mode overrides - match Hugo $grey10: #14141F */
|
||||
[data-theme="dark"] rapi-doc,
|
||||
html:has(link[title="dark-theme"]:not([disabled])) rapi-doc {
|
||||
--bg: #14141F;
|
||||
/* Subtle border colors for dark mode with transparency */
|
||||
--border-color: rgba(0, 163, 255, 0.35);
|
||||
--light-border-color: rgba(0, 163, 255, 0.2);
|
||||
--bg2: #1a1a2a;
|
||||
--bg3: #252535;
|
||||
--fg: #D4D7DD;
|
||||
--fg2: #c8ccd2;
|
||||
--fg3: #b0b4ba;
|
||||
/* HTTP method colors - darker palette for dark theme */
|
||||
--blue: #066FC5; /* $b-ocean - GET */
|
||||
--green: #009F5F; /* $gr-viridian - POST */
|
||||
--orange: #FFC800; /* $y-thunder - PUT (brighter for dark mode) */
|
||||
--red: #DC4E58; /* $r-fire - DELETE */
|
||||
--purple: #8E1FC3; /* $p-amethyst - PATCH */
|
||||
}
|
||||
|
||||
/* Custom examples section styling */
|
||||
.api-custom-examples {
|
||||
padding: 1.5rem;
|
||||
background: var(--bg2, #f7f8fa);
|
||||
border-radius: 4px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.api-custom-examples h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.api-example {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.api-example:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.api-example h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.api-example pre {
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
background: var(--bg3, #eef0f3);
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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" . }}
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
</div>
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
{{ end }}
|
||||
|
||||
<div class="api-response-body">
|
||||
{{ partial "api/hugo-native/schema.html" (dict "schema" $resolvedSchema "spec" $spec "level" 0) }}
|
||||
{{ partial "api/schema.html" (dict "schema" $resolvedSchema "spec" $spec "level" 0) }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
|
@ -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 }}
|
||||
|
|
@ -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 */}}
|
||||
<section class="api-operations">
|
||||
{{ range $operations }}
|
||||
{{ partial "api/hugo-native/operation.html" (dict
|
||||
{{ partial "api/operation.html" (dict
|
||||
"operation" .
|
||||
"spec" $spec
|
||||
"context" $page
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue