refactor(api): replace hardcoded product configs with auto-discovery

Replace the 110-line productConfigs map with auto-discovery from
.config.yml files. The generation script now derives Hugo paths,
menu keys, and static file names from directory structure and
existing frontmatter.

Key changes:
- discoverProducts() scans api-docs/ for .config.yml files
- Each API entry generates independently (no mergeArticleData)
- New frontmatter params: specDownloadPath, articleDataKey, articleSection
- Templates use frontmatter lookup instead of hardcoded URL-to-key maps
- list.html download button simplified from 50 lines to 5
- Cypress tests updated for Hugo-native-only URLs
feat-api-uplift
Jason Stirnaman 2026-03-15 17:01:27 -05:00
parent 313fca7294
commit 4f430457cb
8 changed files with 2348 additions and 3293 deletions

View File

@ -42,8 +42,10 @@ Replace the current API reference documentation implementation (RapiDoc web comp
5. ✅ **Update Cypress tests** - Simplify tests for static HTML
6. ✅ **Clean up styles** - Remove RapiDoc CSS and dead auth modal code
7. ✅ **Fix generation script cleanup** - Added `--clean` (default) and `--dry-run` flags
8. **Apply Cache Data tag split** - Enterprise spec update (planned)
9. **Migrate remaining products** - Apply to all InfluxDB products (planned)
8. ✅ **Add inline code samples** - curl examples and Ask AI links per operation
9. ✅ **Refine API styling** - Theme-aware code blocks, font normalization, layout width, TOC border
10. **Apply Cache Data tag split** - Enterprise spec update (planned)
11. **Migrate remaining products** - Apply to all InfluxDB products (planned)
## Related Files

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -19,36 +19,15 @@ const fakeGoogleTagManager = {
};
describe('API reference content', () => {
// API section index pages (generated from article data)
const subjects = [
'/influxdb/cloud/api/',
'/influxdb/cloud/api/v1/',
'/influxdb/cloud/api/v1-compatibility/',
'/influxdb/cloud/api/v2/',
'/influxdb/v2/api/',
'/influxdb/v2/api/v1/',
'/influxdb/v2/api/v1-compatibility/',
'/influxdb/v2/api/v2/',
'/influxdb3/cloud-dedicated/api/',
'/influxdb3/cloud-dedicated/api/management/',
'/influxdb3/cloud-dedicated/api/v1/',
'/influxdb3/cloud-dedicated/api/v1-compatibility/',
'/influxdb3/cloud-dedicated/api/v2/',
'/influxdb3/cloud-serverless/api/',
'/influxdb3/cloud-serverless/api/v1/',
'/influxdb3/cloud-serverless/api/v1-compatibility/',
'/influxdb3/cloud-serverless/api/v2/',
'/influxdb3/clustered/api/',
// TODO '/influxdb3/clustered/api/management/',
'/influxdb3/clustered/api/v1/',
'/influxdb3/clustered/api/v1-compatibility/',
'/influxdb3/clustered/api/v2/',
'/influxdb3/core/api/',
'/influxdb3/enterprise/api/',
'/influxdb3/cloud-dedicated/api/',
'/influxdb3/cloud-serverless/api/',
'/influxdb3/clustered/api/',
'/influxdb/cloud/api/',
'/influxdb/v2/api/',
];
subjects.forEach((subject) => {
@ -365,6 +344,51 @@ describe('All endpoints page', () => {
});
});
/**
* API Download Button Tests
* Tests that each tag page has a download button linking to the correct spec
*/
describe('API spec download buttons', () => {
const downloadTests = [
{
page: '/influxdb3/core/api/write-data/',
specPath: '/openapi/influxdb3-core.yml',
},
{
page: '/influxdb3/enterprise/api/write-data/',
specPath: '/openapi/influxdb3-enterprise.yml',
},
];
downloadTests.forEach(({ page, specPath }) => {
describe(`Download button 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('has a download button', () => {
cy.get('.api-spec-download').should('exist');
});
it(`download button links to ${specPath}`, () => {
cy.get('.api-spec-download')
.should('have.attr', 'href', specPath)
.and('have.attr', 'download');
});
});
});
});
/**
* API Code Sample Tests
* Tests that inline curl examples render correctly on tag pages

View File

@ -1,17 +1,9 @@
{{/* 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 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", ""] */}}
{{ $pathParts := split .RelPermalink "/" }}
{{ $product := "" }}
{{ $version := "" }}
{{ if ge (len $pathParts) 3 }}
{{ $product = index $pathParts 1 }}
{{ $version = index $pathParts 2 }}
{{ end }}
{{/* API Documentation Layout
Two modes:
1. Section index (no 'tag' param) - lists tag pages from article data
2. Tag page (has 'tag' param) - shows operations via Hugo-native templates
Conceptual pages (isConceptual: true) show content without operations.
*/}}
<!-- USING_API_LIST_HTML_TEMPLATE -->
{{ partial "header.html" . }} {{ partial "topnav.html" . }}
@ -24,91 +16,40 @@ For conceptual pages (isConceptual: true), shows content without operations. */}
<article class="article article--content api-reference" role="main">
<header class="article--header">
<h1 class="article--title">{{ .Title }}</h1>
{{/* Only show description in header for section index pages, not tag
pages */}} {{/* Tag pages show their full description in the Overview
section below */}} {{ if not (isset .Params "tag") }} {{ with
.Description }}
<p class="article--description">{{ . }}</p>
{{ end }} {{ end }}
{{/* Only show description in header for section index pages */}}
{{ if not (isset .Params "tag") }}
{{ with .Description }}
<p class="article--description">{{ . }}</p>
{{ end }}
{{ end }}
</header>
{{/* Check if this is a section index (no tag param) or a tag page */}}
{{ $hasTag := isset .Params "tag" }} {{ if not $hasTag }} {{/* SECTION
INDEX - Show intro content then tag-based children */}}
{{/* Dual download buttons for Clustered and Cloud Dedicated */}}
{{ if or (eq $version "clustered") (eq $version "cloud-dedicated") }}
<div class="api-spec-actions api-spec-actions--dual">
<a href="/openapi/influxdb-{{ $version }}-v2-data-api.yml" class="btn api-spec-download" download>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M8 12L3 7h3V2h4v5h3L8 12z"/>
<path d="M1 14h14v2H1v-2z"/>
</svg>
Download Data API Spec
</a>
<a href="/openapi/influxdb-{{ $version }}-management-api.yml" class="btn api-spec-download" download>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M8 12L3 7h3V2h4v5h3L8 12z"/>
<path d="M1 14h14v2H1v-2z"/>
</svg>
Download Management API Spec
</a>
</div>
{{ end }}
{{ $hasTag := isset .Params "tag" }}
{{ if not $hasTag }}
{{/* SECTION INDEX - Show intro content then tag-based children */}}
{{ with .Content }}
<section class="api-section-content">{{ . }}</section>
{{ end }} {{/* Always show tag pages from article data */}} {{ partial
"api/section-children.html" . }} {{ else }} {{/* TAG PAGE - Show
operations or conceptual content */}} {{ $isConceptual :=
.Params.isConceptual | default false }} {{ if $isConceptual }} {{/*
Conceptual Page - Show content directly */}}
{{ end }}
{{ partial "api/section-children.html" . }}
{{ else }}
{{/* TAG PAGE - Show operations or conceptual content */}}
{{ $isConceptual := .Params.isConceptual | default false }}
{{ if $isConceptual }}
<section class="api-conceptual-content">
{{ with .Content }} {{ . }} {{ else }} {{ with .Params.tagDescription
}} {{ . | markdownify }} {{ end }} {{ end }}
{{ with .Content }} {{ . }} {{ else }}
{{ with .Params.tagDescription }}{{ . | markdownify }}{{ end }}
{{ end }}
</section>
{{ else }} {{/* Operational Page - Show all operations */}}
{{/* Download OpenAPI spec button - context-aware for Clustered/Cloud Dedicated */}}
{{ with .Params.staticFilePath }}
{{/* Extract product name from path like /openapi/influxdb-oss-v2/tags/... */}}
{{ $productName := replaceRE `^/openapi/([^/]+)/.*$` "$1" . }}
{{/* Check if this is a dual-API product (Clustered or Cloud Dedicated) */}}
{{ $isDualApi := or (strings.Contains $productName "clustered") (strings.Contains $productName "cloud-dedicated") }}
{{ if $isDualApi }}
{{/* Determine API type from path */}}
{{ $isManagementApi := strings.Contains . "management-api" }}
{{ if $isManagementApi }}
{{ $specPath := printf "/openapi/%s-management-api.yml" $productName }}
<div class="api-spec-actions">
<a href="{{ $specPath }}" class="btn api-spec-download" download>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M8 12L3 7h3V2h4v5h3L8 12z"/>
<path d="M1 14h14v2H1v-2z"/>
</svg>
Download Management API Spec
</a>
</div>
{{ else }}
{{ $specPath := printf "/openapi/%s-v2-data-api.yml" $productName }}
{{/* Operational Page - Show all operations */}}
{{/* Download OpenAPI spec button — uses specDownloadPath from frontmatter */}}
{{ with .Params.specDownloadPath }}
<div class="api-spec-actions">
<a href="{{ $specPath }}" class="btn api-spec-download" download>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M8 12L3 7h3V2h4v5h3L8 12z"/>
<path d="M1 14h14v2H1v-2z"/>
</svg>
Download Data API Spec
</a>
</div>
{{ end }}
{{ else }}
{{/* Single-spec products - existing behavior */}}
{{ $completeSpecPath := printf "/openapi/%s.yml" $productName }}
<div class="api-spec-actions">
<a href="{{ $completeSpecPath }}" class="btn api-spec-download" download>
<a href="{{ . }}" class="btn api-spec-download" download>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M8 12L3 7h3V2h4v5h3L8 12z"/>
<path d="M1 14h14v2H1v-2z"/>
@ -117,7 +58,6 @@ For conceptual pages (isConceptual: true), shows content without operations. */}
</a>
</div>
{{ end }}
{{ end }}
{{/* Hugo page content if any (for custom intro content) */}}
{{ with .Content }}
@ -257,23 +197,4 @@ For conceptual pages (isConceptual: true), shows content without operations. */}
}
}
/* Dual download buttons container for Clustered/Cloud Dedicated */
.api-spec-actions--dual {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin: 1rem 0;
}
/* Responsive - stack buttons on smaller screens */
@media (max-width: 600px) {
.api-spec-actions--dual {
flex-direction: column;
}
.api-spec-actions--dual .api-spec-download {
width: 100%;
justify-content: center;
}
}
</style>

View File

@ -5,50 +5,28 @@
Links point to tag pages with hash anchors (e.g., /api/cache-data/#operation/PostConfigureDistinctCache)
Excludes conceptual/trait tag operations.
Uses data from:
- data/article_data/influxdb/{product}/articles.yml
Uses frontmatter params:
- articleDataKey: product data key (e.g., 'influxdb3-core')
- articleSection: section slug (e.g., 'api' or 'management-api')
*/}}
{{ $currentPage := . }}
{{/* Extract product and version from URL */}}
{{ $productPathData := findRE "[^/]+.*?" .RelPermalink }}
{{ $product := index $productPathData 0 }}
{{ $version := index $productPathData 1 }}
{{/* Read data key and section from frontmatter */}}
{{ $dataKey := .Params.articleDataKey | default "" }}
{{ $section := .Params.articleSection | default "" }}
{{/* Build data key for article data lookup */}}
{{ $dataKey := "" }}
{{ if eq $product "influxdb3" }}
{{/* Core and Enterprise use influxdb3_ prefix; cloud-* and clustered don't */}}
{{ if or (eq $version "core") (eq $version "enterprise") }}
{{ $dataKey = print "influxdb3_" $version }}
{{ else }}
{{ $dataKey = $version }}
{{ end }}
{{ else if eq $product "influxdb" }}
{{/* Map URL versions to data directory names */}}
{{ if eq $version "v2" }}
{{ $dataKey = "oss-v2" }}
{{ else if eq $version "cloud" }}
{{ $dataKey = "cloud-v2" }}
{{ else if eq $version "v1" }}
{{ $dataKey = "oss-v1" }}
{{ else }}
{{ $dataKey = $version }}
{{ end }}
{{ else if eq $product "enterprise_influxdb" }}
{{ $dataKey = print "enterprise-" $version }}
{{ else }}
{{ $dataKey = $product }}
{{ end }}
{{/* Get article data for this product */}}
{{/* Get article data using frontmatter-driven lookup */}}
{{ $articles := slice }}
{{ with site.Data.article_data }}
{{ with index . "influxdb" }}
{{ with index . $dataKey }}
{{ with index . "articles" }}
{{ with .articles }}
{{ $articles = . }}
{{ if and $dataKey $section }}
{{ with site.Data.article_data }}
{{ with index . "influxdb" }}
{{ with index . $dataKey }}
{{ with index . $section }}
{{ with index . "articles" }}
{{ with .articles }}
{{ $articles = . }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
@ -144,7 +122,10 @@
{{ $op := .data.op }}
{{ $articlePath := .data.articlePath }}
{{/* Build tag page URL with hash anchor: operation/{operationId} */}}
{{ $tagPageUrl := printf "/%s/%s/%s/" $product $version $articlePath }}
{{/* Build tag page URL relative to the section (parent of all-endpoints) */}}
{{ $sectionUrl := $currentPage.Parent.RelPermalink }}
{{ $tagSlug := path.Base $articlePath }}
{{ $tagPageUrl := printf "%s%s/" $sectionUrl $tagSlug }}
{{ $hashAnchor := printf "#operation/%s" $op.operationId }}
<a href="{{ $tagPageUrl }}{{ $hashAnchor | safeURL }}" class="api-operation-card">
<span class="api-method api-method--{{ lower $op.method }}">{{ upper $op.method }}</span>
@ -165,7 +146,10 @@
{{ $op := .data.op }}
{{ $articlePath := .data.articlePath }}
{{/* Build tag page URL with hash anchor: operation/{operationId} */}}
{{ $tagPageUrl := printf "/%s/%s/%s/" $product $version $articlePath }}
{{/* Build tag page URL relative to the section (parent of all-endpoints) */}}
{{ $sectionUrl := $currentPage.Parent.RelPermalink }}
{{ $tagSlug := path.Base $articlePath }}
{{ $tagPageUrl := printf "%s%s/" $sectionUrl $tagSlug }}
{{ $hashAnchor := printf "#operation/%s" $op.operationId }}
<a href="{{ $tagPageUrl }}{{ $hashAnchor | safeURL }}" class="api-operation-card">
<span class="api-method api-method--{{ lower $op.method }}">{{ upper $op.method }}</span>
@ -187,7 +171,10 @@
{{ $op := .data.op }}
{{ $articlePath := .data.articlePath }}
{{/* Build tag page URL with hash anchor: operation/{operationId} */}}
{{ $tagPageUrl := printf "/%s/%s/%s/" $product $version $articlePath }}
{{/* Build tag page URL relative to the section (parent of all-endpoints) */}}
{{ $sectionUrl := $currentPage.Parent.RelPermalink }}
{{ $tagSlug := path.Base $articlePath }}
{{ $tagPageUrl := printf "%s%s/" $sectionUrl $tagSlug }}
{{ $hashAnchor := printf "#operation/%s" $op.operationId }}
<a href="{{ $tagPageUrl }}{{ $hashAnchor | safeURL }}" class="api-operation-card">
<span class="api-method api-method--{{ lower $op.method }}">{{ upper $op.method }}</span>
@ -215,7 +202,10 @@
{{ $op := .data.op }}
{{ $articlePath := .data.articlePath }}
{{/* Build tag page URL with hash anchor: operation/{operationId} */}}
{{ $tagPageUrl := printf "/%s/%s/%s/" $product $version $articlePath }}
{{/* Build tag page URL relative to the section (parent of all-endpoints) */}}
{{ $sectionUrl := $currentPage.Parent.RelPermalink }}
{{ $tagSlug := path.Base $articlePath }}
{{ $tagPageUrl := printf "%s%s/" $sectionUrl $tagSlug }}
{{ $hashAnchor := printf "#operation/%s" $op.operationId }}
<a href="{{ $tagPageUrl }}{{ $hashAnchor | safeURL }}" class="api-operation-card">
<span class="api-method api-method--{{ lower $op.method }}">{{ upper $op.method }}</span>

View File

@ -4,50 +4,30 @@
Renders tag pages from article data as a children list.
Sort order: conceptual tags (traitTags) first, then other tags alphabetically.
Uses data from:
- data/article_data/influxdb/{product}/articles.yml
Uses frontmatter params from generated pages:
- articleDataKey: product data key (e.g., 'influxdb3-core')
- articleSection: section slug (e.g., 'api' or 'management-api')
Data path: data/article_data/influxdb/{articleDataKey}/{articleSection}/articles.yml
*/}}
{{ $currentPage := . }}
{{/* Extract product and version from URL */}}
{{ $productPathData := findRE "[^/]+.*?" .RelPermalink }}
{{ $product := index $productPathData 0 }}
{{ $version := index $productPathData 1 }}
{{/* Read data key and section from frontmatter */}}
{{ $dataKey := .Params.articleDataKey | default "" }}
{{ $section := .Params.articleSection | default "" }}
{{/* Build data key for article data lookup */}}
{{ $dataKey := "" }}
{{ if eq $product "influxdb3" }}
{{/* Core and Enterprise use influxdb3_ prefix; cloud-* and clustered don't */}}
{{ if or (eq $version "core") (eq $version "enterprise") }}
{{ $dataKey = print "influxdb3_" $version }}
{{ else }}
{{ $dataKey = $version }}
{{ end }}
{{ else if eq $product "influxdb" }}
{{/* Map URL versions to data directory names */}}
{{ if eq $version "v2" }}
{{ $dataKey = "oss-v2" }}
{{ else if eq $version "cloud" }}
{{ $dataKey = "cloud-v2" }}
{{ else if eq $version "v1" }}
{{ $dataKey = "oss-v1" }}
{{ else }}
{{ $dataKey = $version }}
{{ end }}
{{ else if eq $product "enterprise_influxdb" }}
{{ $dataKey = print "enterprise-" $version }}
{{ else }}
{{ $dataKey = $product }}
{{ end }}
{{/* Get article data for this product */}}
{{/* Get article data using frontmatter-driven lookup */}}
{{ $articles := slice }}
{{ with site.Data.article_data }}
{{ with index . "influxdb" }}
{{ with index . $dataKey }}
{{ with index . "articles" }}
{{ with .articles }}
{{ $articles = . }}
{{ if and $dataKey $section }}
{{ with site.Data.article_data }}
{{ with index . "influxdb" }}
{{ with index . $dataKey }}
{{ with index . $section }}
{{ with index . "articles" }}
{{ with .articles }}
{{ $articles = . }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
@ -100,7 +80,8 @@
{{ $fields := index . "fields" }}
{{ $tag := index $fields "tag" }}
{{ $description := index $fields "description" | default "" }}
{{ $tagPageUrl := print "/" $product "/" $version "/" $path "/" | relURL }}
{{/* Build URL relative to the current section page */}}
{{ $tagPageUrl := print $currentPage.RelPermalink (path.Base $path) "/" | relURL }}
<h3 id="{{ $tag | urlize }}"><a href="{{ $tagPageUrl }}">{{ $tag }}</a></h3>
{{ with $description }}