feat(api): add inline curl examples and Ask AI links to API operations

Generate curl examples at Hugo template time from OpenAPI specs, with
server URL variable resolution, $ref handling, and JSON body generation
from schema properties. Add "Ask AI about this example" links using
existing Kapa integration. Add client library related link to all
InfluxDB 3 API tag pages. Improve visual separation between operations
with distinct badge colors and response body indentation.
docs-v2-pr6622
Jason Stirnaman 2026-03-07 23:00:22 -06:00
parent 895d56058d
commit df9c9977cf
33 changed files with 1550 additions and 1180 deletions

View File

@ -29,7 +29,7 @@ info:
name: InfluxData
url: https://www.influxdata.com
email: support@influxdata.com
x-influxdata-related:
x-related:
- title: Migrate from InfluxDB v1 or v2
href: /influxdb3/core/get-started/migrate-from-influxdb-v1-v2/
servers:
@ -68,7 +68,7 @@ tags:
values to cache, the maximum number of distinct value combinations to cache, and
the maximum age of cached values. A DVC is associated with a table, which can
have multiple DVCs.
x-influxdata-related:
x-related:
- title: Manage the Distinct Value Cache
href: /influxdb3/core/admin/distinct-value-cache/
- name: Cache last value
@ -83,7 +83,7 @@ tags:
what fields to cache, what tags to use to identify each series, and the
number of values to cache for each unique series.
An LVC is associated with a table, which can have multiple LVCs.
x-influxdata-related:
x-related:
- title: Manage the Last Value Cache
href: /influxdb3/core/admin/last-value-cache/
- name: Migrate from InfluxDB v1 or v2
@ -190,6 +190,11 @@ tags:
- name: Auth token
description: Manage tokens for authentication and authorization
- name: Write data
x-related:
- title: Write data using HTTP APIs
href: /influxdb3/core/write-data/http-api/
- title: Line protocol reference
href: /influxdb3/core/reference/syntax/line-protocol/
description: |
Write data to InfluxDB 3 Core using line protocol format.
@ -331,7 +336,7 @@ paths:
- QuerystringAuthentication: []
tags:
- Write data
x-influxdata-related:
x-related:
- title: Use compatibility APIs to write data
href: /influxdb3/core/write-data/http-api/compatibility-apis/
/api/v2/write:
@ -422,7 +427,7 @@ paths:
- TokenAuthentication: []
tags:
- Write data
x-influxdata-related:
x-related:
- title: Use compatibility APIs to write data
href: /influxdb3/core/write-data/http-api/compatibility-apis/
/api/v3/write_lp:
@ -816,7 +821,7 @@ paths:
- QuerystringAuthentication: []
tags:
- Query data
x-influxdata-related:
x-related:
- title: Use the InfluxDB v1 HTTP query API and InfluxQL to query data
href: /influxdb3/core/query-data/execute-queries/influxdb-v1-api/
post:
@ -938,7 +943,7 @@ paths:
- QuerystringAuthentication: []
tags:
- Query data
x-influxdata-related:
x-related:
- title: Use the InfluxDB v1 HTTP query API and InfluxQL to query data
href: /influxdb3/core/query-data/execute-queries/influxdb-v1-api/
/health:

View File

@ -29,7 +29,7 @@ info:
name: InfluxData
url: https://www.influxdata.com
email: support@influxdata.com
x-influxdata-related:
x-related:
- title: Migrate from InfluxDB v1 or v2
href: /influxdb3/enterprise/get-started/migrate-from-influxdb-v1-v2/
servers:
@ -321,7 +321,7 @@ paths:
tags:
- Write data
x-influxdata-related:
x-related:
- title: Use compatibility APIs to write data
href: /influxdb3/enterprise/write-data/http-api/compatibility-apis/
/api/v2/write:
@ -414,7 +414,7 @@ paths:
tags:
- Write data
x-influxdata-related:
x-related:
- title: Use compatibility APIs to write data
href: /influxdb3/enterprise/write-data/http-api/compatibility-apis/
/api/v3/write_lp:
@ -838,7 +838,7 @@ paths:
tags:
- Query data
x-influxdata-related:
x-related:
- title: Use the InfluxDB v1 HTTP query API and InfluxQL to query data
href: /influxdb3/enterprise/query-data/execute-queries/influxdb-v1-api/
post:
@ -957,7 +957,7 @@ paths:
tags:
- Query data
x-influxdata-related:
x-related:
- title: Use the InfluxDB v1 HTTP query API and InfluxQL to query data
href: /influxdb3/enterprise/query-data/execute-queries/influxdb-v1-api/
/health:

View File

@ -7,16 +7,20 @@
* for all InfluxDB products.
*
* This script:
* 1. Runs getswagger.sh to fetch/bundle OpenAPI specs
* 2. Copies specs to static directory for download
* 3. Generates path group fragments (YAML and JSON)
* 4. Creates article metadata (YAML and JSON)
* 5. Generates Hugo content pages from article data
* 1. Cleans output directories (unless --no-clean)
* 2. Runs getswagger.sh to fetch/bundle OpenAPI specs
* 3. Copies specs to static directory for download
* 4. Generates path group fragments (YAML and JSON)
* 5. Creates article metadata (YAML and JSON)
* 6. Generates Hugo content pages from article data
*
* Usage:
* node generate-openapi-articles.js # Generate all products
* node generate-openapi-articles.js cloud-v2 # Generate single product
* node generate-openapi-articles.js cloud-v2 oss-v2 # Generate multiple products
* node generate-openapi-articles.js # Clean and generate all products
* node generate-openapi-articles.js cloud-v2 # Clean and generate single product
* node generate-openapi-articles.js --no-clean # Generate without cleaning
* node generate-openapi-articles.js --dry-run # Preview what would be cleaned
* node generate-openapi-articles.js --skip-fetch # Skip getswagger.sh fetch step
* node generate-openapi-articles.js --validate-links # Validate documentation links
*
* @module generate-openapi-articles
*/
@ -586,6 +590,25 @@ All {{% product-name %}} API endpoints, sorted by path.
) {
frontmatter.related = article.fields.related;
}
// Add client library related link for InfluxDB 3 products
if (contentPath.includes('influxdb3/') && !isConceptual) {
// Extract product segment from contentPath (e.g., "core" from ".../influxdb3/core")
const influxdb3Match = contentPath.match(/influxdb3\/([^/]+)/);
if (influxdb3Match) {
const productSegment = influxdb3Match[1];
const clientLibLink = {
title: 'InfluxDB 3 API client libraries',
href: `/influxdb3/${productSegment}/reference/client-libraries/v3/`,
};
const existing = frontmatter.related || [];
const alreadyHas = existing.some(
(r) => typeof r === 'object' && r.href === clientLibLink.href
);
if (!alreadyHas) {
frontmatter.related = [...existing, clientLibLink];
}
}
}
// Add alt_links for cross-product API navigation
if (apiProductsMap.size > 0) {
const altLinks = {};
@ -641,12 +664,16 @@ function mergeArticleData(articlesFiles, outputPath) {
...article.fields.operations,
];
}
// Merge related links
// Merge related links (dedup by href for both strings and objects)
if (article.fields.related && article.fields.related.length > 0) {
const existingRelated = existing.fields.related || [];
const newRelated = article.fields.related.filter(
(r) => !existingRelated.includes(r)
const existingHrefs = new Set(
existingRelated.map((r) => (typeof r === 'string' ? r : r.href))
);
const newRelated = article.fields.related.filter((r) => {
const href = typeof r === 'string' ? r : r.href;
return !existingHrefs.has(href);
});
existing.fields.related = [...existingRelated, ...newRelated];
}
// Keep the longest/most detailed description

File diff suppressed because it is too large Load Diff

View File

@ -557,7 +557,7 @@ function generateTagPagesFromArticleData(
menuGroup?: string;
staticFilePath?: string;
operations?: OperationMeta[];
related?: string[];
related?: (string | { title: string; href: string })[];
weight?: number;
};
}>;
@ -738,6 +738,27 @@ All {{% product-name %}} API endpoints, sorted by path.
frontmatter.related = article.fields.related;
}
// Add client library related link for InfluxDB 3 products
if (contentPath.includes('influxdb3/') && !isConceptual) {
// Extract product segment from contentPath (e.g., "core" from ".../influxdb3/core")
const influxdb3Match = contentPath.match(/influxdb3\/([^/]+)/);
if (influxdb3Match) {
const productSegment = influxdb3Match[1];
const clientLibLink = {
title: 'InfluxDB 3 API client libraries',
href: `/influxdb3/${productSegment}/reference/client-libraries/v3/`,
};
const existing =
(frontmatter.related as Array<{ title: string; href: string }>) || [];
const alreadyHas = existing.some(
(r) => typeof r === 'object' && r.href === clientLibLink.href
);
if (!alreadyHas) {
frontmatter.related = [...existing, clientLibLink];
}
}
}
// Add alt_links for cross-product API navigation
if (apiProductsMap.size > 0) {
const altLinks: Record<string, string> = {};
@ -781,7 +802,7 @@ interface ArticleData {
menuGroup?: string;
staticFilePath?: string;
operations?: OperationMeta[];
related?: string[];
related?: (string | { title: string; href: string })[];
source?: string;
};
}>;
@ -827,12 +848,16 @@ function mergeArticleData(articlesFiles: string[], outputPath: string): void {
];
}
// Merge related links
// Merge related links (dedup by href for both strings and objects)
if (article.fields.related && article.fields.related.length > 0) {
const existingRelated = existing.fields.related || [];
const newRelated = article.fields.related.filter(
(r) => !existingRelated.includes(r)
const existingHrefs = new Set(
existingRelated.map((r) => (typeof r === 'string' ? r : r.href))
);
const newRelated = article.fields.related.filter((r) => {
const href = typeof r === 'string' ? r : r.href;
return !existingHrefs.has(href);
});
existing.fields.related = [...existingRelated, ...newRelated];
}

View File

@ -11,6 +11,14 @@ import * as yaml from 'js-yaml';
import * as fs from 'fs';
import * as path from 'path';
/**
* Related link with title and href (from x-influxdata-related)
*/
interface RelatedLink {
title: string;
href: string;
}
/**
* OpenAPI path item object
*/
@ -41,8 +49,10 @@ interface Operation {
externalDocs?: ExternalDocs;
/** Compatibility version for migration context (v1 or v2) */
'x-compatibility-version'?: string;
/** Related documentation links (replaces inline "Related guides" sections) */
/** Related documentation links as plain URLs */
'x-influxdatadocs-related'?: string[];
/** Related documentation links with title and href */
'x-related'?: RelatedLink[];
[key: string]: unknown;
}
@ -218,8 +228,10 @@ interface Tag {
externalDocs?: ExternalDocs;
/** Indicates this is a conceptual/supplementary tag (no operations) */
'x-traitTag'?: boolean;
/** Related documentation links (replaces inline "Related guides" sections) */
/** Related documentation links as plain URLs */
'x-influxdatadocs-related'?: string[];
/** Related documentation links with title and href */
'x-related'?: RelatedLink[];
[key: string]: unknown;
}
@ -239,8 +251,10 @@ interface OperationMeta {
description: string;
url: string;
};
/** Related documentation links */
/** Related documentation links (plain URLs) */
related?: string[];
/** Related documentation links with title and href */
relatedLinks?: RelatedLink[];
}
/**
@ -265,8 +279,8 @@ interface Article {
tags?: string[];
source?: string;
staticFilePath?: string;
/** Related documentation links extracted from x-relatedLinks */
related?: string[];
/** Related documentation links (plain URLs or {title, href} objects) */
related?: (string | RelatedLink)[];
/** OpenAPI tags from operations (for Hugo frontmatter) */
apiTags?: string[];
/** Menu display name (actual endpoint path, different from Hugo path) */
@ -473,6 +487,11 @@ function extractOperationsByTag(
opMeta.related = operation['x-influxdatadocs-related'];
}
// Extract x-related (title/href objects) if present
if (operation['x-related'] && Array.isArray(operation['x-related'])) {
opMeta.relatedLinks = operation['x-related'] as RelatedLink[];
}
// Add operation to each of its tags
(operation.tags || []).forEach((tag) => {
if (!tagOperations.has(tag)) {
@ -1115,36 +1134,51 @@ function createArticleDataForTag(
article.fields.weight = 100;
}
// Aggregate unique related URLs from multiple sources into article-level related
// Aggregate related links from multiple sources into article-level related
// This populates Hugo frontmatter `related` field for "Related content" links
const relatedUrls = new Set<string>();
// Supports both plain URL strings and {title, href} objects
const relatedItems: (string | RelatedLink)[] = [];
const seenHrefs = new Set<string>();
// First check tag-level x-influxdatadocs-related
if (tagMeta?.['x-influxdatadocs-related']) {
(tagMeta['x-influxdatadocs-related'] as string[]).forEach((url) =>
relatedUrls.add(url)
);
}
// Then check tag-level externalDocs (legacy single link)
if (tagMeta?.externalDocs?.url) {
relatedUrls.add(tagMeta.externalDocs.url);
}
// Then aggregate from operations
operations.forEach((op) => {
// Check operation-level x-influxdatadocs-related (via opMeta.related)
if (op.related) {
op.related.forEach((url) => relatedUrls.add(url));
// Helper to add a link, deduplicating by href
const addRelated = (item: string | RelatedLink): void => {
const href = typeof item === 'string' ? item : item.href;
if (!seenHrefs.has(href)) {
seenHrefs.add(href);
relatedItems.push(item);
}
};
// Tag-level x-related ({title, href} objects)
if (tagMeta?.['x-related']) {
(tagMeta['x-related'] as RelatedLink[]).forEach(addRelated);
}
// Tag-level x-influxdatadocs-related (plain URLs)
if (tagMeta?.['x-influxdatadocs-related']) {
(tagMeta['x-influxdatadocs-related'] as string[]).forEach(addRelated);
}
// Tag-level externalDocs (legacy single link)
if (tagMeta?.externalDocs?.url) {
addRelated(tagMeta.externalDocs.url);
}
// Operation-level related links
operations.forEach((op) => {
if (op.relatedLinks) {
op.relatedLinks.forEach(addRelated);
}
if (op.related) {
op.related.forEach(addRelated);
}
// Check operation-level externalDocs (legacy single link)
if (op.externalDocs?.url) {
relatedUrls.add(op.externalDocs.url);
addRelated(op.externalDocs.url);
}
});
if (relatedUrls.size > 0) {
article.fields.related = Array.from(relatedUrls);
if (relatedItems.length > 0) {
article.fields.related = relatedItems;
}
return article;
@ -1231,6 +1265,14 @@ function writeOpenapiTagArticleData(
opMeta.related = operation['x-influxdatadocs-related'];
}
// Extract x-related (title/href objects)
if (
operation['x-related'] &&
Array.isArray(operation['x-related'])
) {
opMeta.relatedLinks = operation['x-related'] as RelatedLink[];
}
operations.push(opMeta);
}
});

View File

@ -0,0 +1,67 @@
// API Code Samples
// Styles for inline curl examples and Ask AI links within API operations
.api-code-sample {
margin: $api-spacing-lg 0;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: $api-border-radius;
overflow: hidden;
.dark-theme & {
border-color: rgba(255, 255, 255, 0.1);
}
}
.api-code-sample-header {
margin: 0;
padding: $api-spacing-sm $api-spacing-md;
font-size: 0.85rem;
font-weight: 600;
background: rgba(0, 0, 0, 0.03);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
.dark-theme & {
background: rgba(255, 255, 255, 0.03);
border-bottom-color: rgba(255, 255, 255, 0.1);
}
}
.api-code-sample-body {
position: relative;
}
pre.api-code-block {
margin: 0;
padding: $api-spacing-md;
padding-bottom: $api-spacing-lg + 0.5rem;
overflow-x: auto;
background: #1e1e2e;
color: #cdd6f4;
font-size: 0.8rem;
line-height: 1.5;
border-radius: 0;
code {
background: none;
padding: 0;
color: inherit;
font-size: inherit;
line-height: inherit;
white-space: pre;
}
}
.api-code-ask-ai {
position: absolute;
right: $api-spacing-md;
bottom: $api-spacing-sm;
font-size: 0.8rem;
color: #89b4fa;
text-decoration: none;
opacity: 0.7;
transition: opacity 0.2s;
&:hover {
opacity: 1;
}
}

View File

@ -8,17 +8,17 @@ $api-spacing-md: 1rem;
$api-spacing-lg: 1.5rem;
$api-spacing-xl: 2rem;
// Method colors (matching existing theme)
// Method colors
$method-get: #00A3FF;
$method-post: #34BB55;
$method-put: #FFB94A;
$method-delete: #F95F53;
$method-delete: #D63031;
$method-patch: #9b2aff;
// Status code colors
// Status code colors intentionally distinct from method colors
$status-success: #34BB55;
$status-redirect: #FFB94A;
$status-client-error: #F95F53;
$status-client-error: #E17055;
$status-server-error: #9b2aff;
// ============================================
@ -31,8 +31,8 @@ $status-server-error: #9b2aff;
.api-operation {
margin-bottom: $api-spacing-xl;
padding-top: $api-spacing-lg;
border-top: 1px solid $nav-border;
padding-top: $api-spacing-xl;
border-top: 2px solid $nav-border;
&:first-child {
border-top: none;
@ -430,7 +430,10 @@ $status-server-error: #9b2aff;
}
.api-response-body {
padding-top: $api-spacing-sm;
margin-top: $api-spacing-sm;
margin-left: $api-spacing-lg;
padding: $api-spacing-sm 0 $api-spacing-sm $api-spacing-md;
border-left: 2px solid rgba($article-text, 0.1);
}
// ============================================

View File

@ -35,7 +35,8 @@
"layouts/v3-wayfinding",
"layouts/api-layout",
"layouts/api-security-schemes",
"layouts/api-operations";
"layouts/api-operations",
"layouts/api-code-samples";
// Import Components
@import "components/influxdb-version-detector",

View File

@ -93,6 +93,9 @@ operations:
summary: Delete an authorization
tags:
- Authorizations (API tokens)
related:
- title: InfluxDB 3 API client libraries
href: /influxdb3/cloud-serverless/reference/client-libraries/v3/
alt_links:
core: /influxdb3/core/api/
enterprise: /influxdb3/enterprise/api/

View File

@ -34,6 +34,9 @@ operations:
summary: Update a measurement schema
tags:
- Bucket Schemas
related:
- title: InfluxDB 3 API client libraries
href: /influxdb3/cloud-serverless/reference/client-libraries/v3/
alt_links:
core: /influxdb3/core/api/
enterprise: /influxdb3/enterprise/api/

View File

@ -112,6 +112,9 @@ operations:
summary: Remove an owner from a bucket
tags:
- Buckets
related:
- title: InfluxDB 3 API client libraries
href: /influxdb3/cloud-serverless/reference/client-libraries/v3/
alt_links:
core: /influxdb3/core/api/
enterprise: /influxdb3/enterprise/api/

View File

@ -63,6 +63,9 @@ operations:
summary: Delete a database retention policy
tags:
- DBRPs
related:
- title: InfluxDB 3 API client libraries
href: /influxdb3/cloud-serverless/reference/client-libraries/v3/
alt_links:
core: /influxdb3/core/api/
enterprise: /influxdb3/enterprise/api/

View File

@ -16,6 +16,9 @@ operations:
summary: Delete data
tags:
- Delete
related:
- title: InfluxDB 3 API client libraries
href: /influxdb3/cloud-serverless/reference/client-libraries/v3/
alt_links:
core: /influxdb3/core/api/
enterprise: /influxdb3/enterprise/api/

View File

@ -69,6 +69,9 @@ operations:
summary: Find script parameters.
tags:
- Invokable Scripts
related:
- title: InfluxDB 3 API client libraries
href: /influxdb3/cloud-serverless/reference/client-libraries/v3/
alt_links:
core: /influxdb3/core/api/
enterprise: /influxdb3/enterprise/api/

View File

@ -16,6 +16,9 @@ operations:
summary: Retrieve limits for an organization
tags:
- Limits
related:
- title: InfluxDB 3 API client libraries
href: /influxdb3/cloud-serverless/reference/client-libraries/v3/
alt_links:
core: /influxdb3/core/api/
enterprise: /influxdb3/enterprise/api/

View File

@ -82,6 +82,9 @@ operations:
summary: Remove an owner from an organization
tags:
- Organizations
related:
- title: InfluxDB 3 API client libraries
href: /influxdb3/cloud-serverless/reference/client-libraries/v3/
alt_links:
core: /influxdb3/core/api/
enterprise: /influxdb3/enterprise/api/

View File

@ -16,6 +16,9 @@ operations:
summary: List all known resources
tags:
- Resources
related:
- title: InfluxDB 3 API client libraries
href: /influxdb3/cloud-serverless/reference/client-libraries/v3/
alt_links:
core: /influxdb3/core/api/
enterprise: /influxdb3/enterprise/api/

View File

@ -16,6 +16,9 @@ operations:
summary: List all top level routes
tags:
- Routes
related:
- title: InfluxDB 3 API client libraries
href: /influxdb3/cloud-serverless/reference/client-libraries/v3/
alt_links:
core: /influxdb3/core/api/
enterprise: /influxdb3/enterprise/api/

View File

@ -34,6 +34,9 @@ operations:
summary: Delete secrets from an organization
tags:
- Secrets
related:
- title: InfluxDB 3 API client libraries
href: /influxdb3/cloud-serverless/reference/client-libraries/v3/
alt_links:
core: /influxdb3/core/api/
enterprise: /influxdb3/enterprise/api/

View File

@ -40,6 +40,9 @@ operations:
summary: Delete an authorization
tags:
- Security and access endpoints
related:
- title: InfluxDB 3 API client libraries
href: /influxdb3/cloud-serverless/reference/client-libraries/v3/
alt_links:
core: /influxdb3/core/api/
enterprise: /influxdb3/enterprise/api/

View File

@ -22,6 +22,9 @@ operations:
summary: List all known resources
tags:
- System information endpoints
related:
- title: InfluxDB 3 API client libraries
href: /influxdb3/cloud-serverless/reference/client-libraries/v3/
alt_links:
core: /influxdb3/core/api/
enterprise: /influxdb3/enterprise/api/

View File

@ -165,6 +165,9 @@ operations:
summary: Retry a task run
tags:
- Tasks
related:
- title: InfluxDB 3 API client libraries
href: /influxdb3/cloud-serverless/reference/client-libraries/v3/
alt_links:
core: /influxdb3/core/api/
enterprise: /influxdb3/enterprise/api/

View File

@ -94,6 +94,9 @@ operations:
summary: Remove an owner from a Telegraf config
tags:
- Telegrafs
related:
- title: InfluxDB 3 API client libraries
href: /influxdb3/cloud-serverless/reference/client-libraries/v3/
alt_links:
core: /influxdb3/core/api/
enterprise: /influxdb3/enterprise/api/

View File

@ -87,6 +87,9 @@ operations:
summary: Export a new template
tags:
- Templates
related:
- title: InfluxDB 3 API client libraries
href: /influxdb3/cloud-serverless/reference/client-libraries/v3/
alt_links:
core: /influxdb3/core/api/
enterprise: /influxdb3/enterprise/api/

View File

@ -16,6 +16,9 @@ operations:
summary: Retrieve usage for an organization
tags:
- Usage
related:
- title: InfluxDB 3 API client libraries
href: /influxdb3/cloud-serverless/reference/client-libraries/v3/
alt_links:
core: /influxdb3/core/api/
enterprise: /influxdb3/enterprise/api/

View File

@ -64,6 +64,9 @@ operations:
summary: Delete a label from a variable
tags:
- Variables
related:
- title: InfluxDB 3 API client libraries
href: /influxdb3/cloud-serverless/reference/client-libraries/v3/
alt_links:
core: /influxdb3/core/api/
enterprise: /influxdb3/enterprise/api/

View File

@ -7,6 +7,7 @@
* 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
* 4. Related links from OpenAPI x-related frontmatter rendered HTML
*
* Run with:
* node cypress/support/run-e2e-specs.js --spec "cypress/e2e/content/api-reference.cy.js" content/influxdb3/core/reference/api/_index.md
@ -346,11 +347,11 @@ describe('All endpoints page', () => {
);
});
it('operation cards link to tag pages with method anchors', () => {
it('operation cards link to tag pages with operation anchors', () => {
cy.get('.api-operation-card')
.first()
.should('have.attr', 'href')
.and('match', /\/api\/.*\/#(get|post|put|patch|delete)-/i);
.and('match', /\/api\/.*\/#operation\//);
});
it('is accessible from navigation', () => {
@ -363,3 +364,167 @@ describe('All endpoints page', () => {
});
});
});
/**
* API Code Sample Tests
* Tests that inline curl examples render correctly on tag pages
*/
describe('API code samples', () => {
const tagPages = [
'/influxdb3/core/api/write-data/',
'/influxdb3/enterprise/api/write-data/',
];
tagPages.forEach((page) => {
describe(`Code samples on ${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(page);
});
it('each operation has a code sample', () => {
cy.get('.api-operation').each(($op) => {
cy.wrap($op).find('.api-code-sample').should('have.length', 1);
});
});
it('code samples have header and code block', () => {
cy.get('.api-code-sample')
.first()
.within(() => {
cy.get('.api-code-sample-header').should(
'contain',
'Example request'
);
cy.get('.api-code-block code').should('exist');
});
});
it('code block contains a curl command', () => {
cy.get('.api-code-block code')
.first()
.invoke('text')
.should('match', /curl --request (GET|POST|PUT|PATCH|DELETE)/);
});
it('curl command includes Authorization header', () => {
cy.get('.api-code-block code')
.first()
.invoke('text')
.should('include', 'Authorization: Bearer INFLUX_TOKEN');
});
it('POST operations include request body in curl', () => {
cy.get('.api-operation[data-method="post"]')
.first()
.find('.api-code-block code')
.invoke('text')
.should('include', '--data-raw');
});
it('code samples have Ask AI links', () => {
cy.get('.api-code-sample .api-code-ask-ai')
.first()
.should('have.attr', 'data-query')
.and('not.be.empty');
});
});
});
});
/**
* API Client Library Related Link Tests
* Tests that InfluxDB 3 tag pages include client library related links
*/
describe('API client library related links', () => {
const influxdb3Pages = [
'/influxdb3/core/api/write-data/',
'/influxdb3/enterprise/api/write-data/',
];
influxdb3Pages.forEach((page) => {
describe(`Client library link on ${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(page);
});
it('includes InfluxDB 3 API client libraries in related links', () => {
cy.get('.related ul li a')
.filter(':contains("InfluxDB 3 API client libraries")')
.should('have.length', 1)
.and('have.attr', 'href')
.and('match', /\/influxdb3\/\w+\/reference\/client-libraries\/v3\//);
});
});
});
});
/**
* API Related Links Tests
* Tests that x-related from OpenAPI specs renders as related links on tag pages
*/
describe('API related links', () => {
const pagesWithRelated = ['/influxdb3/core/api/write-data/'];
pagesWithRelated.forEach((page) => {
describe(`Related links on ${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(page);
});
it('displays a related section', () => {
cy.get('.related').should('exist');
cy.get('.related h4#related').should('contain', 'Related');
});
it('renders related links from x-related as anchor elements', () => {
cy.get('.related ul li a').should('have.length.at.least', 2);
});
it('related links have title text and valid href', () => {
cy.get('.related ul li a').each(($a) => {
// Each link has non-empty text
cy.wrap($a).invoke('text').should('not.be.empty');
// Each link has an href starting with /
cy.wrap($a).should('have.attr', 'href').and('match', /^\//);
});
});
it('related links resolve to valid pages', () => {
cy.get('.related ul li a').each(($a) => {
const href = $a.attr('href');
cy.request(href).its('status').should('eq', 200);
});
});
});
});
});

View File

@ -1,455 +1,125 @@
# API Code Samples, Ask AI, & Client Library Integration Plan
# API Code Samples & Ask AI Integration Plan
## Context
## Scope
The docs-v2 site recently migrated from RapiDoc to Hugo-native API templates. The current API pages render operation details (method, path, parameters, request body, response schemas) but have **no code samples**. This plan adds:
This plan covers:
1. **curl request examples** generated at build time, displayed **inline within each operation** in the article flow
2. **Response body schema** for the successful response, shown inline below the request example
3. **"Ask AI about this example"** button on code blocks (API code samples first, then all code blocks site-wide)
4. **Client library integration** (phased — links first, code samples later)
1. **Inline curl examples** for each API operation, generated at Hugo template time from the OpenAPI spec
2. **"Ask AI about this example"** link on each curl example, using the existing Kapa integration
3. **Client library related link** on all InfluxDB 3 API tag pages
## Design Decision: Inline Code Samples (Not Sidebar)
**Out of scope** (separate plans):
Code samples render **within the article flow** inside each operation block, not crammed into the right sidebar. The TOC stays at its current 200px width, unchanged.
- Site-wide Ask AI on all code blocks (render-codeblock hook)
- Client library tabbed code samples with language tabs
- Duplicate response schema rendering (already shown in Responses section)
Each operation block gets a code sample section at the bottom:
***
```
┌──────────────────────────────────────────────────┐
│ POST /api/v2/write │
│ ───────────────── │
│ Summary: Write data │
│ Description: ... │
│ │
│ Parameters │
│ ┌────────┬──────┬────────────────────────┐ │
│ │ Name │ Type │ Description │ │
│ └────────┴──────┴────────────────────────┘ │
│ │
│ Request Body │
│ ┌────────────────────────────────────────┐ │
│ │ schema details... │ │
│ └────────────────────────────────────────┘ │
│ │
│ Responses │
│ ┌────────────────────────────────────────┐ │
│ │ 204 Success │ │
│ │ 400 Bad Request │ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌─ Example ──────────────────────────────┐ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ curl --request POST \ │ │ │
│ │ │ "http://localhost:8181/..." \ │ │ │
│ │ │ --header "Authorization: ..." \│ │ │
│ │ │ --header "Content-Type: ..." \ │ │ │
│ │ │ --data-raw "mem,host=a v=1.0" │ │ │
│ │ └──────────────────────────────────┘ │ │
│ │ [Ask AI about this example] │ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌─ Client Libraries ────────────────────┐ │
│ │ [Python] [JavaScript] [Go] [Java] [C#]│ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ from influxdb_client_3 import │ │ │
│ │ │ InfluxDBClient3 │ │ │
│ │ │ client = InfluxDBClient3(...) │ │ │
│ │ │ client.write(record="...") │ │ │
│ │ └──────────────────────────────────┘ │ │
│ │ 📖 Full guide: Write data with │ │
│ │ client libraries → │ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌─ Response ──────────────────────────────┐ │
│ │ 200 OK │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ id string │ │ │
│ │ │ name string │ │ │
│ │ │ orgID string │ │ │
│ │ │ ... │ │ │
│ │ └──────────────────────────────────┘ │ │
│ │ ──── or ──── │ │
│ │ 204 No Content (empty body) │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
```
## Architecture
---
**No build script changes for curl generation.** The curl example is constructed entirely in a Hugo partial (`api/code-sample.html`) using data already loaded by `tag-renderer.html` — the full parsed OpenAPI spec with server URLs, parameters, request body schemas, and examples.
## Phase 1: curl Examples in Article Flow (MVP)
The existing `influxdb-url.js` automatically replaces the default placeholder host in `<pre>` elements with the user's custom URL. No new JavaScript is needed for URL personalization.
### 1.1 Generate curl examples in build script
### Operation layout order (revised)
**Modify:** `api-docs/scripts/generate-openapi-articles.ts`
1. Header (method + path + summary)
2. Description
3. Parameters
4. Request Body
5. **Example (curl + Ask AI)** — new
6. Responses
For each operation, generate a `curlExample` string and store it in the operation metadata. The build script already processes every operation to extract `operationId`, `method`, `path`, `summary`, and `tags` for frontmatter.
***
Add a function `generateCurlExample(operation, spec)` that:
1. Gets server URL from `spec.servers[0].url` (fallback: `http://localhost:8181`)
2. Builds the full URL with path parameter placeholders (e.g., `{bucketID}`)
3. Adds required query parameters with placeholder values
4. Adds `Authorization: Bearer $INFLUX_TOKEN` header
5. Adds `Content-Type` header when request body exists
6. Generates request body from:
- `requestBody.content["application/json"].schema.example` (first choice)
- `requestBody.content["application/json"].example` (second choice)
- A minimal JSON object from `required` + `properties` with type-based defaults (fallback)
- For `text/plain` content types (like line protocol), uses a sample line protocol string
7. Stores the complete curl command as a multiline string
## curl Example Generation
The operation frontmatter entry becomes:
```yaml
operations:
- operationId: PostWrite
method: POST
path: /api/v2/write
summary: Write data
tags: [Write]
curlExample: |
curl --request POST \
"http://localhost:8181/api/v2/write?bucket=DATABASE_NAME&precision=ns" \
--header "Authorization: Bearer $INFLUX_TOKEN" \
--header "Content-Type: text/plain; charset=utf-8" \
--data-raw "mem,host=host1 used_percent=23.43234543 1556896326"
```
### Partial: `layouts/partials/api/code-sample.html`
### 1.2 Create curl display partial
Receives the operation definition (`$opDef`), spec (`$spec`), and operation metadata from `operation.html`. Constructs a curl command:
**New file:** `layouts/partials/api/code-sample.html`
1. **Server URL**: `spec.servers[0].url` — falls back to the product's `placeholder_host`. The existing `influxdb-url.js` replaces this in the DOM if the user has a custom URL.
2. **Method**: Always explicit `--request METHOD`
3. **Path**: Appended to server URL. `{param}` placeholders left as-is in the URL.
4. **Query parameters**: Only required ones. Uses `example` value if available, otherwise an `UPPER_SNAKE_CASE` placeholder derived from the parameter name.
5. **Headers**:
- Always: `--header "Authorization: Bearer INFLUX_TOKEN"`
- When request body exists: `--header "Content-Type: ..."` derived from the first key in `requestBody.content`
6. **Request body**:
- `application/json`: Uses `schema.example` if present. If no example, body is omitted entirely — no synthesized fake data.
- `text/plain` (line protocol): Hardcoded sample: `--data-raw "measurement,tag=value field=1.0"`
- No example and no special content type: body omitted, shows only URL + headers.
Renders the code sample block within the operation. Receives the operation dict (with `curlExample` field) and page context.
### Ask AI link
Output:
```html
<div class="api-code-sample">
<div class="api-code-sample-header">
<h5>Example</h5>
</div>
<div class="api-code-sample-body">
<pre class="api-code-block"><code class="language-sh">{{ .operation.curlExample }}</code></pre>
<a href="#" class="ask-ai-open api-code-ask-ai"
data-query="Explain this API request: POST /api/v2/write - Write data">
Ask AI about this example
</a>
</div>
</div>
```
### 1.3 Integrate into operation.html
**Modify:** `layouts/partials/api/operation.html`
After the Responses section, add:
```html
{{ with .operation.curlExample }}
{{ partial "api/code-sample.html" (dict "operation" $.operation "context" $.context) }}
{{ end }}
```
### 1.4 Add styles
**New file:** `assets/styles/layouts/_api-code-samples.scss`
Key styles:
- `.api-code-sample`: full-width block within the operation, subtle border/background
- `.api-code-sample-header`: section header with "Example" title
- `.api-code-block`: dark background code block (always dark, regardless of theme), horizontal scroll for overflow
- `.api-code-ask-ai`: small link below code block, uses existing ask-ai-open styling conventions
- Smooth transition/animation when expanding
**Modify:** `assets/styles/styles-default.scss` — add import for `_api-code-samples.scss`.
---
## Phase 2: Successful Response Schema
### 2.1 Render the response body schema for the successful status code
Instead of generating response *examples*, show the **response body schema** for the successful response (typically `200` or `201`) directly below the curl request example.
This reuses the existing `responses.html` and `schema.html` partials, which already resolve `$ref` and render schema properties with types and descriptions. The difference is we show only the **successful response** in the code sample section (rather than all status codes, which are already shown in the Responses section above).
**Approach:**
- No build script changes needed — the schema is already in the OpenAPI spec, parsed at Hugo build time
- The `code-sample.html` partial reads the operation from the spec (already loaded by `tag-renderer.html`), finds the first 2xx response, and renders its body schema
**Modify:** `layouts/partials/api/code-sample.html`
Below the curl request, add:
```html
{{ $successResponse := false }}
{{ range $code, $resp := $opDef.responses }}
{{ if hasPrefix $code "2" }}
{{ $successResponse = $resp }}
{{ end }}
{{ end }}
{{ with $successResponse }}
{{ $content := .content | default dict }}
{{ $jsonContent := index $content "application/json" | default dict }}
{{ with $jsonContent.schema }}
<div class="api-code-response">
<h5>Response Body</h5>
{{ partial "api/schema.html" (dict "schema" . "spec" $spec "level" 0) }}
</div>
{{ else }}
<div class="api-code-response api-code-response--empty">
<h5>Response</h5>
<p>No response body</p>
</div>
{{ end }}
{{ end }}
```
This means for a `204 No Content` response (like the write endpoint), it shows "No response body." For a `200 OK` with a JSON schema (like list endpoints), it renders the full schema properties table — the same compact rendering already used in the Responses section but focused on just the success case.
**Note:** This requires passing the full operation definition and spec to `code-sample.html`, not just the frontmatter operation metadata. The partial needs access to the parsed spec to resolve the response schema. This is already available within `tag-renderer.html` where `code-sample.html` is called.
---
## Phase 3: "Ask AI about this example" — All Code Blocks
### 3.1 API code samples (included in Phase 1)
Already covered — each code sample gets an "Ask AI about this example" link using the existing `ask-ai-open` class and `data-query` attribute, leveraging the Kapa.ai widget.
### 3.2 Hugo render hook for all code blocks
**New file:** `layouts/_default/_markup/render-codeblock.html`
A Hugo markdown render hook that wraps every fenced code block with an optional "Ask AI" link:
Each code sample block includes an "Ask AI about this example" link using the existing `ask-ai-open` CSS class and `data-query` attribute. The existing `ask-ai-trigger.js` handles click events and opens the Kapa widget — no new JavaScript needed.
```html
<div class="code-block-wrapper">
<pre><code class="language-{{ .Type }}">{{ .Inner }}</code></pre>
{{ if .Type }}
<a href="#" class="ask-ai-open code-block-ask-ai"
data-query="Explain this {{ .Type }} code example from the {{ $.Page.Title }} documentation: {{ .Inner | truncate 200 }}">
Ask AI about this example
</a>
{{ end }}
</div>
<a href="#" class="ask-ai-open api-code-ask-ai"
data-query="Explain this API request: POST /api/v2/write - Write data">
Ask AI about this example
</a>
```
Considerations:
- Only show on code blocks with a language identifier (skip plain text blocks)
- Truncate the code in the query to avoid overly long prompts
- Use page title for context
- Add subtle styling (small text, appears on hover or below the code block)
- Test with existing code block rendering (syntax highlighting, copy button, etc.)
- Must not break existing `pytest-codeblocks` annotations or other code block features
***
### 3.3 Add styles for code block Ask AI
## Client Library Related Link
**Modify:** `assets/styles/layouts/_api-code-samples.scss` (or create a separate partial)
The generation script adds a related link to `/influxdb3/{product}/reference/client-libraries/v3/` for all InfluxDB 3 product tag pages.
```scss
.code-block-ask-ai {
display: block;
font-size: 0.75rem;
color: $nav-item;
text-decoration: none;
padding: 0.25rem 0;
opacity: 0.6;
transition: opacity 0.2s;
**InfluxDB 3 products** (identified by `pagesDir` containing `influxdb3/`):
&:hover {
opacity: 1;
color: $nav-item-hover;
}
}
```
- `influxdb3_core`
- `influxdb3_enterprise`
- `cloud-dedicated`
- `cloud-serverless`
- `clustered`
---
**Excluded** (future plan with v2 client library links):
## Phase 4: Client Library Integration
- `cloud-v2`, `oss-v2`, `oss-v1`, `enterprise-v1`
Multi-language code tabs are **only for client libraries** — not general-purpose language examples. The curl example (Phase 1) covers the raw HTTP request. The client library section helps users accomplish the same task using the InfluxDB 3 client libraries (Python, JavaScript, Go, Java, C#).
The `{product}` segment is derived from the `pagesDir` (e.g., `content/influxdb3/core` yields `core`).
Users and agents can create their own language-specific boilerplate for direct HTTP calls, but the client libraries abstract away HTTP details and provide idiomatic interfaces — that's what we want to surface here.
***
### 4.1 Client library links on relevant operations
## File Changes
**New file:** `data/api-client-library-links.yml`
### New files
A mapping from operation tags/paths to client library documentation:
```yaml
write:
description: "Write data using InfluxDB 3 client libraries"
operations:
- PostWrite
- PostWriteV1
links:
- name: Python
url: /influxdb3/{version}/reference/client-libraries/v3/python/
guide: /influxdb3/{version}/write-data/client-libraries/
- name: JavaScript
url: /influxdb3/{version}/reference/client-libraries/v3/javascript/
- name: Go
url: /influxdb3/{version}/reference/client-libraries/v3/go/
- name: Java
url: /influxdb3/{version}/reference/client-libraries/v3/java/
- name: C#
url: /influxdb3/{version}/reference/client-libraries/v3/csharp/
query:
operations:
- PostQuery
links:
- name: Python
url: /influxdb3/{version}/reference/client-libraries/v3/python/
# ...
```
| File | Purpose |
| ---------------------------------------------- | ---------------------------- |
| `layouts/partials/api/code-sample.html` | curl example + Ask AI link |
| `assets/styles/layouts/_api-code-samples.scss` | Styles for code sample block |
**Modify:** `layouts/partials/api/code-sample.html`
### Modified files
Below the curl example, render a "Client Libraries" section when the operation matches. This section includes:
1. Links to the relevant client library reference pages
2. A link to the relevant guide (e.g., "Write data with client libraries")
| File | Change |
| ----------------------------------------------- | ------------------------------------------------------------------ |
| `layouts/partials/api/operation.html` | Insert `code-sample.html` between request body and responses |
| `assets/styles/styles-default.scss` | Import `_api-code-samples.scss` |
| `api-docs/scripts/generate-openapi-articles.ts` | Add client library reference related link for InfluxDB 3 tag pages |
```html
<div class="api-client-libraries">
<h6>Client Libraries</h6>
<ul>
<li><a href="/influxdb3/core/reference/client-libraries/v3/python/">Python</a></li>
<li><a href="...">JavaScript</a></li>
...
</ul>
<p><a href="/influxdb3/core/write-data/client-libraries/">Full guide: Write data with client libraries</a></p>
</div>
```
### Not modified
### 4.2 Client library code samples with language tabs
| File | Reason |
| ------------------------------------------------------ | ------------------------ |
| `layouts/api/list.html` | No layout changes needed |
| `assets/js/main.js` | No new JS components |
| `assets/js/components/api-toc.ts` | TOC unchanged |
| `assets/styles/layouts/_api-layout.scss` | Layout unchanged |
| `api-docs/scripts/openapi-paths-to-hugo-data/index.ts` | No data model changes |
Add tabbed code samples showing how to accomplish the same operation using each InfluxDB 3 client library. These are **not** raw HTTP examples in different languages — they show the idiomatic client library usage.
**Scope:** Only operations that have corresponding client library methods:
- **Write** operations: `PostWrite`, `PostWriteV1` — client library `write()` / `writeRecord()` methods
- **Query** operations: `PostQuery` — client library `query()` methods
- Additional operations can be added as client libraries expand their APIs
**Code sample source:** Maintained in a data directory, not in OpenAPI specs.
**New directory:** `data/api-code-samples/`
Structure:
```
data/api-code-samples/
├── write/
│ ├── python.md # Python write example
│ ├── javascript.md # JavaScript write example
│ ├── go.md # Go write example
│ ├── java.md # Java write example
│ └── csharp.md # C# write example
└── query/
├── python.md
├── javascript.md
├── go.md
├── java.md
└── csharp.md
```
Each file contains just the code sample (no frontmatter), e.g. `data/api-code-samples/write/python.md`:
```python
from influxdb_client_3 import InfluxDBClient3
client = InfluxDBClient3(
host="{{< influxdb/host >}}",
token="DATABASE_TOKEN",
database="DATABASE_NAME",
)
client.write(record="home,room=Living\ Room temp=21.1")
```
**Modify:** `layouts/partials/api/code-sample.html`
Below the curl example, render a tabbed client library section:
```html
<div class="api-client-library-samples">
<div class="api-code-tabs">
<button class="api-code-tab is-active" data-lang="python">Python</button>
<button class="api-code-tab" data-lang="javascript">JavaScript</button>
<button class="api-code-tab" data-lang="go">Go</button>
<button class="api-code-tab" data-lang="java">Java</button>
<button class="api-code-tab" data-lang="csharp">C#</button>
</div>
<div class="api-code-tab-content is-active" data-lang="python">
<pre><code class="language-python">...</code></pre>
</div>
<!-- ... other languages ... -->
<p class="api-client-library-guide">
<a href="/influxdb3/core/write-data/client-libraries/">
Full guide: Write data with client libraries
</a>
</p>
</div>
```
**New file:** `assets/js/components/api-code-tabs.ts`
A simple tab switcher component (lightweight, not the full `tabs-wrapper` shortcode):
- Listens for clicks on `.api-code-tab` buttons
- Shows/hides corresponding `.api-code-tab-content` panels
- Remembers the user's language preference in `localStorage`
- Syncs language selection across all client library sections on the page
**Keeping samples in sync:** The code samples in `data/api-code-samples/` are manually maintained. When client library versions change, the samples need to be updated. This is a documentation maintenance task, same as updating any other code example in the docs. The samples should use the **latest stable** client library version and follow the patterns from the client library reference pages.
---
## Files Summary
### New Files
| File | Phase | Purpose |
|------|-------|---------|
| `layouts/partials/api/code-sample.html` | 1 | Renders inline curl + response schema + client library samples within operations |
| `assets/styles/layouts/_api-code-samples.scss` | 1 | Styles for code sample blocks, tabs, and Ask AI links |
| `layouts/_default/_markup/render-codeblock.html` | 3 | Hugo render hook adding Ask AI to all code blocks |
| `data/api-client-library-links.yml` | 4.1 | Maps operations to client library documentation links |
| `data/api-code-samples/write/*.md` | 4.2 | Client library write examples (Python, JS, Go, Java, C#) |
| `data/api-code-samples/query/*.md` | 4.2 | Client library query examples (Python, JS, Go, Java, C#) |
| `assets/js/components/api-code-tabs.ts` | 4.2 | Lightweight tab switcher for client library code samples |
### Modified Files
| File | Phase | Change |
|------|-------|--------|
| `api-docs/scripts/generate-openapi-articles.ts` | 1 | Add `curlExample` generation |
| `layouts/partials/api/operation.html` | 1 | Include `code-sample.html` partial after responses |
| `assets/styles/styles-default.scss` | 1 | Import `_api-code-samples.scss` |
| `layouts/partials/api/code-sample.html` | 2, 4.1, 4.2 | Add response body schema, client library links, tabbed code samples |
| `assets/js/main.js` | 4.2 | Register `api-code-tabs` component |
### NOT Modified (kept as-is)
| File | Reason |
|------|--------|
| `layouts/api/list.html` | TOC sidebar stays unchanged |
| `assets/js/components/api-toc.ts` | TOC behavior stays unchanged |
| `assets/styles/layouts/_api-layout.scss` | Layout widths stay unchanged |
---
## Implementation Order
1. **Phase 1** (MVP): Build script curl generation + inline code sample partial + styles
2. **Phase 2**: Successful response body schema (small addition to Phase 1, no build script changes)
3. **Phase 3.1**: Ask AI on API code samples (included in Phase 1 HTML)
4. **Phase 3.2**: Ask AI on all code blocks site-wide (Hugo render hook)
5. **Phase 4.1**: Client library links on relevant operations (data file + partial update)
6. **Phase 4.2**: Client library code samples with language tabs (data files + tab component + partial update)
Phases 1-3.1 can ship together. Phase 3.2 and 4 are independent follow-ups.
Phase 4.2 starts with write and query operations only, then expands as client libraries add more API coverage.
---
***
## Verification
1. **Regenerate articles**: Run `generate-openapi-articles.ts` → verify `curlExample` in frontmatter YAML
2. **Build**: `npx hugo --quiet` — verify no template errors
3. **Visual**: Dev server → navigate to API tag page → verify each operation has a curl example at the bottom
4. **Ask AI**: Click "Ask AI about this example" → verify Kapa opens with pre-populated query
5. **Dark mode**: Verify code sample blocks look correct in both themes
6. **Responsive**: Verify code samples render well on narrow viewports (no sidebar dependency)
7. **E2E test**: Add Cypress test verifying code samples render on API pages
1. **Build**: `npx hugo --quiet` — no template errors
2. **Visual**: Dev server — navigate to API tag page (e.g., `/influxdb3/core/api/write-data/`) — each operation has a curl example between Request Body and Responses
3. **URL replacement**: Set a custom URL in the URL selector — verify it replaces the host in curl examples
4. **Ask AI**: Click "Ask AI about this example" — Kapa opens with pre-populated query
5. **Related link**: Client library reference link appears at bottom of all InfluxDB 3 API tag pages
6. **Cypress**: Add test verifying `.api-code-sample` elements render on tag pages
7. **Dark/light mode**: Code block renders correctly in both themes
8. **Responsive**: Code sample block handles narrow viewports (horizontal scroll for long curl commands)

View File

@ -0,0 +1,233 @@
{{/*
API Code Sample
Renders an inline curl example for an API operation, constructed from the
OpenAPI spec at Hugo build time. The existing influxdb-url.js replaces
the default host in <pre> elements if the user has a custom URL set.
Params:
- opDef: The operation definition from the parsed spec
- operation: Operation metadata from frontmatter (method, path, summary, operationId)
- spec: The full OpenAPI spec object for resolving $ref
- context: The page context
*/}}
{{ $opDef := .opDef }}
{{ $operation := .operation }}
{{ $spec := .spec }}
{{ $method := upper $operation.method }}
{{ $path := $operation.path }}
{{/* --- Resolve server URL --- */}}
{{ $serverUrl := "" }}
{{ with index ($spec.servers | default slice) 0 }}
{{ $serverUrl = .url | default "" }}
{{/* Resolve {variable} placeholders using variable defaults */}}
{{ range $varName, $varDef := .variables | default dict }}
{{ $placeholder := printf "{%s}" $varName }}
{{ $serverUrl = replace $serverUrl $placeholder ($varDef.default | default "") }}
{{ end }}
{{ end }}
{{ if not $serverUrl }}
{{ $serverUrl = "http://localhost:8086" }}
{{ end }}
{{/* --- Resolve parameters (handle $ref) --- */}}
{{ $params := $opDef.parameters | default slice }}
{{ $resolvedParams := slice }}
{{ range $params }}
{{ $param := . }}
{{ if isset . "$ref" }}
{{ $refPath := index . "$ref" }}
{{ $refParts := split $refPath "/" }}
{{ if ge (len $refParts) 4 }}
{{ $paramName := index $refParts 3 }}
{{ with index $spec.components.parameters $paramName }}
{{ $param = . }}
{{ end }}
{{ end }}
{{ end }}
{{ $resolvedParams = $resolvedParams | append $param }}
{{ end }}
{{/* --- Build query string from required query parameters --- */}}
{{ $queryParts := slice }}
{{ range $resolvedParams }}
{{ if and (eq .in "query") .required }}
{{ $value := "" }}
{{ with .schema }}
{{ if .example }}
{{ $value = .example | string }}
{{ else if .default }}
{{ $value = .default | string }}
{{ end }}
{{ end }}
{{ if not $value }}
{{ $value = .name | upper | replaceRE "[^A-Z0-9]" "_" }}
{{ end }}
{{ $queryParts = $queryParts | append (printf "%s=%s" .name $value) }}
{{ end }}
{{ end }}
{{ $fullUrl := printf "%s%s" $serverUrl $path }}
{{ if gt (len $queryParts) 0 }}
{{ $fullUrl = printf "%s?%s" $fullUrl (delimit $queryParts "&") }}
{{ end }}
{{/* --- Resolve request body (handle $ref) --- */}}
{{ $requestBody := $opDef.requestBody | default dict }}
{{ if isset $requestBody "$ref" }}
{{ $refPath := index $requestBody "$ref" }}
{{ $refParts := split $refPath "/" }}
{{ if ge (len $refParts) 4 }}
{{ $rbName := index $refParts 3 }}
{{ with index $spec.components.requestBodies $rbName }}
{{ $requestBody = . }}
{{ end }}
{{ end }}
{{ end }}
{{/* --- Determine content type and body --- */}}
{{ $contentType := "" }}
{{ $bodyFlag := "" }}
{{ $rbContent := $requestBody.content | default dict }}
{{ if gt (len $rbContent) 0 }}
{{/* Get first content type key */}}
{{ range $ct, $_ := $rbContent }}
{{ if not $contentType }}
{{ $contentType = $ct }}
{{ end }}
{{ end }}
{{ $mediaType := index $rbContent $contentType | default dict }}
{{ if hasPrefix $contentType "text/plain" }}
{{/* Line protocol — use first example value or a default sample */}}
{{ $lpSample := "measurement,tag=value field=1.0" }}
{{ with $mediaType.examples }}
{{ range $_, $ex := . }}
{{ if not $bodyFlag }}
{{ $lpSample = $ex.value | string }}
{{/* Take only the first line for single-line display */}}
{{ $lines := split $lpSample "\n" }}
{{ $lpSample = index $lines 0 }}
{{ end }}
{{ end }}
{{ end }}
{{ $bodyFlag = printf "--data-raw '%s'" $lpSample }}
{{ else if hasPrefix $contentType "application/json" }}
{{/* JSON — use schema.example, or build from properties */}}
{{ with $mediaType.schema }}
{{/* Resolve schema $ref */}}
{{ $schema := . }}
{{ if isset . "$ref" }}
{{ $refPath := index . "$ref" }}
{{ $refParts := split $refPath "/" }}
{{ if ge (len $refParts) 4 }}
{{ $schemaName := index $refParts 3 }}
{{ with index $spec.components.schemas $schemaName }}
{{ $schema = . }}
{{ end }}
{{ end }}
{{ end }}
{{ if $schema.example }}
{{ $bodyFlag = printf "--data-raw '%s'" (jsonify $schema.example) }}
{{ else if $schema.properties }}
{{/* Build example JSON from schema properties */}}
{{ $bodyObj := dict }}
{{ $requiredList := $schema.required | default slice }}
{{ range $propName, $propDef := $schema.properties }}
{{/* Resolve property $ref */}}
{{ $prop := $propDef }}
{{ if isset $propDef "$ref" }}
{{ $pRefPath := index $propDef "$ref" }}
{{ $pRefParts := split $pRefPath "/" }}
{{ if ge (len $pRefParts) 4 }}
{{ $pSchemaName := index $pRefParts 3 }}
{{ with index $spec.components.schemas $pSchemaName }}
{{ $prop = . }}
{{ end }}
{{ end }}
{{ end }}
{{/* Use example → default → enum[0] → type placeholder */}}
{{ $val := "" }}
{{ if ne $prop.example nil }}
{{ $val = $prop.example }}
{{ else if ne $prop.default nil }}
{{ $val = $prop.default }}
{{ else if $prop.enum }}
{{ $val = index $prop.enum 0 }}
{{ else if eq $prop.type "string" }}
{{ $val = printf "%s" ($propName | upper) }}
{{ else if eq $prop.type "integer" }}
{{ $val = 0 }}
{{ else if eq $prop.type "number" }}
{{ $val = 0 }}
{{ else if eq $prop.type "boolean" }}
{{ $val = false }}
{{ else if eq $prop.type "array" }}
{{ if $prop.items }}
{{ if eq $prop.items.type "string" }}
{{ $val = slice "example" }}
{{ else }}
{{ $val = slice }}
{{ end }}
{{ else }}
{{ $val = slice }}
{{ end }}
{{ else if eq $prop.type "object" }}
{{ $val = dict }}
{{ else }}
{{ $val = printf "%s" ($propName | upper) }}
{{ end }}
{{ $bodyObj = merge $bodyObj (dict $propName $val) }}
{{ end }}
{{ $bodyFlag = printf "--data-raw '%s'" (jsonify (dict "indent" " ") $bodyObj) }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{/* --- Assemble curl command --- */}}
{{ $lines := slice }}
{{ $lines = $lines | append (printf "curl --request %s \\" $method) }}
{{ $lines = $lines | append (printf " \"%s\" \\" $fullUrl) }}
{{ $lines = $lines | append " --header \"Authorization: Bearer INFLUX_TOKEN\" \\" }}
{{ if $contentType }}
{{ $lines = $lines | append (printf " --header \"Content-Type: %s\" \\" $contentType) }}
{{ end }}
{{ if $bodyFlag }}
{{/* Last line — no trailing backslash */}}
{{ $lines = $lines | append (printf " %s" $bodyFlag) }}
{{ else }}
{{/* Remove trailing backslash from last header line */}}
{{ $lastIdx := sub (len $lines) 1 }}
{{ $lastLine := index $lines $lastIdx }}
{{ $lastLine = strings.TrimSuffix " \\" $lastLine }}
{{ $newLines := slice }}
{{ range $i, $line := $lines }}
{{ if eq $i $lastIdx }}
{{ $newLines = $newLines | append $lastLine }}
{{ else }}
{{ $newLines = $newLines | append $line }}
{{ end }}
{{ end }}
{{ $lines = $newLines }}
{{ end }}
{{ $curlCommand := delimit $lines "\n" }}
{{/* --- Build Ask AI query --- */}}
{{ $aiQuery := printf "Explain this %s %s API request and its response: %s" $method $path ($operation.summary | default "") }}
{{/* --- Render --- */}}
<div class="api-code-sample">
<h5 class="api-code-sample-header">Example request</h5>
<div class="api-code-sample-body">
<pre class="api-code-block"><code class="language-sh">{{ $curlCommand }}</code></pre>
<a href="#" class="ask-ai-open api-code-ask-ai"
data-query="{{ $aiQuery }}">
Ask AI about this example
</a>
</div>
</div>

View File

@ -54,6 +54,14 @@
{{ partial "api/request-body.html" (dict "requestBody" . "spec" $spec) }}
{{ end }}
{{/* Code Sample Section */}}
{{ partial "api/code-sample.html" (dict
"opDef" $opDef
"operation" $operation
"spec" $spec
"context" .context
) }}
{{/* Responses Section */}}
{{ with $opDef.responses }}
{{ partial "api/responses.html" (dict "responses" . "spec" $spec) }}

View File

@ -34,17 +34,12 @@
{{ end }}
{{ end }}
{{/* Tag description and related links from spec */}}
{{/* Tag description from spec */}}
{{ $tagDescription := "" }}
{{ $tagRelated := slice }}
{{ $tagName := .Params.tag | default "" }}
{{ range $spec.tags }}
{{ if eq .name $tagName }}
{{ $tagDescription = .description | default "" }}
{{/* Extract x-influxdata-related from the tag */}}
{{ with index . "x-influxdata-related" }}
{{ $tagRelated = . }}
{{ end }}
{{ end }}
{{ end }}
@ -69,15 +64,5 @@
{{ end }}
</section>
{{/* Related Guides - displayed at bottom like standard page template */}}
{{ if gt (len $tagRelated) 0 }}
<section class="api-related-guides">
<h4 class="api-related-title">Related guides</h4>
<ul class="api-related-list">
{{ range $tagRelated }}
<li><a href="{{ .href }}">{{ .title }}</a></li>
{{ end }}
</ul>
</section>
{{ end }}
{{/* Related links rendered via frontmatter + article/related.html */}}
</div>

View File

@ -1,32 +1,25 @@
{{ $scratch := newScratch }}
{{ if .Params.related }}
<div class="related">
<h4 id="related">Related</h4>
<ul>
{{ range .Params.related }}
{{ $scratch.Set "relatedItem" . }}
{{ $relatedItem := $scratch.Get "relatedItem" }}
{{ $scratch.Set "title" ""}}
{{ if in $relatedItem "," }}
{{ $scratch.Set "title" (replaceRE `^.+?, ` "" $relatedItem) }}
{{ end }}
{{ $link := replaceRE `\,\s(.*)$` "" $relatedItem }}
{{ $title := $scratch.Get "title" }}
{{ $isExternal := gt (len (findRE `^http` $relatedItem)) 0 }}
{{ if or ($isExternal) (in $relatedItem "/v2/api") (in $relatedItem ",")}}
{{ $link := replaceRE `\,\s(.*)$` "" $relatedItem }}
{{ $title := replaceRE `^(\S*\,\s)` "" $relatedItem }}
{{ $target := cond ($isExternal) "_blank" "" }}
<li><a href="{{ $link }}" target="{{ $target }}">{{ $title }}</a></li>
<!-- Automatically get page title and link from path -->
{{/* Handle {title, href} objects */}}
{{ if reflect.IsMap . }}
<li><a href="{{ .href }}">{{ .title }}</a></li>
{{/* Handle string items: "url, title" or plain path */}}
{{ else }}
{{ $sanitizedPath := replaceRE `\/$` "" (print $relatedItem) }}
{{ with $.Page.GetPage $sanitizedPath }}
<li><a href="{{ .RelPermalink }}" >{{ .Title | .RenderString }}</a></li>
{{ $relatedItem := . }}
{{ $isExternal := gt (len (findRE `^http` $relatedItem)) 0 }}
{{ if or ($isExternal) (in $relatedItem "/v2/api") (in $relatedItem ",")}}
{{ $link := replaceRE `\,\s(.*)$` "" $relatedItem }}
{{ $title := replaceRE `^(\S*\,\s)` "" $relatedItem }}
{{ $target := cond ($isExternal) "_blank" "" }}
<li><a href="{{ $link }}" target="{{ $target }}">{{ $title }}</a></li>
{{ else }}
{{ $sanitizedPath := replaceRE `\/$` "" (print $relatedItem) }}
{{ with $.Page.GetPage $sanitizedPath }}
<li><a href="{{ .RelPermalink }}">{{ .Title | .RenderString }}</a></li>
{{ end }}
{{ end }}
{{ end }}
{{ end }}