From f5535c9d5cb567118b23f6b10435db74c0c417cc Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Sun, 15 Mar 2026 20:38:51 -0500 Subject: [PATCH] feat(api): Hugo-native API reference rendering Hugo-native templates for API reference documentation. Operations render as server-side HTML instead of client-side Redoc, providing faster page loads, full SEO indexability, and consistent styling. Architecture: - Tag-based navigation: operations grouped by OpenAPI tag, accessed via tag pages only (no individual operation URLs) - Generation script auto-discovers products from .config.yml files, deriving Hugo paths and menu keys from directory structure - Per-tag JSON/YAML chunks for fast Hugo template rendering - Inline curl examples generated from OpenAPI specs at build time Templates (layouts/api/, layouts/partials/api/): - tag-renderer.html: renders all operations for a tag - operation.html: individual operation with parameters, request body, responses, and schema rendering - code-sample.html: curl examples with Ask AI integration - section-children.html: tag listing on section index pages - all-endpoints-list.html: all operations sorted by path Generation (api-docs/scripts/generate-openapi-articles.ts): - Auto-discovery from .config.yml replaces hardcoded productConfigs - Each API section generates independently (no cross-spec merging) - Frontmatter-driven template data (specDownloadPath, articleDataKey) - Link transformation: /influxdb/version/ placeholders resolved to product-specific paths Removed: - Redoc renderer, JavaScript components, and CSS - Shadow DOM test infrastructure (~160 lines) - Operation page generation (dead generatePathPages function) - mergeArticleData() and slugifyDisplayName() Styles (assets/styles/layouts/_api-*.scss): - 3-column layout: sidebar, content, sticky TOC - Theme-aware code blocks for curl examples - Method badges (GET/POST/PUT/DELETE) with color coding - Collapsible schema sections with depth tracking Tests (cypress/e2e/content/api-reference.cy.js): - Tag page rendering with operation structure - Download button verification per section - All-endpoints page with operation cards - Related links from x-related OpenAPI extension - Code sample and Ask AI link validation --- .claude/agents/influxdb3-tech-writer.md | 28 +- .claude/agents/ts-component-dev.md | 2 +- .claude/skills/hugo-template-dev/SKILL.md | 18 + .claude/skills/vale-rule-config/SKILL.md | 6 +- .gitignore | 15 + AGENTS.md | 242 +- DOCS-CONTRIBUTING.md | 48 +- DOCS-TESTING.md | 14 +- PLAN.md | 58 + PLATFORM_REFERENCE.md | 78 +- SPELL-CHECK.md | 50 +- .../v1/v1/content/info.yml | 31 + .../v1/v1/content/servers.yml | 2 + api-docs/enterprise_influxdb/v1/v1/ref.yml | 1108 +++++++++ api-docs/generate-api-docs.sh | 8 +- api-docs/influxdb/cloud/tags.yml | 13 +- api-docs/influxdb/v1/v1/content/info.yml | 32 + api-docs/influxdb/v1/v1/content/servers.yml | 2 + api-docs/influxdb/v1/v1/ref.yml | 1093 +++++++++ api-docs/influxdb/v2/tags.yml | 13 +- api-docs/influxdb3/cloud-dedicated/tags.yml | 37 +- api-docs/influxdb3/cloud-serverless/tags.yml | 51 +- api-docs/influxdb3/clustered/tags.yml | 37 +- api-docs/influxdb3/core/tags.yml | 149 +- api-docs/influxdb3/enterprise/tags.yml | 136 +- .../scripts/dist/generate-openapi-articles.js | 850 +++++++ .../dist/openapi-paths-to-hugo-data/index.js | 920 ++++++++ api-docs/scripts/dist/post-process-specs.js | 334 +++ .../scripts/dist/test-post-process-specs.js | 490 ++++ api-docs/scripts/generate-openapi-articles.ts | 2059 +++++++---------- assets/js/components/api-toc.ts | 473 ++++ assets/js/content-interactions.js | 28 +- assets/js/main.js | 2 + assets/styles/layouts/_api-code-samples.scss | 67 + assets/styles/layouts/_api-layout.scss | 785 +++++++ assets/styles/layouts/_api-operations.scss | 540 +++++ assets/styles/layouts/_api-overrides.scss | 270 +-- .../styles/layouts/_api-security-schemes.scss | 92 + assets/styles/layouts/_sidebar.scss | 60 + assets/styles/styles-default.scss | 6 +- .../influxdb/cloud/reference/api/_index.md | 17 - content/influxdb/v2/reference/api/_index.md | 22 - .../cloud-dedicated/management-api/_index.md | 27 + .../management-api/all-endpoints/_index.md | 25 + .../cloud-dedicated/reference/api/_index.md | 41 - .../influxdb3/cloud-serverless/api/_index.md | 27 + .../api/api-compatibility/_index.md | 28 + .../api/authentication/_index.md | 39 + .../api/authorizations-api-tokens/_index.md | 61 + .../api/bucket-schemas/_index.md | 53 + .../cloud-serverless/api/buckets/_index.md | 115 + .../api/common-parameters/_index.md | 28 + .../cloud-serverless/api/dbrps/_index.md | 60 + .../cloud-serverless/api/delete/_index.md | 35 + .../cloud-serverless/api/headers/_index.md | 30 + .../api/invokable-scripts/_index.md | 71 + .../cloud-serverless/api/limits/_index.md | 35 + .../api/organizations/_index.md | 97 + .../cloud-serverless/api/pagination/_index.md | 28 + .../api/quick-start/_index.md | 28 + .../cloud-serverless/api/resources/_index.md | 35 + .../api/response-codes/_index.md | 28 + .../cloud-serverless/api/routes/_index.md | 33 + .../cloud-serverless/api/secrets/_index.md | 53 + .../security-and-access-endpoints/_index.md | 59 + .../api/supported-operations/_index.md | 28 + .../system-information-endpoints/_index.md | 41 + .../cloud-serverless/api/tasks/_index.md | 154 ++ .../cloud-serverless/api/telegrafs/_index.md | 113 + .../cloud-serverless/api/templates/_index.md | 77 + .../cloud-serverless/api/usage/_index.md | 34 + .../cloud-serverless/api/variables/_index.md | 81 + .../cloud-serverless/reference/api/_index.md | 31 - .../clustered/management-api/_index.md | 27 + .../management-api/all-endpoints/_index.md | 25 + .../clustered/reference/api/_index.md | 32 - .../influxdb3/core/reference/api/_index.md | 22 +- .../enterprise/reference/api/_index.md | 20 +- cypress/e2e/content/api-reference.cy.js | 549 ++++- data/products.yml | 52 +- ...ed-cloud-dedicated-api-structure-design.md | 154 ++ ...6-01-07-api-reference-rapidoc-migration.md | 184 ++ docs/plans/2026-01-21-api-tag-pages-design.md | 145 ++ .../2026-02-04-api-link-migration-design.md | 174 ++ ...02-04-api-link-migration-implementation.md | 605 +++++ .../2026-02-04-v1-api-deduplication-design.md | 93 + .../2026-02-13-hugo-native-api-migration.md | 344 +++ ...026-02-17-api-clean-regeneration-design.md | 160 ++ ...7-api-clean-regeneration-implementation.md | 519 +++++ .../2026-03-07-api-code-samples-design.md | 125 + docs/plans/TESTING.md | 77 + helper-scripts/migrate-api-links.cjs | 331 +++ layouts/{_default => }/LLMS-TXT-README.md | 94 +- layouts/_default/api.html | 112 +- layouts/api/all-endpoints.html | 48 + layouts/api/list.html | 200 ++ layouts/api/section.html | 86 + layouts/api/single.html | 152 ++ layouts/partials/api/all-endpoints-list.html | 221 ++ layouts/partials/api/code-sample.html | 235 ++ layouts/partials/api/normalize-path.html | 22 + layouts/partials/api/operation.html | 69 + layouts/partials/api/parameter-row.html | 65 + layouts/partials/api/parameters.html | 85 + layouts/partials/api/renderer.html | 12 + layouts/partials/api/request-body.html | 60 + layouts/partials/api/responses.html | 79 + layouts/partials/api/schema.html | 117 + layouts/partials/api/section-children.html | 112 + layouts/partials/api/security-schemes.html | 68 + layouts/partials/api/tag-renderer.html | 68 + layouts/partials/article/related.html | 39 +- layouts/partials/sidebar.html | 6 +- layouts/partials/sidebar/api-menu-items.html | 102 + layouts/partials/sidebar/nested-menu.html | 29 +- scripts/deploy-staging.sh | 2 + scripts/docs-cli/README.md | 2 +- .../__tests__/editor-resolver.test.js | 4 +- .../__tests__/process-manager.test.js | 4 +- scripts/docs-cli/lib/editor-resolver.js | 13 +- scripts/puppeteer/QUICK-REFERENCE.md | 26 +- scripts/puppeteer/README.md | 23 +- scripts/puppeteer/SETUP.md | 12 +- 123 files changed, 15616 insertions(+), 2070 deletions(-) create mode 100644 PLAN.md create mode 100644 api-docs/enterprise_influxdb/v1/v1/content/info.yml create mode 100644 api-docs/enterprise_influxdb/v1/v1/content/servers.yml create mode 100644 api-docs/enterprise_influxdb/v1/v1/ref.yml create mode 100644 api-docs/influxdb/v1/v1/content/info.yml create mode 100644 api-docs/influxdb/v1/v1/content/servers.yml create mode 100644 api-docs/influxdb/v1/v1/ref.yml create mode 100644 api-docs/scripts/dist/generate-openapi-articles.js create mode 100644 api-docs/scripts/dist/openapi-paths-to-hugo-data/index.js create mode 100644 api-docs/scripts/dist/post-process-specs.js create mode 100644 api-docs/scripts/dist/test-post-process-specs.js create mode 100644 assets/js/components/api-toc.ts create mode 100644 assets/styles/layouts/_api-code-samples.scss create mode 100644 assets/styles/layouts/_api-layout.scss create mode 100644 assets/styles/layouts/_api-operations.scss create mode 100644 assets/styles/layouts/_api-security-schemes.scss delete mode 100644 content/influxdb/cloud/reference/api/_index.md delete mode 100644 content/influxdb/v2/reference/api/_index.md create mode 100644 content/influxdb3/cloud-dedicated/management-api/_index.md create mode 100644 content/influxdb3/cloud-dedicated/management-api/all-endpoints/_index.md delete mode 100644 content/influxdb3/cloud-dedicated/reference/api/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/api-compatibility/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/authentication/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/authorizations-api-tokens/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/bucket-schemas/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/buckets/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/common-parameters/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/dbrps/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/delete/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/headers/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/invokable-scripts/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/limits/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/organizations/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/pagination/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/quick-start/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/resources/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/response-codes/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/routes/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/secrets/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/security-and-access-endpoints/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/supported-operations/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/system-information-endpoints/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/tasks/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/telegrafs/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/templates/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/usage/_index.md create mode 100644 content/influxdb3/cloud-serverless/api/variables/_index.md delete mode 100644 content/influxdb3/cloud-serverless/reference/api/_index.md create mode 100644 content/influxdb3/clustered/management-api/_index.md create mode 100644 content/influxdb3/clustered/management-api/all-endpoints/_index.md delete mode 100644 content/influxdb3/clustered/reference/api/_index.md create mode 100644 docs/plans/2025-02-06-clustered-cloud-dedicated-api-structure-design.md create mode 100644 docs/plans/2026-01-07-api-reference-rapidoc-migration.md create mode 100644 docs/plans/2026-01-21-api-tag-pages-design.md create mode 100644 docs/plans/2026-02-04-api-link-migration-design.md create mode 100644 docs/plans/2026-02-04-api-link-migration-implementation.md create mode 100644 docs/plans/2026-02-04-v1-api-deduplication-design.md create mode 100644 docs/plans/2026-02-13-hugo-native-api-migration.md create mode 100644 docs/plans/2026-02-17-api-clean-regeneration-design.md create mode 100644 docs/plans/2026-02-17-api-clean-regeneration-implementation.md create mode 100644 docs/plans/2026-03-07-api-code-samples-design.md create mode 100644 docs/plans/TESTING.md create mode 100755 helper-scripts/migrate-api-links.cjs rename layouts/{_default => }/LLMS-TXT-README.md (63%) create mode 100644 layouts/api/all-endpoints.html create mode 100644 layouts/api/list.html create mode 100644 layouts/api/section.html create mode 100644 layouts/api/single.html create mode 100644 layouts/partials/api/all-endpoints-list.html create mode 100644 layouts/partials/api/code-sample.html create mode 100644 layouts/partials/api/normalize-path.html create mode 100644 layouts/partials/api/operation.html create mode 100644 layouts/partials/api/parameter-row.html create mode 100644 layouts/partials/api/parameters.html create mode 100644 layouts/partials/api/renderer.html create mode 100644 layouts/partials/api/request-body.html create mode 100644 layouts/partials/api/responses.html create mode 100644 layouts/partials/api/schema.html create mode 100644 layouts/partials/api/section-children.html create mode 100644 layouts/partials/api/security-schemes.html create mode 100644 layouts/partials/api/tag-renderer.html create mode 100644 layouts/partials/sidebar/api-menu-items.html diff --git a/.claude/agents/influxdb3-tech-writer.md b/.claude/agents/influxdb3-tech-writer.md index 395312141..a7ce4cf09 100644 --- a/.claude/agents/influxdb3-tech-writer.md +++ b/.claude/agents/influxdb3-tech-writer.md @@ -9,6 +9,7 @@ You are an expert InfluxDB 3 technical writer with deep knowledge of InfluxData' ## Core Expertise Areas **InfluxDB 3 Products & Architecture:** + - **Self-hosted products:** - InfluxDB 3 Core (`influxdata/influxdb/influxdb3*` source code) - open source - InfluxDB 3 Enterprise - licensed @@ -29,6 +30,7 @@ You are an expert InfluxDB 3 technical writer with deep knowledge of InfluxData' - `content/shared/` - Shared content across products, versions, and editions **APIs & Interfaces:** + - InfluxDB 3 HTTP APIs: - v1 compatibility API (InfluxQL write/query) - v2 compatibility API (Flux) @@ -42,6 +44,7 @@ You are an expert InfluxDB 3 technical writer with deep knowledge of InfluxData' - Telegraf integration patterns and plugin ecosystem **Documentation Standards:** + - Google Developer Documentation Style guidelines - InfluxData documentation structure and conventions (from CLAUDE.md context) - Hugo shortcodes and frontmatter requirements @@ -51,6 +54,7 @@ You are an expert InfluxDB 3 technical writer with deep knowledge of InfluxData' ## Your Responsibilities **Content Creation & Review:** + - Write technically accurate documentation that reflects actual product behavior - Create comprehensive API documentation with proper OpenAPI specifications - Develop clear, testable code examples with proper annotations @@ -59,6 +63,7 @@ You are an expert InfluxDB 3 technical writer with deep knowledge of InfluxData' - Identify when content should be shared vs. product-specific **Technical Accuracy:** + - Verify code examples work with current product versions - Cross-reference implementation details with source code when needed - Validate API endpoints, parameters, and response formats @@ -71,6 +76,7 @@ You are an expert InfluxDB 3 technical writer with deep knowledge of InfluxData' - Cloud: Managed features, quotas, billing **Style & Standards Compliance:** + - Apply Google Developer Documentation Style consistently - Use semantic line feeds and proper Markdown formatting - Implement appropriate shortcodes for product-specific content @@ -80,30 +86,25 @@ You are an expert InfluxDB 3 technical writer with deep knowledge of InfluxData' ## Content Development Process -1. **Analyze Requirements:** +1. **Analyze Requirements:** - Understand the target audience, product version(s), and documentation type - Determine if content should be shared or product-specific - -2. **Research Implementation:** +2. **Research Implementation:** - Reference source code, APIs, and existing documentation for accuracy - Identify product-specific behaviors and differences - -3. **Structure Content:** +3. **Structure Content:** - Use appropriate frontmatter, headings, and shortcodes for the content type - Apply shared content pattern when content applies to multiple products - Use product-specific conditionals when needed - -4. **Create Examples:** +4. **Create Examples:** - Develop working, testable code examples with proper annotations - Include examples for relevant products and deployment models - -5. **Apply Standards:** +5. **Apply Standards:** - Ensure compliance with style guidelines and documentation conventions - Use docs CLI tools for content creation and validation - -6. **Cross-Reference:** +6. **Cross-Reference:** - Verify consistency with related documentation and product variants - - Add alt_links for cross-product navigation + - Add alt\_links for cross-product navigation - Link related concepts and procedures ## Quality Assurance @@ -114,11 +115,12 @@ You are an expert InfluxDB 3 technical writer with deep knowledge of InfluxData' - Use placeholder conventions consistently (UPPERCASE for user-replaceable values) - Ensure proper cross-linking between related concepts and procedures - Verify shared content works correctly across all target products -- Test cross-product navigation (alt_links) +- Test cross-product navigation (alt\_links) ## Product-Specific Considerations **When documenting, consider:** + - **Core vs Enterprise:** Feature availability (clustering, HA, RBAC) - **Self-hosted vs Cloud:** Configuration methods, authentication, quotas - **Clustered vs Dedicated:** Deployment model, scaling, management diff --git a/.claude/agents/ts-component-dev.md b/.claude/agents/ts-component-dev.md index 2f202d96e..107042460 100644 --- a/.claude/agents/ts-component-dev.md +++ b/.claude/agents/ts-component-dev.md @@ -197,7 +197,7 @@ assets/js/ │ ├── api-nav.ts # API navigation behavior │ ├── api-toc.ts # Table of contents │ ├── api-tabs.ts # Tab switching -│ └── api-scalar.ts # Scalar/RapiDoc integration +│ └── api-rapidoc.ts # RapiDoc integration └── utils/ ├── dom-helpers.ts # Shared DOM utilities └── debug-helpers.js # Debugging utilities diff --git a/.claude/skills/hugo-template-dev/SKILL.md b/.claude/skills/hugo-template-dev/SKILL.md index 2c2d7336a..b463ca1e8 100644 --- a/.claude/skills/hugo-template-dev/SKILL.md +++ b/.claude/skills/hugo-template-dev/SKILL.md @@ -353,6 +353,24 @@ The **only** acceptable inline scripts are minimal initialization that MUST run Everything else belongs in `assets/js/`. +### File Organization for Components + +``` +assets/ +├── js/ +│ ├── main.js # Entry point, component registry +│ ├── components/ +│ │ ├── api-nav.ts # API navigation behavior +│ │ ├── api-toc.ts # Table of contents +│ │ ├── api-tabs.ts # Tab switching (if needed beyond CSS) +│ │ └── api-rapidoc.ts # RapiDoc integration +│ └── utils/ +│ └── dom-helpers.ts # Shared DOM utilities +└── styles/ + └── layouts/ + └── _api-layout.scss # API-specific styles +``` + ### TypeScript Component Checklist When creating a new interactive feature: diff --git a/.claude/skills/vale-rule-config/SKILL.md b/.claude/skills/vale-rule-config/SKILL.md index 0ef5c04db..332b5115c 100644 --- a/.claude/skills/vale-rule-config/SKILL.md +++ b/.claude/skills/vale-rule-config/SKILL.md @@ -102,7 +102,6 @@ level: warning ### Critical: Vale Uses regexp2, Not RE2 - Vale uses the [regexp2](https://pkg.go.dev/github.com/dlclark/regexp2) library, **not** Go's standard `regexp` package (which uses RE2). This is a common source of confusion because Vale is written in Go. ### Supported Regex Features @@ -155,11 +154,13 @@ tokens: ### tokens vs raw **tokens:** + - Automatically wrapped in word boundaries - Converted to non-capturing groups - Good for simple patterns **raw:** + - Full control over the pattern - No automatic processing - Use for complex regex @@ -333,16 +334,19 @@ print(matches2) # Should be empty ### Common Issues **Pattern not matching:** + 1. Check if you need `nonword: true` for punctuation 2. Verify scope is appropriate (`sentence`, `heading`, etc.) 3. Test with `raw` instead of `tokens` for complex patterns **Too many false positives:** + 1. Add exceptions using negative lookahead/lookbehind 2. Adjust scope to be more specific 3. Consider using substitution rule with exceptions **Pattern works in Python but not Vale:** + - Unlikely if you're using PCRE features (Vale supports them) - Check for differences in whitespace handling - Try `raw` field for exact pattern control diff --git a/.gitignore b/.gitignore index 732fc4c94..f90d27f81 100644 --- a/.gitignore +++ b/.gitignore @@ -12,9 +12,22 @@ package-lock.json # Content generation /content/influxdb*/**/api/**/*.html +/content/influxdb*/**/api/**/*.md !api-docs/**/.config.yml /api-docs/redoc-static.html* /api-docs/_build/ + +# API documentation generation (generated by api-docs/scripts/) +/content/influxdb/*/api/** +/content/influxdb3/*/api/** +/content/influxdb3/*/reference/api/** +/content/enterprise_influxdb/*/api/** +/static/openapi +/data/article_data + +# Exception: hand-crafted API conceptual pages (not generated) +!/content/influxdb3/*/api/administration/ +!/content/influxdb3/*/api/administration/_index.md /helper-scripts/output/* /telegraf-build !telegraf-build/templates @@ -39,6 +52,8 @@ tmp # TypeScript build output **/dist/ +# Exception: include compiled API doc scripts for easier use +!api-docs/scripts/dist/ **/dist-lambda/ # User context files for AI assistant tools diff --git a/AGENTS.md b/AGENTS.md index bd27e9efa..9fc79161b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,22 +1,36 @@ # InfluxData Documentation (docs-v2) -> **Shared project guidelines for all AI assistants** +> **For general AI assistants (Claude, ChatGPT, Gemini, etc.)** +> +> This guide provides comprehensive instructions for AI assistants helping with the InfluxData documentation repository. It focuses on content creation, writing workflows, and style guidelines. > > **Other instruction resources**: -> - [.github/copilot-instructions.md](.github/copilot-instructions.md) - GitHub Copilot (CLI tools, workflows, repo structure) -> - [CLAUDE.md](CLAUDE.md) - Claude with MCP (pointer file) +> +> - [.github/copilot-instructions.md](.github/copilot-instructions.md) - For GitHub Copilot (focused on coding and automation) +> - [CLAUDE.md](CLAUDE.md) - For Claude with MCP (minimal pointer) > - [.claude/](.claude/) - Claude MCP configuration (commands, agents, skills) > - [.github/instructions/](.github/instructions/) - File pattern-specific instructions -## Commands +## Project Overview -| Task | Command | Notes | -|------|---------|-------| -| Install | `CYPRESS_INSTALL_BINARY=0 yarn install` | ~4s | -| Build | `npx hugo --quiet` | ~75s — **NEVER CANCEL** | -| Dev server | `npx hugo server` | ~92s, port 1313 | -| Test code blocks | `yarn test:codeblocks:all` | 15-45m — **NEVER CANCEL** | -| Lint | `yarn lint` | ~1m | +This repository powers [docs.influxdata.com](https://docs.influxdata.com), a Hugo-based static documentation site covering InfluxDB 3, InfluxDB v2/v1, Telegraf, and related products. + +**Key Characteristics:** + +- **Scale**: 5,359+ pages +- **Build time**: \~75 seconds (NEVER cancel Hugo builds) +- **Tech stack**: Hugo, Node.js, Docker, Vale, Pytest, Cypress +- **Test time**: 15-45 minutes for full code block tests + +## Quick Commands + +| Task | Command | Time | +| -------------------- | --------------------------------------- | ------ | +| Install dependencies | `CYPRESS_INSTALL_BINARY=0 yarn install` | \~4s | +| Build site | `npx hugo --quiet` | \~75s | +| Dev server | `npx hugo server` | \~92s | +| Test code blocks | `yarn test:codeblocks:all` | 15-45m | +| Lint | `yarn lint` | \~1m | ## Repository Structure @@ -31,7 +45,7 @@ docs-v2/ │ └── example.md # Shortcode testing playground ├── layouts/ # Hugo templates and shortcodes ├── assets/ # JS, CSS, TypeScript -├── api-docs/ # InfluxDB OpenAPI specifications, API reference documentation generation scripts +├── api-docs/ # OpenAPI specifications ├── data/ # YAML/JSON data files ├── public/ # Build output (gitignored, ~529MB) └── .github/ @@ -40,18 +54,19 @@ docs-v2/ **Content Paths**: See [copilot-instructions.md](.github/copilot-instructions.md#content-organization) -## Documentation MCP Server - -A hosted MCP server provides semantic search over all InfluxDB documentation. -Use it to verify technical accuracy, check API syntax, and find related docs. - -See the [InfluxDB documentation MCP server guide](https://docs.influxdata.com/influxdb3/core/admin/mcp-server/) for setup instructions. - ## Common Workflows -### Creating/Editing Content +### Editing a page in your browser + +1. Navigate to the desired page on [docs.influxdata.com](https://docs.influxdata.com) +2. Click the "Edit this page" link at the bottom +3. Make changes in the GitHub web editor +4. Commit changes via a pull request + +### Creating/Editing Content Manually **Frontmatter** (page metadata): + ```yaml title: Page Title # Required - becomes h1 description: Brief desc # Required - for SEO @@ -63,15 +78,18 @@ weight: 1 # Required - sort order ``` **Shared Content** (avoid duplication): + ```yaml source: /shared/path/to/content.md ``` Shared content files (`/shared/path/to/content.md`): + - Don't store frontmatter - Can use `{{% show-in %}}`, `{{% hide-in %}}`, and the `version` keyword (`/influxdb3/version/content.md`) **Common Shortcodes**: + - Callouts: `> [!Note]`, `> [!Warning]`, `> [!Important]`, `> [!Tip]` - Tabs: `{{< tabs-wrapper >}}` + `{{% tabs %}}` + `{{% tab-content %}}` - Required: `{{< req >}}` or `{{< req type="key" >}}` @@ -82,6 +100,7 @@ Shared content files (`/shared/path/to/content.md`): ### Testing Changes **Always test before committing**: + ```bash # Verify server renders (check 200 status) curl -s -o /dev/null -w "%{http_code}" http://localhost:1313/influxdb3/core/ @@ -95,83 +114,140 @@ yarn test:links content/influxdb3/core/**/*.md **📖 Complete Reference**: [DOCS-TESTING.md](DOCS-TESTING.md) +### Committing Changes -## Constraints +**Commit Message Format**: -- **NEVER cancel** Hugo builds (~75s) or test runs (15-45m) — the site has 5,359+ pages -- Set timeouts: Hugo 180s+, tests 30m+ -- Use `python` not `py` for code block language identifiers (pytest won't collect `py` blocks) -- Shared content files (`content/shared/`) have no frontmatter — the consuming page provides it -- Product names and versions come from `data/products.yml` (single source of truth) -- Commit format: `type(scope): description` — see [DOCS-CONTRIBUTING.md](DOCS-CONTRIBUTING.md#commit-guidelines) -- Network-restricted environments: Cypress (`CYPRESS_INSTALL_BINARY=0`), Docker builds, and Alpine packages may fail +``` +type(scope): description -## Style Rules +Examples: +- fix(enterprise): correct Docker environment variable +- feat(influxdb3): add new plugin documentation +- docs(core): update configuration examples +``` -Follows [Google Developer Documentation Style Guide](https://developers.google.com/style) with these project-specific additions: +**Types**: `fix`, `feat`, `style`, `refactor`, `test`, `chore` -- **Semantic line feeds** — one sentence per line (better diffs) -- **No h1 in content** — `title` frontmatter auto-generates h1 -- Active voice, present tense, second person -- Long options in CLI examples (`--output` not `-o`) -- Code blocks within 80 characters +**Scopes**: `enterprise`, `influxdb3`, `core`, `cloud`, `telegraf`, etc. -## Content Structure +**Pre-commit hooks** run automatically (Vale, Prettier, tests). Skip with: -**Required frontmatter**: `title`, `description`, `menu`, `weight` -— see [DOCS-FRONTMATTER.md](DOCS-FRONTMATTER.md) +```bash +git commit -m "message" --no-verify +``` -**Shared content**: `source: /shared/path/to/content.md` -— shared files use `{{% show-in %}}` / `{{% hide-in %}}` for product-specific content +**📖 Complete Reference**: [DOCS-CONTRIBUTING.md](DOCS-CONTRIBUTING.md#commit-guidelines) -**Shortcodes**: Callouts use `> [!Note]` / `> [!Warning]` syntax -— see [DOCS-SHORTCODES.md](DOCS-SHORTCODES.md) and [content/example.md](content/example.md) +## Key Patterns -## Product Content Paths +### Content Organization -Canonical paths from `data/products.yml`: +- **Product versions**: Managed in `/data/products.yml` +- **Semantic line feeds**: One sentence per line for better diffs +- **Heading hierarchy**: Use h2-h6 only (h1 auto-generated from frontmatter) +- **Image naming**: `project/version-context-description.png` -| Product | Content Path | -|---------|-------------| -| InfluxDB 3 Core | `content/influxdb3/core/` | -| InfluxDB 3 Enterprise | `content/influxdb3/enterprise/` | -| InfluxDB 3 Explorer | `content/influxdb3/explorer/` | -| InfluxDB Cloud Serverless | `content/influxdb3/cloud-serverless/` | -| InfluxDB Cloud Dedicated | `content/influxdb3/cloud-dedicated/` | -| InfluxDB Clustered | `content/influxdb3/clustered/` | -| InfluxDB OSS v2 | `content/influxdb/v2/` | -| InfluxDB OSS v1 | `content/influxdb/v1/` | -| InfluxDB Cloud (TSM) | `content/influxdb/cloud/` | -| InfluxDB Enterprise v1 | `content/enterprise_influxdb/` | -| Telegraf | `content/telegraf/` | -| Chronograf | `content/chronograf/` | -| Kapacitor | `content/kapacitor/` | -| Flux | `content/flux/` | -| Shared content | `content/shared/` | +### Code Examples -## Doc Review Pipeline +**Testable code blocks** (pytest): -Automated PR review for documentation changes. -See [.github/LABEL_GUIDE.md](.github/LABEL_GUIDE.md) for the label taxonomy. +```python +print("Hello, world!") +``` -| Resource | Path | -|----------|------| -| Label guide | [.github/LABEL_GUIDE.md](.github/LABEL_GUIDE.md) | -| Triage agent | [.claude/agents/doc-triage-agent.md](.claude/agents/doc-triage-agent.md) | -| Content review instructions | [.github/instructions/content-review.instructions.md](.github/instructions/content-review.instructions.md) | -| Review agent (local) | [.claude/agents/doc-review-agent.md](.claude/agents/doc-review-agent.md) | -| Auto-label workflow | [.github/workflows/auto-label.yml](.github/workflows/auto-label.yml) | -| Doc review workflow | [.github/workflows/doc-review.yml](.github/workflows/doc-review.yml) | + -## Reference +``` +Hello, world! +``` -| Document | Purpose | -|----------|---------| -| [DOCS-CONTRIBUTING.md](DOCS-CONTRIBUTING.md) | Style guidelines, commit format, contribution workflow | -| [DOCS-TESTING.md](DOCS-TESTING.md) | Code block testing, link validation, Vale linting | -| [DOCS-SHORTCODES.md](DOCS-SHORTCODES.md) | Complete shortcode reference | -| [DOCS-FRONTMATTER.md](DOCS-FRONTMATTER.md) | Complete frontmatter field reference | -| [api-docs/README.md](api-docs/README.md) | API documentation workflow | -| [content/example.md](content/example.md) | Live shortcode examples | -| [.github/copilot-instructions.md](.github/copilot-instructions.md) | CLI tools, repo structure, workflows | -| [.github/LABEL_GUIDE.md](.github/LABEL_GUIDE.md) | Label taxonomy and review pipeline | +**Language identifiers**: Use `python` not `py`, `bash` not `sh` (for pytest collection) + +### API Documentation + +- **Location**: `/api-docs/` directory +- **Format**: OpenAPI 3.0 YAML +- **Generation**: Uses Redoc + custom processing +- **📖 Workflow**: [api-docs/README.md](api-docs/README.md) + +### JavaScript/TypeScript + +- **Entry point**: `assets/js/main.js` +- **Pattern**: Component-based with `data-component` attributes +- **Debugging**: Source maps or debug helpers available +- **📖 Details**: [DOCS-CONTRIBUTING.md](DOCS-CONTRIBUTING.md#javascript-in-the-documentation-ui) + +## Important Constraints + +### Performance + +- **NEVER cancel Hugo builds** - they take \~75s normally +- **NEVER cancel test runs** - code block tests take 15-45 minutes +- **Set timeouts**: Hugo (180s+), tests (30+ minutes) + +### Style Guidelines + +- Use Google Developer Documentation style +- Active voice, present tense, second person for instructions +- No emojis unless explicitly requested +- Use long options in CLI examples (`--option` vs `-o`) +- Format code blocks within 80 characters + +### Network Restrictions + +Some operations may fail in restricted environments: + +- Docker builds requiring external repos +- `docker compose up local-dev` (Alpine packages) +- Cypress installation (use `CYPRESS_INSTALL_BINARY=0`) + +## Documentation References + +| Document | Purpose | +| ------------------------------------------------------------------ | ------------------------------------------------ | +| [DOCS-CONTRIBUTING.md](DOCS-CONTRIBUTING.md) | Contribution workflow, style guidelines | +| [DOCS-TESTING.md](DOCS-TESTING.md) | Testing procedures (code blocks, links, linting) | +| [DOCS-SHORTCODES.md](DOCS-SHORTCODES.md) | Complete shortcode reference | +| [DOCS-FRONTMATTER.md](DOCS-FRONTMATTER.md) | Complete frontmatter field reference | +| [.github/copilot-instructions.md](.github/copilot-instructions.md) | Primary AI assistant instructions | +| [api-docs/README.md](api-docs/README.md) | API documentation workflow | +| [content/example.md](content/example.md) | Live shortcode examples for testing | + +## Specialized Topics + +### Working with Specific Products + +| Product | Content Path | Special Notes | +| ------------------------ | ----------------------------------------------------------------------------- | --------------------------------------- | +| InfluxDB 3 Core | `/content/influxdb3/core/` | Latest architecture | +| InfluxDB 3 Enterprise | `/content/influxdb3/enterprise/` | Core + licensed features, clustered | +| InfluxDB Cloud Dedicated | `/content/influxdb3/cloud-dedicated/`, `/content/influxdb3/cloud-serverless/` | Managed and distributed | +| InfluxDB Clustered | `/content/influxdb3/clustered/` | Self-managed and distributed | +| InfluxDB Cloud | `/content/influxdb/cloud/` | Legacy but active | +| InfluxDB v2 | `/content/influxdb/v2/` | Legacy but active | +| InfluxDB Enterprise v1 | `/content/enterprise_influxdb/v1/` | Legacy but active enterprise, clustered | + +### Advanced Tasks + +- **Vale configuration**: `.ci/vale/styles/` for custom rules +- **Link checking**: Uses custom `link-checker` binary +- **Docker testing**: `compose.yaml` defines test services +- **Lefthook**: Git hooks configuration in `lefthook.yml` + +## Troubleshooting + +| Issue | Solution | +| ------------------------ | ------------------------------------------------------ | +| Pytest collected 0 items | Use `python` not `py` for code block language | +| Hugo build errors | Check `/config/_default/` configuration | +| Link validation slow | Test specific files: `yarn test:links content/file.md` | +| Vale errors | Check `.ci/vale/styles/config/vocabularies` | + +## Critical Reminders + +1. **Be a critical thinking partner** - Challenge assumptions, identify issues +2. **Test before committing** - Run relevant tests locally +3. **Reference, don't duplicate** - Link to detailed docs instead of copying +4. **Respect build times** - Don't cancel long-running operations +5. **Follow conventions** - Use established patterns for consistency diff --git a/DOCS-CONTRIBUTING.md b/DOCS-CONTRIBUTING.md index 7f0971553..5fcd6db1a 100644 --- a/DOCS-CONTRIBUTING.md +++ b/DOCS-CONTRIBUTING.md @@ -1,6 +1,7 @@ # Contributing to InfluxData Documentation + ## Quick Start Ready to contribute? @@ -14,7 +15,7 @@ Ready to contribute? For detailed setup and reference information, see the sections below. ---- +*** ## Legal & Getting Started @@ -27,18 +28,19 @@ What constitutes a "substantial" change is at the discretion of InfluxData docum [Sign the InfluxData CLA](https://www.influxdata.com/legal/cla/) -_**Note:** Typo and broken link fixes are greatly appreciated and do not require signing the CLA._ +***Note:** Typo and broken link fixes are greatly appreciated and do not require signing the CLA.* -_If you're new to contributing or you're looking for an easy update, see [`docs-v2` good-first-issues](https://github.com/influxdata/docs-v2/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue)._ +*If you're new to contributing or you're looking for an easy update, see [`docs-v2` good-first-issues](https://github.com/influxdata/docs-v2/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue).* ### Fork and clone InfluxData Documentation Repository [Fork this repository](https://help.github.com/articles/fork-a-repo/) and [clone it](https://help.github.com/articles/cloning-a-repository/) to your local machine. ---- +*** + ## Development Environment Setup ### Prerequisites @@ -76,9 +78,9 @@ dev dependencies used in pre-commit hooks for linting, syntax-checking, and test Dev dependencies include: - [Lefthook](https://github.com/evilmartians/lefthook): configures and -manages git pre-commit and pre-push hooks for linting and testing Markdown content. + manages git pre-commit and pre-push hooks for linting and testing Markdown content. - [prettier](https://prettier.io/docs/en/): formats code, including Markdown, according to style rules for consistency -- [Cypress]: e2e testing for UI elements and URLs in content +- \[Cypress]: e2e testing for UI elements and URLs in content ### Install Vale (style linting) @@ -114,11 +116,11 @@ docs-v2 contains a `./.vscode/settings.json` that configures the following exten - Vale: shows linter errors and suggestions in the editor. - YAML Schemas: validates frontmatter attributes. ---- +*** -## Making Changes +## Making Changes ### Style Guidelines @@ -130,7 +132,7 @@ Content follows Google Developer Documentation Style Guide and YouTube API docum Most docs-v2 documentation content uses [Markdown](https://en.wikipedia.org/wiki/Markdown). -_Some parts of the documentation, such as `./api-docs`, contain Markdown within YAML and rely on additional tooling._ +*Some parts of the documentation, such as `./api-docs`, contain Markdown within YAML and rely on additional tooling.* #### Semantic line feeds @@ -270,6 +272,7 @@ Documentation audit tools should: 4. Support both single-line and multi-line exclusion lists + ### Common Shortcodes Reference #### Callouts (notes and warnings) @@ -327,7 +330,7 @@ For the complete shortcodes reference with all available shortcodes and usage ex Test shortcodes with working examples in **[content/example.md](content/example.md)**. ---- +*** ### InfluxDB API documentation @@ -338,11 +341,10 @@ InfluxDB API documentation when documentation is deployed. For more information about editing and generating InfluxDB API documentation, see the [API Documentation README](https://github.com/influxdata/docs-v2/tree/master/api-docs#readme). ---- +*** ## Testing & Quality Assurance - Pre-commit hooks run automatically when you commit changes, testing your staged files with Vale, Prettier, Cypress, and Pytest. To skip hooks if needed: ```sh @@ -364,13 +366,14 @@ yarn test:links content/influxdb3/core/**/*.md For comprehensive testing information, including code block testing, link validation, style linting, and advanced testing procedures, see **[DOCS-TESTING.md](DOCS-TESTING.md)**. - ---- +*** + ## Submission Process + ### Commit Guidelines When creating commits, follow these guidelines: @@ -383,6 +386,7 @@ When creating commits, follow these guidelines: - For multiple issues, use comma separation: `closes influxdata/DAR#517, closes influxdata/DAR#518` **Examples:** + ``` fix(enterprise): correct Docker environment variable name for license email fix(influxdb3): correct Docker environment variable and compose examples for monolith @@ -394,7 +398,7 @@ chore(ci): update Vale configuration Push your changes up to your forked repository, then [create a new pull request](https://help.github.com/articles/creating-a-pull-request/). ---- +*** ## Reference Documentation @@ -404,6 +408,7 @@ For detailed reference documentation, see: - **[DOCS-SHORTCODES.md](DOCS-SHORTCODES.md)** - Complete shortcodes reference with usage examples for all available shortcodes + ### Advanced Configuration #### Vale style linting configuration @@ -434,6 +439,7 @@ To add accepted/rejected terms for specific products, configure a style for the To learn more about configuration and rules, see [Vale configuration](https://vale.sh/docs/topics/config). + #### JavaScript in the documentation UI The InfluxData documentation UI uses TypeScript and JavaScript with ES6+ syntax and @@ -450,13 +456,14 @@ If you're adding UI functionality that requires JavaScript, follow these steps: ```html
- ``` + ``` 2. Following the component pattern, create a single-purpose JavaScript module (`assets/js/components/my-component.js`) that exports a single function that receives the component element and initializes it. + 3. In `assets/js/main.js`, import the module and register the component to ensure - the component is initialized on page load. + the component is initialized on page load. ##### Debugging JavaScript @@ -470,7 +477,7 @@ To debug JavaScript code used in the InfluxData documentation UI, choose one of 1. In VS Code, select Run > Start Debugging. 2. Select the "Debug Docs (source maps)" configuration. 3. Click the play button to start the debugger. -5. Set breakpoints in the JavaScript source files--files in the +4. Set breakpoints in the JavaScript source files--files in the `assets/js/ns-hugo-imp:` namespace-- in the VS Code editor or in the Chrome Developer Tools Sources panel: @@ -484,8 +491,9 @@ To debug JavaScript code used in the InfluxData documentation UI, choose one of 1. In your JavaScript module, import debug helpers from `assets/js/utils/debug-helpers.js`. These helpers provide breakpoints and console logging as a workaround or alternative for using source maps and the Chrome DevTools debugger. + 2. Insert debug statements by calling the helper functions in your code--for example: - + ```js import { debugLog, debugBreak, debugInspect } from './utils/debug-helpers.js'; @@ -512,4 +520,4 @@ Your system uses the configuration in `launch.json` to launch the site in Chrome and attach the debugger to the Developer Tools console. Make sure to remove the debug statements before merging your changes. -The debug helpers are designed to be used in development and should not be used in production. \ No newline at end of file +The debug helpers are designed to be used in development and should not be used in production. diff --git a/DOCS-TESTING.md b/DOCS-TESTING.md index 17506fc04..95bf9ba94 100644 --- a/DOCS-TESTING.md +++ b/DOCS-TESTING.md @@ -11,13 +11,13 @@ This guide covers all testing procedures for the InfluxData documentation, inclu ## Test Types Overview -| Test Type | Purpose | Command | -| ----------------------- | ----------------------------------- | ---------------------------- | -| **Code blocks** | Validate shell/Python code examples | `yarn test:codeblocks:all` | -| **Link validation** | Check internal/external links | `yarn test:links` | -| **Style linting** | Enforce writing standards | `.ci/vale/vale.sh` | -| **Markdown generation** | Generate LLM-friendly Markdown | `yarn build:md` | -| **E2E tests** | UI and functionality testing | `yarn test:e2e` | +| Test Type | Purpose | Command | +| ----------------------- | ----------------------------------- | -------------------------- | +| **Code blocks** | Validate shell/Python code examples | `yarn test:codeblocks:all` | +| **Link validation** | Check internal/external links | `yarn test:links` | +| **Style linting** | Enforce writing standards | `.ci/vale/vale.sh` | +| **Markdown generation** | Generate LLM-friendly Markdown | `yarn build:md` | +| **E2E tests** | UI and functionality testing | `yarn test:e2e` | ## Code Block Testing diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 000000000..fca5b5330 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,58 @@ +--- +branch: feat-api-uplift +repo: docs-v2 +created: 2025-12-02T15:28:32Z +status: in-progress +--- + +# feat-api-uplift + +## Overview + +Replace the current API reference documentation implementation (RapiDoc web components) with Hugo-native templates. + +## Phase 1: Core Infrastructure (completed) + +### Build process + +- `yarn build:api` parses OpenAPI specs into Hugo data +- Generates Hugo pages with frontmatter for Algolia search integration +- Static JSON chunks for faster page loads + +### OpenAPI tag cleanup + +- Removed unused tags from OpenAPI specs +- Updated tags to be consistent and descriptive + +### Hugo-native POC + +- Implemented Hugo-native templates in `layouts/partials/api/hugo-native/` +- Tested with InfluxDB 3 Core product + +## Phase 2: Migration to Hugo-Native (in progress) + +**Plan**: @docs/plans/2026-02-13-hugo-native-api-migration.md + +### Task Order + +1. ✅ **Promote Hugo-native templates** - Move from POC to production +2. ✅ **Remove RapiDoc templates** - Delete templates and partials +3. ✅ **Remove RapiDoc JavaScript** - Delete components +4. ✅ **Remove operation pages** - Delete individual operation page generation +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. ✅ **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 + +- Branch: `feat-api-uplift` +- Plan: `plans/2026-02-13-hugo-native-api-migration.md` + +## Notes + +- Use Chrome devtools and Cypress to debug +- No individual operation pages - operations accessed only via tag pages diff --git a/PLATFORM_REFERENCE.md b/PLATFORM_REFERENCE.md index c4e5f5d67..374eb6be9 100644 --- a/PLATFORM_REFERENCE.md +++ b/PLATFORM_REFERENCE.md @@ -1,65 +1,79 @@ + Use the following information to help determine which InfluxDB version and product the user is asking about: InfluxDB OSS v2: - - Documentation: https://docs.influxdata.com/influxdb/v2/ - - Query languages: InfluxQL and Flux - - Clients: Telegraf, influx CLI, v1/v2 client libraries + +- Documentation: +- Query languages: InfluxQL and Flux +- Clients: Telegraf, influx CLI, v1/v2 client libraries InfluxDB OSS v1: - - Documentation: https://docs.influxdata.com/influxdb/v1/ - - Query languages: InfluxQL and Flux - - Clients: Telegraf, influx CLI, v1/v2 client libraries + +- Documentation: +- Query languages: InfluxQL and Flux +- Clients: Telegraf, influx CLI, v1/v2 client libraries InfluxDB Enterprise v1: - - Documentation: https://docs.influxdata.com/enterprise_influxdb/v1.12/ - - Query languages: InfluxQL and Flux - - Clients: Telegraf, influx CLI, v1/v2 client libraries + +- Documentation: +- Query languages: InfluxQL and Flux +- Clients: Telegraf, influx CLI, v1/v2 client libraries InfluxDB Cloud (TSM): - - Documentation: https://docs.influxdata.com/influxdb/cloud/ - - Query languages: InfluxQL and Flux - - Clients: Telegraf, influx CLI, v2 client libraries + +- Documentation: +- Query languages: InfluxQL and Flux +- Clients: Telegraf, influx CLI, v2 client libraries InfluxDB Cloud Serverless: - - Documentation: https://docs.influxdata.com/influxdb3/cloud-serverless/ - - Query languages: SQL and InfluxQL and Flux - - Clients: Telegraf, influxctl CLI, v3 client libraries + +- Documentation: +- Query languages: SQL and InfluxQL and Flux +- Clients: Telegraf, influxctl CLI, v3 client libraries InfluxDB Cloud Dedicated: - - Documentation: https://docs.influxdata.com/influxdb3/cloud-dedicated/ - - Query languages: SQL and InfluxQL - - Clients: Telegraf, influxctl CLI, v3 client libraries + +- Documentation: +- Query languages: SQL and InfluxQL +- Clients: Telegraf, influxctl CLI, v3 client libraries InfluxDB Clustered: - - Documentation: https://docs.influxdata.com/influxdb3/clustered/ - - Query languages: SQL and InfluxQL - - Clients: Telegraf, influxctl CLI, v3 client libraries + +- Documentation: +- Query languages: SQL and InfluxQL +- Clients: Telegraf, influxctl CLI, v3 client libraries InfluxDB 3 Core: - - Documentation: https://docs.influxdata.com/influxdb3/core/ - - Query languages: SQL and InfluxQL - - Clients: Telegraf, influxdb3 CLI, v3 client libraries, InfluxDB 3 Explorer + +- Documentation: +- Query languages: SQL and InfluxQL +- Clients: Telegraf, influxdb3 CLI, v3 client libraries, InfluxDB 3 Explorer InfluxDB 3 Enterprise: - - Documentation: https://docs.influxdata.com/influxdb3/enterprise/ - - Query languages: SQL and InfluxQL - - Clients: Telegraf, influxdb3 CLI, v3 client libraries, InfluxDB 3 Explorer + +- Documentation: +- Query languages: SQL and InfluxQL +- Clients: Telegraf, influxdb3 CLI, v3 client libraries, InfluxDB 3 Explorer InfluxDB 3 Explorer: - - Documentation: https://docs.influxdata.com/influxdb3/explorer/ + +- Documentation: Telegraf: - - Documentation: https://docs.influxdata.com/telegraf/v1.37/ + +- Documentation: Chronograf: - - Documentation: https://docs.influxdata.com/chronograf/v1.11/ + +- Documentation: Kapacitor: - - Documentation: https://docs.influxdata.com/kapacitor/v1.8/ + +- Documentation: Flux: - - Documentation: https://docs.influxdata.com/flux/v0.x/ +- Documentation: diff --git a/SPELL-CHECK.md b/SPELL-CHECK.md index 4ef0eeb6d..f66336fce 100644 --- a/SPELL-CHECK.md +++ b/SPELL-CHECK.md @@ -11,14 +11,14 @@ The docs-v2 repository uses **two complementary spell-checking tools**: ## Tool Comparison -| Feature | Vale | Codespell | -|---------|------|-----------| -| **Purpose** | Document spell checking | Code comment spell checking | -| **Integration** | Pre-commit hooks (Docker) | CI/CD pipeline | -| **False Positives** | Low (comprehensive filters) | Low (clear dictionary only) | -| **Customization** | YAML rules | INI config + dictionary lists | -| **Performance** | Moderate | Fast | -| **True Positive Detection** | Document-level | Code-level | +| Feature | Vale | Codespell | +| --------------------------- | --------------------------- | ----------------------------- | +| **Purpose** | Document spell checking | Code comment spell checking | +| **Integration** | Pre-commit hooks (Docker) | CI/CD pipeline | +| **False Positives** | Low (comprehensive filters) | Low (clear dictionary only) | +| **Customization** | YAML rules | INI config + dictionary lists | +| **Performance** | Moderate | Fast | +| **True Positive Detection** | Document-level | Code-level | ## Vale Configuration @@ -51,17 +51,20 @@ Unlike other documentation style checkers, this configuration **intentionally in ### Filter Patterns Explained -#### 1. camelCase and snake_case Identifiers +#### 1. camelCase and snake\_case Identifiers + ```regex (?:_*[a-z]+(?:[A-Z][a-z0-9]*)+(?:[A-Z][a-zA-Z0-9]*)*|[a-z_][a-z0-9]*_[a-z0-9_]*) ``` + **Why**: Prevents false positives on variable/method names while NOT matching normal prose **Breakdown**: + - **camelCase**: `_*[a-z]+(?:[A-Z][a-z0-9]*)+(?:[A-Z][a-zA-Z0-9]*)*` - Requires at least one uppercase letter (distinguishes `myVariable` from `provide`) - Allows leading underscores for private variables (`_privateVar`, `__dunder__`) -- **snake_case**: `[a-z_][a-z0-9]*_[a-z0-9_]*` +- **snake\_case**: `[a-z_][a-z0-9]*_[a-z0-9_]*` - Requires at least one underscore - Distinguishes `my_variable` from normal words @@ -69,49 +72,61 @@ Unlike other documentation style checkers, this configuration **intentionally in **Examples NOT Ignored** (caught by spell-checker): `provide`, `database`, `variable` (normal prose) -#### 2. UPPER_CASE Constants +#### 2. UPPER\_CASE Constants + ```regex [A-Z_][A-Z0-9_]+ ``` + **Why**: Prevents false positives on environment variables and constants **Examples Ignored**: `API_KEY`, `AWS_REGION`, `INFLUXDB_TOKEN` **Note**: Matches AWS, API (even single uppercase acronyms) - acceptable in docs #### 3. Version Numbers + ```regex \d+\.\d+(?:\.\d+)* ``` + **Why**: Version numbers aren't words **Examples Ignored**: `1.0`, `2.3.1`, `0.101.0`, `1.2.3.4`, `v1.2.3` **Note**: Handles any number of version parts (2-part, 3-part, 4-part, etc.) #### 4. Hexadecimal Values + ```regex 0[xX][0-9a-fA-F]+ ``` + **Why**: Hex values appear in code and aren't dictionary words **Examples Ignored**: `0xFF`, `0xDEADBEEF`, `0x1A` #### 5. URLs and Paths + ```regex /[a-zA-Z0-9/_\-\.\{\}]+ # Paths: /api/v2/write https?://[^\s\)\]>"]+ # Full URLs: https://docs.example.com ``` + **Why**: URLs contain hyphens, slashes, and special chars **Examples Ignored**: `/api/v2/write`, `/kapacitor/v1/`, `https://docs.influxdata.com` #### 6. Shortcode Attributes + ```regex (?:endpoint|method|url|href|src|path)="[^"]+" ``` + **Why**: Hugo shortcode attribute values often contain hyphens and special chars **Examples Ignored**: `endpoint="https://..."`, `method="POST"` **Future Enhancement**: Add more attributes as needed (name, value, data, etc.) #### 7. Code Punctuation + ```regex [@#$%^&*()_+=\[\]{};:,.<>?/\\|-]+ ``` + **Why**: Symbols and special characters aren't words **Examples Ignored**: `()`, `{}`, `[]`, `->`, `=>`, `|`, etc. @@ -134,15 +149,15 @@ To add a word that should be ignored, edit the appropriate file. - `clear` - Unambiguous spelling errors only - Examples: "recieve" → "receive", "occured" → "occurred" - - False positive rate: ~1% + - False positive rate: \~1% - `rare` - Includes uncommon but valid English words - Would flag legitimate technical terms - - False positive rate: ~15-20% + - False positive rate: \~15-20% - `code` - Includes code-specific words - Too aggressive for documentation - - False positive rate: ~25-30% + - False positive rate: \~25-30% #### Skip Directories @@ -167,6 +182,7 @@ ignore-words-list = aks,invokable - **`invokable`** - InfluxData product branding term (scriptable tasks/queries) **To add more**: + 1. Edit `.codespellrc` 2. Add word to `ignore-words-list` (comma-separated) 3. Add inline comment explaining why @@ -178,6 +194,7 @@ ignore-words-list = aks,invokable Vale automatically runs on files you commit via Lefthook. **Manual check**: + ```bash # Check all content docker compose run -T vale content/**/*.md @@ -233,7 +250,8 @@ echo "recieve the data" | codespell **Problem**: Vale flags a word that should be valid **Solutions**: -1. Check if it's a code identifier (camelCase, UPPER_CASE, hex, version) + +1. Check if it's a code identifier (camelCase, UPPER\_CASE, hex, version) 2. Add to `InfluxDataDocs/Terms/ignore.txt` if it's a technical term 3. Add filter pattern to `.ci/vale/styles/InfluxDataDocs/Spelling.yml` if it's a pattern @@ -242,6 +260,7 @@ echo "recieve the data" | codespell **Problem**: Codespell flags a legitimate term **Solutions**: + 1. Add to `ignore-words-list` in `.codespellrc` 2. Add skip directory if entire directory should be excluded 3. Use `-i 3` (interactive mode) to review before accepting @@ -251,6 +270,7 @@ echo "recieve the data" | codespell **Problem**: A real typo isn't caught **Solutions**: + 1. Verify it's actually a typo (not a branding term or intentional) 2. Check if it's in excluded scope (tables, URLs, code identifiers) 3. Report as GitHub issue for tool improvement diff --git a/api-docs/enterprise_influxdb/v1/v1/content/info.yml b/api-docs/enterprise_influxdb/v1/v1/content/info.yml new file mode 100644 index 000000000..470121c38 --- /dev/null +++ b/api-docs/enterprise_influxdb/v1/v1/content/info.yml @@ -0,0 +1,31 @@ +title: InfluxDB Enterprise v1 HTTP API +x-influxdata-short-title: InfluxDB Enterprise v1 API +x-influxdata-short-description: >- + The InfluxDB Enterprise v1 HTTP API provides a programmatic interface for writing, + querying, and managing InfluxDB Enterprise v1 clusters. +version: 1.11.6 +description: | + The InfluxDB Enterprise v1 HTTP API provides a simple way to interact with the database. + It uses HTTP response codes, authentication with username and password credentials + or API tokens, and JSON-formatted response data. + + ## Cluster Features + + InfluxDB Enterprise includes additional parameters for cluster operations: + - **Write Consistency**: Control write consistency across cluster nodes + + ## Authentication + + InfluxDB Enterprise v1 supports multiple authentication methods: + + - **Basic Authentication**: Use HTTP Basic Auth with username and password + - **Query String Authentication**: Pass `u` (username) and `p` (password) as query parameters + - **Token Authentication** (v2-compatible): Use `Authorization: Token username:password` header + + Authentication is optional unless [enabled in the configuration](/enterprise_influxdb/v1/administration/authentication_and_authorization/). +license: + name: Proprietary + url: https://www.influxdata.com/legal/slsa/ +contact: + name: InfluxData + url: https://www.influxdata.com diff --git a/api-docs/enterprise_influxdb/v1/v1/content/servers.yml b/api-docs/enterprise_influxdb/v1/v1/content/servers.yml new file mode 100644 index 000000000..56066093b --- /dev/null +++ b/api-docs/enterprise_influxdb/v1/v1/content/servers.yml @@ -0,0 +1,2 @@ +- url: http://localhost:8086 + description: Local InfluxDB Enterprise data node diff --git a/api-docs/enterprise_influxdb/v1/v1/ref.yml b/api-docs/enterprise_influxdb/v1/v1/ref.yml new file mode 100644 index 000000000..becac77be --- /dev/null +++ b/api-docs/enterprise_influxdb/v1/v1/ref.yml @@ -0,0 +1,1108 @@ +openapi: 3.0.0 +info: + title: InfluxDB Enterprise v1 HTTP API + version: 1.11.6 + description: | + The InfluxDB Enterprise v1 HTTP API provides a simple way to interact with the database. + It uses HTTP response codes, authentication with username and password credentials + or API tokens, and JSON-formatted response data. + + ## Cluster Features + + InfluxDB Enterprise includes additional parameters for cluster operations: + - **Write Consistency**: Control write consistency across cluster nodes + + ## Authentication + + InfluxDB Enterprise v1 supports multiple authentication methods: + + - **Basic Authentication**: Use HTTP Basic Auth with username and password + - **Query String Authentication**: Pass `u` (username) and `p` (password) as query parameters + - **Token Authentication** (v2-compatible): Use `Authorization: Token username:password` header + + Authentication is optional unless [enabled in the configuration](/enterprise_influxdb/v1/administration/authentication_and_authorization/). + contact: + name: InfluxData + url: https://www.influxdata.com + license: + name: Proprietary + url: https://www.influxdata.com/legal/slsa/ +servers: + - url: http://localhost:8086 + description: Local InfluxDB Enterprise data node +security: + - BasicAuth: [] + - QueryAuth: [] +tags: + - name: System Information + description: | + Endpoints for checking server status, health, and version information. + - name: Query + description: | + Query data using InfluxQL. The `/query` endpoint supports both read queries + (SELECT, SHOW) and write queries (CREATE, DROP, ALTER, etc.). + - name: Write + description: | + Write time series data using InfluxDB line protocol. + + **Enterprise Feature**: Use the `consistency` parameter to control write consistency + across cluster nodes. + - name: Debug + description: | + Debugging and profiling endpoints for troubleshooting and performance analysis. + - name: v2 Compatibility + description: | + InfluxDB 2.x API compatibility endpoints. These endpoints allow you to use + InfluxDB 2.x client libraries with InfluxDB Enterprise 1.8+. + + Use the `Token` scheme with v1.x credentials: + ``` + Authorization: Token username:password + ``` + - name: Authentication + x-traitTag: true + description: | + InfluxDB Enterprise v1 supports multiple authentication methods: + + ### Basic Authentication + ```bash + curl -u username:password http://localhost:8086/query?q=SHOW+DATABASES + ``` + + ### Query String Authentication + ```bash + curl "http://localhost:8086/query?u=username&p=password&q=SHOW+DATABASES" + ``` + + ### Token Authentication (v2-compatible) + For v2-compatible endpoints, use the Token scheme: + ```bash + curl -H "Authorization: Token username:password" http://localhost:8086/api/v2/query + ``` +paths: + /ping: + get: + operationId: GetPing + summary: Check server status + description: | + Check the status of your InfluxDB Enterprise instance and retrieve version information. + The `/ping` endpoint returns a `204 No Content` response by default. + + Use the `verbose=true` query parameter to return a `200 OK` response, + which is required for [Google Cloud Load Balancing](https://cloud.google.com/load-balancing/docs/health-check-concepts) health checks. + tags: + - System Information + parameters: + - name: verbose + in: query + description: | + When `true`, returns HTTP 200 instead of 204. Required for Google Cloud Load Balancing health checks. + schema: + type: boolean + default: false + responses: + '200': + description: Server is running (verbose mode) + headers: + X-Influxdb-Build: + description: InfluxDB build type (`ENT` for Enterprise) + schema: + type: string + enum: + - ENT + X-Influxdb-Version: + description: InfluxDB version + schema: + type: string + '204': + description: Server is running + headers: + X-Influxdb-Build: + description: InfluxDB build type (`ENT` for Enterprise) + schema: + type: string + enum: + - ENT + X-Influxdb-Version: + description: InfluxDB version + schema: + type: string + head: + operationId: HeadPing + summary: Check server status (HEAD) + description: | + Check the status of your InfluxDB Enterprise instance using a HEAD request. + Returns the same headers as GET but without a response body. + tags: + - System Information + responses: + '204': + description: Server is running + headers: + X-Influxdb-Build: + description: InfluxDB build type (`ENT` for Enterprise) + schema: + type: string + enum: + - ENT + X-Influxdb-Version: + description: InfluxDB version + schema: + type: string + /health: + get: + operationId: GetHealth + summary: Check server health + description: | + Check the health of your InfluxDB Enterprise instance. + Returns health status as JSON with a `pass` or `fail` status. + tags: + - System Information + - v2 Compatibility + responses: + '200': + description: Server is healthy + content: + application/json: + schema: + $ref: '#/components/schemas/HealthCheck' + example: + name: influxdb + message: ready for queries and writes + status: pass + version: 1.11.6 + '503': + description: Server is unhealthy + content: + application/json: + schema: + $ref: '#/components/schemas/HealthCheck' + example: + name: influxdb + message: service unavailable + status: fail + version: 1.11.6 + /query: + get: + operationId: GetQuery + summary: Query data (GET) + description: | + Query data using InfluxQL. Use GET for read-only queries that start with: + - `SELECT` (except queries with `INTO` clause) + - `SHOW` + + For write operations (CREATE, DROP, ALTER, etc.), use POST. + tags: + - Query + parameters: + - $ref: '#/components/parameters/QueryDB' + - $ref: '#/components/parameters/QueryQ' + - $ref: '#/components/parameters/QueryEpoch' + - $ref: '#/components/parameters/QueryPretty' + - $ref: '#/components/parameters/QueryChunked' + - $ref: '#/components/parameters/AuthUsername' + - $ref: '#/components/parameters/AuthPassword' + responses: + '200': + description: Query executed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/QueryResponse' + example: + results: + - statement_id: 0 + series: + - name: mymeas + columns: + - time + - myfield + - mytag1 + - mytag2 + values: + - - '2017-03-01T00:16:18Z' + - 33.1 + - null + - null + - - '2017-03-01T00:17:18Z' + - 12.4 + - '12' + - '14' + application/csv: + schema: + type: string + example: | + name,tags,time,myfield,mytag1,mytag2 + mymeas,,1488327378000000000,33.1,, + mymeas,,1488327438000000000,12.4,12,14 + '400': + description: Bad request (syntax error in query) + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: 'error parsing query: found EOF, expected FROM at line 1, char 9' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: authorization failed + post: + operationId: PostQuery + summary: Query data (POST) + description: | + Query data or execute database management commands using InfluxQL. + Use POST for queries that start with: + - `SELECT` with `INTO` clause + - `ALTER` + - `CREATE` + - `DELETE` + - `DROP` + - `GRANT` + - `KILL` + - `REVOKE` + tags: + - Query + parameters: + - $ref: '#/components/parameters/QueryDB' + - $ref: '#/components/parameters/QueryEpoch' + - $ref: '#/components/parameters/QueryPretty' + - $ref: '#/components/parameters/QueryChunked' + - $ref: '#/components/parameters/AuthUsername' + - $ref: '#/components/parameters/AuthPassword' + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - q + properties: + q: + type: string + description: InfluxQL query string + example: CREATE DATABASE mydb + params: + type: string + description: | + JSON object containing bind parameter values. + Use `$` syntax in the query to reference parameters. + example: '{"tag_value":"12","field_value":30}' + multipart/form-data: + schema: + type: object + properties: + q: + type: string + format: binary + description: File containing InfluxQL queries (separated by semicolons) + async: + type: boolean + description: Execute queries asynchronously + default: false + responses: + '200': + description: Query executed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/QueryResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /write: + post: + operationId: PostWrite + summary: Write data + description: | + Write time series data to InfluxDB Enterprise using line protocol format. + + Data must be sent as binary encoded line protocol in the request body. + Use the `--data-binary` flag with curl to preserve newlines. + + **Enterprise Feature**: Use the `consistency` parameter to control write + consistency across cluster nodes. + + **Best Practices:** + - Write points in batches of 5,000 to 10,000 for optimal performance + - Use the least precise timestamp precision possible for better compression + tags: + - Write + parameters: + - name: db + in: query + required: true + description: Target database for the write + schema: + type: string + example: mydb + - name: rp + in: query + description: | + Target retention policy. If not specified, writes to the default retention policy. + schema: + type: string + - name: precision + in: query + description: | + Timestamp precision. InfluxDB assumes nanoseconds if not specified. + schema: + type: string + enum: + - 'n' + - u + - ms + - s + - m + - h + default: 'n' + - name: consistency + in: query + description: | + **Enterprise only.** Sets the write consistency for the point. + See [write consistency documentation](/enterprise_influxdb/v1/concepts/clustering#write-consistency) + for detailed descriptions of each option. + schema: + type: string + enum: + - any + - one + - quorum + - all + default: one + - $ref: '#/components/parameters/AuthUsername' + - $ref: '#/components/parameters/AuthPassword' + requestBody: + required: true + description: | + Line protocol data. Multiple points should be separated by newlines. + Use `@filename` to write from a file. + content: + text/plain: + schema: + type: string + example: | + mymeas,mytag=1 myfield=90 1463683075000000000 + mymeas,mytag=2 myfield=34 1463683076000000000 + responses: + '204': + description: Write successful + '400': + description: Bad request (line protocol syntax error or type conflict) + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + badTimestamp: + summary: Invalid timestamp + value: + error: 'unable to parse ''mymeas,mytag=1 myfield=91 abc123'': bad timestamp' + typeConflict: + summary: Field type conflict + value: + error: 'field type conflict: input field "myfield" on measurement "mymeas" is type int64, already exists as type float' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Database not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: 'database not found: "mydb1"' + '413': + description: Request entity too large + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: Request Entity Too Large + '500': + description: Internal server error (e.g., retention policy not found) + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: 'retention policy not found: myrp' + /debug/pprof: + get: + operationId: GetDebugPprof + summary: Get profiling index + description: | + Returns an HTML page listing available Go pprof profiles. + Use the individual profile endpoints to retrieve specific profile data. + tags: + - Debug + responses: + '200': + description: HTML page with profile links + content: + text/html: + schema: + type: string + /debug/pprof/{profile}: + get: + operationId: GetDebugPprofProfile + summary: Get profile data + description: | + Retrieve a specific Go pprof profile. Available profiles: + - `block`: Stack traces that led to blocking on synchronization primitives + - `goroutine`: Stack traces of all current goroutines + - `heap`: Sampling of stack traces for heap allocations + - `mutex`: Stack traces of holders of contended mutexes + - `threadcreate`: Stack traces that led to creation of new OS threads + - `profile`: CPU profile (use `seconds` parameter to specify duration) + - `trace`: Execution trace (use `seconds` parameter to specify duration) + tags: + - Debug + parameters: + - name: profile + in: path + required: true + description: Profile name + schema: + type: string + enum: + - block + - goroutine + - heap + - mutex + - threadcreate + - profile + - trace + - allocs + - cmdline + - name: seconds + in: query + description: Duration in seconds for CPU profile or trace + schema: + type: integer + default: 30 + - name: debug + in: query + description: Return human-readable text output instead of binary + schema: + type: integer + enum: + - 0 + - 1 + default: 0 + responses: + '200': + description: Profile data + content: + application/octet-stream: + schema: + type: string + format: binary + text/plain: + schema: + type: string + /debug/pprof/all: + get: + operationId: GetDebugPprofAll + summary: Get all profiles archive + description: | + Generate a `profiles.tar.gz` archive containing all standard Go profiling + information and additional debugging data. Intended primarily for use by + InfluxData support. + + Use the `cpu` parameter to include a CPU profile of the specified duration. + tags: + - Debug + parameters: + - name: cpu + in: query + description: | + Duration for CPU profile. Specify as a duration string (e.g., `30s`). + schema: + type: string + example: 30s + responses: + '200': + description: Compressed archive containing all profiles + content: + application/gzip: + schema: + type: string + format: binary + /debug/requests: + get: + operationId: GetDebugRequests + summary: Track HTTP requests + description: | + Track HTTP client requests to the `/write` and `/query` endpoints. + Returns the number of writes and queries per username and IP address + over the specified time interval. + tags: + - Debug + parameters: + - name: seconds + in: query + description: Duration in seconds to collect request data + schema: + type: integer + default: 10 + responses: + '200': + description: Request statistics by user and IP + content: + application/json: + schema: + type: object + additionalProperties: + type: object + properties: + writes: + type: integer + queries: + type: integer + example: + user1:123.45.678.91: + writes: 1 + queries: 0 + user1:000.0.0.0: + writes: 0 + queries: 16 + /debug/vars: + get: + operationId: GetDebugVars + summary: Get server statistics + description: | + Retrieve runtime statistics and information about the InfluxDB Enterprise instance. + Returns detailed metrics in JSON format including memory usage, + goroutine counts, and database statistics. + + The [InfluxDB Telegraf input plugin](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/influxdb) + can collect these metrics automatically. + tags: + - Debug + responses: + '200': + description: Server statistics in JSON format + content: + application/json: + schema: + type: object + description: Server runtime statistics + /api/v2/query: + post: + operationId: PostApiV2Query + summary: Query with Flux (v2 compatible) + description: | + Query data using [Flux](/enterprise_influxdb/v1/flux/) language. + This endpoint provides forward compatibility with InfluxDB 2.x client libraries. + + **Required Headers:** + - `Accept: application/csv` + - `Content-type: application/vnd.flux` + - `Authorization: Token username:password` (if authentication is enabled) + tags: + - v2 Compatibility + - Query + security: + - TokenAuth: [] + requestBody: + required: true + content: + application/vnd.flux: + schema: + type: string + example: | + from(bucket:"telegraf") + |> range(start:-5m) + |> filter(fn:(r) => r._measurement == "cpu") + responses: + '200': + description: Query results in CSV format + content: + application/csv: + schema: + type: string + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /api/v2/write: + post: + operationId: PostApiV2Write + summary: Write data (v2 compatible) + description: | + Write data using the InfluxDB 2.x API format. + This endpoint provides forward compatibility with InfluxDB 2.x client libraries. + + **Bucket Mapping:** + The `bucket` parameter maps to InfluxDB 1.x database and retention policy: + - `database/retention-policy` - specific retention policy + - `database/` or `database` - default retention policy + + The `org` parameter is ignored in InfluxDB Enterprise 1.x. + tags: + - v2 Compatibility + - Write + security: + - TokenAuth: [] + parameters: + - name: bucket + in: query + required: true + description: | + Database and retention policy in format `database/retention-policy`. + Use `database/` or `database` for the default retention policy. + schema: + type: string + example: mydb/autogen + - name: org + in: query + description: Organization (ignored in InfluxDB Enterprise 1.x) + schema: + type: string + - name: precision + in: query + description: Timestamp precision + schema: + type: string + enum: + - ns + - us + - ms + - s + default: ns + requestBody: + required: true + content: + text/plain: + schema: + type: string + example: mem,host=host1 used_percent=23.43234543 1556896326 + responses: + '204': + description: Write successful + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /api/v2/buckets: + get: + operationId: GetApiV2Buckets + summary: List buckets (v2 compatible) + description: | + List all databases as buckets. Provides forward compatibility with + InfluxDB 2.x client libraries. + tags: + - v2 Compatibility + security: + - TokenAuth: [] + responses: + '200': + description: List of buckets + content: + application/json: + schema: + $ref: '#/components/schemas/BucketList' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + post: + operationId: PostApiV2Buckets + summary: Create bucket (v2 compatible) + description: | + Create a new database as a bucket. Provides forward compatibility with + InfluxDB 2.x client libraries. + tags: + - v2 Compatibility + security: + - TokenAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BucketCreate' + responses: + '201': + description: Bucket created + content: + application/json: + schema: + $ref: '#/components/schemas/Bucket' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /api/v2/buckets/{bucketID}: + delete: + operationId: DeleteApiV2BucketsBucketID + summary: Delete bucket (v2 compatible) + description: | + Delete a database/retention policy combination. + The bucketID format is `database/retention-policy`. + tags: + - v2 Compatibility + security: + - TokenAuth: [] + parameters: + - name: bucketID + in: path + required: true + description: Bucket ID in format `database/retention-policy` + schema: + type: string + example: test/autogen + responses: + '204': + description: Bucket deleted + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Bucket not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /api/v2/delete: + post: + operationId: PostApiV2Delete + summary: Delete data (v2 compatible) + description: | + Delete data from InfluxDB Enterprise using predicate expressions. + Supports deletion by tag value, timestamp, and measurement. + + **Predicate Syntax:** + ``` + _measurement="example" AND tagKey="tagValue" + ``` + + See [delete predicate syntax](/influxdb/v2/reference/syntax/delete-predicate/) + for detailed syntax and [limitations](/influxdb/v2/reference/syntax/delete-predicate/#limitations). + tags: + - v2 Compatibility + security: + - TokenAuth: [] + parameters: + - name: bucket + in: query + required: true + description: Database and retention policy in format `database/retention-policy` + schema: + type: string + example: exampleDB/autogen + - name: precision + in: query + description: Timestamp precision + schema: + type: string + enum: + - ns + - us + - ms + - s + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteRequest' + examples: + timeRange: + summary: Delete by time range + value: + start: '2020-03-01T00:00:00Z' + stop: '2020-11-14T00:00:00Z' + withPredicate: + summary: Delete with predicate + value: + start: '2020-03-01T00:00:00Z' + stop: '2020-11-14T00:00:00Z' + predicate: _measurement="example-measurement" AND exampleTag="exampleTagValue" + responses: + '204': + description: Delete successful + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + securitySchemes: + BasicAuth: + type: http + scheme: basic + description: | + Use HTTP Basic Authentication by including your username and password in the request. + + ```bash + curl -u username:password "http://localhost:8086/query?q=SHOW+DATABASES" + ``` + + Or encode credentials in the URL (not recommended for production): + + ```bash + curl "http://username:password@localhost:8086/query?q=SHOW+DATABASES" + ``` + QueryAuth: + type: apiKey + in: query + name: u + description: | + Pass your credentials as query parameters. Use `u` for username and `p` for password. + + ```bash + curl "http://localhost:8086/query?u=username&p=password&q=SHOW+DATABASES" + ``` + + > [!Note] + > Query string authentication exposes credentials in URLs and server logs. + > Use Basic Authentication or Token Authentication for production environments. + TokenAuth: + type: http + scheme: bearer + bearerFormat: Token + description: | + For v2-compatible endpoints (`/api/v2/*`), use the `Authorization` header with the `Token` scheme. + + Include your InfluxDB 1.x username and password separated by a colon: + + ```bash + curl -H "Authorization: Token username:password" \ + "http://localhost:8086/api/v2/query" + ``` + + This format is compatible with InfluxDB 2.x client libraries, allowing you to + use the same code with both InfluxDB 1.8+ and InfluxDB 2.x. + parameters: + QueryDB: + name: db + in: query + description: Target database for the query + schema: + type: string + example: mydb + QueryQ: + name: q + in: query + required: true + description: InfluxQL query string + schema: + type: string + example: SELECT * FROM "mymeas" + QueryEpoch: + name: epoch + in: query + description: | + Return timestamps as Unix epoch values with specified precision. + By default, timestamps are returned in RFC3339 format. + schema: + type: string + enum: + - ns + - u + - µ + - ms + - s + - m + - h + QueryPretty: + name: pretty + in: query + description: Enable pretty-printed JSON output (not recommended for production) + schema: + type: boolean + default: false + QueryChunked: + name: chunked + in: query + description: | + Return results in streamed batches. Set to `true` to chunk by series + or every 10,000 points. Set to a number to chunk by that many points. + schema: + oneOf: + - type: boolean + - type: integer + AuthUsername: + name: u + in: query + description: Username for authentication + schema: + type: string + AuthPassword: + name: p + in: query + description: Password for authentication + schema: + type: string + schemas: + HealthCheck: + type: object + properties: + name: + type: string + example: influxdb + message: + type: string + example: ready for queries and writes + status: + type: string + enum: + - pass + - fail + version: + type: string + example: 1.11.6 + QueryResponse: + type: object + properties: + results: + type: array + items: + type: object + properties: + statement_id: + type: integer + series: + type: array + items: + $ref: '#/components/schemas/Series' + error: + type: string + Series: + type: object + properties: + name: + type: string + description: Measurement name + tags: + type: object + additionalProperties: + type: string + columns: + type: array + items: + type: string + values: + type: array + items: + type: array + items: {} + Error: + type: object + required: + - error + properties: + error: + type: string + description: Error message + DeleteRequest: + type: object + required: + - start + - stop + properties: + start: + type: string + format: date-time + description: Start time (inclusive) + stop: + type: string + format: date-time + description: Stop time (exclusive) + predicate: + type: string + description: | + InfluxQL-like predicate expression to filter data to delete. + Example: `_measurement="example" AND tagKey="tagValue"` + BucketList: + type: object + properties: + buckets: + type: array + items: + $ref: '#/components/schemas/Bucket' + Bucket: + type: object + properties: + id: + type: string + description: Bucket ID (database/retention-policy) + name: + type: string + description: Bucket name + retentionRules: + type: array + items: + type: object + properties: + type: + type: string + enum: + - expire + everySeconds: + type: integer + BucketCreate: + type: object + required: + - name + properties: + name: + type: string + description: Database name + retentionRules: + type: array + items: + type: object + properties: + type: + type: string + enum: + - expire + everySeconds: + type: integer + description: Retention period in seconds diff --git a/api-docs/generate-api-docs.sh b/api-docs/generate-api-docs.sh index 451002ccc..f28baa49f 100755 --- a/api-docs/generate-api-docs.sh +++ b/api-docs/generate-api-docs.sh @@ -222,15 +222,17 @@ for configPath in $(find . -name '.config.yml' -not -path './.config.yml' -not - done # --------------------------------------------------------------------------- -# Step 3: Copy specs to static/openapi/ for download +# Step 3: Generate Hugo-native article data and content pages # --------------------------------------------------------------------------- +# Discovers products from .config.yml, processes specs from _build/, +# generates tag-based article data and Hugo content pages. echo "" echo "========================================" -echo "Step 3: Copying specs to static/openapi/" +echo "Step 3: Generating article data and pages" echo "========================================" cd .. -node api-docs/scripts/dist/generate-openapi-articles.js --static-only +node api-docs/scripts/dist/generate-openapi-articles.js --skip-fetch cd api-docs echo "" diff --git a/api-docs/influxdb/cloud/tags.yml b/api-docs/influxdb/cloud/tags.yml index a757b2d19..13decd4ea 100644 --- a/api-docs/influxdb/cloud/tags.yml +++ b/api-docs/influxdb/cloud/tags.yml @@ -187,9 +187,16 @@ tags: Supported operations: x-traitTag: true - description: > - Overview of the common CRUD operations supported by the InfluxDB Cloud - API. + description: | + The InfluxDB Cloud API supports create, read, update, and delete operations + on resources. Most endpoints follow standard HTTP method conventions: + + | Operation | HTTP Method | Description | + |:----------|:------------|:------------| + | **Write** | `POST` | Send data or create a resource. | + | **Read** | `GET` | Retrieve a resource or list resources. | + | **Update** | `PUT`, `PATCH` | Replace or modify an existing resource. | + | **Delete** | `DELETE` | Remove a resource. | System information endpoints: description: > diff --git a/api-docs/influxdb/v1/v1/content/info.yml b/api-docs/influxdb/v1/v1/content/info.yml new file mode 100644 index 000000000..f8ed3b915 --- /dev/null +++ b/api-docs/influxdb/v1/v1/content/info.yml @@ -0,0 +1,32 @@ +title: InfluxDB v1 HTTP API +x-influxdata-short-title: InfluxDB v1 API +x-influxdata-short-description: >- + The InfluxDB v1 HTTP API provides a programmatic interface for writing, + querying, and managing InfluxDB v1 databases. +version: 1.8.10 +description: | + The InfluxDB v1 HTTP API provides a simple way to interact with the database. + It uses HTTP response codes, authentication with username and password credentials + or API tokens, and JSON-formatted response data. + + ## InfluxDB 3 Compatibility + + InfluxDB 3 supports the v1 `/write` and `/query` HTTP API endpoints. + If you're getting started with InfluxDB v1, we recommend using the + InfluxDB v1 client libraries and InfluxQL for future compatibility. + + ## Authentication + + InfluxDB v1 supports two authentication methods: + + - **Basic Authentication**: Use HTTP Basic Auth with username and password + - **Query String Authentication**: Pass `u` (username) and `p` (password) as query parameters + - **Token Authentication** (v2-compatible): Use `Authorization: Token username:password` header + + Authentication is optional unless [enabled in the configuration](/influxdb/v1/administration/authentication_and_authorization/). +license: + name: MIT + url: https://opensource.org/licenses/MIT +contact: + name: InfluxData + url: https://www.influxdata.com diff --git a/api-docs/influxdb/v1/v1/content/servers.yml b/api-docs/influxdb/v1/v1/content/servers.yml new file mode 100644 index 000000000..076333dd9 --- /dev/null +++ b/api-docs/influxdb/v1/v1/content/servers.yml @@ -0,0 +1,2 @@ +- url: http://localhost:8086 + description: Local InfluxDB instance diff --git a/api-docs/influxdb/v1/v1/ref.yml b/api-docs/influxdb/v1/v1/ref.yml new file mode 100644 index 000000000..4f35df22c --- /dev/null +++ b/api-docs/influxdb/v1/v1/ref.yml @@ -0,0 +1,1093 @@ +openapi: 3.0.0 +info: + title: InfluxDB v1 HTTP API + version: 1.8.10 + description: | + The InfluxDB v1 HTTP API provides a simple way to interact with the database. + It uses HTTP response codes, authentication with username and password credentials + or API tokens, and JSON-formatted response data. + + ## InfluxDB 3 Compatibility + + InfluxDB 3 supports the v1 `/write` and `/query` HTTP API endpoints. + If you're getting started with InfluxDB v1, we recommend using the + InfluxDB v1 client libraries and InfluxQL for future compatibility. + + ## Authentication + + InfluxDB v1 supports two authentication methods: + + - **Basic Authentication**: Use HTTP Basic Auth with username and password + - **Query String Authentication**: Pass `u` (username) and `p` (password) as query parameters + - **Token Authentication** (v2-compatible): Use `Authorization: Token username:password` header + + Authentication is optional unless [enabled in the configuration](/influxdb/v1/administration/authentication_and_authorization/). + contact: + name: InfluxData + url: https://www.influxdata.com + license: + name: MIT + url: https://opensource.org/licenses/MIT +servers: + - url: http://localhost:8086 + description: Local InfluxDB instance +security: + - BasicAuth: [] + - QueryAuth: [] +tags: + - name: System Information + description: | + Endpoints for checking server status, health, and version information. + - name: Query + description: | + Query data using InfluxQL. The `/query` endpoint supports both read queries + (SELECT, SHOW) and write queries (CREATE, DROP, ALTER, etc.). + - name: Write + description: | + Write time series data using InfluxDB line protocol. + - name: Debug + description: | + Debugging and profiling endpoints for troubleshooting and performance analysis. + - name: v2 Compatibility + description: | + InfluxDB 2.x API compatibility endpoints. These endpoints allow you to use + InfluxDB 2.x client libraries with InfluxDB 1.8+. + + Use the `Token` scheme with v1.x credentials: + ``` + Authorization: Token username:password + ``` + - name: Authentication + x-traitTag: true + description: | + InfluxDB v1 supports multiple authentication methods: + + ### Basic Authentication + ```bash + curl -u username:password http://localhost:8086/query?q=SHOW+DATABASES + ``` + + ### Query String Authentication + ```bash + curl "http://localhost:8086/query?u=username&p=password&q=SHOW+DATABASES" + ``` + + ### Token Authentication (v2-compatible) + For v2-compatible endpoints, use the Token scheme: + ```bash + curl -H "Authorization: Token username:password" http://localhost:8086/api/v2/query + ``` +paths: + /ping: + get: + operationId: GetPing + summary: Check server status + description: | + Check the status of your InfluxDB instance and retrieve version information. + The `/ping` endpoint returns a `204 No Content` response by default. + + Use the `verbose=true` query parameter to return a `200 OK` response, + which is required for [Google Cloud Load Balancing](https://cloud.google.com/load-balancing/docs/health-check-concepts) health checks. + tags: + - System Information + parameters: + - name: verbose + in: query + description: | + When `true`, returns HTTP 200 instead of 204. Required for Google Cloud Load Balancing health checks. + schema: + type: boolean + default: false + responses: + '200': + description: Server is running (verbose mode) + headers: + X-Influxdb-Build: + description: InfluxDB build type (`OSS` or `ENT`) + schema: + type: string + enum: + - OSS + - ENT + X-Influxdb-Version: + description: InfluxDB version + schema: + type: string + '204': + description: Server is running + headers: + X-Influxdb-Build: + description: InfluxDB build type (`OSS` or `ENT`) + schema: + type: string + enum: + - OSS + - ENT + X-Influxdb-Version: + description: InfluxDB version + schema: + type: string + head: + operationId: HeadPing + summary: Check server status (HEAD) + description: | + Check the status of your InfluxDB instance using a HEAD request. + Returns the same headers as GET but without a response body. + tags: + - System Information + responses: + '204': + description: Server is running + headers: + X-Influxdb-Build: + description: InfluxDB build type (`OSS` or `ENT`) + schema: + type: string + enum: + - OSS + - ENT + X-Influxdb-Version: + description: InfluxDB version + schema: + type: string + /health: + get: + operationId: GetHealth + summary: Check server health + description: | + Check the health of your InfluxDB instance. + Returns health status as JSON with a `pass` or `fail` status. + tags: + - System Information + - v2 Compatibility + responses: + '200': + description: Server is healthy + content: + application/json: + schema: + $ref: '#/components/schemas/HealthCheck' + example: + name: influxdb + message: ready for queries and writes + status: pass + version: 1.8.10 + '503': + description: Server is unhealthy + content: + application/json: + schema: + $ref: '#/components/schemas/HealthCheck' + example: + name: influxdb + message: service unavailable + status: fail + version: 1.8.10 + /query: + get: + operationId: GetQuery + summary: Query data (GET) + description: | + Query data using InfluxQL. Use GET for read-only queries that start with: + - `SELECT` (except queries with `INTO` clause) + - `SHOW` + + For write operations (CREATE, DROP, ALTER, etc.), use POST. + tags: + - Query + parameters: + - $ref: '#/components/parameters/QueryDB' + - $ref: '#/components/parameters/QueryQ' + - $ref: '#/components/parameters/QueryEpoch' + - $ref: '#/components/parameters/QueryPretty' + - $ref: '#/components/parameters/QueryChunked' + - $ref: '#/components/parameters/AuthUsername' + - $ref: '#/components/parameters/AuthPassword' + responses: + '200': + description: Query executed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/QueryResponse' + example: + results: + - statement_id: 0 + series: + - name: mymeas + columns: + - time + - myfield + - mytag1 + - mytag2 + values: + - - '2017-03-01T00:16:18Z' + - 33.1 + - null + - null + - - '2017-03-01T00:17:18Z' + - 12.4 + - '12' + - '14' + application/csv: + schema: + type: string + example: | + name,tags,time,myfield,mytag1,mytag2 + mymeas,,1488327378000000000,33.1,, + mymeas,,1488327438000000000,12.4,12,14 + '400': + description: Bad request (syntax error in query) + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: 'error parsing query: found EOF, expected FROM at line 1, char 9' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: authorization failed + post: + operationId: PostQuery + summary: Query data (POST) + description: | + Query data or execute database management commands using InfluxQL. + Use POST for queries that start with: + - `SELECT` with `INTO` clause + - `ALTER` + - `CREATE` + - `DELETE` + - `DROP` + - `GRANT` + - `KILL` + - `REVOKE` + tags: + - Query + parameters: + - $ref: '#/components/parameters/QueryDB' + - $ref: '#/components/parameters/QueryEpoch' + - $ref: '#/components/parameters/QueryPretty' + - $ref: '#/components/parameters/QueryChunked' + - $ref: '#/components/parameters/AuthUsername' + - $ref: '#/components/parameters/AuthPassword' + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - q + properties: + q: + type: string + description: InfluxQL query string + example: CREATE DATABASE mydb + params: + type: string + description: | + JSON object containing bind parameter values. + Use `$` syntax in the query to reference parameters. + example: '{"tag_value":"12","field_value":30}' + multipart/form-data: + schema: + type: object + properties: + q: + type: string + format: binary + description: File containing InfluxQL queries (separated by semicolons) + async: + type: boolean + description: Execute queries asynchronously + default: false + responses: + '200': + description: Query executed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/QueryResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /write: + post: + operationId: PostWrite + summary: Write data + description: | + Write time series data to InfluxDB using line protocol format. + + Data must be sent as binary encoded line protocol in the request body. + Use the `--data-binary` flag with curl to preserve newlines. + + **Best Practices:** + - Write points in batches of 5,000 to 10,000 for optimal performance + - Use the least precise timestamp precision possible for better compression + tags: + - Write + parameters: + - name: db + in: query + required: true + description: Target database for the write + schema: + type: string + example: mydb + - name: rp + in: query + description: | + Target retention policy. If not specified, writes to the default retention policy. + schema: + type: string + - name: precision + in: query + description: | + Timestamp precision. InfluxDB assumes nanoseconds if not specified. + schema: + type: string + enum: + - 'n' + - u + - ms + - s + - m + - h + default: 'n' + - $ref: '#/components/parameters/AuthUsername' + - $ref: '#/components/parameters/AuthPassword' + requestBody: + required: true + description: | + Line protocol data. Multiple points should be separated by newlines. + Use `@filename` to write from a file. + content: + text/plain: + schema: + type: string + example: | + mymeas,mytag=1 myfield=90 1463683075000000000 + mymeas,mytag=2 myfield=34 1463683076000000000 + responses: + '204': + description: Write successful + '400': + description: Bad request (line protocol syntax error or type conflict) + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + badTimestamp: + summary: Invalid timestamp + value: + error: 'unable to parse ''mymeas,mytag=1 myfield=91 abc123'': bad timestamp' + typeConflict: + summary: Field type conflict + value: + error: 'field type conflict: input field "myfield" on measurement "mymeas" is type int64, already exists as type float' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Database not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: 'database not found: "mydb1"' + '413': + description: Request entity too large + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: Request Entity Too Large + '500': + description: Internal server error (e.g., retention policy not found) + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: 'retention policy not found: myrp' + /debug/pprof: + get: + operationId: GetDebugPprof + summary: Get profiling index + description: | + Returns an HTML page listing available Go pprof profiles. + Use the individual profile endpoints to retrieve specific profile data. + tags: + - Debug + responses: + '200': + description: HTML page with profile links + content: + text/html: + schema: + type: string + /debug/pprof/{profile}: + get: + operationId: GetDebugPprofProfile + summary: Get profile data + description: | + Retrieve a specific Go pprof profile. Available profiles: + - `block`: Stack traces that led to blocking on synchronization primitives + - `goroutine`: Stack traces of all current goroutines + - `heap`: Sampling of stack traces for heap allocations + - `mutex`: Stack traces of holders of contended mutexes + - `threadcreate`: Stack traces that led to creation of new OS threads + - `profile`: CPU profile (use `seconds` parameter to specify duration) + - `trace`: Execution trace (use `seconds` parameter to specify duration) + tags: + - Debug + parameters: + - name: profile + in: path + required: true + description: Profile name + schema: + type: string + enum: + - block + - goroutine + - heap + - mutex + - threadcreate + - profile + - trace + - allocs + - cmdline + - name: seconds + in: query + description: Duration in seconds for CPU profile or trace + schema: + type: integer + default: 30 + - name: debug + in: query + description: Return human-readable text output instead of binary + schema: + type: integer + enum: + - 0 + - 1 + default: 0 + responses: + '200': + description: Profile data + content: + application/octet-stream: + schema: + type: string + format: binary + text/plain: + schema: + type: string + /debug/pprof/all: + get: + operationId: GetDebugPprofAll + summary: Get all profiles archive + description: | + Generate a `profiles.tar.gz` archive containing all standard Go profiling + information and additional debugging data. Intended primarily for use by + InfluxData support. + + Use the `cpu` parameter to include a CPU profile of the specified duration. + tags: + - Debug + parameters: + - name: cpu + in: query + description: | + Duration for CPU profile. Specify as a duration string (e.g., `30s`). + For InfluxDB 1.8.3 and earlier, use `cpu=true`. + schema: + type: string + example: 30s + responses: + '200': + description: Compressed archive containing all profiles + content: + application/gzip: + schema: + type: string + format: binary + /debug/requests: + get: + operationId: GetDebugRequests + summary: Track HTTP requests + description: | + Track HTTP client requests to the `/write` and `/query` endpoints. + Returns the number of writes and queries per username and IP address + over the specified time interval. + tags: + - Debug + parameters: + - name: seconds + in: query + description: Duration in seconds to collect request data + schema: + type: integer + default: 10 + responses: + '200': + description: Request statistics by user and IP + content: + application/json: + schema: + type: object + additionalProperties: + type: object + properties: + writes: + type: integer + queries: + type: integer + example: + user1:123.45.678.91: + writes: 1 + queries: 0 + user1:000.0.0.0: + writes: 0 + queries: 16 + /debug/vars: + get: + operationId: GetDebugVars + summary: Get server statistics + description: | + Retrieve runtime statistics and information about the InfluxDB instance. + Returns detailed metrics in JSON format including memory usage, + goroutine counts, and database statistics. + + The [InfluxDB Telegraf input plugin](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/influxdb) + can collect these metrics automatically. + tags: + - Debug + responses: + '200': + description: Server statistics in JSON format + content: + application/json: + schema: + type: object + description: Server runtime statistics + /api/v2/query: + post: + operationId: PostApiV2Query + summary: Query with Flux (v2 compatible) + description: | + Query data using [Flux](/influxdb/v1/flux/) language. + This endpoint provides forward compatibility with InfluxDB 2.x client libraries. + + **Required Headers:** + - `Accept: application/csv` + - `Content-type: application/vnd.flux` + - `Authorization: Token username:password` (if authentication is enabled) + tags: + - v2 Compatibility + - Query + security: + - TokenAuth: [] + requestBody: + required: true + content: + application/vnd.flux: + schema: + type: string + example: | + from(bucket:"telegraf") + |> range(start:-5m) + |> filter(fn:(r) => r._measurement == "cpu") + responses: + '200': + description: Query results in CSV format + content: + application/csv: + schema: + type: string + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /api/v2/write: + post: + operationId: PostApiV2Write + summary: Write data (v2 compatible) + description: | + Write data using the InfluxDB 2.x API format. + This endpoint provides forward compatibility with InfluxDB 2.x client libraries. + + **Bucket Mapping:** + The `bucket` parameter maps to InfluxDB 1.x database and retention policy: + - `database/retention-policy` - specific retention policy + - `database/` or `database` - default retention policy + + The `org` parameter is ignored in InfluxDB 1.x. + tags: + - v2 Compatibility + - Write + security: + - TokenAuth: [] + parameters: + - name: bucket + in: query + required: true + description: | + Database and retention policy in format `database/retention-policy`. + Use `database/` or `database` for the default retention policy. + schema: + type: string + example: mydb/autogen + - name: org + in: query + description: Organization (ignored in InfluxDB 1.x) + schema: + type: string + - name: precision + in: query + description: Timestamp precision + schema: + type: string + enum: + - ns + - us + - ms + - s + default: ns + requestBody: + required: true + content: + text/plain: + schema: + type: string + example: mem,host=host1 used_percent=23.43234543 1556896326 + responses: + '204': + description: Write successful + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /api/v2/buckets: + get: + operationId: GetApiV2Buckets + summary: List buckets (v2 compatible) + description: | + List all databases as buckets. Provides forward compatibility with + InfluxDB 2.x client libraries. + tags: + - v2 Compatibility + security: + - TokenAuth: [] + responses: + '200': + description: List of buckets + content: + application/json: + schema: + $ref: '#/components/schemas/BucketList' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + post: + operationId: PostApiV2Buckets + summary: Create bucket (v2 compatible) + description: | + Create a new database as a bucket. Provides forward compatibility with + InfluxDB 2.x client libraries. + tags: + - v2 Compatibility + security: + - TokenAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BucketCreate' + responses: + '201': + description: Bucket created + content: + application/json: + schema: + $ref: '#/components/schemas/Bucket' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /api/v2/buckets/{bucketID}: + delete: + operationId: DeleteApiV2BucketsBucketID + summary: Delete bucket (v2 compatible) + description: | + Delete a database/retention policy combination. + The bucketID format is `database/retention-policy`. + tags: + - v2 Compatibility + security: + - TokenAuth: [] + parameters: + - name: bucketID + in: path + required: true + description: Bucket ID in format `database/retention-policy` + schema: + type: string + example: test/autogen + responses: + '204': + description: Bucket deleted + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Bucket not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /api/v2/delete: + post: + operationId: PostApiV2Delete + summary: Delete data (v2 compatible) + description: | + Delete data from InfluxDB using predicate expressions. + Supports deletion by tag value, timestamp, and measurement. + + **Predicate Syntax:** + ``` + _measurement="example" AND tagKey="tagValue" + ``` + + See [delete predicate syntax](/influxdb/v2/reference/syntax/delete-predicate/) + for detailed syntax and [limitations](/influxdb/v2/reference/syntax/delete-predicate/#limitations). + tags: + - v2 Compatibility + security: + - TokenAuth: [] + parameters: + - name: bucket + in: query + required: true + description: Database and retention policy in format `database/retention-policy` + schema: + type: string + example: exampleDB/autogen + - name: precision + in: query + description: Timestamp precision + schema: + type: string + enum: + - ns + - us + - ms + - s + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteRequest' + examples: + timeRange: + summary: Delete by time range + value: + start: '2020-03-01T00:00:00Z' + stop: '2020-11-14T00:00:00Z' + withPredicate: + summary: Delete with predicate + value: + start: '2020-03-01T00:00:00Z' + stop: '2020-11-14T00:00:00Z' + predicate: _measurement="example-measurement" AND exampleTag="exampleTagValue" + responses: + '204': + description: Delete successful + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + securitySchemes: + BasicAuth: + type: http + scheme: basic + description: | + Use HTTP Basic Authentication by including your username and password in the request. + + ```bash + curl -u username:password "http://localhost:8086/query?q=SHOW+DATABASES" + ``` + + Or encode credentials in the URL (not recommended for production): + + ```bash + curl "http://username:password@localhost:8086/query?q=SHOW+DATABASES" + ``` + QueryAuth: + type: apiKey + in: query + name: u + description: | + Pass your credentials as query parameters. Use `u` for username and `p` for password. + + ```bash + curl "http://localhost:8086/query?u=username&p=password&q=SHOW+DATABASES" + ``` + + > [!Note] + > Query string authentication exposes credentials in URLs and server logs. + > Use Basic Authentication or Token Authentication for production environments. + TokenAuth: + type: http + scheme: bearer + bearerFormat: Token + description: | + For v2-compatible endpoints (`/api/v2/*`), use the `Authorization` header with the `Token` scheme. + + Include your InfluxDB 1.x username and password separated by a colon: + + ```bash + curl -H "Authorization: Token username:password" \ + "http://localhost:8086/api/v2/query" + ``` + + This format is compatible with InfluxDB 2.x client libraries, allowing you to + use the same code with both InfluxDB 1.8+ and InfluxDB 2.x. + parameters: + QueryDB: + name: db + in: query + description: Target database for the query + schema: + type: string + example: mydb + QueryQ: + name: q + in: query + required: true + description: InfluxQL query string + schema: + type: string + example: SELECT * FROM "mymeas" + QueryEpoch: + name: epoch + in: query + description: | + Return timestamps as Unix epoch values with specified precision. + By default, timestamps are returned in RFC3339 format. + schema: + type: string + enum: + - ns + - u + - µ + - ms + - s + - m + - h + QueryPretty: + name: pretty + in: query + description: Enable pretty-printed JSON output (not recommended for production) + schema: + type: boolean + default: false + QueryChunked: + name: chunked + in: query + description: | + Return results in streamed batches. Set to `true` to chunk by series + or every 10,000 points. Set to a number to chunk by that many points. + schema: + oneOf: + - type: boolean + - type: integer + AuthUsername: + name: u + in: query + description: Username for authentication + schema: + type: string + AuthPassword: + name: p + in: query + description: Password for authentication + schema: + type: string + schemas: + HealthCheck: + type: object + properties: + name: + type: string + example: influxdb + message: + type: string + example: ready for queries and writes + status: + type: string + enum: + - pass + - fail + version: + type: string + example: 1.8.10 + QueryResponse: + type: object + properties: + results: + type: array + items: + type: object + properties: + statement_id: + type: integer + series: + type: array + items: + $ref: '#/components/schemas/Series' + error: + type: string + Series: + type: object + properties: + name: + type: string + description: Measurement name + tags: + type: object + additionalProperties: + type: string + columns: + type: array + items: + type: string + values: + type: array + items: + type: array + items: {} + Error: + type: object + required: + - error + properties: + error: + type: string + description: Error message + DeleteRequest: + type: object + required: + - start + - stop + properties: + start: + type: string + format: date-time + description: Start time (inclusive) + stop: + type: string + format: date-time + description: Stop time (exclusive) + predicate: + type: string + description: | + InfluxQL-like predicate expression to filter data to delete. + Example: `_measurement="example" AND tagKey="tagValue"` + BucketList: + type: object + properties: + buckets: + type: array + items: + $ref: '#/components/schemas/Bucket' + Bucket: + type: object + properties: + id: + type: string + description: Bucket ID (database/retention-policy) + name: + type: string + description: Bucket name + retentionRules: + type: array + items: + type: object + properties: + type: + type: string + enum: + - expire + everySeconds: + type: integer + BucketCreate: + type: object + required: + - name + properties: + name: + type: string + description: Database name + retentionRules: + type: array + items: + type: object + properties: + type: + type: string + enum: + - expire + everySeconds: + type: integer + description: Retention period in seconds diff --git a/api-docs/influxdb/v2/tags.yml b/api-docs/influxdb/v2/tags.yml index 1ea9b699d..b82671d3f 100644 --- a/api-docs/influxdb/v2/tags.yml +++ b/api-docs/influxdb/v2/tags.yml @@ -225,9 +225,16 @@ tags: Supported operations: x-traitTag: true - description: > - Overview of the common CRUD operations supported by the InfluxDB OSS v2 - API. + description: | + The InfluxDB OSS v2 API supports create, read, update, and delete operations + on resources. Most endpoints follow standard HTTP method conventions: + + | Operation | HTTP Method | Description | + |:----------|:------------|:------------| + | **Write** | `POST` | Send data or create a resource. | + | **Read** | `GET` | Retrieve a resource or list resources. | + | **Update** | `PUT`, `PATCH` | Replace or modify an existing resource. | + | **Delete** | `DELETE` | Remove a resource. | System information endpoints: description: > diff --git a/api-docs/influxdb3/cloud-dedicated/tags.yml b/api-docs/influxdb3/cloud-dedicated/tags.yml index bc108cd59..827d593f7 100644 --- a/api-docs/influxdb3/cloud-dedicated/tags.yml +++ b/api-docs/influxdb3/cloud-dedicated/tags.yml @@ -1,9 +1,40 @@ tags: API compatibility: x-traitTag: true - description: > - Overview of the InfluxDB v1 and v2 compatible write and query endpoints - available in InfluxDB 3 Cloud Dedicated. + description: | + ### Write data + + InfluxDB 3 Cloud Dedicated provides the following HTTP API endpoints for writing data: + + - **Recommended**: [`/api/v2/write` endpoint](#operation/PostWrite) for new write workloads or for bringing existing InfluxDB v2 write workloads to InfluxDB 3. + - [`/write` endpoint](#operation/PostLegacyWrite) for bringing existing InfluxDB v1 write workloads to InfluxDB 3. + + Both endpoints accept the same line protocol format and process data in the same way. + + ### Query data + + InfluxDB 3 Cloud Dedicated provides the following protocols for executing a query: + + - **Recommended**: _Flight+gRPC_ request that contains an SQL or InfluxQL query. + - HTTP API [`/query` request](#operation/GetLegacyQuery) that contains an InfluxQL query. + Use this protocol when bringing existing InfluxDB v1 query workloads to InfluxDB 3. + + ### InfluxDB v2 compatibility + + The HTTP API [`/api/v2/write` endpoint](#operation/PostWrite) works with the [`Bearer`](#section/Authentication/BearerAuthentication) and [`Token`](#section/Authentication/TokenAuthentication) authentication schemes and existing InfluxDB 2.x tools and code. + + ### InfluxDB v1 compatibility + + The HTTP API [`/write` endpoint](#operation/PostLegacyWrite) and [`/query` endpoint](#operation/GetLegacyQuery) work with InfluxDB 1.x username/password [authentication schemes](#section/Authentication/) and existing InfluxDB 1.x tools and code. + x-related: + - title: Get started querying InfluxDB + href: /influxdb3/cloud-dedicated/get-started/query/ + - title: Write data + href: /influxdb3/cloud-dedicated/write-data/ + - title: Use the v2 HTTP API with Cloud Dedicated + href: /influxdb3/cloud-dedicated/guides/api-compatibility/v2/ + - title: Use the v1 HTTP API with Cloud Dedicated + href: /influxdb3/cloud-dedicated/guides/api-compatibility/v1/ Authentication: x-traitTag: true diff --git a/api-docs/influxdb3/cloud-serverless/tags.yml b/api-docs/influxdb3/cloud-serverless/tags.yml index a6e5ae13a..7cec23eb4 100644 --- a/api-docs/influxdb3/cloud-serverless/tags.yml +++ b/api-docs/influxdb3/cloud-serverless/tags.yml @@ -1,9 +1,41 @@ tags: API compatibility: x-traitTag: true - description: > - Overview of the InfluxDB v1 and v2 compatible write and query endpoints - available in InfluxDB 3 Cloud Serverless. + description: | + ### Write data + + InfluxDB 3 Cloud Serverless provides the following HTTP API endpoints for writing data: + + - **Recommended**: [`/api/v2/write` endpoint](#operation/PostWrite) + for new write workloads or for bringing existing InfluxDB v2 write workloads to InfluxDB 3. + - [`/write` endpoint](#operation/PostLegacyWrite) for bringing existing InfluxDB v1 write workloads to InfluxDB 3. + + Both endpoints accept the same line protocol format and process data in the same way. + + ### Query data + + InfluxDB 3 Cloud Serverless provides the following protocols for executing a query: + + - **Recommended**: _Flight+gRPC_ request that contains an SQL or InfluxQL query. + - HTTP API [`/query` request](#operation/GetLegacyQuery) that contains an InfluxQL query. + Use this protocol when bringing existing InfluxDB v1 query workloads to InfluxDB 3. + + ### InfluxDB v2 compatibility + + The HTTP API [`/api/v2/write` endpoint](#operation/PostWrite) works with the [`Token` authentication scheme](#section/Authentication/TokenAuthentication) and existing InfluxDB 2.x tools and code. + + ### InfluxDB v1 compatibility + + The HTTP API [`/write` endpoint](#operation/PostLegacyWrite) and [`/query` endpoint](#operation/GetLegacyQuery) work with InfluxDB 1.x username/password [authentication schemes](#section/Authentication/) and existing InfluxDB 1.x tools and code. + x-related: + - title: Get started querying InfluxDB + href: /influxdb3/cloud-serverless/get-started/query/ + - title: Write data + href: /influxdb3/cloud-serverless/write-data/ + - title: Use the v2 HTTP API with Cloud Serverless + href: /influxdb3/cloud-serverless/guides/api-compatibility/v2/ + - title: Use the v1 HTTP API with Cloud Serverless + href: /influxdb3/cloud-serverless/guides/api-compatibility/v1/ Authentication: x-traitTag: true @@ -190,9 +222,16 @@ tags: Supported operations: x-traitTag: true - description: > - Overview of the common CRUD operations supported by the InfluxDB 3 Cloud - Serverless API. + description: | + The InfluxDB 3 Cloud Serverless API supports create, read, update, and delete + operations on resources. Most endpoints follow standard HTTP method conventions: + + | Operation | HTTP Method | Description | + |:----------|:------------|:------------| + | **Write** | `POST` | Send data or create a resource. | + | **Read** | `GET` | Retrieve a resource or list resources. | + | **Update** | `PUT`, `PATCH` | Replace or modify an existing resource. | + | **Delete** | `DELETE` | Remove a resource. | System information endpoints: description: > diff --git a/api-docs/influxdb3/clustered/tags.yml b/api-docs/influxdb3/clustered/tags.yml index 37c753cb6..ea26eea93 100644 --- a/api-docs/influxdb3/clustered/tags.yml +++ b/api-docs/influxdb3/clustered/tags.yml @@ -1,9 +1,40 @@ tags: API compatibility: x-traitTag: true - description: > - Overview of the InfluxDB v1 and v2 compatible write and query endpoints - available in InfluxDB 3 Clustered. + description: | + ### Write data + + InfluxDB 3 Clustered provides the following HTTP API endpoints for writing data: + + - **Recommended**: [`/api/v2/write` endpoint](#operation/PostWrite) for new write workloads or for bringing existing InfluxDB v2 write workloads to InfluxDB 3. + - [`/write` endpoint](#operation/PostLegacyWrite) for bringing existing InfluxDB v1 write workloads to InfluxDB 3. + + Both endpoints accept the same line protocol format and process data in the same way. + + ### Query data + + InfluxDB 3 Clustered provides the following protocols for executing a query: + + - **Recommended**: _Flight+gRPC_ request that contains an SQL or InfluxQL query. + - HTTP API [`/query` request](#operation/GetLegacyQuery) that contains an InfluxQL query. + Use this protocol when bringing existing InfluxDB v1 query workloads to InfluxDB 3. + + ### InfluxDB v2 compatibility + + The HTTP API [`/api/v2/write` endpoint](#operation/PostWrite) works with the [`Bearer`](#section/Authentication/BearerAuthentication) and [`Token`](#section/Authentication/TokenAuthentication) authentication schemes and existing InfluxDB 2.x tools and code. + + ### InfluxDB v1 compatibility + + The HTTP API [`/write` endpoint](#operation/PostLegacyWrite) and [`/query` endpoint](#operation/GetLegacyQuery) work with InfluxDB 1.x username/password [authentication schemes](#section/Authentication/) and existing InfluxDB 1.x tools and code. + x-related: + - title: Get started querying InfluxDB + href: /influxdb3/clustered/get-started/query/ + - title: Write data + href: /influxdb3/clustered/write-data/ + - title: Use the v2 HTTP API with Clustered + href: /influxdb3/clustered/guides/api-compatibility/v2/ + - title: Use the v1 HTTP API with Clustered + href: /influxdb3/clustered/guides/api-compatibility/v1/ Authentication: x-traitTag: true diff --git a/api-docs/influxdb3/core/tags.yml b/api-docs/influxdb3/core/tags.yml index 0665fbea7..01af6fc2c 100644 --- a/api-docs/influxdb3/core/tags.yml +++ b/api-docs/influxdb3/core/tags.yml @@ -11,28 +11,50 @@ tags: Cache distinct values: - description: > - Manage the Distinct Value Cache (DVC), an in-memory cache that stores - distinct values for specific columns in a table to improve query - performance for distinct tag and field value lookups. + description: | + The Distinct Value Cache (DVC) lets you cache distinct + values of one or more columns in a table, improving the performance of + queries that return distinct tag and field values. + + The DVC is an in-memory cache that stores distinct values for specific columns + in a table. When you create a DVC, you can specify what columns' distinct + 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-related: - title: Manage the Distinct Value Cache href: /influxdb3/core/admin/distinct-value-cache/ Cache last value: - description: > - Manage the Last Value Cache (LVC), an in-memory cache that stores the - most recent N values for specific fields in a table to improve query - performance for last-value lookups. + description: | + The Last Value Cache (LVC) lets you cache the most recent + values for specific fields in a table, improving the performance of queries that + return the most recent value of a field for specific series or the last N values + of a field. + + The LVC is an in-memory cache that stores the last N number of values for + specific fields of series in a table. When you create an LVC, you can specify + 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-related: - title: Manage the Last Value Cache href: /influxdb3/core/admin/last-value-cache/ Migrate from InfluxDB v1 or v2: x-traitTag: true - description: > - Reference information for migrating existing InfluxDB v1 or v2 workloads - to InfluxDB 3 Core using compatibility endpoints. + description: | + Migrate your existing InfluxDB v1 or v2 workloads to InfluxDB 3 Core. + + InfluxDB 3 provides compatibility endpoints that work with InfluxDB 1.x and 2.x client libraries and tools. + Operations marked with v1 or v2 badges are compatible with the respective InfluxDB version. + + ### Migration guides + + - [Migrate from InfluxDB v1](/influxdb3/core/guides/migrate/influxdb-1x/) - For users migrating from InfluxDB 1.x + - [Migrate from InfluxDB v2](/influxdb3/core/guides/migrate/influxdb-2x/) - For users migrating from InfluxDB 2.x or Cloud + - [Use compatibility APIs to write data](/influxdb3/core/write-data/http-api/compatibility-apis/) - v1 and v2 write endpoints + - [Use the v1 HTTP query API](/influxdb3/core/query-data/execute-queries/influxdb-v1-api/) - InfluxQL queries via HTTP Database: description: > @@ -43,15 +65,37 @@ tags: Headers and parameters: x-traitTag: true - description: > - Common HTTP request headers and query parameters used by InfluxDB 3 Core - API endpoints. + description: | + Most InfluxDB API endpoints require parameters in the request--for example, specifying the database to use. + + ### Common parameters + + The following table shows common parameters used by many InfluxDB API endpoints. + Many endpoints may require other parameters in the query string or in the + request body that perform functions specific to those endpoints. + + | Query parameter | Value type | Description | + |:------------------------ |:--------------------- |:-------------------------------------------| + | `db` | string | The database name | + + InfluxDB HTTP API endpoints use standard HTTP request and response headers. + The following table shows common headers used by many InfluxDB API endpoints. + Some endpoints may use other headers that perform functions more specific to those endpoints--for example, + the write endpoints accept the `Content-Encoding` header to indicate that line protocol is compressed in the request body. + + | Header | Value type | Description | + |:------------------------ |:--------------------- |:-------------------------------------------| + | `Accept` | string | The content type that the client can understand. | + | `Authorization` | string | The [authorization scheme and credential](/influxdb/version/api/authentication/). | + | `Content-Length` | integer | The size of the entity-body, in bytes. | + | `Content-Type` | string | The format of the data in the request body. | Processing engine: - description: > - Manage Processing engine triggers, test plugins, and invoke On Request - plugins. The processing engine is an embedded Python VM that runs plugins - in response to database events. + description: | + Manage Processing engine triggers, test plugins, and send requests to trigger On Request plugins. + + InfluxDB 3 Core provides the InfluxDB 3 processing engine, an embedded Python VM that can dynamically load and trigger Python plugins in response to events in your database. + Use Processing engine plugins and triggers to run code and perform tasks for different database events. x-related: - title: Processing engine and Python plugins href: /influxdb3/core/processing-engine/ @@ -65,9 +109,49 @@ tags: Quick start: x-traitTag: true - description: > - Get started authenticating, writing, and querying data with the - InfluxDB 3 Core API. + description: | + Authenticate, write, and query with the API: + + 1. Create an admin token to authorize API requests. + + ```bash + curl -X POST "http://localhost:8181/api/v3/configure/token/admin" + ``` + 2. Check the status of the InfluxDB server. + + ```bash + curl "http://localhost:8181/health" \ + --header "Authorization: Bearer ADMIN_TOKEN" + ``` + + 3. Write data to InfluxDB. + + ```bash + curl "http://localhost:8181/api/v3/write_lp?db=sensors&precision=auto" + --header "Authorization: Bearer ADMIN_TOKEN" \ + --data-raw "home,room=Kitchen temp=72.0 + home,room=Living\ room temp=71.5" + ``` + + If all data is written, the response is `204 No Content`. + + 4. Query data from InfluxDB. + + ```bash + curl -G "http://localhost:8181/api/v3/query_sql" \ + --header "Authorization: Bearer ADMIN_TOKEN" \ + --data-urlencode "db=sensors" \ + --data-urlencode "q=SELECT * FROM home WHERE room='Living room'" \ + --data-urlencode "format=jsonl" + ``` + + Output: + + ```jsonl + {"room":"Living room","temp":71.5,"time":"2025-02-25T20:19:34.984098"} + ``` + + For more information about using InfluxDB 3 Core, see the [Get started](/influxdb3/core/get-started/) guide. Server information: description: > @@ -90,9 +174,26 @@ tags: href: /influxdb3/core/admin/tokens/ Write data: - description: > - Write time series data to InfluxDB 3 Core in line protocol format using - the v1, v2, or v3 write endpoints. + description: | + Write data to InfluxDB 3 Core using line protocol format. + + #### Timestamp precision across write APIs + + InfluxDB 3 provides multiple write endpoints for compatibility with different InfluxDB versions. + The following table compares timestamp precision support across v1, v2, and v3 write APIs: + + | Precision | v1 (`/write`) | v2 (`/api/v2/write`) | v3 (`/api/v3/write_lp`) | + |-----------|---------------|----------------------|-------------------------| + | **Auto detection** | ❌ No | ❌ No | ✅ `auto` (default) | + | **Seconds** | ✅ `s` | ✅ `s` | ✅ `second` | + | **Milliseconds** | ✅ `ms` | ✅ `ms` | ✅ `millisecond` | + | **Microseconds** | ✅ `u` or `µ` | ✅ `us` | ✅ `microsecond` | + | **Nanoseconds** | ✅ `ns` | ✅ `ns` | ✅ `nanosecond` | + | **Minutes** | ✅ `m` | ❌ No | ❌ No | + | **Hours** | ✅ `h` | ❌ No | ❌ No | + | **Default** | Nanosecond | Nanosecond | **Auto** (guessed) | + + All timestamps are stored internally as nanoseconds. x-related: - title: Write data using HTTP APIs href: /influxdb3/core/write-data/http-api/ diff --git a/api-docs/influxdb3/enterprise/tags.yml b/api-docs/influxdb3/enterprise/tags.yml index 873c12c5a..7cac19867 100644 --- a/api-docs/influxdb3/enterprise/tags.yml +++ b/api-docs/influxdb3/enterprise/tags.yml @@ -11,10 +11,33 @@ tags: Cache data: - description: > - Manage in-memory caches for InfluxDB 3 Enterprise, including the Distinct - Value Cache (DVC) for distinct column values and the Last Value Cache - (LVC) for most-recent field values. + description: | + Manage the in-memory cache. + + #### Distinct Value Cache + + The Distinct Value Cache (DVC) lets you cache distinct + values of one or more columns in a table, improving the performance of + queries that return distinct tag and field values. + + The DVC is an in-memory cache that stores distinct values for specific columns + in a table. When you create an DVC, you can specify what columns' distinct + 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. + + #### Last value cache + + The Last Value Cache (LVC) lets you cache the most recent + values for specific fields in a table, improving the performance of queries that + return the most recent value of a field for specific series or the last N values + of a field. + + The LVC is an in-memory cache that stores the last N number of values for + specific fields of series in a table. When you create an LVC, you can specify + 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-related: - title: Manage the Distinct Value Cache href: /influxdb3/enterprise/admin/distinct-value-cache/ @@ -30,15 +53,37 @@ tags: Headers and parameters: x-traitTag: true - description: > - Common HTTP request headers and query parameters used by InfluxDB 3 - Enterprise API endpoints. + description: | + Most InfluxDB API endpoints require parameters in the request--for example, specifying the database to use. + + ### Common parameters + + The following table shows common parameters used by many InfluxDB API endpoints. + Many endpoints may require other parameters in the query string or in the + request body that perform functions specific to those endpoints. + + | Query parameter | Value type | Description | + |:------------------------ |:--------------------- |:-------------------------------------------| + | `db` | string | The database name | + + InfluxDB HTTP API endpoints use standard HTTP request and response headers. + The following table shows common headers used by many InfluxDB API endpoints. + Some endpoints may use other headers that perform functions more specific to those endpoints--for example, + the write endpoints accept the `Content-Encoding` header to indicate that line protocol is compressed in the request body. + + | Header | Value type | Description | + |:------------------------ |:--------------------- |:-------------------------------------------| + | `Accept` | string | The content type that the client can understand. | + | `Authorization` | string | The authorization scheme and credential. | + | `Content-Length` | integer | The size of the entity-body, in bytes. | + | `Content-Type` | string | The format of the data in the request body. | Processing engine: - description: > - Manage Processing engine triggers, test plugins, and invoke On Request - plugins. The processing engine is an embedded Python VM that runs plugins - in response to database events. + description: | + Manage Processing engine triggers, test plugins, and send requests to trigger On Request plugins. + + InfluxDB 3 Enterprise provides the InfluxDB 3 processing engine, an embedded Python VM that can dynamically load and trigger Python plugins in response to events in your database. + Use Processing engine plugins and triggers to run code and perform tasks for different database events. x-related: - title: Processing engine and Python plugins href: /influxdb3/enterprise/processing-engine/ @@ -52,9 +97,49 @@ tags: Quick start: x-traitTag: true - description: > - Get started authenticating, writing, and querying data with the - InfluxDB 3 Enterprise API. + description: | + Authenticate, write, and query with the API: + + 1. Create an admin token to authorize API requests. + + ```bash + curl -X POST "http://localhost:8181/api/v3/configure/token/admin" + ``` + 2. Check the status of the InfluxDB server. + + ```bash + curl "http://localhost:8181/health" \ + --header "Authorization: Bearer ADMIN_TOKEN" + ``` + + 3. Write data to InfluxDB. + + ```bash + curl "http://localhost:8181/api/v3/write_lp?db=sensors&precision=auto" + --header "Authorization: Bearer ADMIN_TOKEN" \ + --data-raw "home,room=Kitchen temp=72.0 + home,room=Living\ room temp=71.5" + ``` + + If all data is written, the response is `204 No Content`. + + 4. Query data from InfluxDB. + + ```bash + curl -G "http://localhost:8181/api/v3/query_sql" \ + --header "Authorization: Bearer ADMIN_TOKEN" \ + --data-urlencode "db=sensors" \ + --data-urlencode "q=SELECT * FROM home WHERE room='Living room'" \ + --data-urlencode "format=jsonl" + ``` + + Output: + + ```jsonl + {"room":"Living room","temp":71.5,"time":"2025-02-25T20:19:34.984098"} + ``` + + For more information, see the [Get started](/influxdb3/enterprise/get-started/) guide. Server information: description: > @@ -77,9 +162,26 @@ tags: href: /influxdb3/enterprise/admin/tokens/ Write data: - description: > - Write time series data to InfluxDB 3 Enterprise in line protocol format - using the v1, v2, or v3 write endpoints. + description: | + Write data to InfluxDB 3 Enterprise using line protocol format. + + #### Timestamp precision across write APIs + + InfluxDB 3 provides multiple write endpoints for compatibility with different InfluxDB versions. + The following table compares timestamp precision support across v1, v2, and v3 write APIs: + + | Precision | v1 (`/write`) | v2 (`/api/v2/write`) | v3 (`/api/v3/write_lp`) | + |-----------|---------------|----------------------|-------------------------| + | **Auto detection** | ❌ No | ❌ No | ✅ `auto` (default) | + | **Seconds** | ✅ `s` | ✅ `s` | ✅ `second` | + | **Milliseconds** | ✅ `ms` | ✅ `ms` | ✅ `millisecond` | + | **Microseconds** | ✅ `u` or `µ` | ✅ `us` | ✅ `microsecond` | + | **Nanoseconds** | ✅ `ns` | ✅ `ns` | ✅ `nanosecond` | + | **Minutes** | ✅ `m` | ❌ No | ❌ No | + | **Hours** | ✅ `h` | ❌ No | ❌ No | + | **Default** | Nanosecond | Nanosecond | **Auto** (guessed) | + + All timestamps are stored internally as nanoseconds. x-related: - title: Write data using HTTP APIs href: /influxdb3/enterprise/write-data/http-api/ diff --git a/api-docs/scripts/dist/generate-openapi-articles.js b/api-docs/scripts/dist/generate-openapi-articles.js new file mode 100644 index 000000000..2b1bc7adb --- /dev/null +++ b/api-docs/scripts/dist/generate-openapi-articles.js @@ -0,0 +1,850 @@ +#!/usr/bin/env node +"use strict"; +/** + * Generate OpenAPI Articles Script + * + * Generates Hugo data files and content pages from OpenAPI specifications + * for all InfluxDB products. + * + * Products are auto-discovered by scanning api-docs/ for .config.yml files. + * Hugo paths, menu keys, and static file names are derived from directory + * structure and existing Hugo frontmatter. + * + * This script: + * 1. Discovers products from .config.yml files + * 2. Cleans output directories (unless --no-clean) + * 3. Transforms documentation links in specs + * 4. Copies specs to static directory for download + * 5. Generates tag-based data fragments (YAML and JSON) + * 6. Generates Hugo content pages from article data + * + * Usage: + * node generate-openapi-articles.js # Clean and generate all products + * node generate-openapi-articles.js influxdb3-core # 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 + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.LINK_PATTERN = exports.MARKDOWN_FIELDS = void 0; +exports.discoverProducts = discoverProducts; +exports.processProduct = processProduct; +exports.processApiSection = processApiSection; +exports.transformDocLinks = transformDocLinks; +exports.validateDocLinks = validateDocLinks; +exports.resolveContentPath = resolveContentPath; +exports.deriveStaticDirName = deriveStaticDirName; +exports.getSectionSlug = getSectionSlug; +exports.parseApiEntry = parseApiEntry; +exports.readMenuKey = readMenuKey; +const child_process_1 = require("child_process"); +const path = __importStar(require("path")); +const fs = __importStar(require("fs")); +// Import the OpenAPI to Hugo converter +const openapiPathsToHugo = require('./openapi-paths-to-hugo-data/index.js'); +// --------------------------------------------------------------------------- +// Constants and CLI flags +// --------------------------------------------------------------------------- +const DOCS_ROOT = '.'; +const API_DOCS_ROOT = 'api-docs'; +const validateLinks = process.argv.includes('--validate-links'); +const skipFetch = process.argv.includes('--skip-fetch'); +const noClean = process.argv.includes('--no-clean'); +const dryRun = process.argv.includes('--dry-run'); +// --------------------------------------------------------------------------- +// Utility functions +// --------------------------------------------------------------------------- +/** + * Load products with API paths from data/products.yml. + * Returns a map of alt_link_key to API path for alt_links generation. + */ +function loadApiProducts() { + const yaml = require('js-yaml'); + const productsFile = path.join(DOCS_ROOT, 'data/products.yml'); + if (!fs.existsSync(productsFile)) { + console.warn('⚠️ products.yml not found, skipping alt_links generation'); + return new Map(); + } + const productsContent = fs.readFileSync(productsFile, 'utf8'); + const products = yaml.load(productsContent); + const apiProducts = new Map(); + for (const [, product] of Object.entries(products)) { + if (product.api_path && product.alt_link_key) { + apiProducts.set(product.alt_link_key, product.api_path); + } + } + return apiProducts; +} +const apiProductsMap = loadApiProducts(); +/** Execute a shell command and handle errors */ +function execCommand(command, description) { + try { + if (description) { + console.log(`\n${description}...`); + } + console.log(`Executing: ${command}\n`); + (0, child_process_1.execSync)(command, { stdio: 'inherit' }); + } + catch (error) { + console.error(`\n❌ Error executing command: ${command}`); + if (error instanceof Error) { + console.error(error.message); + } + process.exit(1); + } +} +// --------------------------------------------------------------------------- +// Auto-discovery functions +// --------------------------------------------------------------------------- +/** + * Recursively find all .config.yml files under api-docs/. + * Excludes the root api-docs/.config.yml and internal directories. + */ +function findConfigFiles(rootDir) { + const configs = []; + const skipDirs = new Set([ + 'node_modules', + 'dist', + '_build', + 'scripts', + 'openapi', + ]); + function scanDir(dir, depth) { + if (depth > 5) + return; + let entries; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } + catch { + return; + } + for (const entry of entries) { + if (skipDirs.has(entry.name)) + continue; + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + scanDir(fullPath, depth + 1); + } + else if (entry.name === '.config.yml' && dir !== rootDir) { + configs.push(fullPath); + } + } + } + scanDir(rootDir, 0); + return configs.sort(); +} +/** + * Parse an API entry key like 'v3@3' into apiKey and version. + */ +function parseApiEntry(entry) { + const atIdx = entry.indexOf('@'); + if (atIdx === -1) { + return { apiKey: entry, version: '0' }; + } + return { + apiKey: entry.substring(0, atIdx), + version: entry.substring(atIdx + 1), + }; +} +/** + * Determine Hugo section slug from API key. + * 'management' → 'management-api', everything else → 'api'. + */ +function getSectionSlug(apiKey) { + if (apiKey === 'management') + return 'management-api'; + return 'api'; +} +/** + * Derive a clean static directory name from a product directory path. + * Replaces path separators and underscores with hyphens. + * + * @example 'influxdb3/core' → 'influxdb3-core' + * @example 'enterprise_influxdb/v1' → 'enterprise-influxdb-v1' + */ +function deriveStaticDirName(productDir) { + return productDir.replace(/[/_]/g, '-'); +} +/** + * Read the cascade.product field from a product's _index.md frontmatter. + * This value serves as the Hugo menu key. + */ +function readMenuKey(pagesDir) { + const yaml = require('js-yaml'); + const indexFile = path.join(pagesDir, '_index.md'); + if (!fs.existsSync(indexFile)) { + console.warn(`⚠️ Product index not found: ${indexFile}`); + return ''; + } + const content = fs.readFileSync(indexFile, 'utf8'); + const fmMatch = content.match(/^---\n([\s\S]*?)\n---/); + if (!fmMatch) + return ''; + try { + const fm = yaml.load(fmMatch[1]); + const cascade = fm.cascade; + if (cascade?.product) + return cascade.product; + // Fallback: first key of the menu map + if (fm.menu && typeof fm.menu === 'object') { + const keys = Object.keys(fm.menu); + if (keys.length > 0) + return keys[0]; + } + } + catch { + console.warn(`⚠️ Could not parse frontmatter in ${indexFile}`); + } + return ''; +} +/** + * Check whether a hand-maintained api/_index.md already has a menu entry. + * If so, the generator should skip adding its own parent menu entry. + * + * Only detects genuinely hand-maintained files — files previously generated + * by this script (which have articleDataKey in frontmatter) are ignored, + * since they'll be regenerated during this run. + */ +function hasExistingApiMenu(pagesDir) { + const yaml = require('js-yaml'); + const apiIndex = path.join(pagesDir, 'api', '_index.md'); + if (!fs.existsSync(apiIndex)) + return false; + const content = fs.readFileSync(apiIndex, 'utf8'); + const fmMatch = content.match(/^---\n([\s\S]*?)\n---/); + if (!fmMatch) + return false; + try { + const fm = yaml.load(fmMatch[1]); + // Skip files generated by this script (they have articleDataKey) + if (fm.articleDataKey) + return false; + return !!fm.menu; + } + catch { + return false; + } +} +/** + * Discover all products by scanning api-docs/ for .config.yml files. + * Derives Hugo paths from directory structure and existing frontmatter. + */ +function discoverProducts() { + const yaml = require('js-yaml'); + const products = []; + const configFiles = findConfigFiles(API_DOCS_ROOT); + for (const configPath of configFiles) { + const configDir = path.dirname(configPath); + const productDir = path.relative(API_DOCS_ROOT, configDir); + let config; + try { + const raw = fs.readFileSync(configPath, 'utf8'); + config = yaml.load(raw); + } + catch (err) { + console.warn(`⚠️ Could not parse ${configPath}: ${err}`); + continue; + } + if (!config.apis || Object.keys(config.apis).length === 0) { + continue; + } + const pagesDir = path.join(DOCS_ROOT, 'content', productDir); + const staticDirName = deriveStaticDirName(productDir); + const menuKey = readMenuKey(pagesDir); + const skipParentMenu = hasExistingApiMenu(pagesDir); + // Parse API entries, skipping compatibility specs + const apis = []; + for (const [entryKey, entry] of Object.entries(config.apis)) { + const { apiKey, version } = parseApiEntry(entryKey); + // Skip v1-compatibility entries (being removed in pipeline restructure) + if (apiKey.includes('compatibility')) + continue; + // Prefer post-processed spec from _build/ (has overlays and tag configs), + // fall back to source spec for standalone usage + const sourceSpec = path.join(configDir, entry.root); + const buildSpec = path.join(API_DOCS_ROOT, '_build', productDir, entry.root); + const specFile = fs.existsSync(buildSpec) ? buildSpec : sourceSpec; + const sectionSlug = getSectionSlug(apiKey); + apis.push({ apiKey, version, specFile, sectionSlug }); + } + if (apis.length === 0) + continue; + products.push({ + configDir, + productDir, + productName: config['x-influxdata-product-name'] || productDir, + pagesDir, + menuKey, + skipParentMenu, + staticDirName, + apis, + }); + } + return products; +} +// --------------------------------------------------------------------------- +// Cleanup functions +// --------------------------------------------------------------------------- +/** + * Get all paths that would be cleaned for a product. + * + * @param product - The product to clean + * @param allStaticDirNames - Names of all products (to avoid prefix collisions) + */ +function getCleanupPaths(product, allStaticDirNames) { + const staticPath = path.join(DOCS_ROOT, 'static/openapi'); + const directories = []; + const files = []; + // Tag specs directory: static/openapi/{staticDirName}/ + const tagSpecsDir = path.join(staticPath, product.staticDirName); + if (fs.existsSync(tagSpecsDir)) { + directories.push(tagSpecsDir); + } + // Article data directory: data/article_data/influxdb/{staticDirName}/ + const articleDataDir = path.join(DOCS_ROOT, `data/article_data/influxdb/${product.staticDirName}`); + if (fs.existsSync(articleDataDir)) { + directories.push(articleDataDir); + } + // Content pages: content/{pagesDir}/{sectionSlug}/ for each API + for (const api of product.apis) { + const contentDir = path.join(product.pagesDir, api.sectionSlug); + if (fs.existsSync(contentDir)) { + directories.push(contentDir); + } + } + // Root spec files: static/openapi/{staticDirName}*.yml and .json + // Avoid matching files that belong to products with longer names + // (e.g., 'influxdb-cloud' should not match 'influxdb-cloud-dedicated-*.yml') + const longerPrefixes = allStaticDirNames.filter((n) => n !== product.staticDirName && n.startsWith(product.staticDirName + '-')); + if (fs.existsSync(staticPath)) { + const staticFiles = fs.readdirSync(staticPath); + staticFiles + .filter((f) => { + if (!f.startsWith(product.staticDirName)) + return false; + // Exclude files belonging to a longer-named product + for (const longer of longerPrefixes) { + if (f.startsWith(longer)) + return false; + } + return f.endsWith('.yml') || f.endsWith('.json'); + }) + .forEach((f) => { + files.push(path.join(staticPath, f)); + }); + } + return { directories, files }; +} +/** Clean output directories for a product before regeneration. */ +function cleanProductOutputs(product, allStaticDirNames) { + const { directories, files } = getCleanupPaths(product, allStaticDirNames); + for (const dir of directories) { + console.log(`🧹 Removing directory: ${dir}`); + fs.rmSync(dir, { recursive: true, force: true }); + } + for (const file of files) { + console.log(`🧹 Removing file: ${file}`); + fs.unlinkSync(file); + } + const total = directories.length + files.length; + if (total > 0) { + console.log(`✓ Cleaned ${directories.length} directories, ${files.length} files for ${product.staticDirName}`); + } +} +/** Display dry-run preview of what would be cleaned. */ +function showDryRunPreview(product, allStaticDirNames) { + const { directories, files } = getCleanupPaths(product, allStaticDirNames); + console.log(`\nDRY RUN: Would clean the following for ${product.staticDirName}:\n`); + if (directories.length > 0) { + console.log('Directories to remove:'); + directories.forEach((dir) => console.log(` - ${dir}`)); + } + if (files.length > 0) { + console.log('\nFiles to remove:'); + files.forEach((file) => console.log(` - ${file}`)); + } + if (directories.length === 0 && files.length === 0) { + console.log(' (no files to clean)'); + } + console.log(`\nSummary: ${directories.length} directories, ${files.length} files would be removed`); +} +// --------------------------------------------------------------------------- +// Link transformation +// --------------------------------------------------------------------------- +/** Fields that can contain markdown with links */ +const MARKDOWN_FIELDS = new Set(['description', 'summary']); +exports.MARKDOWN_FIELDS = MARKDOWN_FIELDS; +/** Link placeholder pattern */ +const LINK_PATTERN = /\/influxdb\/version\//g; +exports.LINK_PATTERN = LINK_PATTERN; +/** + * Transform documentation links in OpenAPI spec markdown fields. + * Replaces `/influxdb/version/` with the actual product path. + */ +function transformDocLinks(spec, productPath) { + function transformValue(value) { + if (typeof value === 'string') { + return value.replace(LINK_PATTERN, `${productPath}/`); + } + if (Array.isArray(value)) { + return value.map(transformValue); + } + if (value !== null && typeof value === 'object') { + return transformObject(value); + } + return value; + } + function transformObject(obj) { + const result = {}; + for (const [key, value] of Object.entries(obj)) { + if (MARKDOWN_FIELDS.has(key) && typeof value === 'string') { + result[key] = value.replace(LINK_PATTERN, `${productPath}/`); + } + else if (value !== null && typeof value === 'object') { + result[key] = transformValue(value); + } + else { + result[key] = value; + } + } + return result; + } + return transformObject(spec); +} +/** + * Resolve a URL path to a content file path. + * + * @example '/influxdb3/core/api/auth/' → 'content/influxdb3/core/api/auth/_index.md' + */ +function resolveContentPath(urlPath, contentDir) { + const normalized = urlPath.replace(/\/$/, ''); + const indexPath = path.join(contentDir, normalized, '_index.md'); + const directPath = path.join(contentDir, normalized + '.md'); + if (fs.existsSync(indexPath)) + return indexPath; + if (fs.existsSync(directPath)) + return directPath; + return indexPath; +} +/** + * Validate that transformed links point to existing content. + */ +function validateDocLinks(spec, contentDir) { + const errors = []; + const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g; + function extractLinks(value, jsonPath) { + if (typeof value === 'string') { + let match; + while ((match = linkPattern.exec(value)) !== null) { + const [, linkText, linkUrl] = match; + if (linkUrl.startsWith('/') && !linkUrl.startsWith('//')) { + const contentPath = resolveContentPath(linkUrl, contentDir); + if (!fs.existsSync(contentPath)) { + errors.push(`Broken link at ${jsonPath}: [${linkText}](${linkUrl})`); + } + } + } + linkPattern.lastIndex = 0; + } + else if (Array.isArray(value)) { + value.forEach((item, index) => extractLinks(item, `${jsonPath}[${index}]`)); + } + else if (value !== null && typeof value === 'object') { + for (const [key, val] of Object.entries(value)) { + extractLinks(val, `${jsonPath}.${key}`); + } + } + } + extractLinks(spec, 'spec'); + return errors; +} +/** + * 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 Hugo-native + * templates. Includes operation metadata for TOC generation. + */ +function generateTagPagesFromArticleData(options) { + const { articlesPath, contentPath, sectionSlug, menuKey, menuParent, productDescription, skipParentMenu, specDownloadPath, articleDataKey, articleSection, } = 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; + } + const articlesContent = fs.readFileSync(articlesFile, 'utf8'); + const data = yaml.load(articlesContent); + if (!data.articles || !Array.isArray(data.articles)) { + console.warn(`⚠️ No articles found in ${articlesFile}`); + return; + } + if (!fs.existsSync(contentPath)) { + fs.mkdirSync(contentPath, { recursive: true }); + } + // Generate parent _index.md for the section + const sectionDir = path.join(contentPath, sectionSlug); + const parentIndexFile = path.join(sectionDir, '_index.md'); + if (!fs.existsSync(sectionDir)) { + fs.mkdirSync(sectionDir, { recursive: true }); + } + if (!fs.existsSync(parentIndexFile)) { + const apiDescription = productDescription || + `Use the InfluxDB HTTP API to write data, query data, and manage databases, tables, and tokens.`; + const parentFrontmatter = { + title: menuParent || 'InfluxDB HTTP API', + description: apiDescription, + weight: 104, + type: 'api', + articleDataKey, + articleSection, + }; + if (menuKey && !skipParentMenu) { + parentFrontmatter.menu = { + [menuKey]: { + name: menuParent || 'InfluxDB HTTP API', + identifier: `api-reference-${articleDataKey}-${sectionSlug}`, + parent: 'Reference', + }, + }; + } + if (apiProductsMap.size > 0) { + const altLinks = {}; + apiProductsMap.forEach((apiPath, productName) => { + altLinks[productName] = apiPath; + }); + parentFrontmatter.alt_links = altLinks; + } + const introText = apiDescription.replace('InfluxDB', '{{% product-name %}}'); + const parentContent = `--- +${yaml.dump(parentFrontmatter)}--- + +${introText} + +{{< children >}} +`; + fs.writeFileSync(parentIndexFile, parentContent); + console.log(`✓ Generated parent index at ${parentIndexFile}`); + } + // Generate "All endpoints" page + const allEndpointsDir = path.join(sectionDir, 'all-endpoints'); + const allEndpointsFile = path.join(allEndpointsDir, '_index.md'); + if (!fs.existsSync(allEndpointsDir)) { + fs.mkdirSync(allEndpointsDir, { recursive: true }); + } + const allEndpointsFrontmatter = { + title: 'All endpoints', + description: `View all API endpoints sorted by path.`, + type: 'api', + layout: 'all-endpoints', + weight: 999, + isAllEndpoints: true, + articleDataKey, + articleSection, + }; + if (menuKey) { + allEndpointsFrontmatter.menu = { + [menuKey]: { + name: 'All endpoints', + identifier: `all-endpoints-${articleDataKey}-${sectionSlug}`, + parent: menuParent || 'InfluxDB HTTP API', + }, + }; + } + if (apiProductsMap.size > 0) { + const altLinks = {}; + apiProductsMap.forEach((apiPath, productName) => { + altLinks[productName] = apiPath; + }); + allEndpointsFrontmatter.alt_links = altLinks; + } + const allEndpointsContent = `--- +${yaml.dump(allEndpointsFrontmatter)}--- + +All {{% product-name %}} API endpoints, sorted by path. +`; + fs.writeFileSync(allEndpointsFile, allEndpointsContent); + console.log(`✓ Generated all-endpoints page at ${allEndpointsFile}`); + // Generate a page for each article (tag) + for (const article of data.articles) { + const pagePath = path.join(contentPath, article.path); + const pageFile = path.join(pagePath, '_index.md'); + if (!fs.existsSync(pagePath)) { + fs.mkdirSync(pagePath, { recursive: true }); + } + const title = article.fields.title || article.fields.name || article.path; + const isConceptual = article.fields.isConceptual === true; + const weight = article.fields.weight ?? 100; + const frontmatter = { + title, + description: article.fields.description || `API reference for ${title}`, + type: 'api', + layout: isConceptual ? 'single' : 'list', + staticFilePath: article.fields.staticFilePath, + weight, + tag: article.fields.tag, + isConceptual, + menuGroup: article.fields.menuGroup, + specDownloadPath, + articleDataKey, + articleSection, + }; + if (!isConceptual && + article.fields.operations && + article.fields.operations.length > 0) { + frontmatter.operations = article.fields.operations; + } + if (isConceptual && article.fields.tagDescription) { + frontmatter.tagDescription = article.fields.tagDescription; + } + if (article.fields.showSecuritySchemes) { + frontmatter.showSecuritySchemes = true; + } + // Add related links if present + if (article.fields.related && + Array.isArray(article.fields.related) && + article.fields.related.length > 0) { + frontmatter.related = article.fields.related; + } + // Add client library related link for InfluxDB 3 products + if (contentPath.includes('influxdb3/') && !isConceptual) { + 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]; + } + } + } + if (apiProductsMap.size > 0) { + const altLinks = {}; + apiProductsMap.forEach((apiPath, productName) => { + altLinks[productName] = apiPath; + }); + frontmatter.alt_links = altLinks; + } + const pageContent = `--- +${yaml.dump(frontmatter)}--- +`; + fs.writeFileSync(pageFile, pageContent); + } + console.log(`✓ Generated ${data.articles.length} tag-based content pages in ${contentPath}`); +} +// --------------------------------------------------------------------------- +// Spec processing +// --------------------------------------------------------------------------- +/** + * Process a single API section: transform links, write static spec, + * generate tag data, and create Hugo content pages. + */ +function processApiSection(product, api, staticBasePath) { + const yaml = require('js-yaml'); + const isDualApi = product.apis.length > 1; + console.log(`\n📄 Processing ${api.sectionSlug} section (${api.apiKey})`); + // --- 1. Determine paths --- + // Root spec download: single → {dir}.yml, dual → {dir}-{section}.yml + const specSuffix = isDualApi ? `-${api.sectionSlug}` : ''; + const staticSpecPath = path.join(staticBasePath, `${product.staticDirName}${specSuffix}.yml`); + const staticJsonSpecPath = staticSpecPath.replace('.yml', '.json'); + // Tag specs directory + const tagSpecsBase = isDualApi + ? path.join(staticBasePath, product.staticDirName, api.sectionSlug) + : path.join(staticBasePath, product.staticDirName); + // Article data + const articlesPath = path.join(DOCS_ROOT, 'data/article_data/influxdb', product.staticDirName, api.sectionSlug); + // Download path for frontmatter + const specDownloadPath = `/openapi/${product.staticDirName}${specSuffix}.yml`; + // Path spec files for per-operation rendering + const pathSpecsDir = isDualApi + ? path.join(staticBasePath, product.staticDirName, api.sectionSlug, 'paths') + : path.join(staticBasePath, product.staticDirName, 'paths'); + // --- 2. Read and transform spec --- + if (!fs.existsSync(api.specFile)) { + console.warn(`⚠️ Spec file not found: ${api.specFile}`); + return; + } + const specContent = fs.readFileSync(api.specFile, 'utf8'); + const specObject = yaml.load(specContent); + const productPath = `/${product.productDir}`; + const transformedSpec = transformDocLinks(specObject, productPath); + console.log(`✓ Transformed documentation links for ${api.apiKey} to ${productPath}`); + // Validate links if enabled + if (validateLinks) { + const contentDir = path.join(DOCS_ROOT, 'content'); + const linkErrors = validateDocLinks(transformedSpec, contentDir); + if (linkErrors.length > 0) { + console.warn(`\n⚠️ Link validation warnings for ${api.specFile}:`); + linkErrors.forEach((err) => console.warn(` ${err}`)); + } + } + // --- 3. Write transformed spec to static folder --- + if (!fs.existsSync(staticBasePath)) { + fs.mkdirSync(staticBasePath, { recursive: true }); + } + fs.writeFileSync(staticSpecPath, yaml.dump(transformedSpec)); + console.log(`✓ Wrote transformed spec to ${staticSpecPath}`); + fs.writeFileSync(staticJsonSpecPath, JSON.stringify(transformedSpec, null, 2)); + console.log(`✓ Generated JSON spec at ${staticJsonSpecPath}`); + // --- 4. Generate tag-based data --- + console.log(`\n📋 Generating tag-based data for ${api.apiKey} in ${tagSpecsBase}...`); + openapiPathsToHugo.generateHugoDataByTag({ + specFile: staticSpecPath, + dataOutPath: tagSpecsBase, + articleOutPath: articlesPath, + includePaths: true, + }); + // Generate path-specific specs + openapiPathsToHugo.generatePathSpecificSpecs(staticSpecPath, pathSpecsDir); + // --- 5. Generate Hugo content pages --- + generateTagPagesFromArticleData({ + articlesPath, + contentPath: product.pagesDir, + sectionSlug: api.sectionSlug, + menuKey: product.menuKey, + menuParent: 'InfluxDB HTTP API', + skipParentMenu: product.skipParentMenu, + specDownloadPath, + articleDataKey: product.staticDirName, + articleSection: api.sectionSlug, + }); +} +/** + * Process a single product: clean outputs and process each API section. + */ +function processProduct(product, allStaticDirNames) { + console.log('\n' + '='.repeat(80)); + console.log(`Processing ${product.productName}`); + console.log('='.repeat(80)); + // Clean output directories before regeneration + if (!noClean && !dryRun) { + cleanProductOutputs(product, allStaticDirNames); + } + const staticBasePath = path.join(DOCS_ROOT, 'static/openapi'); + // Fetch specs if needed + if (!skipFetch) { + const getswaggerScript = path.join(API_DOCS_ROOT, 'getswagger.sh'); + if (fs.existsSync(getswaggerScript)) { + // The build function in generate-api-docs.sh handles per-product + // fetching. When called standalone, use product directory name. + execCommand(`cd ${API_DOCS_ROOT} && ./getswagger.sh ${product.productDir} -B`, `Fetching OpenAPI spec for ${product.productName}`); + } + else { + console.log(`⚠️ getswagger.sh not found, skipping fetch step`); + } + } + else { + console.log(`⏭️ Skipping getswagger.sh (--skip-fetch flag set)`); + } + // Process each API section independently + for (const api of product.apis) { + processApiSection(product, api, staticBasePath); + } + console.log(`\n✅ Successfully processed ${product.productName}\n`); +} +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- +function main() { + const args = process.argv.slice(2).filter((arg) => !arg.startsWith('--')); + // Discover all products from .config.yml files + const allProducts = discoverProducts(); + if (allProducts.length === 0) { + console.error('❌ No products discovered. Ensure .config.yml files exist under api-docs/.'); + process.exit(1); + } + // Determine which products to process + let productsToProcess; + if (args.length === 0) { + productsToProcess = allProducts; + console.log(`\n📋 Discovered ${allProducts.length} products, processing all...\n`); + } + else { + // Match by staticDirName or productDir + productsToProcess = []; + const invalid = []; + for (const arg of args) { + const found = allProducts.find((p) => p.staticDirName === arg || + p.productDir === arg || + p.productDir.replace(/\//g, '-') === arg); + if (found) { + productsToProcess.push(found); + } + else { + invalid.push(arg); + } + } + if (invalid.length > 0) { + console.error(`\n❌ Unknown product identifier(s): ${invalid.join(', ')}`); + console.error('\nDiscovered products:'); + allProducts.forEach((p) => { + console.error(` - ${p.staticDirName} (${p.productName}) [${p.productDir}]`); + }); + process.exit(1); + } + console.log(`\n📋 Processing specified products: ${productsToProcess.map((p) => p.staticDirName).join(', ')}\n`); + } + // Collect all staticDirNames for prefix-safe cleanup + const allStaticDirNames = allProducts.map((p) => p.staticDirName); + // Handle dry-run mode + if (dryRun) { + console.log('\n📋 DRY RUN MODE - No files will be modified\n'); + productsToProcess.forEach((p) => showDryRunPreview(p, allStaticDirNames)); + console.log('\nDry run complete. No files were modified.'); + return; + } + // Process each product + productsToProcess.forEach((product) => { + processProduct(product, allStaticDirNames); + }); + console.log('\n' + '='.repeat(80)); + console.log('✅ All products processed successfully!'); + console.log('='.repeat(80) + '\n'); +} +// Execute if run directly +if (require.main === module) { + main(); +} +//# sourceMappingURL=generate-openapi-articles.js.map \ No newline at end of file diff --git a/api-docs/scripts/dist/openapi-paths-to-hugo-data/index.js b/api-docs/scripts/dist/openapi-paths-to-hugo-data/index.js new file mode 100644 index 000000000..4874d1000 --- /dev/null +++ b/api-docs/scripts/dist/openapi-paths-to-hugo-data/index.js @@ -0,0 +1,920 @@ +"use strict"; +/** + * OpenAPI to Hugo Data Converter + * + * Converts OpenAPI v3 specifications into Hugo-compatible data files. + * Generates both YAML and JSON versions of spec fragments grouped by path. + * + * @module openapi-paths-to-hugo-data + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.writePathSpecificSpecs = writePathSpecificSpecs; +exports.generateHugoDataByTag = generateHugoDataByTag; +exports.generateHugoData = generateHugoData; +exports.generatePathSpecificSpecs = generatePathSpecificSpecs; +const yaml = __importStar(require("js-yaml")); +const fs = __importStar(require("fs")); +const path = __importStar(require("path")); +/** + * Read a YAML file and parse it + * + * @param filepath - Path to the YAML file + * @param encoding - File encoding (default: 'utf8') + * @returns Parsed YAML content + */ +function readFile(filepath, encoding = 'utf8') { + const content = fs.readFileSync(filepath, encoding); + return yaml.load(content); +} +/** + * Write data to a YAML file + * + * @param data - Data to write + * @param outputTo - Output file path + */ +function writeDataFile(data, outputTo) { + fs.writeFileSync(outputTo, yaml.dump(data)); +} +/** + * Write data to a JSON file + * + * @param data - Data to write + * @param outputTo - Output file path + */ +function writeJsonFile(data, outputTo) { + fs.writeFileSync(outputTo, JSON.stringify(data, null, 2)); +} +/** + * OpenAPI utility functions + */ +const openapiUtils = { + /** + * Check if a path fragment is a placeholder (e.g., {id}) + * + * @param str - Path fragment to check + * @returns True if the fragment is a placeholder + */ + isPlaceholderFragment(str) { + const placeholderRegex = /^\{.*\}$/; + return placeholderRegex.test(str); + }, +}; +/** + * Convert tag name to URL-friendly slug + * + * @param tagName - Tag name (e.g., "Write data", "Processing engine") + * @returns URL-friendly slug (e.g., "write-data", "processing-engine") + */ +function slugifyTag(tagName) { + return tagName + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, ''); +} +/** + * Menu group mappings for tag-based navigation + * Maps OpenAPI tags to sidebar groups + */ +const TAG_MENU_GROUPS = { + // Concepts group + 'Quick start': 'Concepts', + Authentication: 'Concepts', + 'Headers and parameters': 'Concepts', + 'Response codes': 'Concepts', + // Data Operations group + 'Write data': 'Data Operations', + 'Query data': 'Data Operations', + 'Cache data': 'Data Operations', + // Administration group + Database: 'Administration', + Table: 'Administration', + Token: 'Administration', + // Processing Engine group + 'Processing engine': 'Processing Engine', + // Server group + 'Server information': 'Server', + // Compatibility group + 'Compatibility endpoints': 'Compatibility', +}; +/** + * Get menu group for a tag + * + * @param tagName - Tag name + * @returns Menu group name or 'Other' if not mapped + */ +function getMenuGroupForTag(tagName) { + return TAG_MENU_GROUPS[tagName] || 'Other'; +} +/** + * HTTP methods to check for operations + */ +const HTTP_METHODS = [ + 'get', + 'post', + 'put', + 'patch', + 'delete', + 'options', + 'head', + 'trace', +]; +/** + * Extract all operations from an OpenAPI document grouped by tag + * + * @param openapi - OpenAPI document + * @returns Map of tag name to operations with that tag + */ +function extractOperationsByTag(openapi) { + const tagOperations = new Map(); + Object.entries(openapi.paths).forEach(([pathKey, pathItem]) => { + HTTP_METHODS.forEach((method) => { + const operation = pathItem[method]; + if (operation) { + const opMeta = { + operationId: operation.operationId || `${method}-${pathKey}`, + method: method.toUpperCase(), + path: pathKey, + summary: operation.summary || '', + tags: operation.tags || [], + }; + // Extract compatibility version if present + if (operation['x-compatibility-version']) { + opMeta.compatVersion = operation['x-compatibility-version']; + } + // Extract externalDocs if present + if (operation.externalDocs) { + opMeta.externalDocs = { + description: operation.externalDocs.description || '', + url: operation.externalDocs.url, + }; + } + // Extract x-influxdatadocs-related if present + if (operation['x-influxdatadocs-related'] && + Array.isArray(operation['x-influxdatadocs-related'])) { + 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']; + } + // Add operation to each of its tags + (operation.tags || []).forEach((tag) => { + if (!tagOperations.has(tag)) { + tagOperations.set(tag, []); + } + tagOperations.get(tag).push(opMeta); + }); + } + }); + }); + return tagOperations; +} +/** + * Write OpenAPI specs grouped by tag to separate files + * Generates both YAML and JSON versions per tag + * + * @param openapi - OpenAPI document + * @param prefix - Filename prefix for output files + * @param outPath - Output directory path + */ +function writeTagOpenapis(openapi, prefix, outPath) { + const tagOperations = extractOperationsByTag(openapi); + // Process each tag + tagOperations.forEach((operations, tagName) => { + // Deep copy openapi + const doc = JSON.parse(JSON.stringify(openapi)); + // Filter paths to only include those with operations for this tag + const filteredPaths = {}; + Object.entries(openapi.paths).forEach(([pathKey, pathItem]) => { + const filteredPathItem = {}; + let hasOperations = false; + HTTP_METHODS.forEach((method) => { + const operation = pathItem[method]; + if (operation?.tags?.includes(tagName)) { + // Clone the operation and restrict tags to only this tag + // 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; + hasOperations = true; + } + }); + // Include path-level parameters if we have operations + if (hasOperations) { + if (pathItem.parameters) { + filteredPathItem.parameters = pathItem.parameters; + } + filteredPaths[pathKey] = filteredPathItem; + } + }); + doc.paths = filteredPaths; + // Filter tags to only include this tag (and trait tags for context) + if (doc.tags) { + doc.tags = doc.tags.filter((tag) => tag.name === tagName || tag['x-traitTag']); + } + // Update info + const tagSlug = slugifyTag(tagName); + doc.info.title = tagName; + doc.info.description = `API reference for ${tagName}`; + doc['x-tagGroup'] = tagName; + try { + if (!fs.existsSync(outPath)) { + fs.mkdirSync(outPath, { recursive: true }); + } + const baseFilename = `${prefix}${tagSlug}`; + const yamlPath = path.resolve(outPath, `${baseFilename}.yaml`); + const jsonPath = path.resolve(outPath, `${baseFilename}.json`); + writeDataFile(doc, yamlPath); + writeJsonFile(doc, jsonPath); + console.log(`Generated tag spec: ${baseFilename}.yaml (${Object.keys(filteredPaths).length} paths, ${operations.length} operations)`); + } + catch (err) { + console.error(`Error writing tag group ${tagName}:`, err); + } + }); + // Also create specs for conceptual tags (x-traitTag) without operations + (openapi.tags || []).forEach((tag) => { + if (tag['x-traitTag'] && !tagOperations.has(tag.name)) { + const doc = JSON.parse(JSON.stringify(openapi)); + doc.paths = {}; + doc.tags = [tag]; + doc.info.title = tag.name; + doc.info.description = tag.description || `API reference for ${tag.name}`; + doc['x-tagGroup'] = tag.name; + const tagSlug = slugifyTag(tag.name); + try { + const baseFilename = `${prefix}${tagSlug}`; + const yamlPath = path.resolve(outPath, `${baseFilename}.yaml`); + const jsonPath = path.resolve(outPath, `${baseFilename}.json`); + writeDataFile(doc, yamlPath); + writeJsonFile(doc, jsonPath); + console.log(`Generated conceptual tag spec: ${baseFilename}.yaml`); + } + catch (err) { + console.error(`Error writing conceptual tag ${tag.name}:`, err); + } + } + }); +} +/** + * Convert API path to filename-safe slug + * + * @param apiPath - API path (e.g., "/api/v3/configure/token/admin") + * @returns Filename-safe slug (e.g., "api-v3-configure-token-admin") + */ +function pathToFileSlug(apiPath) { + return apiPath + .replace(/^\//, '') // Remove leading slash + .replace(/\//g, '-') // Replace slashes with dashes + .replace(/[{}]/g, '') // Remove curly braces from path params + .replace(/-+/g, '-') // Collapse multiple dashes + .replace(/-$/, ''); // Remove trailing dash +} +/** + * Write path-specific OpenAPI specs (one file per exact API path) + * + * Each file contains all HTTP methods for a single path, enabling + * operation pages to filter by method only (no path prefix conflicts). + * + * @param openapi - OpenAPI document + * @param outPath - Output directory path (e.g., "static/openapi/{product}/paths") + * @returns Map of API path to spec file path (for use in frontmatter) + */ +function writePathSpecificSpecs(openapi, outPath) { + const pathSpecFiles = new Map(); + if (!fs.existsSync(outPath)) { + fs.mkdirSync(outPath, { recursive: true }); + } + Object.entries(openapi.paths).forEach(([apiPath, pathItem]) => { + // Deep clone pathItem to avoid mutating original + const clonedPathItem = JSON.parse(JSON.stringify(pathItem)); + // Limit each operation to a single tag to prevent duplicate rendering + // Operations with multiple tags would be rendered once per tag + const usedTags = new Set(); + HTTP_METHODS.forEach((method) => { + const operation = clonedPathItem[method]; + if (operation?.tags && operation.tags.length > 0) { + // Select the most specific tag to avoid duplicate rendering + // Prefer "Auth token" over "Authentication" for token-related operations + let primaryTag = operation.tags[0]; + if (operation.tags.includes('Auth token')) { + primaryTag = 'Auth token'; + } + operation.tags = [primaryTag]; + usedTags.add(primaryTag); + } + }); + // Create spec with just this path (all its methods) + // Include global security requirements so auth info displays correctly + const pathSpec = { + openapi: openapi.openapi, + info: { + ...openapi.info, + title: apiPath, + description: `API reference for ${apiPath}`, + }, + paths: { [apiPath]: clonedPathItem }, + components: openapi.components, // Include for $ref resolution + servers: openapi.servers, + security: openapi.security, // Global security requirements + }; + // Filter spec-level tags to only include those used by operations + if (openapi.tags) { + pathSpec.tags = openapi.tags.filter((tag) => usedTags.has(tag.name) && !tag['x-traitTag']); + } + // Write files + const slug = pathToFileSlug(apiPath); + const yamlPath = path.resolve(outPath, `${slug}.yaml`); + const jsonPath = path.resolve(outPath, `${slug}.json`); + writeDataFile(pathSpec, yamlPath); + writeJsonFile(pathSpec, jsonPath); + // Store the web-accessible path (without "static/" prefix) + // Hugo serves files from static/ at the root, so we extract the path after 'static/' + const staticMatch = yamlPath.match(/static\/(.+)$/); + const webPath = staticMatch ? `/${staticMatch[1]}` : yamlPath; + pathSpecFiles.set(apiPath, webPath); + }); + console.log(`Generated ${pathSpecFiles.size} path-specific specs in ${outPath}`); + return pathSpecFiles; +} +/** + * Write OpenAPI specs grouped by path to separate files + * Generates both YAML and JSON versions + * + * @param openapi - OpenAPI document + * @param prefix - Filename prefix for output files + * @param outPath - Output directory path + */ +function writePathOpenapis(openapi, prefix, outPath) { + const pathGroups = {}; + // Group paths by their base path (first 3-4 segments, excluding placeholders) + Object.keys(openapi.paths) + .sort() + .forEach((p) => { + const delimiter = '/'; + let key = p.split(delimiter); + // Check if this is an item path (ends with a placeholder) + let isItemPath = openapiUtils.isPlaceholderFragment(key[key.length - 1]); + if (isItemPath) { + key = key.slice(0, -1); + } + // Take first 4 segments + key = key.slice(0, 4); + // Check if the last segment is still a placeholder + isItemPath = openapiUtils.isPlaceholderFragment(key[key.length - 1]); + if (isItemPath) { + key = key.slice(0, -1); + } + const groupKey = key.join('/'); + pathGroups[groupKey] = pathGroups[groupKey] || {}; + pathGroups[groupKey][p] = openapi.paths[p]; + }); + // Write each path group to separate YAML and JSON files + Object.keys(pathGroups).forEach((pg) => { + // Deep copy openapi + const doc = JSON.parse(JSON.stringify(openapi)); + doc.paths = pathGroups[pg]; + // Collect tags used by operations in this path group + const usedTags = new Set(); + Object.values(doc.paths).forEach((pathItem) => { + const httpMethods = [ + 'get', + 'post', + 'put', + 'patch', + 'delete', + 'options', + 'head', + 'trace', + ]; + httpMethods.forEach((method) => { + const operation = pathItem[method]; + if (operation?.tags) { + operation.tags.forEach((tag) => usedTags.add(tag)); + } + }); + }); + // Filter tags to only include those used by operations in this path group + // Exclude x-traitTag tags (supplementary documentation tags) + if (doc.tags) { + doc.tags = doc.tags.filter((tag) => usedTags.has(tag.name) && !tag['x-traitTag']); + } + // Simplify info for path-specific docs + doc.info.title = pg; + doc.info.description = `API reference for ${pg}`; + doc['x-pathGroup'] = pg; + try { + if (!fs.existsSync(outPath)) { + fs.mkdirSync(outPath, { recursive: true }); + } + const baseFilename = `${prefix}${pg.replaceAll('/', '-').replace(/^-/, '')}`; + const yamlPath = path.resolve(outPath, `${baseFilename}.yaml`); + const jsonPath = path.resolve(outPath, `${baseFilename}.json`); + // Write both YAML and JSON versions + writeDataFile(doc, yamlPath); + writeJsonFile(doc, jsonPath); + console.log(`Generated: ${baseFilename}.yaml and ${baseFilename}.json`); + } + catch (err) { + console.error(`Error writing path group ${pg}:`, err); + } + }); +} +/** + * Create article metadata for a path group + * + * @param openapi - OpenAPI document with x-pathGroup + * @returns Article metadata object + */ +function createArticleDataForPathGroup(openapi) { + const article = { + path: '', + fields: { + name: openapi['x-pathGroup'] || '', + describes: Object.keys(openapi.paths), + }, + }; + /** + * Convert OpenAPI path to Hugo-friendly article path + * Legacy endpoints (without /api/ prefix) go under api/ directly + * Versioned endpoints (with /api/vN/) keep their structure + * + * @param p - Path to convert (e.g., '/health', '/api/v3/query_sql') + * @returns Path suitable for Hugo content directory (e.g., 'api/health', 'api/v3/query_sql') + */ + const toHugoPath = (p) => { + if (!p) { + return ''; + } + // If path doesn't start with /api/, it's a legacy endpoint + // Place it directly under api/ to avoid collision with /api/v1/* paths + if (!p.startsWith('/api/')) { + // /health -> api/health + // /write -> api/write + return `api${p}`; + } + // /api/v1/health -> api/v1/health + // /api/v2/write -> api/v2/write + // /api/v3/query_sql -> api/v3/query_sql + return p.replace(/^\//, ''); + }; + /** + * Convert path to tag-friendly format (dashes instead of slashes) + * + * @param p - Path to convert + * @returns Tag-friendly path + */ + const toTagPath = (p) => { + if (!p) { + return ''; + } + return p.replace(/^\//, '').replaceAll('/', '-'); + }; + const pathGroup = openapi['x-pathGroup'] || ''; + article.path = toHugoPath(pathGroup); + // Store original path for menu display (shows actual endpoint path) + article.fields.menuName = pathGroup; + article.fields.title = openapi.info?.title; + article.fields.description = openapi.description; + const pathGroupFrags = path.parse(openapi['x-pathGroup'] || ''); + article.fields.tags = [pathGroupFrags?.dir, pathGroupFrags?.name] + .filter(Boolean) + .map((t) => toTagPath(t)); + // Extract x-relatedLinks and OpenAPI tags from path items or operations + const relatedLinks = []; + const apiTags = []; + const httpMethods = [ + 'get', + 'post', + 'put', + 'patch', + 'delete', + 'options', + 'head', + 'trace', + ]; + Object.values(openapi.paths).forEach((pathItem) => { + // Check path-level x-relatedLinks + if (pathItem['x-relatedLinks'] && + Array.isArray(pathItem['x-relatedLinks'])) { + relatedLinks.push(...pathItem['x-relatedLinks'].filter((link) => !relatedLinks.includes(link))); + } + // Check operation-level x-relatedLinks and tags + httpMethods.forEach((method) => { + const operation = pathItem[method]; + if (operation) { + // Extract x-relatedLinks + if (operation['x-relatedLinks'] && + Array.isArray(operation['x-relatedLinks'])) { + relatedLinks.push(...operation['x-relatedLinks'].filter((link) => !relatedLinks.includes(link))); + } + // Extract OpenAPI tags from operation + if (operation.tags && Array.isArray(operation.tags)) { + operation.tags.forEach((tag) => { + if (!apiTags.includes(tag)) { + apiTags.push(tag); + } + }); + } + } + }); + }); + // Only add related if there are links + if (relatedLinks.length > 0) { + article.fields.related = relatedLinks; + } + // Add OpenAPI tags from operations (for Hugo frontmatter) + if (apiTags.length > 0) { + article.fields.apiTags = apiTags; + } + return article; +} +/** + * Write OpenAPI article metadata to Hugo data files + * Generates articles.yml and articles.json + * + * @param sourcePath - Path to directory containing OpenAPI fragment files + * @param targetPath - Output path for article data + * @param opts - Options including file pattern filter + */ +function writeOpenapiArticleData(sourcePath, targetPath, opts) { + /** + * Check if path is a file + */ + const isFile = (filePath) => { + return fs.lstatSync(filePath).isFile(); + }; + /** + * Check if filename matches pattern + */ + const matchesPattern = (filePath) => { + return opts.filePattern + ? path.parse(filePath).name.startsWith(opts.filePattern) + : true; + }; + try { + const articles = fs + .readdirSync(sourcePath) + .map((fileName) => path.join(sourcePath, fileName)) + .filter(matchesPattern) + .filter(isFile) + .filter((filePath) => filePath.endsWith('.yaml') || filePath.endsWith('.yml')) // Only process YAML files + .map((filePath) => { + const openapi = readFile(filePath); + const article = createArticleDataForPathGroup(openapi); + article.fields.source = filePath; + // Hugo omits "/static" from the URI when serving files stored in "./static" + article.fields.staticFilePath = filePath.replace(/^static\//, '/'); + return article; + }); + if (!fs.existsSync(targetPath)) { + fs.mkdirSync(targetPath, { recursive: true }); + } + const articleCollection = { articles }; + // Write both YAML and JSON versions + const yamlPath = path.resolve(targetPath, 'articles.yml'); + const jsonPath = path.resolve(targetPath, 'articles.json'); + writeDataFile(articleCollection, yamlPath); + writeJsonFile(articleCollection, jsonPath); + console.log(`Generated ${articles.length} articles in ${targetPath}`); + } + catch (e) { + console.error('Error writing article data:', e); + } +} +/** + * Sanitize markdown description by removing fragment links and ReDoc directives + * + * Handles three cases: + * 1. OpenAPI fragment links: [text](#section/...) -> text (removes the link entirely) + * 2. Relative links with fragments: [text](/path/#anchor) -> [text](/path/) (keeps link, removes fragment) + * 3. ReDoc injection directives: (removes entirely) + * + * This sanitization is necessary because fragment links don't work when article + * descriptions are rendered via the {{< children >}} shortcode on parent pages. + * + * @param description - Markdown description that may contain fragment links + * @returns Sanitized description suitable for children shortcode rendering + */ +function sanitizeDescription(description) { + if (!description) { + return ''; + } + let sanitized = description; + // Remove ReDoc injection directives (e.g., ) + sanitized = sanitized.replace(//g, ''); + // Handle markdown links: + // 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: OpenAPI fragment links (starts with #section/, #operation/, #tag/) + if (url.match(/^#(section|operation|tag)\//)) { + return text; // Just return the link text, no markdown link + } + // Case 2: Relative link with fragment (starts with /, contains #) + if (url.startsWith('/') && url.includes('#')) { + const urlWithoutFragment = url.split('#')[0]; + if (urlWithoutFragment === '/' || urlWithoutFragment === '') { + return text; + } + return `[${text}](${urlWithoutFragment})`; + } + // Case 3: Keep other links as-is (external links, non-fragment links) + return match; + }); + // Clean up extra whitespace left by directive removals + sanitized = sanitized.replace(/\n\n\n+/g, '\n\n').trim(); + return sanitized; +} +/** + * Create article data for a tag-based grouping + * + * @param openapi - OpenAPI document with x-tagGroup + * @param operations - Operations for this tag + * @param tagMeta - Tag metadata from OpenAPI spec + * @returns Article metadata object + */ +function createArticleDataForTag(openapi, operations, tagMeta) { + const tagName = openapi['x-tagGroup'] || ''; + const tagSlug = slugifyTag(tagName); + const isConceptual = tagMeta?.['x-traitTag'] === true; + const article = { + path: `api/${tagSlug}`, + fields: { + name: tagName, + describes: Object.keys(openapi.paths), + title: tagName, + description: sanitizeDescription(tagMeta?.description || + openapi.info?.description || + `API reference for ${tagName}`), + tag: tagName, + isConceptual, + menuGroup: getMenuGroupForTag(tagName), + operations: operations.map((op) => ({ + operationId: op.operationId, + method: op.method, + path: op.path, + summary: op.summary, + tags: op.tags, + ...(op.compatVersion && { compatVersion: op.compatVersion }), + ...(op.externalDocs && { externalDocs: op.externalDocs }), + })), + }, + }; + // Add tag description for conceptual pages (sanitized for children shortcode) + if (tagMeta?.description) { + article.fields.tagDescription = sanitizeDescription(tagMeta.description); + } + // Show security schemes section on Authentication pages + if (tagName === 'Authentication') { + article.fields.showSecuritySchemes = true; + } + // Set custom weight for Quick start to appear first in nav + if (tagName === 'Quick start') { + article.fields.weight = 1; + } + // Set default weight for consistent sorting (articles without explicit weight) + if (article.fields.weight === undefined) { + article.fields.weight = 100; + } + // Aggregate related links from multiple sources into article-level related + // This populates Hugo frontmatter `related` field for "Related content" links + // Supports both plain URL strings and {title, href} objects + const relatedItems = []; + const seenHrefs = new Set(); + // Helper to add a link, deduplicating by href + const addRelated = (item) => { + 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'].forEach(addRelated); + } + // Tag-level x-influxdatadocs-related (plain URLs) + if (tagMeta?.['x-influxdatadocs-related']) { + tagMeta['x-influxdatadocs-related'].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); + } + if (op.externalDocs?.url) { + addRelated(op.externalDocs.url); + } + }); + if (relatedItems.length > 0) { + article.fields.related = relatedItems; + } + return article; +} +/** + * Write tag-based OpenAPI article metadata to Hugo data files + * Generates articles.yml and articles.json + * + * @param sourcePath - Path to directory containing tag-based OpenAPI fragment files + * @param targetPath - Output path for article data + * @param openapi - Original OpenAPI document (for tag metadata) + * @param opts - Options including file pattern filter + */ +function writeOpenapiTagArticleData(sourcePath, targetPath, openapi, opts) { + const isFile = (filePath) => { + return fs.lstatSync(filePath).isFile(); + }; + const matchesPattern = (filePath) => { + return opts.filePattern + ? path.parse(filePath).name.startsWith(opts.filePattern) + : true; + }; + // Create tag metadata lookup + const tagMetaMap = new Map(); + (openapi.tags || []).forEach((tag) => { + tagMetaMap.set(tag.name, tag); + }); + try { + const articles = fs + .readdirSync(sourcePath) + .map((fileName) => path.join(sourcePath, fileName)) + .filter(matchesPattern) + .filter(isFile) + .filter((filePath) => filePath.endsWith('.yaml') || filePath.endsWith('.yml')) + .map((filePath) => { + const tagOpenapi = readFile(filePath); + const tagName = tagOpenapi['x-tagGroup'] || tagOpenapi.info?.title || ''; + const tagMeta = tagMetaMap.get(tagName); + // Extract operations from the tag-filtered spec + const operations = []; + Object.entries(tagOpenapi.paths).forEach(([pathKey, pathItem]) => { + HTTP_METHODS.forEach((method) => { + const operation = pathItem[method]; + if (operation) { + const opMeta = { + operationId: operation.operationId || `${method}-${pathKey}`, + method: method.toUpperCase(), + path: pathKey, + summary: operation.summary || '', + tags: operation.tags || [], + }; + // Extract compatibility version if present + if (operation['x-compatibility-version']) { + opMeta.compatVersion = operation['x-compatibility-version']; + } + // Extract externalDocs if present + if (operation.externalDocs) { + opMeta.externalDocs = { + description: operation.externalDocs.description || '', + url: operation.externalDocs.url, + }; + } + // Extract x-influxdatadocs-related if present + if (operation['x-influxdatadocs-related'] && + Array.isArray(operation['x-influxdatadocs-related'])) { + 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']; + } + operations.push(opMeta); + } + }); + }); + const article = createArticleDataForTag(tagOpenapi, operations, tagMeta); + article.fields.source = filePath; + article.fields.staticFilePath = filePath.replace(/^static\//, '/'); + return article; + }); + if (!fs.existsSync(targetPath)) { + fs.mkdirSync(targetPath, { recursive: true }); + } + const articleCollection = { articles }; + // Write both YAML and JSON versions + const yamlPath = path.resolve(targetPath, 'articles.yml'); + const jsonPath = path.resolve(targetPath, 'articles.json'); + writeDataFile(articleCollection, yamlPath); + writeJsonFile(articleCollection, jsonPath); + console.log(`Generated ${articles.length} tag-based articles in ${targetPath}`); + } + catch (e) { + console.error('Error writing tag article data:', e); + } +} +/** + * Generate Hugo data files from an OpenAPI specification grouped by tag + * + * This function: + * 1. Reads the OpenAPI spec file + * 2. Groups operations by their OpenAPI tags + * 3. Writes each tag group to separate YAML and JSON files + * 4. Generates tag-based article metadata for Hugo + * + * @param options - Generation options + */ +function generateHugoDataByTag(options) { + const filenamePrefix = `${path.parse(options.specFile).name}-`; + const sourceFile = readFile(options.specFile, 'utf8'); + // Optionally generate path-based files for backwards compatibility + if (options.includePaths) { + console.log(`\nGenerating OpenAPI path files in ${options.dataOutPath}....`); + writePathOpenapis(sourceFile, filenamePrefix, options.dataOutPath); + } + // Generate tag-based files + const tagOutPath = options.includePaths + ? path.join(options.dataOutPath, 'tags') + : options.dataOutPath; + console.log(`\nGenerating OpenAPI tag files in ${tagOutPath}....`); + writeTagOpenapis(sourceFile, filenamePrefix, tagOutPath); + console.log(`\nGenerating OpenAPI tag article data in ${options.articleOutPath}...`); + writeOpenapiTagArticleData(tagOutPath, options.articleOutPath, sourceFile, { + filePattern: filenamePrefix, + }); + console.log('\nTag-based generation complete!\n'); +} +/** + * Generate Hugo data files from an OpenAPI specification + * + * This function: + * 1. Reads the OpenAPI spec file + * 2. Groups paths by their base path + * 3. Writes each group to separate YAML and JSON files + * 4. Generates article metadata for Hugo + * + * @param options - Generation options + */ +function generateHugoData(options) { + const filenamePrefix = `${path.parse(options.specFile).name}-`; + const sourceFile = readFile(options.specFile, 'utf8'); + console.log(`\nGenerating OpenAPI path files in ${options.dataOutPath}....`); + writePathOpenapis(sourceFile, filenamePrefix, options.dataOutPath); + console.log(`\nGenerating OpenAPI article data in ${options.articleOutPath}...`); + writeOpenapiArticleData(options.dataOutPath, options.articleOutPath, { + filePattern: filenamePrefix, + }); + console.log('\nGeneration complete!\n'); +} +/** + * Generate path-specific OpenAPI specs from a spec file + * + * Convenience wrapper that reads the spec file and generates path-specific specs. + * + * @param specFile - Path to OpenAPI spec file + * @param outPath - Output directory for path-specific specs + * @returns Map of API path to spec file web path (for use in frontmatter) + */ +function generatePathSpecificSpecs(specFile, outPath) { + const openapi = readFile(specFile, 'utf8'); + return writePathSpecificSpecs(openapi, outPath); +} +// CommonJS export for backward compatibility +module.exports = { + generateHugoData, + generateHugoDataByTag, + generatePathSpecificSpecs, + writePathSpecificSpecs, +}; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/api-docs/scripts/dist/post-process-specs.js b/api-docs/scripts/dist/post-process-specs.js new file mode 100644 index 000000000..7df4abd67 --- /dev/null +++ b/api-docs/scripts/dist/post-process-specs.js @@ -0,0 +1,334 @@ +#!/usr/bin/env node +"use strict"; +/** + * Post-Process Specs + * + * Applies content overlays and tag configuration to bundled OpenAPI specs. + * Runs after `getswagger.sh` bundles specs and before + * `generate-openapi-articles.ts` generates Hugo pages. + * + * Replaces Redocly decorators for: + * - info.yml overlays (title, description, version, license, contact, x-* fields) + * - servers.yml overlays (replaces spec.servers array) + * - tags.yml config (rename, describe, add x-related links to tags) + * + * Usage: + * node api-docs/scripts/dist/post-process-specs.js # All products + * node api-docs/scripts/dist/post-process-specs.js influxdb3/core # One product + * + * @module post-process-specs + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +const fs = __importStar(require("fs")); +const path = __importStar(require("path")); +const yaml = __importStar(require("js-yaml")); +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- +const LOG_PREFIX = '[post-process]'; +/** Build output directory for resolved specs. Source specs are never mutated. */ +const BUILD_DIR = '_build'; +/** Product directories that contain a .config.yml with `apis:` entries. */ +const PRODUCT_DIRS = [ + 'influxdb3/core', + 'influxdb3/enterprise', + 'influxdb3/cloud-dedicated', + 'influxdb3/cloud-serverless', + 'influxdb3/clustered', + 'influxdb/cloud', + 'influxdb/v2', + 'influxdb/v1', + 'enterprise_influxdb/v1', +]; +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- +/** + * Parse a YAML file and return the parsed object, or null if the file does + * not exist. + */ +function loadYaml(filePath) { + if (!fs.existsSync(filePath)) + return null; + const raw = fs.readFileSync(filePath, 'utf8'); + return yaml.load(raw); +} +/** + * Write an object to a YAML file. + */ +function writeYaml(filePath, data) { + fs.writeFileSync(filePath, yaml.dump(data, { lineWidth: -1 }), 'utf8'); +} +function log(msg) { + process.stderr.write(`${LOG_PREFIX} ${msg}\n`); +} +// --------------------------------------------------------------------------- +// Content overlays +// --------------------------------------------------------------------------- +/** + * Resolve a content file path using the same convention as the Redocly + * docs-content.cjs helper: try API-specific directory first, fall back to + * product-level directory. + * + * @param filename - e.g. 'info.yml' or 'servers.yml' + * @param specDir - Absolute path to the directory containing the spec file. + * @param productAbsDir - Absolute path to the product directory. + * @returns Absolute path to the content file, or null if not found. + */ +function resolveContentFile(filename, specDir, productAbsDir) { + // API-specific: {specDir}/content/{filename} + const apiSpecific = path.join(specDir, 'content', filename); + if (fs.existsSync(apiSpecific)) + return apiSpecific; + // Product-level fallback: {productAbsDir}/content/{filename} + const productLevel = path.join(productAbsDir, 'content', filename); + if (fs.existsSync(productLevel)) + return productLevel; + return null; +} +/** + * Apply info.yml overlay to the spec. Merges each field present in the + * overlay into spec.info, preserving fields not mentioned in the overlay. + * + * @returns true if any fields were applied. + */ +function applyInfoOverlay(spec, specDir, productAbsDir, label) { + const infoPath = resolveContentFile('info.yml', specDir, productAbsDir); + if (!infoPath) + return false; + const overlay = loadYaml(infoPath); + if (!overlay) + return false; + if (!spec.info) + spec.info = {}; + let applied = 0; + for (const [key, value] of Object.entries(overlay)) { + spec.info[key] = value; + applied++; + } + if (applied > 0) { + log(`${label}: applied ${applied} info field(s) from ${path.relative(productAbsDir, infoPath)}`); + } + return applied > 0; +} +/** + * Apply servers.yml overlay to the spec. Replaces spec.servers entirely. + * + * @returns true if servers were applied. + */ +function applyServersOverlay(spec, specDir, productAbsDir, label) { + const serversPath = resolveContentFile('servers.yml', specDir, productAbsDir); + if (!serversPath) + return false; + const servers = loadYaml(serversPath); + if (!servers || !Array.isArray(servers)) + return false; + spec.servers = servers; + log(`${label}: applied ${servers.length} server(s) from ${path.relative(productAbsDir, serversPath)}`); + return true; +} +// --------------------------------------------------------------------------- +// Tag config +// --------------------------------------------------------------------------- +/** + * Collect every tag name referenced across all operations in the spec. + */ +function collectOperationTags(spec) { + const found = new Set(); + for (const pathItem of Object.values(spec.paths ?? {})) { + for (const operation of Object.values(pathItem)) { + if (operation && + typeof operation === 'object' && + Array.isArray(operation.tags)) { + for (const t of operation.tags) + found.add(t); + } + } + } + return found; +} +/** + * Rename a tag throughout the spec: in `tags[]` and in every operation. + */ +function renameTag(spec, oldName, newName) { + for (const tag of spec.tags ?? []) { + if (tag.name === oldName) + tag.name = newName; + } + for (const pathItem of Object.values(spec.paths ?? {})) { + for (const operation of Object.values(pathItem)) { + if (operation && + typeof operation === 'object' && + Array.isArray(operation.tags)) { + operation.tags = operation.tags.map((t) => t === oldName ? newName : t); + } + } + } +} +/** + * Apply tag config from a `tags.yml` file to the spec. + * + * @returns true if any tags were patched. + */ +function applyTagConfig(spec, tagConfigPath, label) { + const tagsCfg = loadYaml(tagConfigPath); + if (!tagsCfg || !tagsCfg.tags) { + log(`${label}: tags.yml has no 'tags' key — skipping`); + return false; + } + if (!Array.isArray(spec.tags)) + spec.tags = []; + const operationTags = collectOperationTags(spec); + const configKeys = Object.keys(tagsCfg.tags); + // Warn: config references a tag not in the spec + for (const cfgKey of configKeys) { + const effectiveName = tagsCfg.tags[cfgKey]?.rename ?? cfgKey; + if (!operationTags.has(cfgKey) && !operationTags.has(effectiveName)) { + log(`WARN ${label}: config tag '${cfgKey}' not found in spec operations`); + } + } + // Warn: spec has operation tags with no config entry + Array.from(operationTags).forEach((opTag) => { + const hasEntry = configKeys.some((k) => k === opTag || tagsCfg.tags[k]?.rename === opTag); + if (!hasEntry) { + log(`WARN ${label}: spec tag '${opTag}' has no config entry in tags.yml`); + } + }); + // Apply transformations + for (const [tagKey, cfg] of Object.entries(tagsCfg.tags)) { + if (cfg.rename) { + log(`${label}: renaming tag '${tagKey}' → '${cfg.rename}'`); + renameTag(spec, tagKey, cfg.rename); + } + const resolvedName = cfg.rename ?? tagKey; + let tagObj = spec.tags.find((t) => t.name === resolvedName); + if (!tagObj) { + tagObj = { name: resolvedName }; + spec.tags.push(tagObj); + } + if (cfg.description !== undefined) + tagObj.description = cfg.description.trim(); + if (cfg['x-traitTag'] !== undefined) + tagObj['x-traitTag'] = cfg['x-traitTag']; + if (cfg['x-related'] !== undefined) + tagObj['x-related'] = cfg['x-related']; + } + log(`${label}: patched ${configKeys.length} tag(s)`); + return true; +} +// --------------------------------------------------------------------------- +// Core logic +// --------------------------------------------------------------------------- +/** + * Process a single product directory: read `.config.yml`, find spec files, + * apply content overlays and tag configs, write resolved specs to _build/. + * + * Source specs in api-docs/ are never mutated. Resolved output goes to + * api-docs/_build/{productDir}/{specFile} for downstream consumers + * (Redoc HTML, generate-openapi-articles.ts). + */ +function processProduct(apiDocsRoot, productDir) { + const productAbsDir = path.join(apiDocsRoot, productDir); + const configPath = path.join(productAbsDir, '.config.yml'); + const config = loadYaml(configPath); + if (!config || !config.apis) { + log(`${productDir}: no .config.yml or no 'apis' key — skipping`); + return; + } + for (const [apiKey, apiEntry] of Object.entries(config.apis)) { + const specRelPath = apiEntry.root; + const specAbsPath = path.join(productAbsDir, specRelPath); + const specDir = path.join(productAbsDir, path.dirname(specRelPath)); + const label = path.join(productDir, specRelPath); + if (!fs.existsSync(specAbsPath)) { + log(`${label}: spec not found at ${specAbsPath} — skipping`); + continue; + } + // Load spec once + const spec = loadYaml(specAbsPath); + if (!spec) { + log(`${label}: failed to parse spec — skipping`); + continue; + } + // Apply all transforms + applyInfoOverlay(spec, specDir, productAbsDir, label); + applyServersOverlay(spec, specDir, productAbsDir, label); + const tagConfigPath = path.join(specDir, 'tags.yml'); + if (fs.existsSync(tagConfigPath)) { + applyTagConfig(spec, tagConfigPath, label); + } + // Write resolved spec to _build/, mirroring the source path structure + const outPath = path.join(apiDocsRoot, BUILD_DIR, productDir, specRelPath); + const outDir = path.dirname(outPath); + if (!fs.existsSync(outDir)) { + fs.mkdirSync(outDir, { recursive: true }); + } + writeYaml(outPath, spec); + log(`${label}: wrote ${path.relative(apiDocsRoot, outPath)}`); + } +} +// --------------------------------------------------------------------------- +// Entry point +// --------------------------------------------------------------------------- +function main() { + const args = process.argv.slice(2); + // Optional --root flag for testing — overrides the default resolution. + let apiDocsRoot = path.resolve(__dirname, '../..'); // api-docs/scripts/dist -> api-docs/ + let targetProduct; + for (let i = 0; i < args.length; i++) { + if (args[i] === '--root' && args[i + 1]) { + apiDocsRoot = path.resolve(args[i + 1]); + i++; + } + else { + targetProduct = args[i]; + } + } + const products = targetProduct ? [targetProduct] : PRODUCT_DIRS; + let hasError = false; + for (const productDir of products) { + try { + processProduct(apiDocsRoot, productDir); + } + catch (err) { + log(`ERROR ${productDir}: ${err.message}`); + hasError = true; + } + } + process.exit(hasError ? 1 : 0); +} +main(); +//# sourceMappingURL=post-process-specs.js.map \ No newline at end of file diff --git a/api-docs/scripts/dist/test-post-process-specs.js b/api-docs/scripts/dist/test-post-process-specs.js new file mode 100644 index 000000000..48203b551 --- /dev/null +++ b/api-docs/scripts/dist/test-post-process-specs.js @@ -0,0 +1,490 @@ +#!/usr/bin/env node +"use strict"; +/** + * Tests for post-process-specs.ts + * + * Standalone test script — no test runner required. + * + * Usage: + * node api-docs/scripts/dist/test-post-process-specs.js + * + * Creates temporary fixtures in $TMPDIR, runs the compiled script against + * them via child_process, and reports pass/fail for each case. + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +const fs = __importStar(require("fs")); +const path = __importStar(require("path")); +const os = __importStar(require("os")); +const child_process_1 = require("child_process"); +const yaml = __importStar(require("js-yaml")); +// --------------------------------------------------------------------------- +// Paths +// --------------------------------------------------------------------------- +const SCRIPT = path.resolve(__dirname, 'post-process-specs.js'); +function makeSpec(tags, operationTags, overrides) { + return { + openapi: '3.0.0', + info: { title: 'Test', version: '1.0.0' }, + tags, + paths: { + '/test': { + get: { + operationId: 'testOp', + tags: operationTags, + responses: {}, + }, + }, + }, + ...overrides, + }; +} +function createTmpRoot() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'post-process-test-')); + const productDir = path.join(root, 'influxdb3', 'core'); + const specDir = path.join(productDir, 'v3'); + const specPath = path.join(specDir, 'openapi.yaml'); + const buildSpecPath = path.join(root, '_build', 'influxdb3', 'core', 'v3', 'openapi.yaml'); + fs.mkdirSync(specDir, { recursive: true }); + const config = { + apis: { + data: { + root: 'v3/openapi.yaml', + }, + }, + }; + fs.writeFileSync(path.join(productDir, '.config.yml'), yaml.dump(config)); + return { root, productDir, specDir, specPath, buildSpecPath }; +} +function writeYaml(filePath, data) { + fs.writeFileSync(filePath, yaml.dump(data, { lineWidth: -1 }), 'utf8'); +} +function readYaml(filePath) { + return yaml.load(fs.readFileSync(filePath, 'utf8')); +} +function runScript(root, productFilter) { + const scriptArgs = ['--root', root]; + if (productFilter) + scriptArgs.push(productFilter); + const result = (0, child_process_1.spawnSync)('node', [SCRIPT, ...scriptArgs], { + encoding: 'utf8', + timeout: 10_000, + }); + return { + stdout: result.stdout ?? '', + stderr: result.stderr ?? '', + exitCode: result.status ?? 1, + }; +} +function cleanup(root) { + fs.rmSync(root, { recursive: true, force: true }); +} +// --------------------------------------------------------------------------- +// Test runner +// --------------------------------------------------------------------------- +let passed = 0; +let failed = 0; +const failures = []; +function pass(name) { + console.log(` PASS ${name}`); + passed++; +} +function fail(name, reason) { + console.log(` FAIL ${name}`); + console.log(` ${reason}`); + failed++; + failures.push(`${name}: ${reason}`); +} +function assert(name, condition, reason) { + if (condition) { + pass(name); + } + else { + fail(name, reason); + } +} +// --------------------------------------------------------------------------- +// Tag config tests +// --------------------------------------------------------------------------- +function testDescriptionSetting() { + const { root, specDir, specPath, buildSpecPath } = createTmpRoot(); + try { + writeYaml(specPath, makeSpec([{ name: 'Write data' }], ['Write data'])); + writeYaml(path.join(specDir, 'tags.yml'), { + tags: { + 'Write data': { description: 'Write line protocol data to InfluxDB.' }, + }, + }); + const { exitCode } = runScript(root, 'influxdb3/core'); + assert('1a. exits 0', exitCode === 0, `exit code was ${exitCode}`); + const spec = readYaml(buildSpecPath); + const tag = spec.tags?.find((t) => t.name === 'Write data'); + assert('1b. description applied to tag', tag?.description === 'Write line protocol data to InfluxDB.', `description was: ${tag?.description}`); + } + finally { + cleanup(root); + } +} +function testTagRename() { + const { root, specDir, specPath, buildSpecPath } = createTmpRoot(); + try { + writeYaml(specPath, makeSpec([{ name: 'Cache data' }], ['Cache data'])); + writeYaml(path.join(specDir, 'tags.yml'), { + tags: { + 'Cache data': { rename: 'Cache distinct values' }, + }, + }); + const { exitCode } = runScript(root, 'influxdb3/core'); + assert('2a. exits 0', exitCode === 0, `exit code was ${exitCode}`); + const spec = readYaml(buildSpecPath); + const oldTag = spec.tags?.find((t) => t.name === 'Cache data'); + assert('2b. old tag name gone from tags[]', !oldTag, 'old tag still present in tags[]'); + const newTag = spec.tags?.find((t) => t.name === 'Cache distinct values'); + assert('2c. new tag name in tags[]', !!newTag, 'renamed tag not found in tags[]'); + const opTags = spec.paths?.['/test']?.['get']?.tags ?? []; + assert('2d. operation.tags[] updated', opTags.includes('Cache distinct values') && + !opTags.includes('Cache data'), `operation tags: ${JSON.stringify(opTags)}`); + } + finally { + cleanup(root); + } +} +function testXRelated() { + const { root, specDir, specPath, buildSpecPath } = createTmpRoot(); + try { + writeYaml(specPath, makeSpec([{ name: 'Write data' }], ['Write data'])); + writeYaml(path.join(specDir, 'tags.yml'), { + tags: { + 'Write data': { + description: 'Write data.', + 'x-related': [ + { title: 'Write data guide', href: '/influxdb3/core/write-data/' }, + ], + }, + }, + }); + const { exitCode } = runScript(root, 'influxdb3/core'); + assert('3a. exits 0', exitCode === 0, `exit code was ${exitCode}`); + const spec = readYaml(buildSpecPath); + const tag = spec.tags?.find((t) => t.name === 'Write data'); + const related = tag?.['x-related']; + assert('3b. x-related present', Array.isArray(related) && related.length === 1, `x-related: ${JSON.stringify(related)}`); + assert('3c. x-related entry correct', related?.[0]?.title === 'Write data guide' && + related?.[0]?.href === '/influxdb3/core/write-data/', `entry: ${JSON.stringify(related?.[0])}`); + } + finally { + cleanup(root); + } +} +function testStaleConfigWarning() { + const { root, specDir, specPath } = createTmpRoot(); + try { + writeYaml(specPath, makeSpec([{ name: 'Write data' }], ['Write data'])); + writeYaml(path.join(specDir, 'tags.yml'), { + tags: { + 'Write data': { description: 'Write data.' }, + 'Ghost tag': { description: 'This tag does not exist in the spec.' }, + }, + }); + const { stderr, exitCode } = runScript(root, 'influxdb3/core'); + assert('4a. exits 0 (warnings are not errors)', exitCode === 0, `exit code was ${exitCode}`); + assert('4b. stale config warning emitted', stderr.includes("config tag 'Ghost tag' not found in spec operations"), `stderr: ${stderr}`); + } + finally { + cleanup(root); + } +} +function testUncoveredTagWarning() { + const { root, specDir, specPath } = createTmpRoot(); + try { + writeYaml(specPath, makeSpec([{ name: 'Write data' }, { name: 'Query data' }], ['Write data', 'Query data'])); + writeYaml(path.join(specDir, 'tags.yml'), { + tags: { + 'Write data': { description: 'Write data.' }, + }, + }); + const { stderr, exitCode } = runScript(root, 'influxdb3/core'); + assert('5a. exits 0 (warnings are not errors)', exitCode === 0, `exit code was ${exitCode}`); + assert('5b. uncovered tag warning emitted', stderr.includes("spec tag 'Query data' has no config entry in tags.yml"), `stderr: ${stderr}`); + } + finally { + cleanup(root); + } +} +function testNoTagsYmlSilentSkip() { + const { root, specPath } = createTmpRoot(); + try { + writeYaml(specPath, makeSpec([{ name: 'Write data' }], ['Write data'])); + const { stderr, exitCode } = runScript(root, 'influxdb3/core'); + assert('6a. exits 0', exitCode === 0, `exit code was ${exitCode}`); + assert('6b. no error output', !stderr.includes('ERROR'), `unexpected error in stderr: ${stderr}`); + } + finally { + cleanup(root); + } +} +function testMalformedYamlFails() { + const { root, specDir, specPath } = createTmpRoot(); + try { + writeYaml(specPath, makeSpec([{ name: 'Write data' }], ['Write data'])); + fs.writeFileSync(path.join(specDir, 'tags.yml'), 'tags:\n Write data:\n description: [\n bad yaml here', 'utf8'); + const { exitCode } = runScript(root, 'influxdb3/core'); + assert('7a. exits 1 on malformed YAML', exitCode === 1, `exit code was ${exitCode}`); + } + finally { + cleanup(root); + } +} +// --------------------------------------------------------------------------- +// Content overlay tests +// --------------------------------------------------------------------------- +// 8. Info overlay — API-specific content/info.yml +function testInfoOverlay() { + const { root, specDir, specPath, buildSpecPath } = createTmpRoot(); + try { + writeYaml(specPath, makeSpec([], [], { + info: { title: 'Original Title', version: '0.0.0' }, + })); + // Create API-specific content/info.yml + const contentDir = path.join(specDir, 'content'); + fs.mkdirSync(contentDir, { recursive: true }); + writeYaml(path.join(contentDir, 'info.yml'), { + title: 'Overridden Title', + version: '2.0.0', + 'x-influxdata-short-title': 'Short', + }); + const { exitCode } = runScript(root, 'influxdb3/core'); + assert('8a. exits 0', exitCode === 0, `exit code was ${exitCode}`); + const spec = readYaml(buildSpecPath); + assert('8b. title overridden', spec.info.title === 'Overridden Title', `title: ${spec.info.title}`); + assert('8c. version overridden', spec.info.version === '2.0.0', `version: ${spec.info.version}`); + assert('8d. x-influxdata-short-title applied', spec.info['x-influxdata-short-title'] === + 'Short', `x-influxdata-short-title: ${spec.info['x-influxdata-short-title']}`); + } + finally { + cleanup(root); + } +} +// 9. Info overlay — product-level fallback +function testInfoOverlayProductFallback() { + const { root, productDir, specPath, buildSpecPath } = createTmpRoot(); + try { + writeYaml(specPath, makeSpec([], [], { + info: { title: 'Original', version: '1.0.0' }, + })); + // Create product-level content/info.yml (NOT in specDir/content/) + const contentDir = path.join(productDir, 'content'); + fs.mkdirSync(contentDir, { recursive: true }); + writeYaml(path.join(contentDir, 'info.yml'), { + title: 'Product-Level Title', + }); + const { exitCode } = runScript(root, 'influxdb3/core'); + assert('9a. exits 0', exitCode === 0, `exit code was ${exitCode}`); + const spec = readYaml(buildSpecPath); + assert('9b. title from product-level', spec.info.title === 'Product-Level Title', `title: ${spec.info.title}`); + assert('9c. version preserved', spec.info.version === '1.0.0', `version: ${spec.info.version}`); + } + finally { + cleanup(root); + } +} +// 10. Servers overlay +function testServersOverlay() { + const { root, specDir, specPath, buildSpecPath } = createTmpRoot(); + try { + writeYaml(specPath, makeSpec([], [], { + servers: [{ url: 'https://old.example.com' }], + })); + const contentDir = path.join(specDir, 'content'); + fs.mkdirSync(contentDir, { recursive: true }); + writeYaml(path.join(contentDir, 'servers.yml'), [ + { + url: 'https://{baseurl}', + description: 'InfluxDB API', + variables: { + baseurl: { + enum: ['localhost:8181'], + default: 'localhost:8181', + description: 'InfluxDB URL', + }, + }, + }, + ]); + const { exitCode } = runScript(root, 'influxdb3/core'); + assert('10a. exits 0', exitCode === 0, `exit code was ${exitCode}`); + const spec = readYaml(buildSpecPath); + assert('10b. servers replaced', spec.servers?.length === 1, `server count: ${spec.servers?.length}`); + assert('10c. server URL correct', spec.servers?.[0]?.url === 'https://{baseurl}', `url: ${spec.servers?.[0]?.url}`); + assert('10d. server variables present', spec.servers?.[0]?.variables !== undefined, 'variables missing'); + } + finally { + cleanup(root); + } +} +// 11. Info overlay preserves fields not in overlay +function testInfoOverlayPreservesFields() { + const { root, specDir, specPath, buildSpecPath } = createTmpRoot(); + try { + writeYaml(specPath, makeSpec([], [], { + info: { + title: 'Original Title', + version: '3.0.0', + description: 'Original description.', + license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT' }, + }, + })); + const contentDir = path.join(specDir, 'content'); + fs.mkdirSync(contentDir, { recursive: true }); + // Overlay only sets x-* fields, no title/version/description + writeYaml(path.join(contentDir, 'info.yml'), { + 'x-influxdata-short-title': 'InfluxDB 3 API', + }); + const { exitCode } = runScript(root, 'influxdb3/core'); + assert('11a. exits 0', exitCode === 0, `exit code was ${exitCode}`); + const spec = readYaml(buildSpecPath); + assert('11b. title preserved', spec.info.title === 'Original Title', `title: ${spec.info.title}`); + assert('11c. version preserved', spec.info.version === '3.0.0', `version: ${spec.info.version}`); + assert('11d. description preserved', spec.info.description === 'Original description.', `desc: ${spec.info.description}`); + assert('11e. x-influxdata-short-title added', spec.info['x-influxdata-short-title'] === + 'InfluxDB 3 API', 'x-influxdata-short-title missing'); + } + finally { + cleanup(root); + } +} +// 12. No content overlays — spec unchanged +function testNoOverlaysNoWrite() { + const { root, specPath, buildSpecPath } = createTmpRoot(); + try { + const original = makeSpec([{ name: 'Write data' }], ['Write data']); + writeYaml(specPath, original); + const mtime = fs.statSync(specPath).mtimeMs; + // Small delay to detect mtime changes + const start = Date.now(); + while (Date.now() - start < 50) { + /* busy wait */ + } + const { exitCode } = runScript(root, 'influxdb3/core'); + assert('12a. exits 0', exitCode === 0, `exit code was ${exitCode}`); + const built = readYaml(buildSpecPath); + assert('12b. build output matches input when no overlays/tags', JSON.stringify(built) === JSON.stringify(original), 'build output differed from source'); + assert('12c. source file untouched', fs.statSync(specPath).mtimeMs === mtime, 'source spec modified'); + } + finally { + cleanup(root); + } +} +// 13. Combined: info + servers + tags applied together +function testCombinedOverlaysAndTags() { + const { root, specDir, specPath, buildSpecPath } = createTmpRoot(); + try { + writeYaml(specPath, makeSpec([{ name: 'Write data' }], ['Write data'], { + info: { title: 'Original', version: '1.0.0' }, + servers: [{ url: 'https://old.example.com' }], + })); + const contentDir = path.join(specDir, 'content'); + fs.mkdirSync(contentDir, { recursive: true }); + writeYaml(path.join(contentDir, 'info.yml'), { + title: 'New Title', + 'x-influxdata-short-title': 'Short', + }); + writeYaml(path.join(contentDir, 'servers.yml'), [ + { url: 'https://new.example.com', description: 'New Server' }, + ]); + writeYaml(path.join(specDir, 'tags.yml'), { + tags: { + 'Write data': { + description: 'Write line protocol data.', + 'x-related': [{ title: 'Guide', href: '/guide/' }], + }, + }, + }); + const { exitCode } = runScript(root, 'influxdb3/core'); + assert('13a. exits 0', exitCode === 0, `exit code was ${exitCode}`); + const spec = readYaml(buildSpecPath); + assert('13b. info title updated', spec.info.title === 'New Title', `title: ${spec.info.title}`); + assert('13c. info version preserved', spec.info.version === '1.0.0', `version: ${spec.info.version}`); + assert('13d. x-influxdata-short-title set', spec.info['x-influxdata-short-title'] === + 'Short', 'missing'); + assert('13e. servers replaced', spec.servers?.[0]?.url === 'https://new.example.com', `url: ${spec.servers?.[0]?.url}`); + const tag = spec.tags?.find((t) => t.name === 'Write data'); + assert('13f. tag description set', tag?.description === 'Write line protocol data.', `desc: ${tag?.description}`); + assert('13g. tag x-related set', Array.isArray(tag?.['x-related']) && tag['x-related'].length === 1, `x-related: ${JSON.stringify(tag?.['x-related'])}`); + } + finally { + cleanup(root); + } +} +// --------------------------------------------------------------------------- +// Run all tests +// --------------------------------------------------------------------------- +const tests = [ + // Tag config tests (carried forward) + ['1. Tag description setting', testDescriptionSetting], + ['2. Tag rename (tags[] and operation.tags[])', testTagRename], + ['3. x-related links', testXRelated], + ['4. Warning: stale config reference', testStaleConfigWarning], + ['5. Warning: uncovered spec tag', testUncoveredTagWarning], + ['6. No tags.yml — silent skip', testNoTagsYmlSilentSkip], + ['7. Malformed YAML — exit 1', testMalformedYamlFails], + // Content overlay tests (new) + ['8. Info overlay — API-specific', testInfoOverlay], + ['9. Info overlay — product-level fallback', testInfoOverlayProductFallback], + ['10. Servers overlay', testServersOverlay], + [ + '11. Info overlay preserves fields not in overlay', + testInfoOverlayPreservesFields, + ], + ['12. No overlays or tags — build mirrors source', testNoOverlaysNoWrite], + ['13. Combined: info + servers + tags', testCombinedOverlaysAndTags], +]; +console.log('\npost-process-specs tests\n'); +for (const [name, fn] of tests) { + console.log(name); + try { + fn(); + } + catch (err) { + fail(name, `threw: ${err.message}`); + } +} +console.log(`\n${passed + failed} tests: ${passed} passed, ${failed} failed`); +if (failures.length > 0) { + console.log('\nFailed:'); + for (const f of failures) + console.log(` - ${f}`); + process.exit(1); +} +//# sourceMappingURL=test-post-process-specs.js.map \ No newline at end of file diff --git a/api-docs/scripts/generate-openapi-articles.ts b/api-docs/scripts/generate-openapi-articles.ts index 2445259c5..c901de7ac 100644 --- a/api-docs/scripts/generate-openapi-articles.ts +++ b/api-docs/scripts/generate-openapi-articles.ts @@ -2,109 +2,149 @@ /** * Generate OpenAPI Articles Script * - * Processes OpenAPI specs for Hugo-native API documentation. This script - * expects specs to already be fetched (via getswagger.sh) and post-processed - * (via post-process-specs.ts) before it runs. + * Generates Hugo data files and content pages from OpenAPI specifications + * for all InfluxDB products. * - * Modes: - * - Default: copy specs to static/ + generate Hugo article pages - * - --static-only: copy specs to static/ only (no article generation) + * Products are auto-discovered by scanning api-docs/ for .config.yml files. + * Hugo paths, menu keys, and static file names are derived from directory + * structure and existing Hugo frontmatter. + * + * This script: + * 1. Discovers products from .config.yml files + * 2. Cleans output directories (unless --no-clean) + * 3. Transforms documentation links in specs + * 4. Copies specs to static directory for download + * 5. Generates tag-based data fragments (YAML and JSON) + * 6. Generates Hugo content pages from article data * * Usage: - * node generate-openapi-articles.js # Full generation (static + articles) - * node generate-openapi-articles.js cloud-v2 # Single product - * node generate-openapi-articles.js --static-only # Copy specs to static/ only + * node generate-openapi-articles.js # Clean and generate all products + * node generate-openapi-articles.js influxdb3-core # 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 */ +import { execSync } from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; // Import the OpenAPI to Hugo converter const openapiPathsToHugo = require('./openapi-paths-to-hugo-data/index.js'); -/** - * Operation metadata structure from tag-based articles - */ +// --------------------------------------------------------------------------- +// Interfaces +// --------------------------------------------------------------------------- + +/** Operation metadata structure from tag-based articles */ interface OperationMeta { operationId: string; method: string; path: string; summary: string; tags: string[]; - /** Compatibility version (v1 or v2) for migration context */ compatVersion?: string; - /** External documentation link */ externalDocs?: { description: string; url: string; }; } -/** - * Spec file configuration with optional display name - */ -interface SpecFileConfig { - /** Path to the OpenAPI spec file */ - path: string; - /** Display name for downloads (e.g., "Management API", "v2 Data API") */ - displayName?: string; +/** Article data structure from articles.yml */ +interface ArticleData { + articles: Array<{ + path: string; + fields: { + name?: string; + title?: string; + description?: string; + tag?: string; + isConceptual?: boolean; + showSecuritySchemes?: boolean; + tagDescription?: string; + menuGroup?: string; + staticFilePath?: string; + operations?: OperationMeta[]; + related?: (string | { title: string; href: string })[]; + source?: string; + weight?: number; + }; + }>; } -/** - * Product configuration for API generation - */ -interface ProductConfig { - /** Path to the OpenAPI spec file (single spec - use specFiles for multiple) */ - specFile?: string; - /** Multiple spec files to merge for this product */ - specFiles?: SpecFileConfig[]; - /** Path to the Hugo content directory for generated pages */ +/** Single API entry from .config.yml */ +interface ApiConfigEntry { + root: string; + 'x-influxdata-docs-aliases'?: string[]; +} + +/** Parsed .config.yml file */ +interface DotConfig { + 'x-influxdata-product-name'?: string; + apis?: Record; +} + +/** A resolved API section within a product */ +interface DiscoveredApi { + /** API key from .config.yml (e.g., 'v3', 'management', 'data') */ + apiKey: string; + /** Version number from the @-suffix (e.g., '3', '0', '2') */ + version: string; + /** Resolved full path to the spec file */ + specFile: string; + /** Hugo section slug: 'api' or 'management-api' */ + sectionSlug: string; +} + +/** A fully resolved product discovered from .config.yml */ +interface DiscoveredProduct { + /** Directory containing .config.yml */ + configDir: string; + /** Product directory relative to api-docs/ (e.g., 'influxdb3/core') */ + productDir: string; + /** Human-readable name from x-influxdata-product-name */ + productName: string; + /** Hugo content directory (e.g., 'content/influxdb3/core') */ pagesDir: string; - /** Optional description of the product */ - description?: string; - /** Hugo menu identifier for this product (e.g., 'influxdb3_core') */ - menuKey?: string; - /** Skip adding menu entry to generated parent page (use when existing reference page has menu entry) */ - skipParentMenu?: boolean; - /** Use tag-based generation instead of path-based (default: false) */ - useTagBasedGeneration?: boolean; + /** Hugo menu key from cascade.product (e.g., 'influxdb3_core') */ + menuKey: string; + /** True if hand-maintained api/_index.md has its own menu entry */ + skipParentMenu: boolean; + /** Static file directory name (e.g., 'influxdb3-core') */ + staticDirName: string; + /** Resolved API sections */ + apis: DiscoveredApi[]; } -/** - * Map of product identifiers to their configuration - */ -type ProductConfigMap = Record; - -// Calculate the relative paths -const DOCS_ROOT = '.'; -// Read resolved specs from _build/ (written by post-process-specs.ts). -// Source specs in api-docs/ are never read directly by this script. -const API_DOCS_ROOT = 'api-docs/_build'; - -// CLI flags -const validateLinks = process.argv.includes('--validate-links'); -const staticOnly = process.argv.includes('--static-only'); -const noClean = process.argv.includes('--no-clean'); -const dryRun = process.argv.includes('--dry-run'); - -/** - * Product data from products.yml with api_path - */ +/** Product data from products.yml with api_path */ interface ProductData { name: string; api_path?: string; alt_link_key?: string; } +// --------------------------------------------------------------------------- +// Constants and CLI flags +// --------------------------------------------------------------------------- + +const DOCS_ROOT = '.'; +const API_DOCS_ROOT = 'api-docs'; + +const validateLinks = process.argv.includes('--validate-links'); +const skipFetch = process.argv.includes('--skip-fetch'); +const noClean = process.argv.includes('--no-clean'); +const dryRun = process.argv.includes('--dry-run'); + +// --------------------------------------------------------------------------- +// Utility functions +// --------------------------------------------------------------------------- + /** - * Load products with API paths from data/products.yml - * Returns a map of alt_link_key to API path for alt_links generation - * The alt_link_key matches what the Hugo product-selector template expects + * Load products with API paths from data/products.yml. + * Returns a map of alt_link_key to API path for alt_links generation. */ function loadApiProducts(): Map { const yaml = require('js-yaml'); @@ -117,12 +157,10 @@ function loadApiProducts(): Map { const productsContent = fs.readFileSync(productsFile, 'utf8'); const products = yaml.load(productsContent) as Record; - const apiProducts = new Map(); - for (const [key, product] of Object.entries(products)) { + for (const [, product] of Object.entries(products)) { if (product.api_path && product.alt_link_key) { - // Use alt_link_key as the key (matches Hugo template expectations) apiProducts.set(product.alt_link_key, product.api_path); } } @@ -130,76 +168,291 @@ function loadApiProducts(): Map { return apiProducts; } -// Load API products at module initialization const apiProductsMap = loadApiProducts(); -/** - * Execute a shell command and handle errors - * - * @param command - Command to execute - * @param description - Human-readable description of the command - * @throws Exits process with code 1 on error - */ -/** - * Generate a clean static directory name from a product key. - * Handles the influxdb3_* products to avoid redundant 'influxdb-influxdb3' prefixes. - * - * @param productKey - Product identifier (e.g., 'cloud-v2', 'influxdb3_core') - * @returns Clean directory name (e.g., 'influxdb-cloud-v2', 'influxdb3-core') - */ -function getStaticDirName(productKey: string): string { - // For influxdb3_* products, convert underscore to hyphen and don't add prefix - if (productKey.startsWith('influxdb3_')) { - return productKey.replace('_', '-'); +/** Execute a shell command and handle errors */ +function execCommand(command: string, description?: string): void { + try { + if (description) { + console.log(`\n${description}...`); + } + console.log(`Executing: ${command}\n`); + execSync(command, { stdio: 'inherit' }); + } catch (error) { + console.error(`\n❌ Error executing command: ${command}`); + if (error instanceof Error) { + console.error(error.message); + } + process.exit(1); } - // For other products, add 'influxdb-' prefix - return `influxdb-${productKey}`; +} + +// --------------------------------------------------------------------------- +// Auto-discovery functions +// --------------------------------------------------------------------------- + +/** + * Recursively find all .config.yml files under api-docs/. + * Excludes the root api-docs/.config.yml and internal directories. + */ +function findConfigFiles(rootDir: string): string[] { + const configs: string[] = []; + const skipDirs = new Set([ + 'node_modules', + 'dist', + '_build', + 'scripts', + 'openapi', + ]); + + function scanDir(dir: string, depth: number): void { + if (depth > 5) return; + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + for (const entry of entries) { + if (skipDirs.has(entry.name)) continue; + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + scanDir(fullPath, depth + 1); + } else if (entry.name === '.config.yml' && dir !== rootDir) { + configs.push(fullPath); + } + } + } + + scanDir(rootDir, 0); + return configs.sort(); } /** - * Get all paths that would be cleaned for a product + * Parse an API entry key like 'v3@3' into apiKey and version. + */ +function parseApiEntry(entry: string): { apiKey: string; version: string } { + const atIdx = entry.indexOf('@'); + if (atIdx === -1) { + return { apiKey: entry, version: '0' }; + } + return { + apiKey: entry.substring(0, atIdx), + version: entry.substring(atIdx + 1), + }; +} + +/** + * Determine Hugo section slug from API key. + * 'management' → 'management-api', everything else → 'api'. + */ +function getSectionSlug(apiKey: string): string { + if (apiKey === 'management') return 'management-api'; + return 'api'; +} + +/** + * Derive a clean static directory name from a product directory path. + * Replaces path separators and underscores with hyphens. * - * @param productKey - Product identifier (e.g., 'influxdb3_core') - * @param config - Product configuration - * @returns Object with directories and files arrays + * @example 'influxdb3/core' → 'influxdb3-core' + * @example 'enterprise_influxdb/v1' → 'enterprise-influxdb-v1' + */ +function deriveStaticDirName(productDir: string): string { + return productDir.replace(/[/_]/g, '-'); +} + +/** + * Read the cascade.product field from a product's _index.md frontmatter. + * This value serves as the Hugo menu key. + */ +function readMenuKey(pagesDir: string): string { + const yaml = require('js-yaml'); + const indexFile = path.join(pagesDir, '_index.md'); + + if (!fs.existsSync(indexFile)) { + console.warn(`⚠️ Product index not found: ${indexFile}`); + return ''; + } + + const content = fs.readFileSync(indexFile, 'utf8'); + const fmMatch = content.match(/^---\n([\s\S]*?)\n---/); + if (!fmMatch) return ''; + + try { + const fm = yaml.load(fmMatch[1]) as Record; + const cascade = fm.cascade as Record | undefined; + if (cascade?.product) return cascade.product; + + // Fallback: first key of the menu map + if (fm.menu && typeof fm.menu === 'object') { + const keys = Object.keys(fm.menu as Record); + if (keys.length > 0) return keys[0]; + } + } catch { + console.warn(`⚠️ Could not parse frontmatter in ${indexFile}`); + } + + return ''; +} + +/** + * Check whether a hand-maintained api/_index.md already has a menu entry. + * If so, the generator should skip adding its own parent menu entry. + * + * Only detects genuinely hand-maintained files — files previously generated + * by this script (which have articleDataKey in frontmatter) are ignored, + * since they'll be regenerated during this run. + */ +function hasExistingApiMenu(pagesDir: string): boolean { + const yaml = require('js-yaml'); + const apiIndex = path.join(pagesDir, 'api', '_index.md'); + + if (!fs.existsSync(apiIndex)) return false; + + const content = fs.readFileSync(apiIndex, 'utf8'); + const fmMatch = content.match(/^---\n([\s\S]*?)\n---/); + if (!fmMatch) return false; + + try { + const fm = yaml.load(fmMatch[1]) as Record; + // Skip files generated by this script (they have articleDataKey) + if (fm.articleDataKey) return false; + return !!fm.menu; + } catch { + return false; + } +} + +/** + * Discover all products by scanning api-docs/ for .config.yml files. + * Derives Hugo paths from directory structure and existing frontmatter. + */ +function discoverProducts(): DiscoveredProduct[] { + const yaml = require('js-yaml'); + const products: DiscoveredProduct[] = []; + const configFiles = findConfigFiles(API_DOCS_ROOT); + + for (const configPath of configFiles) { + const configDir = path.dirname(configPath); + const productDir = path.relative(API_DOCS_ROOT, configDir); + + let config: DotConfig; + try { + const raw = fs.readFileSync(configPath, 'utf8'); + config = yaml.load(raw) as DotConfig; + } catch (err) { + console.warn(`⚠️ Could not parse ${configPath}: ${err}`); + continue; + } + + if (!config.apis || Object.keys(config.apis).length === 0) { + continue; + } + + const pagesDir = path.join(DOCS_ROOT, 'content', productDir); + const staticDirName = deriveStaticDirName(productDir); + const menuKey = readMenuKey(pagesDir); + const skipParentMenu = hasExistingApiMenu(pagesDir); + + // Parse API entries, skipping compatibility specs + const apis: DiscoveredApi[] = []; + for (const [entryKey, entry] of Object.entries(config.apis)) { + const { apiKey, version } = parseApiEntry(entryKey); + + // Skip v1-compatibility entries (being removed in pipeline restructure) + if (apiKey.includes('compatibility')) continue; + + // Prefer post-processed spec from _build/ (has overlays and tag configs), + // fall back to source spec for standalone usage + const sourceSpec = path.join(configDir, entry.root); + const buildSpec = path.join(API_DOCS_ROOT, '_build', productDir, entry.root); + const specFile = fs.existsSync(buildSpec) ? buildSpec : sourceSpec; + const sectionSlug = getSectionSlug(apiKey); + + apis.push({ apiKey, version, specFile, sectionSlug }); + } + + if (apis.length === 0) continue; + + products.push({ + configDir, + productDir, + productName: config['x-influxdata-product-name'] || productDir, + pagesDir, + menuKey, + skipParentMenu, + staticDirName, + apis, + }); + } + + return products; +} + +// --------------------------------------------------------------------------- +// Cleanup functions +// --------------------------------------------------------------------------- + +/** + * Get all paths that would be cleaned for a product. + * + * @param product - The product to clean + * @param allStaticDirNames - Names of all products (to avoid prefix collisions) */ function getCleanupPaths( - productKey: string, - config: ProductConfig -): { directories: string[]; files: string[] } { - const staticDirName = getStaticDirName(productKey); + product: DiscoveredProduct, + allStaticDirNames: string[] +): { + directories: string[]; + files: string[]; +} { const staticPath = path.join(DOCS_ROOT, 'static/openapi'); - const directories: string[] = []; const files: string[] = []; // Tag specs directory: static/openapi/{staticDirName}/ - const tagSpecsDir = path.join(staticPath, staticDirName); + const tagSpecsDir = path.join(staticPath, product.staticDirName); if (fs.existsSync(tagSpecsDir)) { directories.push(tagSpecsDir); } - // Article data directory: data/article_data/influxdb/{productKey}/ + // Article data directory: data/article_data/influxdb/{staticDirName}/ const articleDataDir = path.join( DOCS_ROOT, - `data/article_data/influxdb/${productKey}` + `data/article_data/influxdb/${product.staticDirName}` ); if (fs.existsSync(articleDataDir)) { directories.push(articleDataDir); } - // Content pages directory: content/{pagesDir}/api/ - const contentApiDir = path.join(config.pagesDir, 'api'); - if (fs.existsSync(contentApiDir)) { - directories.push(contentApiDir); + // Content pages: content/{pagesDir}/{sectionSlug}/ for each API + for (const api of product.apis) { + const contentDir = path.join(product.pagesDir, api.sectionSlug); + if (fs.existsSync(contentDir)) { + directories.push(contentDir); + } } - // Root spec files: static/openapi/{staticDirName}-*.yml and .json + // Root spec files: static/openapi/{staticDirName}*.yml and .json + // Avoid matching files that belong to products with longer names + // (e.g., 'influxdb-cloud' should not match 'influxdb-cloud-dedicated-*.yml') + const longerPrefixes = allStaticDirNames.filter( + (n) => + n !== product.staticDirName && n.startsWith(product.staticDirName + '-') + ); + if (fs.existsSync(staticPath)) { const staticFiles = fs.readdirSync(staticPath); - const pattern = new RegExp(`^${staticDirName}-.*\\.(yml|json)$`); staticFiles - .filter((f) => pattern.test(f)) + .filter((f) => { + if (!f.startsWith(product.staticDirName)) return false; + // Exclude files belonging to a longer-named product + for (const longer of longerPrefixes) { + if (f.startsWith(longer)) return false; + } + return f.endsWith('.yml') || f.endsWith('.json'); + }) .forEach((f) => { files.push(path.join(staticPath, f)); }); @@ -208,22 +461,18 @@ function getCleanupPaths( return { directories, files }; } -/** - * Clean output directories for a product before regeneration - * - * @param productKey - Product identifier - * @param config - Product configuration - */ -function cleanProductOutputs(productKey: string, config: ProductConfig): void { - const { directories, files } = getCleanupPaths(productKey, config); +/** Clean output directories for a product before regeneration. */ +function cleanProductOutputs( + product: DiscoveredProduct, + allStaticDirNames: string[] +): void { + const { directories, files } = getCleanupPaths(product, allStaticDirNames); - // Remove directories recursively for (const dir of directories) { console.log(`🧹 Removing directory: ${dir}`); fs.rmSync(dir, { recursive: true, force: true }); } - // Remove individual files for (const file of files) { console.log(`🧹 Removing file: ${file}`); fs.unlinkSync(file); @@ -232,21 +481,21 @@ function cleanProductOutputs(productKey: string, config: ProductConfig): void { const total = directories.length + files.length; if (total > 0) { console.log( - `✓ Cleaned ${directories.length} directories, ${files.length} files for ${productKey}` + `✓ Cleaned ${directories.length} directories, ${files.length} files for ${product.staticDirName}` ); } } -/** - * Display dry-run preview of what would be cleaned - * - * @param productKey - Product identifier - * @param config - Product configuration - */ -function showDryRunPreview(productKey: string, config: ProductConfig): void { - const { directories, files } = getCleanupPaths(productKey, config); +/** Display dry-run preview of what would be cleaned. */ +function showDryRunPreview( + product: DiscoveredProduct, + allStaticDirNames: string[] +): void { + const { directories, files } = getCleanupPaths(product, allStaticDirNames); - console.log(`\nDRY RUN: Would clean the following for ${productKey}:\n`); + console.log( + `\nDRY RUN: Would clean the following for ${product.staticDirName}:\n` + ); if (directories.length > 0) { console.log('Directories to remove:'); @@ -267,786 +516,9 @@ function showDryRunPreview(productKey: string, config: ProductConfig): void { ); } -/** - * Generate Hugo data files from OpenAPI specification - * - * @param specFile - Path to the OpenAPI spec file - * @param dataOutPath - Output path for OpenAPI path fragments - * @param articleOutPath - Output path for article metadata - */ -function generateDataFromOpenAPI( - specFile: string, - dataOutPath: string, - articleOutPath: string -): void { - if (!fs.existsSync(dataOutPath)) { - fs.mkdirSync(dataOutPath, { recursive: true }); - } - - openapiPathsToHugo.generateHugoData({ - dataOutPath, - articleOutPath, - specFile, - }); -} - -/** - * Options for generating pages from article data - */ -interface GeneratePagesOptions { - /** Path to the articles data directory */ - articlesPath: string; - /** Output path for generated content pages */ - contentPath: string; - /** Hugo menu identifier for navigation (e.g., 'influxdb3_core') */ - menuKey?: string; - /** Parent menu item name (e.g., 'InfluxDB HTTP API') */ - menuParent?: string; - /** Product description for the parent page */ - productDescription?: string; - /** Skip adding menu entry to generated parent page */ - skipParentMenu?: boolean; -} - -/** - * 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 Hugo-native templates. - * - * @param options - Generation options - */ -function generatePagesFromArticleData(options: GeneratePagesOptions): void { - const { - articlesPath, - contentPath, - menuKey, - menuParent, - productDescription, - skipParentMenu, - } = 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: Record; - }>; - }; - - if (!data.articles || !Array.isArray(data.articles)) { - console.warn(`⚠️ No articles found in ${articlesFile}`); - return; - } - - // Ensure content directory exists - if (!fs.existsSync(contentPath)) { - fs.mkdirSync(contentPath, { recursive: true }); - } - - // Determine the API parent directory from the first article's path - // e.g., if article path is "api/v1/health", the API root is "api" - const firstArticlePath = data.articles[0]?.path || ''; - const apiRootDir = firstArticlePath.split('/')[0]; - - // Generate parent _index.md for the API section - if (apiRootDir) { - const apiParentDir = path.join(contentPath, apiRootDir); - const parentIndexFile = path.join(apiParentDir, '_index.md'); - - if (!fs.existsSync(apiParentDir)) { - fs.mkdirSync(apiParentDir, { recursive: true }); - } - - if (!fs.existsSync(parentIndexFile)) { - // Build description - use product description or generate from product name - const apiDescription = - productDescription || - `Use the InfluxDB HTTP API to write data, query data, and manage databases, tables, and tokens.`; - - const parentFrontmatter: Record = { - title: menuParent || 'InfluxDB HTTP API', - description: apiDescription, - weight: 104, - type: 'api', - }; - - // Add menu entry for parent page (unless skipParentMenu is true) - if (menuKey && !skipParentMenu) { - parentFrontmatter.menu = { - [menuKey]: { - name: menuParent || 'InfluxDB HTTP API', - parent: 'Reference', - }, - }; - } - - // Build page content with intro paragraph and children listing - const introText = apiDescription.replace( - 'InfluxDB', - '{{% product-name %}}' - ); - const parentContent = `--- -${yaml.dump(parentFrontmatter)}--- - -${introText} - -{{< children >}} -`; - - fs.writeFileSync(parentIndexFile, parentContent); - console.log(`✓ Generated parent index at ${parentIndexFile}`); - } - } - - // Generate a page for each article - for (const article of data.articles) { - const pagePath = path.join(contentPath, article.path); - const pageFile = path.join(pagePath, '_index.md'); - - // Create directory if needed - if (!fs.existsSync(pagePath)) { - fs.mkdirSync(pagePath, { recursive: true }); - } - - // Build frontmatter object - // Use menuName for display (actual endpoint path like /health) - // Fall back to name or path if menuName is not set - const displayName = - article.fields.menuName || article.fields.name || article.path; - const frontmatter: Record = { - title: displayName, - description: `API reference for ${displayName}`, - type: 'api', - // Use explicit layout to override Hugo's default section template lookup - // (Hugo's section lookup ignores `type`, so we need `layout` for the 3-column API layout) - layout: 'list', - staticFilePath: article.fields.staticFilePath, - weight: 100, - }; - - // Add menu entry if menuKey is provided - // Use menuName for menu display (shows actual endpoint path like /health) - if (menuKey) { - frontmatter.menu = { - [menuKey]: { - name: displayName, - ...(menuParent && { parent: menuParent }), - }, - }; - } - - // Add related links if present in article fields - if ( - article.fields.related && - Array.isArray(article.fields.related) && - article.fields.related.length > 0 - ) { - frontmatter.related = article.fields.related; - } - - // Add OpenAPI tags if present in article fields (for frontmatter metadata) - if ( - article.fields.apiTags && - Array.isArray(article.fields.apiTags) && - article.fields.apiTags.length > 0 - ) { - frontmatter.api_tags = article.fields.apiTags; - } - - const pageContent = `--- -${yaml.dump(frontmatter)}--- -`; - - fs.writeFileSync(pageFile, pageContent); - } - - console.log( - `✓ Generated ${data.articles.length} content pages in ${contentPath}` - ); -} - -/** - * Options for generating tag-based pages from article data - */ -interface GenerateTagPagesOptions { - /** Path to the articles data directory */ - articlesPath: string; - /** Output path for generated content pages */ - contentPath: string; - /** Hugo menu identifier for navigation (e.g., 'influxdb3_core') */ - menuKey?: string; - /** Parent menu item name (e.g., 'InfluxDB HTTP API') */ - menuParent?: string; - /** Product description for the parent page */ - productDescription?: string; - /** Skip adding menu entry to generated parent page */ - skipParentMenu?: boolean; - /** Map of API path to path-specific spec file (for single-operation rendering) */ - pathSpecFiles?: Map; -} - -/** - * 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 Hugo-native templates. - * Includes operation metadata for TOC generation. - * - * @param options - Generation options - */ -function generateTagPagesFromArticleData( - options: GenerateTagPagesOptions -): void { - const { - articlesPath, - contentPath, - menuKey, - menuParent, - productDescription, - skipParentMenu, - 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; - description?: string; - tag?: string; - isConceptual?: boolean; - showSecuritySchemes?: boolean; - tagDescription?: string; - menuGroup?: string; - staticFilePath?: string; - operations?: OperationMeta[]; - related?: (string | { title: string; href: string })[]; - weight?: number; - }; - }>; - }; - - if (!data.articles || !Array.isArray(data.articles)) { - console.warn(`⚠️ No articles found in ${articlesFile}`); - return; - } - - // Ensure content directory exists - if (!fs.existsSync(contentPath)) { - fs.mkdirSync(contentPath, { recursive: true }); - } - - // Generate parent _index.md for the API section - const apiParentDir = path.join(contentPath, 'api'); - const parentIndexFile = path.join(apiParentDir, '_index.md'); - - if (!fs.existsSync(apiParentDir)) { - fs.mkdirSync(apiParentDir, { recursive: true }); - } - - if (!fs.existsSync(parentIndexFile)) { - // Build description - use product description or generate from product name - const apiDescription = - productDescription || - `Use the InfluxDB HTTP API to write data, query data, and manage databases, tables, and tokens.`; - - const parentFrontmatter: Record = { - title: menuParent || 'InfluxDB HTTP API', - description: apiDescription, - weight: 104, - type: 'api', - }; - - // Add menu entry for parent page (unless skipParentMenu is true) - if (menuKey && !skipParentMenu) { - parentFrontmatter.menu = { - [menuKey]: { - name: menuParent || 'InfluxDB HTTP API', - parent: 'Reference', - }, - }; - } - - // Add alt_links for cross-product API navigation - if (apiProductsMap.size > 0) { - const altLinks: Record = {}; - apiProductsMap.forEach((apiPath, productName) => { - altLinks[productName] = apiPath; - }); - parentFrontmatter.alt_links = altLinks; - } - - // Build page content with intro paragraph and children listing - const introText = apiDescription.replace( - 'InfluxDB', - '{{% product-name %}}' - ); - const parentContent = `--- -${yaml.dump(parentFrontmatter)}--- - -${introText} - -{{< children >}} -`; - - fs.writeFileSync(parentIndexFile, parentContent); - console.log(`✓ Generated parent index at ${parentIndexFile}`); - } - - // Generate "All endpoints" page - const allEndpointsDir = path.join(apiParentDir, 'all-endpoints'); - const allEndpointsFile = path.join(allEndpointsDir, '_index.md'); - - if (!fs.existsSync(allEndpointsDir)) { - fs.mkdirSync(allEndpointsDir, { recursive: true }); - } - - const allEndpointsFrontmatter: Record = { - title: 'All endpoints', - description: `View all API endpoints sorted by path.`, - type: 'api', - layout: 'all-endpoints', - weight: 999, - isAllEndpoints: true, - }; - - // Add menu entry for all-endpoints page - if (menuKey) { - allEndpointsFrontmatter.menu = { - [menuKey]: { - name: 'All endpoints', - parent: menuParent || 'InfluxDB HTTP API', - }, - }; - } - - // Add alt_links for cross-product API navigation - if (apiProductsMap.size > 0) { - const altLinks: Record = {}; - apiProductsMap.forEach((apiPath, productName) => { - altLinks[productName] = apiPath; - }); - allEndpointsFrontmatter.alt_links = altLinks; - } - - const allEndpointsContent = `--- -${yaml.dump(allEndpointsFrontmatter)}--- - -All {{% product-name %}} API endpoints, sorted by path. -`; - - fs.writeFileSync(allEndpointsFile, allEndpointsContent); - console.log(`✓ Generated all-endpoints page at ${allEndpointsFile}`); - - // Generate a page for each article (tag) - for (const article of data.articles) { - const pagePath = path.join(contentPath, article.path); - const pageFile = path.join(pagePath, '_index.md'); - - // Create directory if needed - if (!fs.existsSync(pagePath)) { - fs.mkdirSync(pagePath, { recursive: true }); - } - - // Build frontmatter object - const title = article.fields.title || article.fields.name || article.path; - const isConceptual = article.fields.isConceptual === true; - - // Determine weight: use article.fields.weight if set, otherwise default to 100 - const weight = article.fields.weight ?? 100; - - const frontmatter: Record = { - title, - description: article.fields.description || `API reference for ${title}`, - type: 'api', - layout: isConceptual ? 'single' : 'list', - staticFilePath: article.fields.staticFilePath, - weight, - // Tag-based fields - tag: article.fields.tag, - isConceptual, - menuGroup: article.fields.menuGroup, - }; - - // Add operations for TOC generation (only for non-conceptual pages) - if ( - !isConceptual && - article.fields.operations && - article.fields.operations.length > 0 - ) { - frontmatter.operations = article.fields.operations; - } - - // Add tag description for conceptual pages - if (isConceptual && article.fields.tagDescription) { - frontmatter.tagDescription = article.fields.tagDescription; - } - - // Add showSecuritySchemes flag for authentication pages - if (article.fields.showSecuritySchemes) { - frontmatter.showSecuritySchemes = true; - } - - // Note: We deliberately don't add menu entries for tag-based API pages. - // The API sidebar navigation (api/sidebar-nav.html) handles navigation - // for API reference pages, avoiding conflicts with existing menu items - // like "Query data" and "Write data" that exist in the main sidebar. - - // Add related links if present in article fields - if ( - article.fields.related && - Array.isArray(article.fields.related) && - article.fields.related.length > 0 - ) { - 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 = {}; - apiProductsMap.forEach((apiPath, productName) => { - altLinks[productName] = apiPath; - }); - frontmatter.alt_links = altLinks; - } - - const pageContent = `--- -${yaml.dump(frontmatter)}--- -`; - - fs.writeFileSync(pageFile, pageContent); - } - - console.log( - `✓ Generated ${data.articles.length} tag-based content pages in ${contentPath}` - ); - - // NOTE: Path page generation is disabled - all operations are now displayed - // 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. -} - -/** - * Article data structure from articles.yml - */ -interface ArticleData { - articles: Array<{ - path: string; - fields: { - name?: string; - title?: string; - description?: string; - tag?: string; - isConceptual?: boolean; - showSecuritySchemes?: boolean; - tagDescription?: string; - menuGroup?: string; - staticFilePath?: string; - operations?: OperationMeta[]; - related?: (string | { title: string; href: string })[]; - source?: string; - }; - }>; -} - -/** - * Merge article data from multiple specs into a single articles.yml - * - * Articles are merged by tag name. Operations from different specs are combined - * into the same article if they share the same tag. - * - * @param articlesFiles - Array of paths to articles.yml files to merge - * @param outputPath - Path to write the merged articles.yml - */ -function mergeArticleData(articlesFiles: string[], outputPath: string): void { - const yaml = require('js-yaml'); - const mergedArticles = new Map(); - - for (const file of articlesFiles) { - if (!fs.existsSync(file)) { - console.warn(`⚠️ Articles file not found for merge: ${file}`); - continue; - } - - const content = fs.readFileSync(file, 'utf8'); - const data = yaml.load(content) as ArticleData; - - if (!data.articles || !Array.isArray(data.articles)) { - console.warn(`⚠️ No articles found in ${file}`); - continue; - } - - for (const article of data.articles) { - const key = article.fields.tag || article.path; - const existing = mergedArticles.get(key); - - if (existing) { - // Merge operations from this spec into existing article - if (article.fields.operations && article.fields.operations.length > 0) { - existing.fields.operations = [ - ...(existing.fields.operations || []), - ...article.fields.operations, - ]; - } - - // 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 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 - if ( - article.fields.description && - (!existing.fields.description || - article.fields.description.length > - existing.fields.description.length) - ) { - existing.fields.description = article.fields.description; - } - - // Merge tagDescription if not already set - if (article.fields.tagDescription && !existing.fields.tagDescription) { - existing.fields.tagDescription = article.fields.tagDescription; - } - } else { - // Add new article - mergedArticles.set(key, JSON.parse(JSON.stringify(article))); - } - } - } - - // Convert map to array and write - const mergedData: ArticleData = { - articles: Array.from(mergedArticles.values()), - }; - - // Ensure output directory exists - const outputDir = path.dirname(outputPath); - if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir, { recursive: true }); - } - - // Write both YAML and JSON versions - const yamlPath = outputPath.endsWith('.yml') - ? outputPath - : `${outputPath}.yml`; - const jsonPath = yamlPath.replace(/\.yml$/, '.json'); - - fs.writeFileSync(yamlPath, yaml.dump(mergedData)); - fs.writeFileSync(jsonPath, JSON.stringify(mergedData, null, 2)); - - console.log( - `✓ Merged ${mergedArticles.size} articles from ${articlesFiles.length} specs to ${outputPath}` - ); -} - -/** - * Product configurations for all InfluxDB editions - * - * Maps product identifiers to their OpenAPI specs and content directories - */ -const productConfigs: ProductConfigMap = { - // InfluxDB v2 products - use tag-based generation for consistency - // These have existing /reference/api/ pages with menu entries, - // so we skip adding menu entries to the generated parent pages. - 'cloud-v2': { - specFiles: [ - { - path: path.join( - API_DOCS_ROOT, - 'influxdb/cloud/influxdb-cloud-v2-openapi.yaml' - ), - displayName: 'API', - }, - ], - pagesDir: path.join(DOCS_ROOT, 'content/influxdb/cloud'), - description: 'InfluxDB Cloud (v2 API)', - menuKey: 'influxdb_cloud', - skipParentMenu: true, - useTagBasedGeneration: true, - }, - 'oss-v2': { - specFiles: [ - { - path: path.join( - API_DOCS_ROOT, - 'influxdb/v2/influxdb-oss-v2-openapi.yaml' - ), - displayName: 'API', - }, - ], - pagesDir: path.join(DOCS_ROOT, 'content/influxdb/v2'), - description: 'InfluxDB OSS v2', - menuKey: 'influxdb_v2', - skipParentMenu: true, - useTagBasedGeneration: true, - }, - // InfluxDB 3 products use tag-based generation for better UX - // Keys use underscores to match Hugo data directory structure - influxdb3_core: { - specFile: path.join( - API_DOCS_ROOT, - 'influxdb3/core/influxdb3-core-openapi.yaml' - ), - pagesDir: path.join(DOCS_ROOT, 'content/influxdb3/core'), - description: 'InfluxDB 3 Core', - menuKey: 'influxdb3_core', - useTagBasedGeneration: true, - }, - influxdb3_enterprise: { - specFile: path.join( - API_DOCS_ROOT, - 'influxdb3/enterprise/influxdb3-enterprise-openapi.yaml' - ), - pagesDir: path.join(DOCS_ROOT, 'content/influxdb3/enterprise'), - description: 'InfluxDB 3 Enterprise', - menuKey: 'influxdb3_enterprise', - useTagBasedGeneration: true, - }, - // Cloud Dedicated and Clustered use multiple specs: - // - Management API: database, token, and cluster management (runs on management console) - // - v2 Data API: write, query, and compatibility endpoints (runs on cluster host) - // Both specs are kept separate for downloads (different servers/auth) but article - // data is merged so the sidebar shows functional tags from both. - // These products have existing /reference/api/ pages with menu entries, - // so we skip adding menu entries to the generated parent pages. - 'cloud-dedicated': { - specFiles: [ - { - path: path.join( - API_DOCS_ROOT, - 'influxdb3/cloud-dedicated/management/openapi.yml' - ), - displayName: 'Management API', - }, - { - path: path.join( - API_DOCS_ROOT, - 'influxdb3/cloud-dedicated/influxdb3-cloud-dedicated-openapi.yaml' - ), - displayName: 'v2 Data API', - }, - ], - pagesDir: path.join(DOCS_ROOT, 'content/influxdb3/cloud-dedicated'), - description: 'InfluxDB Cloud Dedicated', - menuKey: 'influxdb3_cloud_dedicated', - skipParentMenu: true, - useTagBasedGeneration: true, - }, - 'cloud-serverless': { - specFiles: [ - { - path: path.join( - API_DOCS_ROOT, - 'influxdb3/cloud-serverless/influxdb3-cloud-serverless-openapi.yaml' - ), - displayName: 'v2 Data API', - }, - ], - pagesDir: path.join(DOCS_ROOT, 'content/influxdb3/cloud-serverless'), - description: 'InfluxDB Cloud Serverless', - menuKey: 'influxdb3_cloud_serverless', - skipParentMenu: true, - useTagBasedGeneration: true, - }, - clustered: { - specFiles: [ - { - path: path.join( - API_DOCS_ROOT, - 'influxdb3/clustered/management/openapi.yml' - ), - displayName: 'Management API', - }, - { - path: path.join( - API_DOCS_ROOT, - 'influxdb3/clustered/influxdb3-clustered-openapi.yaml' - ), - displayName: 'v2 Data API', - }, - ], - pagesDir: path.join(DOCS_ROOT, 'content/influxdb3/clustered'), - description: 'InfluxDB Clustered', - menuKey: 'influxdb3_clustered', - skipParentMenu: true, - useTagBasedGeneration: true, - }, - // InfluxDB v1 products - use tag-based generation - // These have existing /tools/api/ pages with menu entries, - // so we skip adding menu entries to the generated parent pages. - 'oss-v1': { - specFile: path.join( - API_DOCS_ROOT, - 'influxdb/v1/influxdb-oss-v1-openapi.yaml' - ), - pagesDir: path.join(DOCS_ROOT, 'content/influxdb/v1'), - description: 'InfluxDB OSS v1', - menuKey: 'influxdb_v1', - skipParentMenu: true, - useTagBasedGeneration: true, - }, - 'enterprise-v1': { - specFile: path.join( - API_DOCS_ROOT, - 'enterprise_influxdb/v1/influxdb-enterprise-v1-openapi.yaml' - ), - pagesDir: path.join(DOCS_ROOT, 'content/enterprise_influxdb/v1'), - description: 'InfluxDB Enterprise v1', - menuKey: 'enterprise_influxdb_v1', - skipParentMenu: true, - useTagBasedGeneration: true, - }, -}; +// --------------------------------------------------------------------------- +// Link transformation +// --------------------------------------------------------------------------- /** Fields that can contain markdown with links */ const MARKDOWN_FIELDS = new Set(['description', 'summary']); @@ -1054,34 +526,9 @@ const MARKDOWN_FIELDS = new Set(['description', 'summary']); /** Link placeholder pattern */ const LINK_PATTERN = /\/influxdb\/version\//g; -/** - * Derive documentation root from spec file path. - * - * @example - * 'api-docs/influxdb3/core/influxdb3-core-openapi.yaml' → '/influxdb3/core' - * 'api-docs/influxdb3/enterprise/influxdb3-enterprise-openapi.yaml' → '/influxdb3/enterprise' - * 'api-docs/influxdb/v2/influxdb-oss-v2-openapi.yaml' → '/influxdb/v2' - * 'api-docs/influxdb/v1/influxdb-oss-v1-openapi.yaml' → '/influxdb/v1' - * 'api-docs/enterprise_influxdb/v1/influxdb-enterprise-v1-openapi.yaml' → '/enterprise_influxdb/v1' - */ -function deriveProductPath(specPath: string): string { - // Match: api-docs/[_build/](enterprise_influxdb|influxdb3|influxdb)/(product-or-version)/... - const match = specPath.match( - /api-docs\/(?:_build\/)?(enterprise_influxdb|influxdb3?)\/([\w-]+)\// - ); - if (!match) { - throw new Error(`Cannot derive product path from: ${specPath}`); - } - return `/${match[1]}/${match[2]}`; -} - /** * Transform documentation links in OpenAPI spec markdown fields. * Replaces `/influxdb/version/` with the actual product path. - * - * @param spec - Parsed OpenAPI spec object - * @param productPath - Target path (e.g., '/influxdb3/core') - * @returns Spec with transformed links (new object, original unchanged) */ function transformDocLinks( spec: Record, @@ -1122,29 +569,20 @@ function transformDocLinks( /** * Resolve a URL path to a content file path. * - * @example - * '/influxdb3/core/api/auth/' → 'content/influxdb3/core/api/auth/_index.md' + * @example '/influxdb3/core/api/auth/' → 'content/influxdb3/core/api/auth/_index.md' */ function resolveContentPath(urlPath: string, contentDir: string): string { const normalized = urlPath.replace(/\/$/, ''); const indexPath = path.join(contentDir, normalized, '_index.md'); const directPath = path.join(contentDir, normalized + '.md'); - if (fs.existsSync(indexPath)) { - return indexPath; - } - if (fs.existsSync(directPath)) { - return directPath; - } - return indexPath; // Return expected path for error message + if (fs.existsSync(indexPath)) return indexPath; + if (fs.existsSync(directPath)) return directPath; + return indexPath; } /** * Validate that transformed links point to existing content. - * - * @param spec - Transformed OpenAPI spec - * @param contentDir - Path to Hugo content directory - * @returns Array of error messages for broken links */ function validateDocLinks( spec: Record, @@ -1158,7 +596,6 @@ function validateDocLinks( let match; while ((match = linkPattern.exec(value)) !== null) { const [, linkText, linkUrl] = match; - // Only validate internal links (start with /) if (linkUrl.startsWith('/') && !linkUrl.startsWith('//')) { const contentPath = resolveContentPath(linkUrl, contentDir); if (!fs.existsSync(contentPath)) { @@ -1168,7 +605,6 @@ function validateDocLinks( } } } - // Reset regex lastIndex for next string linkPattern.lastIndex = 0; } else if (Array.isArray(value)) { value.forEach((item, index) => @@ -1187,342 +623,503 @@ function validateDocLinks( return errors; } +// --------------------------------------------------------------------------- +// Page generation +// --------------------------------------------------------------------------- + /** - * Convert display name to filename-safe slug - * - * @param displayName - Display name (e.g., "Management API") - * @returns Filename-safe slug (e.g., "management-api") + * Options for generating tag-based pages from article data */ -function slugifyDisplayName(displayName: string): string { - return displayName - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-|-$/g, ''); +interface GenerateTagPagesOptions { + articlesPath: string; + contentPath: string; + sectionSlug: string; + menuKey?: string; + menuParent?: string; + productDescription?: string; + skipParentMenu?: boolean; + specDownloadPath: string; + articleDataKey: string; + articleSection: string; + pathSpecFiles?: Map; } /** - * Process a single spec file: transform links, write to static folder + * Generate Hugo content pages from tag-based article data. * - * @param specConfig - Spec file configuration - * @param staticPath - Static directory path - * @param staticDirName - Product directory name - * @param productKey - Product identifier - * @returns Object with paths to generated files, or null if processing failed + * Creates markdown files with frontmatter from article metadata. + * Each article becomes a page with type: api that renders via Hugo-native + * templates. Includes operation metadata for TOC generation. */ -function processSpecFile( - specConfig: SpecFileConfig, - staticPath: string, - staticDirName: string, - productKey: string -): { - staticSpecPath: string; - staticJsonSpecPath: string; - articlesPath: string; -} | null { +function generateTagPagesFromArticleData( + options: GenerateTagPagesOptions +): void { + const { + articlesPath, + contentPath, + sectionSlug, + menuKey, + menuParent, + productDescription, + skipParentMenu, + specDownloadPath, + articleDataKey, + articleSection, + } = options; const yaml = require('js-yaml'); + const articlesFile = path.join(articlesPath, 'articles.yml'); - if (!fs.existsSync(specConfig.path)) { - console.warn(`⚠️ Spec file not found: ${specConfig.path}`); - return null; + if (!fs.existsSync(articlesFile)) { + console.warn(`⚠️ Articles file not found: ${articlesFile}`); + return; } - // Generate filename from display name or use default. - // Strip staticDirName prefix from the spec filename to avoid doubled names - // (e.g., influxdb3-core + influxdb3-core-openapi → influxdb3-core-openapi). - let specSlug: string; - if (specConfig.displayName) { - specSlug = slugifyDisplayName(specConfig.displayName); - } else { - const rawName = path.parse(specConfig.path).name; - const prefix = `${staticDirName}-`; - specSlug = rawName.startsWith(prefix) - ? rawName.slice(prefix.length) - : rawName; + const articlesContent = fs.readFileSync(articlesFile, 'utf8'); + const data = yaml.load(articlesContent) as ArticleData; + + if (!data.articles || !Array.isArray(data.articles)) { + console.warn(`⚠️ No articles found in ${articlesFile}`); + return; } + if (!fs.existsSync(contentPath)) { + fs.mkdirSync(contentPath, { recursive: true }); + } + + // Generate parent _index.md for the section + const sectionDir = path.join(contentPath, sectionSlug); + const parentIndexFile = path.join(sectionDir, '_index.md'); + + if (!fs.existsSync(sectionDir)) { + fs.mkdirSync(sectionDir, { recursive: true }); + } + + if (!fs.existsSync(parentIndexFile)) { + const apiDescription = + productDescription || + `Use the InfluxDB HTTP API to write data, query data, and manage databases, tables, and tokens.`; + + const parentFrontmatter: Record = { + title: menuParent || 'InfluxDB HTTP API', + description: apiDescription, + weight: 104, + type: 'api', + articleDataKey, + articleSection, + }; + + if (menuKey && !skipParentMenu) { + parentFrontmatter.menu = { + [menuKey]: { + name: menuParent || 'InfluxDB HTTP API', + identifier: `api-reference-${articleDataKey}-${sectionSlug}`, + parent: 'Reference', + }, + }; + } + + if (apiProductsMap.size > 0) { + const altLinks: Record = {}; + apiProductsMap.forEach((apiPath, productName) => { + altLinks[productName] = apiPath; + }); + parentFrontmatter.alt_links = altLinks; + } + + const introText = apiDescription.replace( + 'InfluxDB', + '{{% product-name %}}' + ); + const parentContent = `--- +${yaml.dump(parentFrontmatter)}--- + +${introText} + +{{< children >}} +`; + + fs.writeFileSync(parentIndexFile, parentContent); + console.log(`✓ Generated parent index at ${parentIndexFile}`); + } + + // Generate "All endpoints" page + const allEndpointsDir = path.join(sectionDir, 'all-endpoints'); + const allEndpointsFile = path.join(allEndpointsDir, '_index.md'); + + if (!fs.existsSync(allEndpointsDir)) { + fs.mkdirSync(allEndpointsDir, { recursive: true }); + } + + const allEndpointsFrontmatter: Record = { + title: 'All endpoints', + description: `View all API endpoints sorted by path.`, + type: 'api', + layout: 'all-endpoints', + weight: 999, + isAllEndpoints: true, + articleDataKey, + articleSection, + }; + + if (menuKey) { + allEndpointsFrontmatter.menu = { + [menuKey]: { + name: 'All endpoints', + identifier: `all-endpoints-${articleDataKey}-${sectionSlug}`, + parent: menuParent || 'InfluxDB HTTP API', + }, + }; + } + + if (apiProductsMap.size > 0) { + const altLinks: Record = {}; + apiProductsMap.forEach((apiPath, productName) => { + altLinks[productName] = apiPath; + }); + allEndpointsFrontmatter.alt_links = altLinks; + } + + const allEndpointsContent = `--- +${yaml.dump(allEndpointsFrontmatter)}--- + +All {{% product-name %}} API endpoints, sorted by path. +`; + + fs.writeFileSync(allEndpointsFile, allEndpointsContent); + console.log(`✓ Generated all-endpoints page at ${allEndpointsFile}`); + + // Generate a page for each article (tag) + for (const article of data.articles) { + const pagePath = path.join(contentPath, article.path); + const pageFile = path.join(pagePath, '_index.md'); + + if (!fs.existsSync(pagePath)) { + fs.mkdirSync(pagePath, { recursive: true }); + } + + const title = article.fields.title || article.fields.name || article.path; + const isConceptual = article.fields.isConceptual === true; + const weight = article.fields.weight ?? 100; + + const frontmatter: Record = { + title, + description: article.fields.description || `API reference for ${title}`, + type: 'api', + layout: isConceptual ? 'single' : 'list', + staticFilePath: article.fields.staticFilePath, + weight, + tag: article.fields.tag, + isConceptual, + menuGroup: article.fields.menuGroup, + specDownloadPath, + articleDataKey, + articleSection, + }; + + if ( + !isConceptual && + article.fields.operations && + article.fields.operations.length > 0 + ) { + frontmatter.operations = article.fields.operations; + } + + if (isConceptual && article.fields.tagDescription) { + frontmatter.tagDescription = article.fields.tagDescription; + } + + if (article.fields.showSecuritySchemes) { + frontmatter.showSecuritySchemes = true; + } + + // Add related links if present + if ( + article.fields.related && + Array.isArray(article.fields.related) && + article.fields.related.length > 0 + ) { + frontmatter.related = article.fields.related; + } + + // Add client library related link for InfluxDB 3 products + if (contentPath.includes('influxdb3/') && !isConceptual) { + 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]; + } + } + } + + if (apiProductsMap.size > 0) { + const altLinks: Record = {}; + apiProductsMap.forEach((apiPath, productName) => { + altLinks[productName] = apiPath; + }); + frontmatter.alt_links = altLinks; + } + + const pageContent = `--- +${yaml.dump(frontmatter)}--- +`; + + fs.writeFileSync(pageFile, pageContent); + } + + console.log( + `✓ Generated ${data.articles.length} tag-based content pages in ${contentPath}` + ); +} + +// --------------------------------------------------------------------------- +// Spec processing +// --------------------------------------------------------------------------- + +/** + * Process a single API section: transform links, write static spec, + * generate tag data, and create Hugo content pages. + */ +function processApiSection( + product: DiscoveredProduct, + api: DiscoveredApi, + staticBasePath: string +): void { + const yaml = require('js-yaml'); + const isDualApi = product.apis.length > 1; + + console.log(`\n📄 Processing ${api.sectionSlug} section (${api.apiKey})`); + + // --- 1. Determine paths --- + + // Root spec download: single → {dir}.yml, dual → {dir}-{section}.yml + const specSuffix = isDualApi ? `-${api.sectionSlug}` : ''; const staticSpecPath = path.join( - staticPath, - `${staticDirName}-${specSlug}.yml` - ); - const staticJsonSpecPath = path.join( - staticPath, - `${staticDirName}-${specSlug}.json` + staticBasePath, + `${product.staticDirName}${specSuffix}.yml` ); + const staticJsonSpecPath = staticSpecPath.replace('.yml', '.json'); + + // Tag specs directory + const tagSpecsBase = isDualApi + ? path.join(staticBasePath, product.staticDirName, api.sectionSlug) + : path.join(staticBasePath, product.staticDirName); + + // Article data const articlesPath = path.join( DOCS_ROOT, - `data/article_data/influxdb/${productKey}/${specSlug}` + 'data/article_data/influxdb', + product.staticDirName, + api.sectionSlug ); - try { - const specContent = fs.readFileSync(specConfig.path, 'utf8'); - const specObject = yaml.load(specContent) as Record; + // Download path for frontmatter + const specDownloadPath = `/openapi/${product.staticDirName}${specSuffix}.yml`; - // Transform documentation links (/influxdb/version/ -> actual product path) - const productPath = deriveProductPath(specConfig.path); - const transformedSpec = transformDocLinks(specObject, productPath); - console.log( - `✓ Transformed documentation links for ${specConfig.displayName || specSlug} to ${productPath}` - ); + // Path spec files for per-operation rendering + const pathSpecsDir = isDualApi + ? path.join(staticBasePath, product.staticDirName, api.sectionSlug, 'paths') + : path.join(staticBasePath, product.staticDirName, 'paths'); - // Validate links if enabled - if (validateLinks) { - const contentDir = path.resolve(__dirname, '../../content'); - const linkErrors = validateDocLinks(transformedSpec, contentDir); - if (linkErrors.length > 0) { - console.warn(`\n⚠️ Link validation warnings for ${specConfig.path}:`); - linkErrors.forEach((err) => console.warn(` ${err}`)); - } - } + // --- 2. Read and transform spec --- - // Write transformed spec to static folder - fs.writeFileSync(staticSpecPath, yaml.dump(transformedSpec)); - console.log(`✓ Wrote transformed spec to ${staticSpecPath}`); - - fs.writeFileSync( - staticJsonSpecPath, - JSON.stringify(transformedSpec, null, 2) - ); - console.log(`✓ Generated JSON spec at ${staticJsonSpecPath}`); - - return { staticSpecPath, staticJsonSpecPath, articlesPath }; - } catch (specError) { - console.warn(`⚠️ Could not process spec: ${specError}`); - return null; + if (!fs.existsSync(api.specFile)) { + console.warn(`⚠️ Spec file not found: ${api.specFile}`); + return; } + + const specContent = fs.readFileSync(api.specFile, 'utf8'); + const specObject = yaml.load(specContent) as Record; + + const productPath = `/${product.productDir}`; + const transformedSpec = transformDocLinks(specObject, productPath); + console.log( + `✓ Transformed documentation links for ${api.apiKey} to ${productPath}` + ); + + // Validate links if enabled + if (validateLinks) { + const contentDir = path.join(DOCS_ROOT, 'content'); + const linkErrors = validateDocLinks(transformedSpec, contentDir); + if (linkErrors.length > 0) { + console.warn(`\n⚠️ Link validation warnings for ${api.specFile}:`); + linkErrors.forEach((err) => console.warn(` ${err}`)); + } + } + + // --- 3. Write transformed spec to static folder --- + + if (!fs.existsSync(staticBasePath)) { + fs.mkdirSync(staticBasePath, { recursive: true }); + } + + fs.writeFileSync(staticSpecPath, yaml.dump(transformedSpec)); + console.log(`✓ Wrote transformed spec to ${staticSpecPath}`); + + fs.writeFileSync( + staticJsonSpecPath, + JSON.stringify(transformedSpec, null, 2) + ); + console.log(`✓ Generated JSON spec at ${staticJsonSpecPath}`); + + // --- 4. Generate tag-based data --- + + console.log( + `\n📋 Generating tag-based data for ${api.apiKey} in ${tagSpecsBase}...` + ); + openapiPathsToHugo.generateHugoDataByTag({ + specFile: staticSpecPath, + dataOutPath: tagSpecsBase, + articleOutPath: articlesPath, + includePaths: true, + }); + + // Generate path-specific specs + openapiPathsToHugo.generatePathSpecificSpecs(staticSpecPath, pathSpecsDir); + + // --- 5. Generate Hugo content pages --- + + generateTagPagesFromArticleData({ + articlesPath, + contentPath: product.pagesDir, + sectionSlug: api.sectionSlug, + menuKey: product.menuKey, + menuParent: 'InfluxDB HTTP API', + skipParentMenu: product.skipParentMenu, + specDownloadPath, + articleDataKey: product.staticDirName, + articleSection: api.sectionSlug, + }); } /** - * Process a single product: fetch spec, generate data, and create pages - * - * Supports both single spec (specFile) and multiple specs (specFiles). - * For multiple specs, article data is merged so the sidebar shows - * functional tags from all specs. - * - * @param productKey - Product identifier (e.g., 'cloud-v2') - * @param config - Product configuration + * Process a single product: clean outputs and process each API section. */ -function processProduct(productKey: string, config: ProductConfig): void { +function processProduct( + product: DiscoveredProduct, + allStaticDirNames: string[] +): void { console.log('\n' + '='.repeat(80)); - console.log(`Processing ${config.description || productKey}`); + console.log(`Processing ${product.productName}`); console.log('='.repeat(80)); - // Clean output directories before regeneration (unless --no-clean, --dry-run, or --static-only) - if (!noClean && !dryRun && !staticOnly) { - cleanProductOutputs(productKey, config); + // Clean output directories before regeneration + if (!noClean && !dryRun) { + cleanProductOutputs(product, allStaticDirNames); } - const staticPath = path.join(DOCS_ROOT, 'static/openapi'); - const staticDirName = getStaticDirName(productKey); - const staticPathsPath = path.join(staticPath, `${staticDirName}/paths`); - const mergedArticlesPath = path.join( - DOCS_ROOT, - `data/article_data/influxdb/${productKey}` - ); + const staticBasePath = path.join(DOCS_ROOT, 'static/openapi'); - // Ensure static directory exists - if (!fs.existsSync(staticPath)) { - fs.mkdirSync(staticPath, { recursive: true }); + // Fetch specs if needed + if (!skipFetch) { + const getswaggerScript = path.join(API_DOCS_ROOT, 'getswagger.sh'); + if (fs.existsSync(getswaggerScript)) { + // The build function in generate-api-docs.sh handles per-product + // fetching. When called standalone, use product directory name. + execCommand( + `cd ${API_DOCS_ROOT} && ./getswagger.sh ${product.productDir} -B`, + `Fetching OpenAPI spec for ${product.productName}` + ); + } else { + console.log(`⚠️ getswagger.sh not found, skipping fetch step`); + } + } else { + console.log(`⏭️ Skipping getswagger.sh (--skip-fetch flag set)`); } - try { - // Determine spec files to process - const specFiles: SpecFileConfig[] = config.specFiles - ? config.specFiles - : config.specFile - ? [{ path: config.specFile }] - : []; - - if (specFiles.length === 0) { - console.warn(`⚠️ No spec files configured for ${productKey}`); - return; - } - - // Check if any spec files exist - const existingSpecs = specFiles.filter((s) => fs.existsSync(s.path)); - if (existingSpecs.length === 0) { - console.warn( - `⚠️ No spec files found for ${productKey}. Run getswagger.sh first if needed.` - ); - return; - } - - // Process each spec file - const processedSpecs: Array<{ - staticSpecPath: string; - articlesPath: string; - }> = []; - const allPathSpecFiles = new Map(); - - for (const specConfig of specFiles) { - console.log( - `\n📄 Processing spec: ${specConfig.displayName || specConfig.path}` - ); - - const result = processSpecFile( - specConfig, - staticPath, - staticDirName, - productKey - ); - - if (result) { - processedSpecs.push(result); - - // Generate tag-based article data for this spec (skip in --static-only mode) - if (!staticOnly && config.useTagBasedGeneration) { - const specSlug = specConfig.displayName - ? slugifyDisplayName(specConfig.displayName) - : path.parse(specConfig.path).name; - const staticTagsPath = path.join( - staticPath, - `${staticDirName}/${specSlug}` - ); - - console.log( - `\n📋 Generating tag-based data for ${specConfig.displayName || specSlug}...` - ); - openapiPathsToHugo.generateHugoDataByTag({ - specFile: result.staticSpecPath, - dataOutPath: staticTagsPath, - articleOutPath: result.articlesPath, - includePaths: true, - }); - - // Generate path-specific specs - const specPathsPath = path.join(staticPathsPath, specSlug); - const pathSpecFiles = openapiPathsToHugo.generatePathSpecificSpecs( - result.staticSpecPath, - specPathsPath - ); - - // Merge path spec files into combined map - pathSpecFiles.forEach((value: string, key: string) => { - allPathSpecFiles.set(key, value); - }); - } - } - } - - // Article generation (skip in --static-only mode) - if (!staticOnly) { - // Merge article data from all specs (for multi-spec products) - if (processedSpecs.length > 1) { - console.log( - `\n📋 Merging article data from ${processedSpecs.length} specs...` - ); - const articlesFiles = processedSpecs.map((s) => - path.join(s.articlesPath, 'articles.yml') - ); - mergeArticleData( - articlesFiles, - path.join(mergedArticlesPath, 'articles.yml') - ); - } else if (processedSpecs.length === 1) { - // Single spec - copy articles to final location if needed - const sourceArticles = path.join( - processedSpecs[0].articlesPath, - 'articles.yml' - ); - const targetArticles = path.join(mergedArticlesPath, 'articles.yml'); - - // Only copy if source and target are different - if ( - sourceArticles !== targetArticles && - fs.existsSync(sourceArticles) - ) { - if (!fs.existsSync(mergedArticlesPath)) { - fs.mkdirSync(mergedArticlesPath, { recursive: true }); - } - fs.copyFileSync(sourceArticles, targetArticles); - fs.copyFileSync( - sourceArticles.replace('.yml', '.json'), - targetArticles.replace('.yml', '.json') - ); - console.log(`✓ Copied article data to ${mergedArticlesPath}`); - } - } - - // Generate Hugo content pages from (merged) article data - if (config.useTagBasedGeneration) { - generateTagPagesFromArticleData({ - articlesPath: mergedArticlesPath, - contentPath: config.pagesDir, - menuKey: config.menuKey, - menuParent: 'InfluxDB HTTP API', - skipParentMenu: config.skipParentMenu, - pathSpecFiles: allPathSpecFiles, - }); - } else { - generatePagesFromArticleData({ - articlesPath: mergedArticlesPath, - contentPath: config.pagesDir, - menuKey: config.menuKey, - menuParent: 'InfluxDB HTTP API', - skipParentMenu: config.skipParentMenu, - }); - } - } - - console.log( - `\n✅ Successfully processed ${config.description || productKey}\n` - ); - } catch (error) { - console.error(`\n❌ Error processing ${productKey}:`, error); - process.exit(1); + // Process each API section independently + for (const api of product.apis) { + processApiSection(product, api, staticBasePath); } + + console.log(`\n✅ Successfully processed ${product.productName}\n`); } -/** - * Main execution function - */ +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + function main(): void { - // Filter out CLI flags from arguments const args = process.argv.slice(2).filter((arg) => !arg.startsWith('--')); - // Determine which products to process - let productsToProcess: string[]; + // Discover all products from .config.yml files + const allProducts = discoverProducts(); - if (args.length === 0) { - // No arguments: process all products - productsToProcess = Object.keys(productConfigs); - console.log('\n📋 Processing all products...\n'); - } else { - // Arguments provided: process only specified products - productsToProcess = args; - console.log( - `\n📋 Processing specified products: ${productsToProcess.join(', ')}\n` - ); - } - - // Validate product keys - const invalidProducts = productsToProcess.filter( - (key) => !productConfigs[key] - ); - if (invalidProducts.length > 0) { + if (allProducts.length === 0) { console.error( - `\n❌ Invalid product identifier(s): ${invalidProducts.join(', ')}` + '❌ No products discovered. Ensure .config.yml files exist under api-docs/.' ); - console.error('\nValid products:'); - Object.keys(productConfigs).forEach((key) => { - console.error(` - ${key}: ${productConfigs[key].description}`); - }); process.exit(1); } + // Determine which products to process + let productsToProcess: DiscoveredProduct[]; + + if (args.length === 0) { + productsToProcess = allProducts; + console.log( + `\n📋 Discovered ${allProducts.length} products, processing all...\n` + ); + } else { + // Match by staticDirName or productDir + productsToProcess = []; + const invalid: string[] = []; + + for (const arg of args) { + const found = allProducts.find( + (p) => + p.staticDirName === arg || + p.productDir === arg || + p.productDir.replace(/\//g, '-') === arg + ); + if (found) { + productsToProcess.push(found); + } else { + invalid.push(arg); + } + } + + if (invalid.length > 0) { + console.error( + `\n❌ Unknown product identifier(s): ${invalid.join(', ')}` + ); + console.error('\nDiscovered products:'); + allProducts.forEach((p) => { + console.error( + ` - ${p.staticDirName} (${p.productName}) [${p.productDir}]` + ); + }); + process.exit(1); + } + + console.log( + `\n📋 Processing specified products: ${productsToProcess.map((p) => p.staticDirName).join(', ')}\n` + ); + } + + // Collect all staticDirNames for prefix-safe cleanup + const allStaticDirNames = allProducts.map((p) => p.staticDirName); + // Handle dry-run mode if (dryRun) { console.log('\n📋 DRY RUN MODE - No files will be modified\n'); - productsToProcess.forEach((productKey) => { - showDryRunPreview(productKey, productConfigs[productKey]); - }); + productsToProcess.forEach((p) => showDryRunPreview(p, allStaticDirNames)); console.log('\nDry run complete. No files were modified.'); return; } // Process each product - productsToProcess.forEach((productKey) => { - const config = productConfigs[productKey]; - processProduct(productKey, config); + productsToProcess.forEach((product) => { + processProduct(product, allStaticDirNames); }); console.log('\n' + '='.repeat(80)); @@ -1537,14 +1134,16 @@ if (require.main === module) { // Export for use as a module export { - productConfigs, + discoverProducts, processProduct, - generateDataFromOpenAPI, - generatePagesFromArticleData, - deriveProductPath, + processApiSection, transformDocLinks, validateDocLinks, resolveContentPath, + deriveStaticDirName, + getSectionSlug, + parseApiEntry, + readMenuKey, MARKDOWN_FIELDS, LINK_PATTERN, }; diff --git a/assets/js/components/api-toc.ts b/assets/js/components/api-toc.ts new file mode 100644 index 000000000..cc75120af --- /dev/null +++ b/assets/js/components/api-toc.ts @@ -0,0 +1,473 @@ +/** + * API Table of Contents Component + * + * Generates "ON THIS PAGE" navigation from content headings or operations data. + * Features: + * - Builds TOC from h2 headings by default + * - Builds TOC from operations data passed via data-operations attribute (tag-based) + * - Highlights current section on scroll (intersection observer) + * - Smooth scroll to anchors + * - Updates when tab changes + * + * Usage: + * + * + * Attributes: + * - data-operations: JSON array of operation objects for server-rendered TOC + * - data-toc-depth: Max heading level to include (default: "2", use "3" for h2+h3) + */ + +interface ComponentOptions { + component: HTMLElement; +} + +interface TocEntry { + id: string; + text: string; + level: number; +} + +/** + * Operation metadata from frontmatter (for tag-based pages) + */ +interface OperationMeta { + operationId: string; + method: string; + path: string; + summary: string; + tags: string[]; +} + +/** + * Get headings from the currently visible content + * + * @param maxLevel - Maximum heading level to include (default: 2) + */ +function getVisibleHeadings(maxLevel: number = 2): TocEntry[] { + // Find the active tab panel or main content area + const activePanel = document.querySelector( + '.tab-content:not([style*="display: none"]), [data-tab-panel]:not([style*="display: none"]), .article--content' + ); + + if (!activePanel) { + return []; + } + + // Build selector based on maxLevel (e.g., 'h2' or 'h2, h3') + const selectors = []; + for (let level = 2; level <= maxLevel; level++) { + selectors.push(`h${level}`); + } + const headings = activePanel.querySelectorAll(selectors.join(', ')); + const entries: TocEntry[] = []; + + headings.forEach((heading) => { + // Skip headings without IDs + if (!heading.id) { + return; + } + + // Skip hidden headings + const rect = heading.getBoundingClientRect(); + if (rect.width === 0 && rect.height === 0) { + return; + } + + const level = parseInt(heading.tagName.charAt(1), 10); + entries.push({ + id: heading.id, + text: heading.textContent?.trim() || '', + level, + }); + }); + + return entries; +} + +/** + * Build TOC HTML from entries + */ +function buildTocHtml(entries: TocEntry[]): string { + if (entries.length === 0) { + // Return empty string - the TOC container can be hidden via CSS when empty + return ''; + } + + let html = '
    '; + + entries.forEach((entry) => { + const indent = entry.level === 3 ? ' api-toc-item--nested' : ''; + html += ` +
  • + ${entry.text} +
  • + `; + }); + + html += '
'; + return html; +} + +/** + * Get method badge class for HTTP method + */ +function getMethodClass(method: string): string { + const m = method.toLowerCase(); + switch (m) { + case 'get': + return 'api-method--get'; + case 'post': + return 'api-method--post'; + case 'put': + return 'api-method--put'; + case 'patch': + return 'api-method--patch'; + case 'delete': + return 'api-method--delete'; + default: + return ''; + } +} + +/** + * Build TOC HTML from operations data (for tag-based pages) + */ +function buildOperationsTocHtml(operations: OperationMeta[]): string { + if (operations.length === 0) { + return '

No operations on this page.

'; + } + + let html = '
    '; + + operations.forEach((op) => { + // Generate anchor ID matching Redocly operation/{operationId} format + const anchorId = `operation/${op.operationId}`; + const methodClass = getMethodClass(op.method); + + html += ` +
  • + + ${op.method.toUpperCase()} + ${op.path} + +
  • + `; + }); + + html += '
'; + return html; +} + +/** + * Parse operations from data attribute + */ +function parseOperationsData(component: HTMLElement): OperationMeta[] | null { + const dataAttr = component.getAttribute('data-operations'); + if (!dataAttr) { + return null; + } + + try { + const operations = JSON.parse(dataAttr) as OperationMeta[]; + return Array.isArray(operations) ? operations : null; + } catch (e) { + console.warn('[API TOC] Failed to parse operations data:', e); + return null; + } +} + +/** + * Set up intersection observer for scroll highlighting + */ +function setupScrollHighlighting( + container: HTMLElement, + entries: TocEntry[] +): IntersectionObserver | null { + if (entries.length === 0) { + return null; + } + + const headingIds = entries.map((e) => e.id); + const links = container.querySelectorAll('.api-toc-link'); + + // Create a map of heading ID to link element + const linkMap = new Map(); + links.forEach((link) => { + const href = link.getAttribute('href'); + if (href?.startsWith('#')) { + linkMap.set(href.slice(1), link); + } + }); + + // Track which headings are visible + const visibleHeadings = new Set(); + + const observer = new IntersectionObserver( + (observerEntries) => { + observerEntries.forEach((entry) => { + const id = entry.target.id; + + if (entry.isIntersecting) { + visibleHeadings.add(id); + } else { + visibleHeadings.delete(id); + } + }); + + // Find the first visible heading (in document order) + let activeId: string | null = null; + for (const id of headingIds) { + if (visibleHeadings.has(id)) { + activeId = id; + break; + } + } + + // If no heading is visible, use the last one that was scrolled past + if (!activeId && visibleHeadings.size === 0) { + const scrollY = window.scrollY; + for (let i = headingIds.length - 1; i >= 0; i--) { + const heading = document.getElementById(headingIds[i]); + if (heading && heading.offsetTop < scrollY + 100) { + activeId = headingIds[i]; + break; + } + } + } + + // Update active state on links + links.forEach((link) => { + link.classList.remove('is-active'); + }); + + if (activeId) { + const activeLink = linkMap.get(activeId); + activeLink?.classList.add('is-active'); + } + }, + { + rootMargin: '-80px 0px -70% 0px', + threshold: 0, + } + ); + + // Observe all headings + headingIds.forEach((id) => { + const heading = document.getElementById(id); + if (heading) { + observer.observe(heading); + } + }); + + return observer; +} + +/** + * Set up smooth scroll for TOC links + */ +function setupSmoothScroll(container: HTMLElement): void { + container.addEventListener('click', (event) => { + const target = event.target as HTMLElement; + const link = target.closest('.api-toc-link'); + + if (!link) { + return; + } + + const href = link.getAttribute('href'); + if (!href?.startsWith('#')) { + return; + } + + const targetElement = document.getElementById(href.slice(1)); + if (!targetElement) { + return; + } + + event.preventDefault(); + + // Scroll with offset for fixed header + const headerOffset = 80; + const elementPosition = targetElement.getBoundingClientRect().top; + const offsetPosition = elementPosition + window.scrollY - headerOffset; + + window.scrollTo({ + top: offsetPosition, + behavior: 'smooth', + }); + + // Update URL hash without jumping + history.pushState(null, '', href); + }); +} + +/** + * Update TOC visibility based on active tab + */ +function updateTocVisibility(container: HTMLElement): void { + const operationsPanel = document.querySelector( + '[data-tab-panel="operations"]' + ); + const isOperationsVisible = + operationsPanel && + !operationsPanel.getAttribute('style')?.includes('display: none'); + + if (isOperationsVisible) { + container.classList.add('is-hidden'); + } else { + container.classList.remove('is-hidden'); + } +} + +/** + * Watch for tab changes to rebuild TOC + */ +function watchTabChanges( + container: HTMLElement, + rebuild: () => void +): MutationObserver { + const tabPanels = document.querySelector('.api-tab-panels'); + + if (!tabPanels) { + return new MutationObserver(() => {}); + } + + const observer = new MutationObserver((mutations) => { + // Check if any tab panel visibility changed + const hasVisibilityChange = mutations.some((mutation) => { + return ( + mutation.type === 'attributes' && + (mutation.attributeName === 'style' || + mutation.attributeName === 'class') + ); + }); + + if (hasVisibilityChange) { + // Update visibility based on active tab + updateTocVisibility(container); + // Debounce rebuild + setTimeout(rebuild, 100); + } + }); + + observer.observe(tabPanels, { + attributes: true, + subtree: true, + attributeFilter: ['style', 'class'], + }); + + return observer; +} + +/** + * Initialize API TOC component + */ +export default function ApiToc({ component }: ComponentOptions): void { + const nav = component.querySelector('.api-toc-nav'); + + if (!nav) { + console.warn('[API TOC] No .api-toc-nav element found'); + return; + } + + // Check if TOC was pre-rendered server-side (has existing links) + const hasServerRenderedToc = nav.querySelectorAll('.api-toc-link').length > 0; + + if (hasServerRenderedToc) { + // Server-side TOC exists - show it, set up navigation and scroll highlighting + component.classList.remove('is-hidden'); + setupSmoothScroll(component); + + // Extract entries from pre-rendered links for scroll highlighting + const preRenderedLinks = + nav.querySelectorAll('.api-toc-link'); + const preRenderedEntries: TocEntry[] = []; + preRenderedLinks.forEach((link) => { + const href = link.getAttribute('href'); + if (href?.startsWith('#')) { + preRenderedEntries.push({ + id: href.slice(1), + text: link.textContent?.trim() || '', + level: 2, + }); + } + }); + if (preRenderedEntries.length > 0) { + setupScrollHighlighting(component, preRenderedEntries); + } + return; + } + + // Check for operations data (tag-based pages) + const operations = parseOperationsData(component); + let observer: IntersectionObserver | null = null; + + // Get max heading level from data attribute (default: 2) + // Use data-toc-depth="3" to include h3 headings if needed + const maxHeadingLevel = parseInt( + component.getAttribute('data-toc-depth') || '2', + 10 + ); + + /** + * Rebuild the TOC + */ + function rebuild(): void { + // Clean up previous observer + if (observer) { + observer.disconnect(); + observer = null; + } + + // If operations data is present, build operations-based TOC + if (operations && operations.length > 0) { + if (nav) { + nav.innerHTML = buildOperationsTocHtml(operations); + } + // Don't hide TOC for tag-based pages - always show operations + component.classList.remove('is-hidden'); + return; + } + + // Otherwise, fall back to heading-based TOC + const entries = getVisibleHeadings(maxHeadingLevel); + if (nav) { + nav.innerHTML = buildTocHtml(entries); + } + + // Hide TOC if no entries, show if entries exist + if (entries.length === 0) { + component.classList.add('is-hidden'); + } else { + component.classList.remove('is-hidden'); + // Set up scroll highlighting only when we have entries + observer = setupScrollHighlighting(component, entries); + } + } + + // Check initial visibility (hide for Operations tab, only for non-operations pages) + if (!operations || operations.length === 0) { + updateTocVisibility(component); + } + + // Initial build + rebuild(); + + // Set up smooth scroll + setupSmoothScroll(component); + + // Watch for tab changes (only for non-operations pages) + if (!operations || operations.length === 0) { + watchTabChanges(component, rebuild); + } + + // Also rebuild on window resize (headings may change visibility) + let resizeTimeout: number; + window.addEventListener('resize', () => { + clearTimeout(resizeTimeout); + resizeTimeout = window.setTimeout(rebuild, 250); + }); +} diff --git a/assets/js/content-interactions.js b/assets/js/content-interactions.js index eb9b4e1bc..4c2a374c0 100644 --- a/assets/js/content-interactions.js +++ b/assets/js/content-interactions.js @@ -122,21 +122,29 @@ function expandAccordions() { // Expand accordions on load based on URL anchor function openAccordionByHash() { - var anchor = window.location.hash; + var hash = window.location.hash; + if (!hash || hash.length <= 1) return; + + // Use native DOM method to handle special characters in IDs (like /) + var id = hash.substring(1); // Remove leading # + var anchorElement = document.getElementById(id); + if (!anchorElement) return; + + var $anchor = $(anchorElement); function expandElement() { - if ($(anchor).parents('.expand').length > 0) { - return $(anchor).closest('.expand').children('.expand-label'); - } else if ($(anchor).hasClass('expand')) { - return $(anchor).children('.expand-label'); + if ($anchor.parents('.expand').length > 0) { + return $anchor.closest('.expand').children('.expand-label'); + } else if ($anchor.hasClass('expand')) { + return $anchor.children('.expand-label'); } + return null; } - if (expandElement() != null) { - if (expandElement().children('.expand-toggle').hasClass('open')) { - // Do nothing? - } else { - expandElement().children('.expand-toggle').trigger('click'); + var $expandLabel = expandElement(); + if ($expandLabel != null) { + if (!$expandLabel.children('.expand-toggle').hasClass('open')) { + $expandLabel.children('.expand-toggle').trigger('click'); } } } diff --git a/assets/js/main.js b/assets/js/main.js index 826ad9a11..bc9d6c89e 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -46,6 +46,7 @@ import SidebarSearch from './components/sidebar-search.js'; import { SidebarToggle } from './sidebar-toggle.js'; import Theme from './theme.js'; import ThemeSwitch from './theme-switch.js'; +import ApiToc from './components/api-toc.ts'; /** * Component Registry @@ -77,6 +78,7 @@ const componentRegistry = { 'sidebar-toggle': SidebarToggle, theme: Theme, 'theme-switch': ThemeSwitch, + 'api-toc': ApiToc, }; /** diff --git a/assets/styles/layouts/_api-code-samples.scss b/assets/styles/layouts/_api-code-samples.scss new file mode 100644 index 000000000..f4dbe6ecc --- /dev/null +++ b/assets/styles/layouts/_api-code-samples.scss @@ -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 { + display: flex; + align-items: center; + justify-content: space-between; + margin: 0; + padding: $api-spacing-sm $api-spacing-md; + 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-title { + font-size: 0.85rem; + font-weight: 600; + } +} + +pre.api-code-block { + margin: 0; + padding: $api-spacing-md; + overflow-x: auto; + background: $article-code-bg; + color: $article-code; + 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 { + font-size: 0.8rem; + font-weight: 400; + text-decoration: none; + color: $article-link; + opacity: 0.7; + transition: opacity 0.2s; + white-space: nowrap; + + &:hover { + opacity: 1; + } +} diff --git a/assets/styles/layouts/_api-layout.scss b/assets/styles/layouts/_api-layout.scss new file mode 100644 index 000000000..2515b00e1 --- /dev/null +++ b/assets/styles/layouts/_api-layout.scss @@ -0,0 +1,785 @@ +/////////////////////////////// API Reference Layout /////////////////////////////// +// +// 3-column layout for API reference documentation: +// - Left: Existing Hugo sidebar + API navigation section +// - Center: Content with page-level tabs (Operations | Server | Auth | Compatibility) +// - Right: "ON THIS PAGE" table of contents +// +//////////////////////////////////////////////////////////////////////////////// + +// Content wrapper becomes flex container when used with API content +// Override overflow:hidden from _content-wrapper.scss to enable sticky positioning +// Widen to compensate for the API TOC so article content matches regular pages +.content-wrapper.api-content { + display: flex; + flex-direction: row; + align-items: flex-start; + overflow: visible; // Required for sticky TOC to work + width: calc(75% + 200px); + max-width: calc(100% - 2rem); +} + +// Main API content area (center column) +.api-main { + flex: 1; + min-width: 0; // Prevent flex item from overflowing + padding-right: 1rem; +} + +// Right-side TOC (third column) +.api-toc { + width: 200px; + flex-shrink: 0; + position: sticky; + top: 80px; // Account for fixed header height + align-self: flex-start; // Critical for sticky to work in flexbox + max-height: calc(100vh - 100px); + overflow-y: auto; + padding: 1rem; + border-left: 1px solid $nav-border; + + // Hidden state (used when a tab panel hides the TOC) + &.is-hidden { + display: none; + } + + &-header { + font-size: 0.75rem; + font-weight: $bold; + text-transform: uppercase; + letter-spacing: 0.08rem; + color: rgba($article-heading, 0.5); + margin: 0 0 1rem; + } + + &-nav { + // TOC list styles + .api-toc-list { + list-style: none; + margin: 0; + padding: 0; + } + + .api-toc-item { + margin: 0; + + &--nested { + padding-left: 0.75rem; + } + } + + .api-toc-link { + display: block; + padding: 0.35rem 0; + font-size: 0.85rem; + color: $nav-item; + text-decoration: none; + transition: color 0.2s; + line-height: 1.4; + + &:hover { + color: $nav-item-hover; + } + + &.is-active { + color: $nav-active; + font-weight: $medium; + } + } + } + + &-empty { + font-size: 0.85rem; + color: rgba($article-text, 0.5); + font-style: italic; + } + + // Operations-based TOC (for tag-based pages) + &-nav .api-toc-list--operations { + .api-toc-item--operation { + margin: 0.35rem 0; + } + + .api-toc-link--operation { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.8rem; + padding: 0.3rem 0; + } + + // HTTP method badges in TOC + .api-method { + display: inline-block; + font-size: 0.6rem; + font-weight: $bold; + text-transform: uppercase; + padding: 0.15rem 0.3rem; + border-radius: 3px; + min-width: 2.2rem; + text-align: center; + flex-shrink: 0; + + &--get { background-color: $b-pool; color: #fff; } // #00A3FF - bright brand blue + &--post { background-color: $gr-rainforest; color: #fff; } // #34BB55 - bright brand green + &--put { background-color: $y-pineapple; color: #fff; } // #FFB94A - bright yellow (distinct from red) + &--patch { background-color: $br-new-purple; color: #fff; } // #9b2aff - distinctive brand purple + &--delete { background-color: $r-curacao; color: #fff; } // #F95F53 - bright brand red + } + + .api-path { + font-family: $code; + font-size: 0.75rem; + word-break: break-all; + color: inherit; + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +////////////////////////// Operations List (Main Content) ////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +// Operations list section +.api-operations-list { + margin: 2rem 0; + + h2 { + margin-bottom: 1rem; + } +} + +// Grid container for operation cards +.api-operations-grid { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +// Individual operation card (clickable link) +.api-operation-card { + display: flex; + align-items: flex-start; + gap: 0.75rem; + padding: 0.75rem 1rem; + background: rgba($article-bg, 0.5); + border: 1px solid $nav-border; + border-radius: $radius; + text-decoration: none; + color: $article-text; + transition: background-color 0.2s, border-color 0.2s; + + &:hover { + background: rgba($article-bg, 0.8); + border-color: $nav-item-hover; + } + + // HTTP method badge + .api-method { + display: inline-block; + font-size: 0.7rem; + font-weight: $bold; + text-transform: uppercase; + padding: 0.2rem 0.4rem; + border-radius: 3px; + min-width: 3.5rem; + text-align: center; + flex-shrink: 0; + margin-top: 0.15rem; + border: 2px solid; + background-color: transparent; + + &--get { border-color: $b-pool; color: $b-pool; } // #00A3FF - bright brand blue + &--post { border-color: $gr-rainforest; color: $gr-rainforest; } // #34BB55 - bright brand green + &--put { border-color: $y-pineapple; color: $y-pineapple; } // #FFB94A - bright yellow (distinct from red) + &--patch { border-color: $br-new-purple; color: $br-new-purple; } // #9b2aff - distinctive brand purple + &--delete { border-color: $r-curacao; color: $r-curacao; } // #F95F53 - bright brand red + } + + // API path in monospace + // Note: Uses element but we override the default code background + // to prevent inconsistent "progress bar" appearance from varying text lengths + .api-path { + font-family: $code; + font-size: 0.9rem; + color: $article-heading; + word-break: break-all; + flex: 1; + min-width: 0; // Allow text to shrink and wrap + background: none; // Override default code background + padding: 0; // Remove default code padding + } + + // Operation summary text + .api-operation-summary { + font-size: 0.875rem; + color: rgba($article-text, 0.8); + flex-shrink: 0; + } +} + +// Responsive: Stack operation cards vertically on small screens +@include media(small) { + .api-operation-card { + flex-direction: column; + align-items: stretch; + gap: 0.5rem; + + .api-method { + align-self: flex-start; + margin-top: 0; + } + + .api-path { + font-size: 0.85rem; + line-height: 1.4; + } + + .api-operation-summary { + font-size: 0.8rem; + line-height: 1.5; + } + } +} + +// Overview/Description section +.api-description { + margin: 2rem 0; + color: $article-text !important; // Override any inherited black color + + h2 { + margin-bottom: 1rem; + } + + // Ensure description text is visible and readable + p, ul, ol, pre, code { + color: $article-text !important; + opacity: 1; + } + + // Also ensure direct text nodes use correct color + & > * { + color: $article-text !important; + } +} + +//////////////////////////////////////////////////////////////////////////////// +////////////////////////// API Navigation in Sidebar /////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +// API navigation wrapper - controls visibility +// Hidden by default, revealed via JS (localStorage) or on API pages +.api-nav-wrapper { + display: none; // Hidden by default + + &.is-revealed { + display: block; // Revealed via JS + } + + // Always show on API pages (server-rendered with .api-reference class) + .api-reference & { + display: block; + } +} + +// API navigation section added to the existing Hugo sidebar +.api-nav { + margin-top: 2rem; + padding-top: 1rem; + border-top: 1px solid $nav-border; + + &-header { + font-size: 0.85rem; + font-weight: $bold; + text-transform: uppercase; + letter-spacing: 0.06rem; + color: rgba($article-heading, 0.6); + margin: 0 0 1rem; + padding-left: 1.5rem; + } + + // API nav groups (collapsible sections) + &-group { + margin-bottom: 0.5rem; + + &-header { + display: flex; + align-items: center; + padding: 0.5rem 0 0.5rem 1.5rem; + font-weight: $medium; + color: $nav-category; + cursor: pointer; + transition: color 0.2s; + // Button reset for dark mode compatibility + background: none; + border: none; + width: 100%; + text-align: left; + font-size: 1.2rem; // Match sidebar .nav-category > a (19.2px) + font-family: inherit; + text-decoration: none; // For anchor version + + &:hover { + color: $nav-category-hover; + } + + &.is-active { + color: $nav-active; + } + + // Collapse/expand indicator (for button headers) + &::before { + content: ""; + display: inline-block; + width: 0; + height: 0; + margin-right: 0.5rem; + border-left: 5px solid $nav-border; + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + transition: transform 0.2s; + flex-shrink: 0; + } + + &.is-open::before { + transform: rotate(90deg); + } + } + + // For anchor headers, keep the ::before arrow (same as button) + // No special handling needed - anchor headers look the same as button headers + a#{&}-header { + // Same styling as button, arrow works via ::before + } + + &-items { + list-style: none; + padding-left: 2.5rem; + margin: 0; + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease-out; + background: $body-bg; // Match sidebar background + + &.is-open { + max-height: 2000px; // Large enough to show all operations + } + } + } + + // Individual API nav items + &-item { + margin: 0.25rem 0; + position: relative; + + a { + display: flex; + align-items: center; + padding: 0.35rem 0; + color: $nav-item; + text-decoration: none; + font-size: 0.95rem; + transition: color 0.2s; + + &:hover { + color: $nav-item-hover; + } + } + + &.is-active a { + color: $nav-active; + font-weight: $medium; + } + + // HTTP method badge (legacy class) + .method-badge { + display: inline-block; + font-size: 0.65rem; + font-weight: $bold; + text-transform: uppercase; + padding: 0.15rem 0.35rem; + margin-right: 0.5rem; + border-radius: 3px; + min-width: 2.5rem; + text-align: center; + + &.get { background-color: $gr-rainforest; color: #fff; } + &.post { background-color: $b-ocean; color: #fff; } + &.put { background-color: $br-galaxy; color: #fff; } + &.patch { background-color: $y-thunder; color: rgba($g5-pepper, 0.75); } + &.delete { background-color: $r-curacao; color: #fff; } + } + + // Tag items that link to tag pages + &.api-nav-tag { + > a { + font-weight: $medium; + } + + // Nested operations list under tag + .api-nav-operations { + list-style: none; + margin: 0.25rem 0 0.5rem; + padding-left: 0.75rem; + + .api-nav-operation { + margin: 0.15rem 0; + + a { + display: flex; + align-items: center; + gap: 0.4rem; + font-size: 0.85rem; + padding: 0.25rem 0; + } + } + } + } + + // Operation items with method badges + &.api-nav-operation, + .api-nav-operation { + .api-method { + display: inline-block; + font-size: 0.55rem; + font-weight: $bold; + text-transform: uppercase; + padding: 0.1rem 0.25rem; + border-radius: 3px; + min-width: 2rem; + text-align: center; + flex-shrink: 0; + + &--get { background-color: $b-pool; color: #fff; } // #00A3FF - bright brand blue + &--post { background-color: $gr-rainforest; color: #fff; } // #34BB55 - bright brand green + &--put { background-color: $y-pineapple; color: #fff; } // #FFB94A - bright yellow (distinct from red) + &--patch { background-color: $br-new-purple; color: #fff; } // #9b2aff - distinctive brand purple + &--delete { background-color: $r-curacao; color: #fff; } // #F95F53 - bright brand red + } + + .api-path { + font-family: $code; + font-size: 0.85rem; + word-break: break-all; + color: inherit; + } + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +/////////////////////////// API Header with Actions //////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +// Header row with title and download button +.article--header-row { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; + flex-wrap: wrap; +} + +.article--header-text { + flex: 1 1 100%; // Take full width, allowing download button to wrap + min-width: 0; +} + +// Summary paragraph in header - ensure full width +.article--summary { + max-width: none; + width: 100%; +} + +// Download OpenAPI spec button +.api-spec-actions { + flex-shrink: 0; +} + +.api-spec-download { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background-color: $g20-white; + color: $article-text; + text-decoration: none; + border-radius: $radius; + font-size: 0.875rem; + font-weight: $medium; + transition: background-color 0.2s, color 0.2s; + border: 1px solid $nav-border; + white-space: nowrap; + + &:hover { + background-color: $r-curacao; + color: $g20-white; + border-color: $r-curacao; + } + + svg { + flex-shrink: 0; + } +} + +//////////////////////////////////////////////////////////////////////////////// +////////////////////////////////// API Tabs //////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +// API-specific tab wrapper (uses api-tabs-wrapper to avoid conflict with +// tabbed-content.js which handles .tabs-wrapper elements) +.api-tabs-wrapper { + margin: 1.5rem 0 1rem; +} + +// API tab navigation bar +.api-tabs-nav { + display: flex; + flex-wrap: wrap; + gap: 2px; + + a { + flex-grow: 1; + position: relative; + font-size: 1rem; + font-weight: $medium; + padding: 0.65rem 1.25rem; + display: inline-block; + white-space: nowrap; + text-align: center; + color: $article-tab-text !important; + border-radius: $radius; + background-color: $article-tab-bg; + text-decoration: none; + transition: background-color 0.2s, color 0.2s; + z-index: 1; + + &::after { + content: ''; + position: absolute; + display: block; + top: 0; + right: 0; + width: 100%; + height: 100%; + border-radius: $radius; + @include gradient($article-btn-gradient); + opacity: 0; + transition: opacity 0.2s; + z-index: -1; + } + + &:hover { + color: $article-tab-active-text !important; + &::after { + opacity: 1; + } + } + + &.is-active { + color: $article-tab-active-text !important; + &::after { + opacity: 1; + @include gradient($article-btn-gradient); + } + } + } +} + +// Tab panels container +.api-tab-panels { + // Tab content visibility (follows existing pattern) + .tab-content:not(:first-of-type) { + display: none; + } +} + +//////////////////////////////////////////////////////////////////////////////// +////////////////////////// Authentication Tab Content ////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +.api-auth-content { + max-width: 800px; +} + +.api-auth-card { + background: $article-bg; + border: 1px solid $nav-border; + border-radius: $radius; + padding: 1.5rem; + margin-bottom: 1.5rem; + + h3 { + margin-top: 0; + margin-bottom: 0.5rem; + } + + h4 { + margin-top: 1rem; + margin-bottom: 0.5rem; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: rgba($article-text, 0.6); + } + + pre { + margin: 0.5rem 0; + padding: 1rem; + background: $article-code-bg; + border-radius: $radius; + overflow-x: auto; + } + + code { + font-family: $code; + font-size: 0.875rem; + } +} + +.api-auth-badge .badge { + display: inline-block; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + font-weight: $bold; + text-transform: uppercase; + border-radius: $radius; + + &.recommended { + background: $gr-rainforest; + color: $g20-white; + } +} + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////// Server Tab Content //////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +.api-server-panel { + max-width: 600px; + + h2 { + margin-top: 0; + } +} + +.server-url-config { + display: flex; + gap: 0.5rem; + align-items: flex-end; + margin: 1rem 0; + flex-wrap: wrap; + + label { + width: 100%; + font-weight: $medium; + margin-bottom: 0.25rem; + } + + input { + flex: 1; + min-width: 200px; + padding: 0.5rem; + border: 1px solid $nav-border; + border-radius: $radius; + font-family: $code; + background: $article-bg; + color: $article-text; + } + + button { + padding: 0.5rem 1rem; + background: $r-curacao; + color: $g20-white; + border: none; + border-radius: $radius; + cursor: pointer; + font-weight: $medium; + + &:hover { + background: darken($r-curacao, 10%); + } + } +} + +.server-info { + margin-top: 1.5rem; + + ul { + list-style: disc; + padding-left: 1.5rem; + } + + li { + margin: 0.5rem 0; + } + + code { + background: $article-code-bg; + padding: 0.2rem 0.4rem; + border-radius: 3px; + font-family: $code; + } +} + +//////////////////////////////////////////////////////////////////////////////// +///////////////////////////////// MEDIA QUERIES //////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +// Tablet: Hide TOC, keep sidebar +@include media(large) { + .content-wrapper.api-content { + flex-direction: column; + width: 75%; // Reset to default when TOC is hidden + } + + .api-toc { + display: none; + } + + .api-main { + padding-right: 0; + } +} + +// Mobile: Standard Hugo sidebar behavior +@include media(medium) { + .content-wrapper.api-content { + flex-direction: column; + } + + .api-toc { + display: none; + } + + .api-main { + padding-right: 0; + } + + // Collapse API nav in mobile view + .api-nav { + margin-top: 1rem; + padding-top: 0.5rem; + + &-group-items { + max-height: none; // Show all items by default in mobile + } + } +} + +// Large screens: Wider TOC +@include media(xlarge) { + .api-toc { + width: 240px; + } +} + +// Compressed layout: narrower TOC, drop border to reduce visual clutter +// TOC is hidden at ≤1280px (large breakpoint), so this targets the +// narrow window where the TOC is visible but space is tight. +@media (min-width: 1281px) and (max-width: 1535px) { + .api-toc { + width: 180px; + border-left: none; + } +} diff --git a/assets/styles/layouts/_api-operations.scss b/assets/styles/layouts/_api-operations.scss new file mode 100644 index 000000000..12ccc3653 --- /dev/null +++ b/assets/styles/layouts/_api-operations.scss @@ -0,0 +1,540 @@ +// API Operations Styles +// Renders OpenAPI operations, parameters, schemas, and responses + +// Variables +$api-border-radius: 6px; +$api-spacing-sm: 0.5rem; +$api-spacing-md: 1rem; +$api-spacing-lg: 1.5rem; +$api-spacing-xl: 2rem; + +// Method colors +$method-get: #00A3FF; +$method-post: #34BB55; +$method-put: #FFB94A; +$method-delete: #D63031; +$method-patch: #9b2aff; + +// Status code colors — intentionally distinct from method colors +$status-success: #34BB55; +$status-redirect: #FFB94A; +$status-client-error: #E17055; +$status-server-error: #9b2aff; + +// ============================================ +// Operation Block +// ============================================ + +.api-hugo-native { + width: 100%; +} + +.api-operation { + margin-bottom: $api-spacing-xl; + padding-top: $api-spacing-xl; + border-top: 2px solid $nav-border; + + // Keep inline code proportional to surrounding text + code { + font-size: inherit; + } + + &:first-child { + border-top: none; + padding-top: 0; + } + + &:target { + animation: highlight-operation 1.5s ease-out; + } +} + +@keyframes highlight-operation { + 0% { + outline: 2px solid rgba($method-get, 0.4); + outline-offset: 8px; + } + 100% { + outline-color: transparent; + } +} + +// Operation Header +.api-operation-header { + margin-bottom: $api-spacing-md; +} + +.api-operation-endpoint { + display: flex; + align-items: center; + gap: $api-spacing-sm; + margin-bottom: $api-spacing-md; + padding-bottom: $api-spacing-md; + border-bottom: 1px solid $nav-border; +} + +.api-method { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + border-radius: 4px; + color: #fff; + + &--get { background-color: $method-get; } + &--post { background-color: $method-post; } + &--put { background-color: $method-put; } + &--delete { background-color: $method-delete; } + &--patch { background-color: $method-patch; } +} + +.api-path { + font-family: $code; + font-size: 0.95rem; + color: $article-text; + background: $article-code-bg; + padding: 0.25rem 0.5rem; + border-radius: 4px; +} + +.api-operation-summary { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: $article-heading; +} + +.api-operation-description { + margin: $api-spacing-md 0; + color: $article-text; + line-height: 1.6; + + p:last-child { + margin-bottom: 0; + } +} + +// ============================================ +// Section Titles +// ============================================ + +.api-section-title { + display: flex; + align-items: center; + gap: $api-spacing-sm; + margin: $api-spacing-lg 0 $api-spacing-md; + padding-bottom: $api-spacing-sm; + font-size: 1rem; + font-weight: 600; + color: $article-heading; + border-bottom: 1px solid $nav-border; +} + +// ============================================ +// Parameters Section +// ============================================ + +.api-parameters { + margin: $api-spacing-lg 0; +} + +.api-param-group { + margin-bottom: $api-spacing-md; +} + +.api-param-group-title { + margin: $api-spacing-sm 0; + font-size: 0.85rem; + font-weight: 500; + color: rgba($article-text, 0.6); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.api-param-list { + // Flat list — no outer border, rows separated by dividers +} + +.api-param-row { + padding: $api-spacing-md 0; + border-bottom: 1px solid $nav-border; + + &:last-child { + border-bottom: none; + } +} + +.api-param-name-line { + display: flex; + align-items: center; + gap: $api-spacing-sm; + margin-bottom: 0.25rem; +} + +.api-param-name { + font-family: $code; + font-size: 0.9rem; + font-weight: 600; + color: $article-heading; +} + +.api-param-type { + font-size: 0.8rem; + color: rgba($article-text, 0.6); +} + +.api-param-description { + margin-top: 0.25rem; + color: $article-text; + line-height: 1.5; + + p { + margin: 0; + } +} + +.api-param-enum, +.api-param-default { + margin-top: 0.5rem; + font-size: 0.85rem; +} + +.api-param-enum-label, +.api-param-default-label { + color: rgba($article-text, 0.6); +} + +.api-param-enum-value, +.api-param-default-value { + font-family: $code; + font-size: 0.8rem; + background: $article-code-bg; + padding: 0.125rem 0.375rem; + border-radius: 3px; +} + +// ============================================ +// Badges +// ============================================ + +.api-badge { + display: inline-flex; + align-items: center; + padding: 0.125rem 0.375rem; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; + border-radius: 3px; + + &--required { + background: rgba($method-delete, 0.1); + color: $method-delete; + } +} + +// ============================================ +// Request Body Section +// ============================================ + +.api-request-body { + margin: $api-spacing-lg 0; +} + +.api-request-body-description { + margin: $api-spacing-sm 0; + color: $article-text; + + p:last-child { + margin-bottom: 0; + } +} + +.api-content-type { + margin: $api-spacing-sm 0; + font-size: 0.85rem; + + code { + font-family: $code; + background: $article-code-bg; + padding: 0.125rem 0.375rem; + border-radius: 3px; + } +} + +.api-content-type-label { + color: rgba($article-text, 0.6); +} + +// ============================================ +// Schema Section +// ============================================ + +.api-schema { + margin: $api-spacing-md 0; + + &--nested { + margin-left: $api-spacing-lg; + padding-left: $api-spacing-md; + border-left: 2px solid $nav-border; + } +} + +.api-schema-properties { + // Flat list — no outer border, rows separated by dividers +} + +.api-schema-property { + padding: $api-spacing-md 0; + border-bottom: 1px solid $nav-border; + + &:last-child { + border-bottom: none; + } + + &--required { + .api-schema-property-name { + &::after { + content: '*'; + color: $method-delete; + margin-left: 0.25rem; + } + } + } +} + +.api-schema-property-header { + display: flex; + align-items: center; + gap: $api-spacing-sm; + margin-bottom: 0.25rem; +} + +.api-schema-property-name { + font-family: $code; + font-size: 0.9rem; + font-weight: 600; + color: $article-heading; +} + +.api-schema-property-type { + font-size: 0.8rem; + color: rgba($article-text, 0.6); +} + +.api-schema-property-description { + margin-top: 0.25rem; + color: $article-text; + line-height: 1.5; + + p { + margin: 0; + } +} + +.api-schema-property-enum, +.api-schema-property-default, +.api-schema-property-example { + margin-top: 0.5rem; + font-size: 0.85rem; +} + +.api-enum-label, +.api-default-label, +.api-example-label { + color: rgba($article-text, 0.6); +} + +.api-enum-value, +.api-default-value, +.api-example-value { + font-family: $code; + font-size: 0.8rem; + background: $article-code-bg; + padding: 0.125rem 0.375rem; + border-radius: 3px; +} + +// Schema Example Block +.api-schema-example { + margin-top: $api-spacing-md; + 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-schema-example-title { + display: block; + 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); + } +} + +pre.api-schema-example-code { + margin: 0; + padding: $api-spacing-sm $api-spacing-md; + background: $article-code-bg; + overflow-x: auto; + + code { + font-family: $code; + font-size: 0.85rem; + } +} + +// ============================================ +// Responses Section +// ============================================ + +.api-responses { + margin: $api-spacing-lg 0; +} + +.api-response-list { + display: flex; + flex-direction: column; + gap: $api-spacing-sm; +} + +.api-response { + padding: $api-spacing-sm 0; +} + +.api-response-header { + display: flex; + align-items: center; + gap: $api-spacing-sm; +} + +.api-response-status { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 3rem; + padding: 0.25rem 0.5rem; + font-size: 0.8rem; + font-weight: 600; + border-radius: 4px; + color: #fff; + + &--success { background-color: $status-success; } + &--redirect { background-color: $status-redirect; } + &--client-error { background-color: $status-client-error; } + &--server-error { background-color: $status-server-error; } + &--info { background-color: rgba($article-text, 0.6); } +} + +.api-response-description { + color: $article-text; +} + +.api-response-body { + 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); +} + +// ============================================ +// Tag Overview +// ============================================ + +.api-tag-overview { + margin-bottom: $api-spacing-xl; + padding-bottom: $api-spacing-lg; + border-bottom: 1px solid $nav-border; +} + +.api-tag-description { + color: $article-text; + line-height: 1.7; + + h4, h5 { + margin-top: $api-spacing-lg; + margin-bottom: $api-spacing-sm; + color: $article-heading; + } + + ul, ol { + padding-left: $api-spacing-lg; + } + + a { + color: $article-link; + + &:hover { + text-decoration: underline; + } + } +} + +// ============================================ +// Related Guides Section +// ============================================ + +.api-related-guides { + margin-top: $api-spacing-xl; + padding-top: $api-spacing-lg; + border-top: 1px solid $nav-border; +} + +.api-related-title { + margin: 0 0 $api-spacing-md; + font-size: 1rem; + font-weight: 600; + color: $article-heading; +} + +.api-related-list { + margin: 0; + padding: 0; + list-style: none; + + li { + margin-bottom: $api-spacing-sm; + } + + a { + color: $article-link; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } +} + +// ============================================ +// Responsive Adjustments +// ============================================ + +@media (max-width: 768px) { + .api-operation-endpoint { + flex-wrap: wrap; + } + + .api-path { + font-size: 0.85rem; + word-break: break-all; + } + + .api-param-name-line, + .api-schema-property-header { + flex-wrap: wrap; + } +} diff --git a/assets/styles/layouts/_api-overrides.scss b/assets/styles/layouts/_api-overrides.scss index bc220588f..00a7b1286 100644 --- a/assets/styles/layouts/_api-overrides.scss +++ b/assets/styles/layouts/_api-overrides.scss @@ -1,9 +1,16 @@ +//////////////////////////////////////////////////////////////////////////////// +// API Documentation Style Overrides +// +// Provides loading spinner and reusable HTTP method badge colors. +// Used by Hugo-native API templates for consistent styling. +//////////////////////////////////////////////////////////////////////////////// + @import "tools/color-palette"; @import "tools/fonts"; // Fonts $proxima: 'Proxima Nova', sans-serif; -$code: 'IBM Plex Mono', monospace;; +$code: 'IBM Plex Mono', monospace; // Font weights $medium: 500; @@ -22,7 +29,7 @@ $bold: 700; } @keyframes spinner { - to {transform: rotate(360deg);} + to { transform: rotate(360deg); } } .spinner:before { @@ -41,256 +48,15 @@ $bold: 700; animation: spinner .6s linear infinite; } -//////////////////////////////// InfluxDB Header /////////////////////////////// - -#influx-header { - font-family: $proxima; - padding: 10px ; - display: flex; - align-items: center; - justify-content: space-between; - background-color: $g2-kevlar; - a { - text-decoration: none; - &.back { - color: $g20-white; - transition: color .2s; - &:hover { - color: $b-pool; - } - &:before { - content: "\e919"; - font-family: 'icomoon-v2'; - margin-right: .65rem; - } - } - &.btn { - padding: .5rem .75rem .5rem .65rem; - font-size: .85rem; - font-weight: 500; - color: $g15-platinum; - background: $g5-pepper; - border-radius: 4.5px; - transition: all .2s; - &:before { - content: "\e934"; - display: inline-block; - font-size: .95rem; - margin-right: .5rem; - font-family: 'icomoon-v2'; - } - &:hover { - color: $g20-white; - background: $b-pool; - } - } - } -} - -// Header Media Queries - -@media (max-width: 600px) { - #influx-header span.version {display: none;} -} - +//////////////////////////////////////////////////////////////////////////////// +/////////////////////////// HTTP Method Badge Colors /////////////////////////// //////////////////////////////////////////////////////////////////////////////// -.cjtbAK { - h1,h2,h3,h4,h5,h6, - p,li,th,td { - font-family: $proxima !important; - } -} - -#redoc { - h1,h2,h3 { - font-weight: $medium !important; - } -} - -// Section title padding -.dluJDj { - padding: 20px 0; -} - -// Page h1 -.dTJWQH { - color: $g7-graphite; - font-size: 2rem; -} - -// Download button -.jIdpVJ { - background: $b-dodger; - color: $g20-white; - border: none; - border-radius: 3px; - font-family: $proxima; - font-size: .85rem; - font-weight: $medium; - transition: background-color .2s; - &:hover { - background-color: $b-pool; - } -} - -// Tag h1s -.WxWXp { - color: $g7-graphite; - font-size: 1.75rem; -} - -// Summaru h2s and table headers -.ioYTqA, .bxcHYI, .hoUoen { - color: $g7-graphite; -} - -// h3s -.espozG { - color: $g8-storm; -} - -// Links -.bnFPhO a { color: $b-dodger; - &:visited {color: $b-dodger;} -} - -.redoc-json { - font-family: $code !important; -} - -// Inline Code -.flfxUM code, -.gDsWLk code, -.kTVySD { - font-family: $code !important; - color: $cp-marguerite; - background: $cp-titan; - border-color: $cp-titan; -} - -// Required tags -.jsTAxL { - color: $r-curacao; -} - -///////////////////////////// RESPONSE COLOR BLOCKS //////////////////////////// - -// Green -.hLVzSF, .fDvFMp { - background-color: rgba($gr-honeydew, .2); - color: $gr-emerald; -} - -// Red -.byLrBg { - background-color: rgba($r-curacao, .1); - color: $r-curacao; -} - - - -/////////////////////////////////// LEFT NAV /////////////////////////////////// - -// Left nav background -.gZdDsM { - background-color: $g19-ghost; -} - -.gpbcFk:hover, .sc-eTuwsz.active { - background-color: $g17-whisper; -} - -// List item text -.SmuWE, .gcUzvG, .bbViyS, .sc-hrWEMg label { - font-family: $proxima !important; -} - -.fyUykq { - font-weight: $medium; -} - -// Request method tags -.cFwMcp { - &.post { background-color: $b-ocean; } - &.get { background-color: $gr-rainforest; } - &.put { background-color: $br-galaxy; } - &.patch { background-color: $y-thunder; color: rgba($g5-pepper, .75);} - &.delete { background-color: $r-curacao; } -} - -// Active nav section -.gcUzvG, .iNzLCk:hover { - color: $br-magenta; -} - -/////////////////////////////// RIGHT CODE COLUMN ////////////////////////////// - -// Right column backgrounds -.dtUibw, .fLUKgj { - background-color: $g2-kevlar; - h3,h4,h5,h6 { - font-family: $proxima !important; - font-weight: $medium !important; - } -} - -// Code backgrounds -.irpqyy > .react-tabs__tab-panel { - background-color: $g0-obsidian; -} -.dHLKeu, .fVaxnA { - padding-left: 10px; - background-color: $g0-obsidian; -} - -// Response code tabs -.irpqyy > ul > li { - background-color: $g0-obsidian; - border-radius: 3px; - &.react-tabs__tab--selected{ color: $br-pulsar;} - &.tab-error { color: $r-fire; } - &.tab-success { color: $gr-viridian; } -} - -// Request methods -.bNYCAJ, -.jBjYbV, -.hOczRB, -.fRsrDc, -.hPskZd { - font-family: $proxima; - font-weight: $medium; - letter-spacing: .04em; - border-radius: 3px; -} -.bNYCAJ { background-color: $b-ocean; } /* Post */ -.jBjYbV { background-color: $gr-viridian; } /* Get */ -.hOczRB { background-color: $br-galaxy; } /* Put */ -.fRsrDc { background-color: $y-thunder; color: $g5-pepper; } /* Patch */ -.hPskZd { background-color: $r-curacao; } /* Delete */ - -// Content type block -.gzAoUb { - background-color: $g2-kevlar; - font-family: $proxima; -} -.iENVAs { font-family: $code; } -.dpMbau { font-family: $proxima; } - -// Code controls -.fCJmC { - font-family: $proxima; - span { border-radius: 3px; } -} - -// Code blocks -.kZHJcC { font-family: $code; } -.jCgylq { - .token.string { - color: $gr-honeydew; - & + a { color: $b-pool; } - } - .token.boolean { color: #f955b0; } -} +// Reusable method badge colors (used by _api-layout.scss .method-badge) +// These follow standard REST API color conventions +$method-get: $gr-rainforest; +$method-post: $b-ocean; +$method-put: $br-galaxy; +$method-patch: $y-thunder; +$method-delete: $r-curacao; diff --git a/assets/styles/layouts/_api-security-schemes.scss b/assets/styles/layouts/_api-security-schemes.scss new file mode 100644 index 000000000..3723786b0 --- /dev/null +++ b/assets/styles/layouts/_api-security-schemes.scss @@ -0,0 +1,92 @@ +//////////////////////////////////////////////////////////////////////////////// +// API Security Schemes Styling +// +// Styles for security schemes sections displayed on conceptual API pages +// (like Authentication). These sections are rendered from OpenAPI spec +// securitySchemes using Hugo templates. +//////////////////////////////////////////////////////////////////////////////// + +.api-security-schemes { + margin-top: 2rem; + padding-top: 2rem; + border-top: 1px solid $g5-pepper; + + h2 { + margin-bottom: 1.5rem; + } + + .security-scheme { + margin-bottom: 2rem; + + h3 { + margin: 0 0 1rem 0; + font-size: 1.1rem; + color: $article-heading; + } + } + + .scheme-details { + margin-bottom: 1rem; + + dl { + display: grid; + grid-template-columns: auto 1fr; + gap: 0.5rem 1rem; + margin: 0; + } + + dt { + font-weight: 600; + color: $g9-mountain; + } + + dd { + margin: 0; + + code { + background: $article-code-bg; + color: $article-code; + padding: 0.2em 0.5em; + border-radius: 3px; + font-size: 0.9em; + } + } + } + + .scheme-description { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid $g5-pepper; + + p:first-child { + margin-top: 0; + } + + pre { + margin: 1rem 0; + } + } +} + +// Dark theme overrides for security schemes +[data-theme="dark"], +html:has(link[title="dark-theme"]:not([disabled])) { + .api-security-schemes { + border-top-color: $grey25; + + .security-scheme { + // Removed background and border - now using plain styling + } + + .scheme-details { + dt { + color: $g15-platinum; + } + } + + .scheme-description { + border-top-color: $grey25; + } + } +} + diff --git a/assets/styles/layouts/_sidebar.scss b/assets/styles/layouts/_sidebar.scss index 30eef2b4e..9d2bb6aa6 100644 --- a/assets/styles/layouts/_sidebar.scss +++ b/assets/styles/layouts/_sidebar.scss @@ -255,6 +255,66 @@ } } } + + // API operation items within Hugo menu + .api-operation { + a { + display: flex; + align-items: center; + gap: 0.4rem; + } + + // Path-based operation display (All endpoints list) + &--path .api-path { + font-family: $proxima; + font-size: 0.9rem; + color: inherit; + word-break: break-all; + } + } + + .api-method { + font-size: 0.6rem; + font-weight: 700; + padding: 0.15rem 0.35rem; + border-radius: 3px; + text-transform: uppercase; + flex-shrink: 0; + line-height: 1; + border: 2px solid; + background-color: transparent; + + // Using lighter InfluxData brand colors - bordered style for readability + &--get { border-color: $b-pool; color: $b-pool; } // #00A3FF - bright brand blue + &--post { border-color: $gr-rainforest; color: $gr-rainforest; } // #34BB55 - bright brand green + &--put { border-color: $y-pineapple; color: $y-pineapple; } // #FFB94A - bright yellow (distinct from red) + &--delete { border-color: $r-curacao; color: $r-curacao; } // #F95F53 - bright brand red + &--patch { border-color: $br-new-purple; color: $br-new-purple; } // #9b2aff - distinctive brand purple + } + + // Compatibility version badge (v1 or v2) + .api-compat-badge { + font-size: 0.55rem; + font-weight: 600; + padding: 0.1rem 0.3rem; + border-radius: 3px; + text-transform: uppercase; + flex-shrink: 0; + line-height: 1; + margin-left: auto; + opacity: 0.8; + cursor: help; + + &--v1 { background: #8b5cf6; color: white; } // Purple for v1 + &--v2 { background: #06b6d4; color: white; } // Cyan for v2 + } + + // Non-link group labels (for multi-tag groups) + .nav-group-label { + color: $nav-item; + font-weight: $medium; + display: inline-block; + } } .feature-board-badge { diff --git a/assets/styles/styles-default.scss b/assets/styles/styles-default.scss index 8852a240c..310f91773 100644 --- a/assets/styles/styles-default.scss +++ b/assets/styles/styles-default.scss @@ -32,7 +32,11 @@ "layouts/v1-overrides", "layouts/notifications", "layouts/code-controls", - "layouts/v3-wayfinding"; + "layouts/v3-wayfinding", + "layouts/api-layout", + "layouts/api-security-schemes", + "layouts/api-operations", + "layouts/api-code-samples"; // Import Components @import "components/influxdb-version-detector", diff --git a/content/influxdb/cloud/reference/api/_index.md b/content/influxdb/cloud/reference/api/_index.md deleted file mode 100644 index 16e3bdfbf..000000000 --- a/content/influxdb/cloud/reference/api/_index.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: InfluxDB HTTP API -description: > - The InfluxDB HTTP API provides a programmatic interface for interactions with InfluxDB, such as writing and querying data, - and managing resources within an InfluxDB instance. - Access the InfluxDB API using the `/api/v2/` endpoint. -menu: - influxdb_cloud: - parent: Reference - name: InfluxDB HTTP API -weight: 3 -influxdb/cloud/tags: [api] -source: /shared/influxdb-v2/reference/api/_index.md ---- - - diff --git a/content/influxdb/v2/reference/api/_index.md b/content/influxdb/v2/reference/api/_index.md deleted file mode 100644 index 47e1ea5fa..000000000 --- a/content/influxdb/v2/reference/api/_index.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: InfluxDB HTTP API -description: > - The InfluxDB HTTP API provides a programmatic interface for interactions with InfluxDB, such as writing and querying data, - and managing resources within an InfluxDB instance. - Access the InfluxDB API using the `/api/v2/` or InfluxDB v1 endpoints. -menu: - influxdb_v2: - parent: Reference - name: InfluxDB HTTP API -weight: 3 -influxdb/v2/tags: [api] -aliases: - - /influxdb/v2/concepts/api/ -related: - - /influxdb/v2/api-guide/api_intro/ - - /influxdb/v2/api-guide/influxdb-1x/ -source: /shared/influxdb-v2/reference/api/_index.md ---- - - diff --git a/content/influxdb3/cloud-dedicated/management-api/_index.md b/content/influxdb3/cloud-dedicated/management-api/_index.md new file mode 100644 index 000000000..77eeafad5 --- /dev/null +++ b/content/influxdb3/cloud-dedicated/management-api/_index.md @@ -0,0 +1,27 @@ +--- +title: InfluxDB HTTP API +description: >- + Use the InfluxDB HTTP API to write data, query data, and manage databases, + tables, and tokens. +weight: 104 +type: api +articleDataKey: influxdb3-cloud-dedicated +articleSection: management-api +menu: + influxdb3_cloud_dedicated: + name: InfluxDB HTTP API + identifier: api-reference-influxdb3-cloud-dedicated-management-api + parent: Reference +alt_links: + core: /influxdb3/core/api/ + enterprise: /influxdb3/enterprise/api/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- + +Use the {{% product-name %}} HTTP API to write data, query data, and manage databases, tables, and tokens. + +{{< children >}} diff --git a/content/influxdb3/cloud-dedicated/management-api/all-endpoints/_index.md b/content/influxdb3/cloud-dedicated/management-api/all-endpoints/_index.md new file mode 100644 index 000000000..51f5f9695 --- /dev/null +++ b/content/influxdb3/cloud-dedicated/management-api/all-endpoints/_index.md @@ -0,0 +1,25 @@ +--- +title: All endpoints +description: View all API endpoints sorted by path. +type: api +layout: all-endpoints +weight: 999 +isAllEndpoints: true +articleDataKey: influxdb3-cloud-dedicated +articleSection: management-api +menu: + influxdb3_cloud_dedicated: + name: All endpoints + identifier: all-endpoints-influxdb3-cloud-dedicated-management-api + parent: InfluxDB HTTP API +alt_links: + core: /influxdb3/core/api/ + enterprise: /influxdb3/enterprise/api/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- + +All {{% product-name %}} API endpoints, sorted by path. diff --git a/content/influxdb3/cloud-dedicated/reference/api/_index.md b/content/influxdb3/cloud-dedicated/reference/api/_index.md deleted file mode 100644 index 806c248ae..000000000 --- a/content/influxdb3/cloud-dedicated/reference/api/_index.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: InfluxDB HTTP API -description: > - The InfluxDB HTTP API provides a programmatic interface for interactions with - InfluxDB, such as writing and querying data, and managing an InfluxDB cluster. - Access the InfluxDB API using the `/api/v2/write`, InfluxDB v1, or - Management API endpoints for InfluxDB Cloud Dedicated. -menu: - influxdb3_cloud_dedicated: - parent: Reference - name: InfluxDB HTTP API -weight: 105 -influxdb3/cloud-dedicated/tags: [api] ---- - -The InfluxDB HTTP API provides a programmatic interface for interactions with -{{% product-name %}}, such as writing and querying data, and managing an InfluxDB cluster. - -Access the InfluxDB HTTP API using the `/api/v2/` endpoint, InfluxDB v1 endpoints, or -Management API endpoints for {{% product-name %}}. - -## InfluxDB v2 Compatibility API reference documentation - -InfluxDB v2 API for {{% product-name %}} - -The API reference describes requests and responses for InfluxDB v2-compatible -endpoints that work with {{% product-name %}} and with InfluxDB 2.x client -libraries and third-party integrations. - -## InfluxDB v1 Compatibility API reference documentation - -InfluxDB v1 API for {{% product-name %}} - -The API reference describes requests and responses for InfluxDB v1-compatible `/write` and `/query` endpoints that work with {{% product-name %}} and with InfluxDB 1.x client libraries and third-party integrations. - -## InfluxDB Management API reference documentation - -InfluxDB Management API for {{% product-name %}} - -The API reference describes requests and responses for InfluxDB Management API endpoints. -The Management API lets cluster administrators manage resources such as databases, partitioning templates, and database tokens. diff --git a/content/influxdb3/cloud-serverless/api/_index.md b/content/influxdb3/cloud-serverless/api/_index.md new file mode 100644 index 000000000..f48f914af --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/_index.md @@ -0,0 +1,27 @@ +--- +title: InfluxDB HTTP API +description: >- + Use the InfluxDB HTTP API to write data, query data, and manage databases, + tables, and tokens. +weight: 104 +type: api +articleDataKey: influxdb3-cloud-serverless +articleSection: api +menu: + influxdb3_cloud_serverless: + name: InfluxDB HTTP API + identifier: api-reference-influxdb3-cloud-serverless-api + parent: Reference +alt_links: + core: /influxdb3/core/api/ + enterprise: /influxdb3/enterprise/api/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- + +Use the {{% product-name %}} HTTP API to write data, query data, and manage databases, tables, and tokens. + +{{< children >}} diff --git a/content/influxdb3/cloud-serverless/api/api-compatibility/_index.md b/content/influxdb3/cloud-serverless/api/api-compatibility/_index.md new file mode 100644 index 000000000..5c313717d --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/api-compatibility/_index.md @@ -0,0 +1,28 @@ +--- +title: API compatibility +description: >- + Overview of the InfluxDB v1 and v2 compatible write and query endpoints + available in InfluxDB 3 Cloud Serverless. +type: api +layout: single +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-api-compatibility.yaml +weight: 100 +tag: API compatibility +isConceptual: true +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +tagDescription: >- + Overview of the InfluxDB v1 and v2 compatible write and query endpoints + available in InfluxDB 3 Cloud Serverless. +alt_links: + core: /influxdb3/core/api/ + enterprise: /influxdb3/enterprise/api/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/authentication/_index.md b/content/influxdb3/cloud-serverless/api/authentication/_index.md new file mode 100644 index 000000000..d1150c2c0 --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/authentication/_index.md @@ -0,0 +1,39 @@ +--- +title: Authentication +description: >- + Use one of the following schemes to authenticate to the InfluxDB 3 Cloud + Serverless API: + + + - Token authentication + + - Basic authentication +type: api +layout: single +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-authentication.yaml +weight: 100 +tag: Authentication +isConceptual: true +menuGroup: Concepts +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +tagDescription: >- + Use one of the following schemes to authenticate to the InfluxDB 3 Cloud + Serverless API: + + + - Token authentication + + - Basic authentication +showSecuritySchemes: true +alt_links: + core: /influxdb3/core/api/ + enterprise: /influxdb3/enterprise/api/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/authorizations-api-tokens/_index.md b/content/influxdb3/cloud-serverless/api/authorizations-api-tokens/_index.md new file mode 100644 index 000000000..b8cbff1a7 --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/authorizations-api-tokens/_index.md @@ -0,0 +1,61 @@ +--- +title: Authorizations (API tokens) +description: >- + Create and manage API token authorizations that grant read and write + permissions to InfluxDB 3 Cloud Serverless organization resources. +type: api +layout: list +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-authorizations-api-tokens.yaml +weight: 100 +tag: Authorizations (API tokens) +isConceptual: false +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +operations: + - operationId: GetAuthorizations + method: GET + path: /api/v2/authorizations + summary: List authorizations + tags: + - Authorizations (API tokens) + - operationId: PostAuthorizations + method: POST + path: /api/v2/authorizations + summary: Create an authorization + tags: + - Authorizations (API tokens) + - operationId: GetAuthorizationsID + method: GET + path: /api/v2/authorizations/{authID} + summary: Retrieve an authorization + tags: + - Authorizations (API tokens) + - operationId: PatchAuthorizationsID + method: PATCH + path: /api/v2/authorizations/{authID} + summary: Update an API token to be active or inactive + tags: + - Authorizations (API tokens) + - operationId: DeleteAuthorizationsID + method: DELETE + path: /api/v2/authorizations/{authID} + summary: Delete an authorization + tags: + - Authorizations (API tokens) +related: + - title: Manage API tokens + href: /influxdb3/cloud-serverless/security/tokens/ + - title: InfluxDB 3 API client libraries + href: /influxdb3/cloud-serverless/reference/client-libraries/v3/ +alt_links: + core: /influxdb3/core/api/ + enterprise: /influxdb3/enterprise/api/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/bucket-schemas/_index.md b/content/influxdb3/cloud-serverless/api/bucket-schemas/_index.md new file mode 100644 index 000000000..753f5c99c --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/bucket-schemas/_index.md @@ -0,0 +1,53 @@ +--- +title: Bucket Schemas +description: >- + Manage explicit schemas for InfluxDB 3 Cloud Serverless buckets to enforce + column names and data types. +type: api +layout: list +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-bucket-schemas.yaml +weight: 100 +tag: Bucket Schemas +isConceptual: false +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +operations: + - operationId: getMeasurementSchemas + method: GET + path: /api/v2/buckets/{bucketID}/schema/measurements + summary: List measurement schemas of a bucket + tags: + - Bucket Schemas + - operationId: createMeasurementSchema + method: POST + path: /api/v2/buckets/{bucketID}/schema/measurements + summary: Create a measurement schema for a bucket + tags: + - Bucket Schemas + - operationId: getMeasurementSchema + method: GET + path: /api/v2/buckets/{bucketID}/schema/measurements/{measurementID} + summary: Retrieve a measurement schema + tags: + - Bucket Schemas + - operationId: updateMeasurementSchema + method: PATCH + path: /api/v2/buckets/{bucketID}/schema/measurements/{measurementID} + 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/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/buckets/_index.md b/content/influxdb3/cloud-serverless/api/buckets/_index.md new file mode 100644 index 000000000..1d5abc98b --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/buckets/_index.md @@ -0,0 +1,115 @@ +--- +title: Buckets +description: >- + Create and manage named storage locations (buckets) in InfluxDB 3 Cloud + Serverless, each with a configurable retention period. +type: api +layout: list +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-buckets.yaml +weight: 100 +tag: Buckets +isConceptual: false +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +operations: + - operationId: GetBuckets + method: GET + path: /api/v2/buckets + summary: List buckets + tags: + - Buckets + - operationId: PostBuckets + method: POST + path: /api/v2/buckets + summary: Create a bucket + tags: + - Buckets + - operationId: GetBucketsID + method: GET + path: /api/v2/buckets/{bucketID} + summary: Retrieve a bucket + tags: + - Buckets + - operationId: PatchBucketsID + method: PATCH + path: /api/v2/buckets/{bucketID} + summary: Update a bucket + tags: + - Buckets + - operationId: DeleteBucketsID + method: DELETE + path: /api/v2/buckets/{bucketID} + summary: Delete a bucket + tags: + - Buckets + - operationId: GetBucketsIDLabels + method: GET + path: /api/v2/buckets/{bucketID}/labels + summary: List all labels for a bucket + tags: + - Buckets + - operationId: PostBucketsIDLabels + method: POST + path: /api/v2/buckets/{bucketID}/labels + summary: Add a label to a bucket + tags: + - Buckets + - operationId: DeleteBucketsIDLabelsID + method: DELETE + path: /api/v2/buckets/{bucketID}/labels/{labelID} + summary: Delete a label from a bucket + tags: + - Buckets + - operationId: GetBucketsIDMembers + method: GET + path: /api/v2/buckets/{bucketID}/members + summary: List all users with member privileges for a bucket + tags: + - Buckets + - operationId: PostBucketsIDMembers + method: POST + path: /api/v2/buckets/{bucketID}/members + summary: Add a member to a bucket + tags: + - Buckets + - operationId: DeleteBucketsIDMembersID + method: DELETE + path: /api/v2/buckets/{bucketID}/members/{userID} + summary: Remove a member from a bucket + tags: + - Buckets + - operationId: GetBucketsIDOwners + method: GET + path: /api/v2/buckets/{bucketID}/owners + summary: List all owners of a bucket + tags: + - Buckets + - operationId: PostBucketsIDOwners + method: POST + path: /api/v2/buckets/{bucketID}/owners + summary: Add an owner to a bucket + tags: + - Buckets + - operationId: DeleteBucketsIDOwnersID + method: DELETE + path: /api/v2/buckets/{bucketID}/owners/{userID} + summary: Remove an owner from a bucket + tags: + - Buckets +related: + - title: Manage buckets + href: /influxdb3/cloud-serverless/admin/buckets/ + - title: InfluxDB 3 API client libraries + href: /influxdb3/cloud-serverless/reference/client-libraries/v3/ +alt_links: + core: /influxdb3/core/api/ + enterprise: /influxdb3/enterprise/api/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/common-parameters/_index.md b/content/influxdb3/cloud-serverless/api/common-parameters/_index.md new file mode 100644 index 000000000..5749a0b06 --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/common-parameters/_index.md @@ -0,0 +1,28 @@ +--- +title: Common parameters +description: >- + Common query parameters used by InfluxDB 3 Cloud Serverless API endpoints, + including `bucket`, `bucketID`, `org`, and `orgID`. +type: api +layout: single +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-common-parameters.yaml +weight: 100 +tag: Common parameters +isConceptual: true +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +tagDescription: >- + Common query parameters used by InfluxDB 3 Cloud Serverless API endpoints, + including `bucket`, `bucketID`, `org`, and `orgID`. +alt_links: + core: /influxdb3/core/api/ + enterprise: /influxdb3/enterprise/api/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/dbrps/_index.md b/content/influxdb3/cloud-serverless/api/dbrps/_index.md new file mode 100644 index 000000000..2dc2ab20e --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/dbrps/_index.md @@ -0,0 +1,60 @@ +--- +title: DBRPs +description: >- + Manage database and retention policy (DBRP) mappings that route InfluxDB + v1-compatible requests to InfluxDB 3 Cloud Serverless buckets. +type: api +layout: list +staticFilePath: /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-dbrps.yaml +weight: 100 +tag: DBRPs +isConceptual: false +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +operations: + - operationId: GetDBRPs + method: GET + path: /api/v2/dbrps + summary: List database retention policy mappings + tags: + - DBRPs + - operationId: PostDBRP + method: POST + path: /api/v2/dbrps + summary: Add a database retention policy mapping + tags: + - DBRPs + - operationId: GetDBRPsID + method: GET + path: /api/v2/dbrps/{dbrpID} + summary: Retrieve a database retention policy mapping + tags: + - DBRPs + - operationId: PatchDBRPID + method: PATCH + path: /api/v2/dbrps/{dbrpID} + summary: Update a database retention policy mapping + tags: + - DBRPs + - operationId: DeleteDBRPID + method: DELETE + path: /api/v2/dbrps/{dbrpID} + summary: Delete a database retention policy + tags: + - DBRPs +related: + - title: Database and retention policy mapping + href: /influxdb3/cloud-serverless/reference/api/influxdb-1x/dbrp/ + - title: InfluxDB 3 API client libraries + href: /influxdb3/cloud-serverless/reference/client-libraries/v3/ +alt_links: + core: /influxdb3/core/api/ + enterprise: /influxdb3/enterprise/api/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/delete/_index.md b/content/influxdb3/cloud-serverless/api/delete/_index.md new file mode 100644 index 000000000..1aa8ba24d --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/delete/_index.md @@ -0,0 +1,35 @@ +--- +title: Delete +description: >- + Delete time series data from an InfluxDB 3 Cloud Serverless bucket by + specifying a time range and optional predicate. +type: api +layout: list +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-delete.yaml +weight: 100 +tag: Delete +isConceptual: false +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +operations: + - operationId: PostDelete + method: POST + path: /api/v2/delete + 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/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/headers/_index.md b/content/influxdb3/cloud-serverless/api/headers/_index.md new file mode 100644 index 000000000..ff8391fb9 --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/headers/_index.md @@ -0,0 +1,30 @@ +--- +title: Headers +description: >- + Standard HTTP request headers used by InfluxDB 3 Cloud Serverless API + endpoints, including `Accept`, `Authorization`, `Content-Length`, and + `Content-Type`. +type: api +layout: single +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-headers.yaml +weight: 100 +tag: Headers +isConceptual: true +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +tagDescription: >- + Standard HTTP request headers used by InfluxDB 3 Cloud Serverless API + endpoints, including `Accept`, `Authorization`, `Content-Length`, and + `Content-Type`. +alt_links: + core: /influxdb3/core/api/ + enterprise: /influxdb3/enterprise/api/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/invokable-scripts/_index.md b/content/influxdb3/cloud-serverless/api/invokable-scripts/_index.md new file mode 100644 index 000000000..edcd703a6 --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/invokable-scripts/_index.md @@ -0,0 +1,71 @@ +--- +title: Invokable Scripts +description: >- + Store, manage, and execute custom Flux scripts in InfluxDB 3 Cloud Serverless. + Scripts accept runtime parameters and can be invoked via dedicated endpoints. +type: api +layout: list +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-invokable-scripts.yaml +weight: 100 +tag: Invokable Scripts +isConceptual: false +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +operations: + - operationId: GetScripts + method: GET + path: /api/v2/scripts + summary: List scripts + tags: + - Invokable Scripts + - operationId: PostScripts + method: POST + path: /api/v2/scripts + summary: Create a script + tags: + - Invokable Scripts + - operationId: GetScriptsID + method: GET + path: /api/v2/scripts/{scriptID} + summary: Retrieve a script + tags: + - Invokable Scripts + - operationId: PatchScriptsID + method: PATCH + path: /api/v2/scripts/{scriptID} + summary: Update a script + tags: + - Invokable Scripts + - operationId: DeleteScriptsID + method: DELETE + path: /api/v2/scripts/{scriptID} + summary: Delete a script + tags: + - Invokable Scripts + - operationId: PostScriptsIDInvoke + method: POST + path: /api/v2/scripts/{scriptID}/invoke + summary: Invoke a script + tags: + - Invokable Scripts + - operationId: GetScriptsIDParams + method: GET + path: /api/v2/scripts/{scriptID}/params + 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/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/limits/_index.md b/content/influxdb3/cloud-serverless/api/limits/_index.md new file mode 100644 index 000000000..1b7473edd --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/limits/_index.md @@ -0,0 +1,35 @@ +--- +title: Limits +description: >- + Retrieve rate limits and usage quotas for an InfluxDB 3 Cloud Serverless + organization. +type: api +layout: list +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-limits.yaml +weight: 100 +tag: Limits +isConceptual: false +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +operations: + - operationId: GetOrgLimitsID + method: GET + path: /api/v2/orgs/{orgID}/limits + 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/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/organizations/_index.md b/content/influxdb3/cloud-serverless/api/organizations/_index.md new file mode 100644 index 000000000..f272615b0 --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/organizations/_index.md @@ -0,0 +1,97 @@ +--- +title: Organizations +description: >- + View and manage InfluxDB 3 Cloud Serverless organizations, which are + workspaces that group users, buckets, and resources. +type: api +layout: list +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-organizations.yaml +weight: 100 +tag: Organizations +isConceptual: false +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +operations: + - operationId: GetOrgs + method: GET + path: /api/v2/orgs + summary: List organizations + tags: + - Organizations + - operationId: PostOrgs + method: POST + path: /api/v2/orgs + summary: Create an organization + tags: + - Organizations + - operationId: GetOrgsID + method: GET + path: /api/v2/orgs/{orgID} + summary: Retrieve an organization + tags: + - Organizations + - operationId: PatchOrgsID + method: PATCH + path: /api/v2/orgs/{orgID} + summary: Update an organization + tags: + - Organizations + - operationId: DeleteOrgsID + method: DELETE + path: /api/v2/orgs/{orgID} + summary: Delete an organization + tags: + - Organizations + - operationId: GetOrgsIDMembers + method: GET + path: /api/v2/orgs/{orgID}/members + summary: List all members of an organization + tags: + - Organizations + - operationId: PostOrgsIDMembers + method: POST + path: /api/v2/orgs/{orgID}/members + summary: Add a member to an organization + tags: + - Organizations + - operationId: DeleteOrgsIDMembersID + method: DELETE + path: /api/v2/orgs/{orgID}/members/{userID} + summary: Remove a member from an organization + tags: + - Organizations + - operationId: GetOrgsIDOwners + method: GET + path: /api/v2/orgs/{orgID}/owners + summary: List all owners of an organization + tags: + - Organizations + - operationId: PostOrgsIDOwners + method: POST + path: /api/v2/orgs/{orgID}/owners + summary: Add an owner to an organization + tags: + - Organizations + - operationId: DeleteOrgsIDOwnersID + method: DELETE + path: /api/v2/orgs/{orgID}/owners/{userID} + summary: Remove an owner from an organization + tags: + - Organizations +related: + - title: View and manage organizations + href: /influxdb3/cloud-serverless/admin/ + - title: InfluxDB 3 API client libraries + href: /influxdb3/cloud-serverless/reference/client-libraries/v3/ +alt_links: + core: /influxdb3/core/api/ + enterprise: /influxdb3/enterprise/api/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/pagination/_index.md b/content/influxdb3/cloud-serverless/api/pagination/_index.md new file mode 100644 index 000000000..90aff698a --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/pagination/_index.md @@ -0,0 +1,28 @@ +--- +title: Pagination +description: >- + Query parameters for paginating results from list operations in the InfluxDB 3 + Cloud Serverless API. +type: api +layout: single +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-pagination.yaml +weight: 100 +tag: Pagination +isConceptual: true +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +tagDescription: >- + Query parameters for paginating results from list operations in the InfluxDB 3 + Cloud Serverless API. +alt_links: + core: /influxdb3/core/api/ + enterprise: /influxdb3/enterprise/api/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/quick-start/_index.md b/content/influxdb3/cloud-serverless/api/quick-start/_index.md new file mode 100644 index 000000000..380c3dadf --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/quick-start/_index.md @@ -0,0 +1,28 @@ +--- +title: Quick start +description: >- + Get started writing and querying data with the InfluxDB 3 Cloud Serverless + API. +type: api +layout: single +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-quick-start.yaml +weight: 1 +tag: Quick start +isConceptual: true +menuGroup: Concepts +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +tagDescription: >- + Get started writing and querying data with the InfluxDB 3 Cloud Serverless + API. +alt_links: + core: /influxdb3/core/api/ + enterprise: /influxdb3/enterprise/api/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/resources/_index.md b/content/influxdb3/cloud-serverless/api/resources/_index.md new file mode 100644 index 000000000..1754e198b --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/resources/_index.md @@ -0,0 +1,35 @@ +--- +title: Resources +description: >- + Retrieve a list of top-level resource types available in the InfluxDB 3 Cloud + Serverless API. +type: api +layout: list +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-resources.yaml +weight: 100 +tag: Resources +isConceptual: false +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +operations: + - operationId: GetResources + method: GET + path: /api/v2/resources + 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/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/response-codes/_index.md b/content/influxdb3/cloud-serverless/api/response-codes/_index.md new file mode 100644 index 000000000..8c5fb7411 --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/response-codes/_index.md @@ -0,0 +1,28 @@ +--- +title: Response codes +description: >- + Standard HTTP status codes returned by InfluxDB 3 Cloud Serverless API + endpoints and their meanings. +type: api +layout: single +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-response-codes.yaml +weight: 100 +tag: Response codes +isConceptual: true +menuGroup: Concepts +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +tagDescription: >- + Standard HTTP status codes returned by InfluxDB 3 Cloud Serverless API + endpoints and their meanings. +alt_links: + core: /influxdb3/core/api/ + enterprise: /influxdb3/enterprise/api/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/routes/_index.md b/content/influxdb3/cloud-serverless/api/routes/_index.md new file mode 100644 index 000000000..9ae68ce76 --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/routes/_index.md @@ -0,0 +1,33 @@ +--- +title: Routes +description: Retrieve top-level routes for the InfluxDB 3 Cloud Serverless API. +type: api +layout: list +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-routes.yaml +weight: 100 +tag: Routes +isConceptual: false +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +operations: + - operationId: GetRoutes + method: GET + path: /api/v2 + 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/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/secrets/_index.md b/content/influxdb3/cloud-serverless/api/secrets/_index.md new file mode 100644 index 000000000..f5e4b0a93 --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/secrets/_index.md @@ -0,0 +1,53 @@ +--- +title: Secrets +description: >- + Create and manage secrets (key-value pairs) for use in InfluxDB 3 Cloud + Serverless Flux scripts and tasks. +type: api +layout: list +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-secrets.yaml +weight: 100 +tag: Secrets +isConceptual: false +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +operations: + - operationId: GetOrgsIDSecrets + method: GET + path: /api/v2/orgs/{orgID}/secrets + summary: List all secret keys for an organization + tags: + - Secrets + - operationId: PatchOrgsIDSecrets + method: PATCH + path: /api/v2/orgs/{orgID}/secrets + summary: Update secrets in an organization + tags: + - Secrets + - operationId: DeleteOrgsIDSecretsID + method: DELETE + path: /api/v2/orgs/{orgID}/secrets/{secretID} + summary: Delete a secret from an organization + tags: + - Secrets + - operationId: PostOrgsIDSecrets + method: POST + path: /api/v2/orgs/{orgID}/secrets/delete + 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/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/security-and-access-endpoints/_index.md b/content/influxdb3/cloud-serverless/api/security-and-access-endpoints/_index.md new file mode 100644 index 000000000..132a5da69 --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/security-and-access-endpoints/_index.md @@ -0,0 +1,59 @@ +--- +title: Security and access endpoints +description: >- + Endpoints for managing authentication and access control in InfluxDB 3 Cloud + Serverless. +type: api +layout: list +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-security-and-access-endpoints.yaml +weight: 100 +tag: Security and access endpoints +isConceptual: false +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +operations: + - operationId: GetAuthorizations + method: GET + path: /api/v2/authorizations + summary: List authorizations + tags: + - Security and access endpoints + - operationId: PostAuthorizations + method: POST + path: /api/v2/authorizations + summary: Create an authorization + tags: + - Security and access endpoints + - operationId: GetAuthorizationsID + method: GET + path: /api/v2/authorizations/{authID} + summary: Retrieve an authorization + tags: + - Security and access endpoints + - operationId: PatchAuthorizationsID + method: PATCH + path: /api/v2/authorizations/{authID} + summary: Update an API token to be active or inactive + tags: + - Security and access endpoints + - operationId: DeleteAuthorizationsID + method: DELETE + path: /api/v2/authorizations/{authID} + 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/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/supported-operations/_index.md b/content/influxdb3/cloud-serverless/api/supported-operations/_index.md new file mode 100644 index 000000000..7d4b6902a --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/supported-operations/_index.md @@ -0,0 +1,28 @@ +--- +title: Supported operations +description: >- + Overview of the common CRUD operations supported by the InfluxDB 3 Cloud + Serverless API. +type: api +layout: single +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-supported-operations.yaml +weight: 100 +tag: Supported operations +isConceptual: true +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +tagDescription: >- + Overview of the common CRUD operations supported by the InfluxDB 3 Cloud + Serverless API. +alt_links: + core: /influxdb3/core/api/ + enterprise: /influxdb3/enterprise/api/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/system-information-endpoints/_index.md b/content/influxdb3/cloud-serverless/api/system-information-endpoints/_index.md new file mode 100644 index 000000000..592a9c0f1 --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/system-information-endpoints/_index.md @@ -0,0 +1,41 @@ +--- +title: System information endpoints +description: >- + Endpoints for retrieving system-level information about the InfluxDB 3 Cloud + Serverless instance. +type: api +layout: list +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-system-information-endpoints.yaml +weight: 100 +tag: System information endpoints +isConceptual: false +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +operations: + - operationId: GetRoutes + method: GET + path: /api/v2 + summary: List all top level routes + tags: + - System information endpoints + - operationId: GetResources + method: GET + path: /api/v2/resources + 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/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/tasks/_index.md b/content/influxdb3/cloud-serverless/api/tasks/_index.md new file mode 100644 index 000000000..9ecd19ba0 --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/tasks/_index.md @@ -0,0 +1,154 @@ +--- +title: Tasks +description: >- + Schedule and manage Flux tasks that process and transform data on a recurring + basis in InfluxDB 3 Cloud Serverless. +type: api +layout: list +staticFilePath: /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-tasks.yaml +weight: 100 +tag: Tasks +isConceptual: false +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +operations: + - operationId: GetTasks + method: GET + path: /api/v2/tasks + summary: List all tasks + tags: + - Tasks + - operationId: PostTasks + method: POST + path: /api/v2/tasks + summary: Create a task + tags: + - Tasks + - operationId: GetTasksID + method: GET + path: /api/v2/tasks/{taskID} + summary: Retrieve a task + tags: + - Tasks + - operationId: PatchTasksID + method: PATCH + path: /api/v2/tasks/{taskID} + summary: Update a task + tags: + - Tasks + - operationId: DeleteTasksID + method: DELETE + path: /api/v2/tasks/{taskID} + summary: Delete a task + tags: + - Tasks + - operationId: GetTasksIDLabels + method: GET + path: /api/v2/tasks/{taskID}/labels + summary: List labels for a task + tags: + - Tasks + - operationId: PostTasksIDLabels + method: POST + path: /api/v2/tasks/{taskID}/labels + summary: Add a label to a task + tags: + - Tasks + - operationId: DeleteTasksIDLabelsID + method: DELETE + path: /api/v2/tasks/{taskID}/labels/{labelID} + summary: Delete a label from a task + tags: + - Tasks + - operationId: GetTasksIDLogs + method: GET + path: /api/v2/tasks/{taskID}/logs + summary: Retrieve all logs for a task + tags: + - Tasks + - operationId: GetTasksIDMembers + method: GET + path: /api/v2/tasks/{taskID}/members + summary: List all task members + tags: + - Tasks + - operationId: PostTasksIDMembers + method: POST + path: /api/v2/tasks/{taskID}/members + summary: Add a member to a task + tags: + - Tasks + - operationId: DeleteTasksIDMembersID + method: DELETE + path: /api/v2/tasks/{taskID}/members/{userID} + summary: Remove a member from a task + tags: + - Tasks + - operationId: GetTasksIDOwners + method: GET + path: /api/v2/tasks/{taskID}/owners + summary: List all owners of a task + tags: + - Tasks + - operationId: PostTasksIDOwners + method: POST + path: /api/v2/tasks/{taskID}/owners + summary: Add an owner for a task + tags: + - Tasks + - operationId: DeleteTasksIDOwnersID + method: DELETE + path: /api/v2/tasks/{taskID}/owners/{userID} + summary: Remove an owner from a task + tags: + - Tasks + - operationId: GetTasksIDRuns + method: GET + path: /api/v2/tasks/{taskID}/runs + summary: List runs for a task + tags: + - Tasks + - operationId: PostTasksIDRuns + method: POST + path: /api/v2/tasks/{taskID}/runs + summary: Start a task run, overriding the schedule + tags: + - Tasks + - operationId: GetTasksIDRunsID + method: GET + path: /api/v2/tasks/{taskID}/runs/{runID} + summary: Retrieve a run for a task. + tags: + - Tasks + - operationId: DeleteTasksIDRunsID + method: DELETE + path: /api/v2/tasks/{taskID}/runs/{runID} + summary: Cancel a running task + tags: + - Tasks + - operationId: GetTasksIDRunsIDLogs + method: GET + path: /api/v2/tasks/{taskID}/runs/{runID}/logs + summary: Retrieve all logs for a run + tags: + - Tasks + - operationId: PostTasksIDRunsIDRetry + method: POST + path: /api/v2/tasks/{taskID}/runs/{runID}/retry + 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/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/telegrafs/_index.md b/content/influxdb3/cloud-serverless/api/telegrafs/_index.md new file mode 100644 index 000000000..68835158b --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/telegrafs/_index.md @@ -0,0 +1,113 @@ +--- +title: Telegrafs +description: >- + Create and manage Telegraf agent configurations that collect and write data to + InfluxDB 3 Cloud Serverless. +type: api +layout: list +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-telegrafs.yaml +weight: 100 +tag: Telegrafs +isConceptual: false +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +operations: + - operationId: GetTelegrafs + method: GET + path: /api/v2/telegrafs + summary: List all Telegraf configurations + tags: + - Telegrafs + - operationId: PostTelegrafs + method: POST + path: /api/v2/telegrafs + summary: Create a Telegraf configuration + tags: + - Telegrafs + - operationId: GetTelegrafsID + method: GET + path: /api/v2/telegrafs/{telegrafID} + summary: Retrieve a Telegraf configuration + tags: + - Telegrafs + - operationId: PutTelegrafsID + method: PUT + path: /api/v2/telegrafs/{telegrafID} + summary: Update a Telegraf configuration + tags: + - Telegrafs + - operationId: DeleteTelegrafsID + method: DELETE + path: /api/v2/telegrafs/{telegrafID} + summary: Delete a Telegraf configuration + tags: + - Telegrafs + - operationId: GetTelegrafsIDLabels + method: GET + path: /api/v2/telegrafs/{telegrafID}/labels + summary: List all labels for a Telegraf config + tags: + - Telegrafs + - operationId: PostTelegrafsIDLabels + method: POST + path: /api/v2/telegrafs/{telegrafID}/labels + summary: Add a label to a Telegraf config + tags: + - Telegrafs + - operationId: DeleteTelegrafsIDLabelsID + method: DELETE + path: /api/v2/telegrafs/{telegrafID}/labels/{labelID} + summary: Delete a label from a Telegraf config + tags: + - Telegrafs + - operationId: GetTelegrafsIDMembers + method: GET + path: /api/v2/telegrafs/{telegrafID}/members + summary: List all users with member privileges for a Telegraf config + tags: + - Telegrafs + - operationId: PostTelegrafsIDMembers + method: POST + path: /api/v2/telegrafs/{telegrafID}/members + summary: Add a member to a Telegraf config + tags: + - Telegrafs + - operationId: DeleteTelegrafsIDMembersID + method: DELETE + path: /api/v2/telegrafs/{telegrafID}/members/{userID} + summary: Remove a member from a Telegraf config + tags: + - Telegrafs + - operationId: GetTelegrafsIDOwners + method: GET + path: /api/v2/telegrafs/{telegrafID}/owners + summary: List all owners of a Telegraf configuration + tags: + - Telegrafs + - operationId: PostTelegrafsIDOwners + method: POST + path: /api/v2/telegrafs/{telegrafID}/owners + summary: Add an owner to a Telegraf configuration + tags: + - Telegrafs + - operationId: DeleteTelegrafsIDOwnersID + method: DELETE + path: /api/v2/telegrafs/{telegrafID}/owners/{userID} + 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/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/templates/_index.md b/content/influxdb3/cloud-serverless/api/templates/_index.md new file mode 100644 index 000000000..cfb0cb003 --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/templates/_index.md @@ -0,0 +1,77 @@ +--- +title: Templates +description: >- + Export and apply InfluxDB templates, and manage template stacks for InfluxDB 3 + Cloud Serverless. +type: api +layout: list +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-templates.yaml +weight: 100 +tag: Templates +isConceptual: false +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +operations: + - operationId: ListStacks + method: GET + path: /api/v2/stacks + summary: List installed stacks + tags: + - Templates + - operationId: CreateStack + method: POST + path: /api/v2/stacks + summary: Create a stack + tags: + - Templates + - operationId: ReadStack + method: GET + path: /api/v2/stacks/{stack_id} + summary: Retrieve a stack + tags: + - Templates + - operationId: UpdateStack + method: PATCH + path: /api/v2/stacks/{stack_id} + summary: Update a stack + tags: + - Templates + - operationId: DeleteStack + method: DELETE + path: /api/v2/stacks/{stack_id} + summary: Delete a stack and associated resources + tags: + - Templates + - operationId: UninstallStack + method: POST + path: /api/v2/stacks/{stack_id}/uninstall + summary: Uninstall a stack + tags: + - Templates + - operationId: ApplyTemplate + method: POST + path: /api/v2/templates/apply + summary: Apply or dry-run a template + tags: + - Templates + - operationId: ExportTemplate + method: POST + path: /api/v2/templates/export + 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/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/usage/_index.md b/content/influxdb3/cloud-serverless/api/usage/_index.md new file mode 100644 index 000000000..c8f9761d3 --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/usage/_index.md @@ -0,0 +1,34 @@ +--- +title: Usage +description: >- + Retrieve usage metrics and cardinality data for an InfluxDB 3 Cloud Serverless + organization. +type: api +layout: list +staticFilePath: /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-usage.yaml +weight: 100 +tag: Usage +isConceptual: false +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +operations: + - operationId: GetOrgUsageID + method: GET + path: /api/v2/orgs/{orgID}/usage + 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/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/api/variables/_index.md b/content/influxdb3/cloud-serverless/api/variables/_index.md new file mode 100644 index 000000000..173b93959 --- /dev/null +++ b/content/influxdb3/cloud-serverless/api/variables/_index.md @@ -0,0 +1,81 @@ +--- +title: Variables +description: Create and manage variables for use in InfluxDB 3 Cloud Serverless dashboards. +type: api +layout: list +staticFilePath: >- + /openapi/influxdb3-cloud-serverless/tags/influxdb3-cloud-serverless-variables.yaml +weight: 100 +tag: Variables +isConceptual: false +menuGroup: Other +specDownloadPath: /openapi/influxdb3-cloud-serverless.yml +articleDataKey: influxdb3-cloud-serverless +articleSection: api +operations: + - operationId: GetVariables + method: GET + path: /api/v2/variables + summary: List all variables + tags: + - Variables + - operationId: PostVariables + method: POST + path: /api/v2/variables + summary: Create a variable + tags: + - Variables + - operationId: GetVariablesID + method: GET + path: /api/v2/variables/{variableID} + summary: Retrieve a variable + tags: + - Variables + - operationId: PutVariablesID + method: PUT + path: /api/v2/variables/{variableID} + summary: Replace a variable + tags: + - Variables + - operationId: PatchVariablesID + method: PATCH + path: /api/v2/variables/{variableID} + summary: Update a variable + tags: + - Variables + - operationId: DeleteVariablesID + method: DELETE + path: /api/v2/variables/{variableID} + summary: Delete a variable + tags: + - Variables + - operationId: GetVariablesIDLabels + method: GET + path: /api/v2/variables/{variableID}/labels + summary: List all labels for a variable + tags: + - Variables + - operationId: PostVariablesIDLabels + method: POST + path: /api/v2/variables/{variableID}/labels + summary: Add a label to a variable + tags: + - Variables + - operationId: DeleteVariablesIDLabelsID + method: DELETE + path: /api/v2/variables/{variableID}/labels/{labelID} + 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/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- diff --git a/content/influxdb3/cloud-serverless/reference/api/_index.md b/content/influxdb3/cloud-serverless/reference/api/_index.md deleted file mode 100644 index a895050e5..000000000 --- a/content/influxdb3/cloud-serverless/reference/api/_index.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: InfluxDB HTTP API -description: > - The InfluxDB HTTP API provides a programmatic interface for interactions with InfluxDB. - Access the InfluxDB API using the `/api/v2/write` or InfluxDB v1 endpoints. -menu: - influxdb3_cloud_serverless: - parent: Reference - name: InfluxDB HTTP API -weight: 104 -influxdb3/cloud-serverless/tags: [api] ---- - -The InfluxDB HTTP API provides a programmatic interface for interactions with -{{% product-name %}}, such as writing and querying data. - -Access the InfluxDB HTTP API using the `/api/v2/` or InfluxDB v1 endpoints. - -## InfluxDB v2 Compatibility API reference documentation - -InfluxDB v2 API for {{% product-name %}} - -The API reference describes requests and responses for InfluxDB v2-compatible -endpoints that work with {{% product-name %}} and with InfluxDB 2.x client -libraries and third-party integrations. - -## InfluxDB v1 Compatibility API reference documentation - -InfluxDB v1 API for {{% product-name %}} - -The API reference describes requests and responses for InfluxDB v1-compatible `/write` and `/query` endpoints that work with {{% product-name %}} and with InfluxDB 1.x client libraries and third-party integrations. diff --git a/content/influxdb3/clustered/management-api/_index.md b/content/influxdb3/clustered/management-api/_index.md new file mode 100644 index 000000000..6966ade7b --- /dev/null +++ b/content/influxdb3/clustered/management-api/_index.md @@ -0,0 +1,27 @@ +--- +title: InfluxDB HTTP API +description: >- + Use the InfluxDB HTTP API to write data, query data, and manage databases, + tables, and tokens. +weight: 104 +type: api +articleDataKey: influxdb3-clustered +articleSection: management-api +menu: + influxdb3_clustered: + name: InfluxDB HTTP API + identifier: api-reference-influxdb3-clustered-management-api + parent: Reference +alt_links: + core: /influxdb3/core/api/ + enterprise: /influxdb3/enterprise/api/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- + +Use the {{% product-name %}} HTTP API to write data, query data, and manage databases, tables, and tokens. + +{{< children >}} diff --git a/content/influxdb3/clustered/management-api/all-endpoints/_index.md b/content/influxdb3/clustered/management-api/all-endpoints/_index.md new file mode 100644 index 000000000..d475b9d7f --- /dev/null +++ b/content/influxdb3/clustered/management-api/all-endpoints/_index.md @@ -0,0 +1,25 @@ +--- +title: All endpoints +description: View all API endpoints sorted by path. +type: api +layout: all-endpoints +weight: 999 +isAllEndpoints: true +articleDataKey: influxdb3-clustered +articleSection: management-api +menu: + influxdb3_clustered: + name: All endpoints + identifier: all-endpoints-influxdb3-clustered-management-api + parent: InfluxDB HTTP API +alt_links: + core: /influxdb3/core/api/ + enterprise: /influxdb3/enterprise/api/ + cloud-serverless: /influxdb3/cloud-serverless/api/ + cloud-dedicated: /influxdb3/cloud-dedicated/api/ + clustered: /influxdb3/clustered/api/ + v2: /influxdb/v2/api/ + cloud: /influxdb/cloud/api/ +--- + +All {{% product-name %}} API endpoints, sorted by path. diff --git a/content/influxdb3/clustered/reference/api/_index.md b/content/influxdb3/clustered/reference/api/_index.md deleted file mode 100644 index 3259b1437..000000000 --- a/content/influxdb3/clustered/reference/api/_index.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: InfluxDB HTTP API -description: > - The InfluxDB HTTP API provides a programmatic interface for interactions with - InfluxDB, such as writing and querying data. - Access the InfluxDB API using the `/api/v2/write` or InfluxDB v1 endpoints. -menu: - influxdb3_clustered: - parent: Reference - name: InfluxDB HTTP API -weight: 127 -influxdb3/clustered/tags: [api] ---- - -The InfluxDB HTTP API provides a programmatic interface for interactions with -{{% product-name %}}, such as writing and querying data. - -Access the InfluxDB HTTP API using the `/api/v2/` or InfluxDB v1 endpoints. - -## InfluxDB v2 Compatibility API reference documentation - -InfluxDB v2 API for {{% product-name %}} - -The API reference describes requests and responses for InfluxDB v2-compatible -endpoints that work with {{% product-name %}} and with InfluxDB 2.x client -libraries and third-party integrations. - -## InfluxDB v1 Compatibility API reference documentation - -InfluxDB v1 API for {{% product-name %}} - -The API reference describes requests and responses for InfluxDB v1-compatible `/write` and `/query` endpoints that work with {{% product-name %}} and with InfluxDB 1.x client libraries and third-party integrations. diff --git a/content/influxdb3/core/reference/api/_index.md b/content/influxdb3/core/reference/api/_index.md index 6a2200b1e..13724ba97 100644 --- a/content/influxdb3/core/reference/api/_index.md +++ b/content/influxdb3/core/reference/api/_index.md @@ -1,20 +1,12 @@ --- title: InfluxDB HTTP API description: > - The InfluxDB HTTP API for {{% product-name %}} provides a programmatic interface - for interactions with InfluxDB, - including writing, querying, and processing data, and managing an InfluxDB 3 - instance. -menu: - influxdb3_core: - parent: Reference - name: InfluxDB HTTP API -weight: 104 -influxdb3/core/tags: [api] -source: /shared/influxdb3-api-reference/_index.md + The InfluxDB HTTP API for InfluxDB 3 Core provides a programmatic interface + for interactions with InfluxDB. +# Redirect to the new location +aliases: + - /influxdb3/core/reference/api/ +redirect: /influxdb3/core/api/ --- - +This page has moved to [InfluxDB HTTP API](/influxdb3/core/api/). diff --git a/content/influxdb3/enterprise/reference/api/_index.md b/content/influxdb3/enterprise/reference/api/_index.md index ea78867f6..a5a831de4 100644 --- a/content/influxdb3/enterprise/reference/api/_index.md +++ b/content/influxdb3/enterprise/reference/api/_index.md @@ -1,20 +1,10 @@ --- title: InfluxDB HTTP API description: > - The InfluxDB HTTP API for {{% product-name %}} provides a programmatic interface - for interactions with InfluxDB, - including writing, querying, and processing data, and managing an InfluxDB 3 - instance. -menu: - influxdb3_enterprise: - parent: Reference - name: InfluxDB HTTP API -weight: 104 -influxdb3/enterprise/tags: [api] -source: /shared/influxdb3-api-reference/_index.md + The InfluxDB HTTP API for InfluxDB 3 Enterprise provides a programmatic interface + for interactions with InfluxDB. +# Redirect to the new location +redirect: /influxdb3/enterprise/api/ --- - +This page has moved to [InfluxDB HTTP API](/influxdb3/enterprise/api/). diff --git a/cypress/e2e/content/api-reference.cy.js b/cypress/e2e/content/api-reference.cy.js index ceeaffeff..a0d50ff78 100644 --- a/cypress/e2e/content/api-reference.cy.js +++ b/cypress/e2e/content/api-reference.cy.js @@ -1,78 +1,76 @@ /// + +/** + * API Reference Documentation E2E Tests + * + * 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 + * 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 + */ + const fakeGoogleTagManager = { trackingOptIn: () => {}, - trackingOptOut: () => {} -} + trackingOptOut: () => {}, +}; 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) => { describe(subject, () => { beforeEach(() => { - // Intercept and modify the page HTML before it loads - cy.intercept('GET', '**', (req) => { - req.continue((res) => { - if (res.headers['content-type']?.includes('text/html')) { - // Modify the Kapa widget script attributes - // Avoid socket errors from fpjs in tests by disabling fingerprinting - res.body = res.body.replace( - /data-user-analytics-fingerprint-enabled="true"/, - 'data-user-analytics-fingerprint-enabled="false"' - ); - } - }); - }); + // Intercept and modify the page HTML before it loads + cy.intercept('GET', '**', (req) => { + req.continue((res) => { + if (res.headers['content-type']?.includes('text/html')) { + // Modify the Kapa widget script attributes + // Avoid socket errors from fpjs in tests by disabling fingerprinting + res.body = res.body.replace( + /data-user-analytics-fingerprint-enabled="true"/, + 'data-user-analytics-fingerprint-enabled="false"' + ); + } + }); + }); cy.visit(subject); - window.fcdsc = fakeGoogleTagManager; cy.stub(window.fcdsc, 'trackingOptIn').as('trackingOptIn'); cy.stub(window.fcdsc, 'trackingOptOut').as('trackingOptOut'); }); it(`has API info`, function () { - cy.get('script[data-user-analytics-fingerprint-enabled=false]').should('have.length', 1); + cy.get('script[data-user-analytics-fingerprint-enabled=false]').should( + 'have.length', + 1 + ); cy.get('h1').first().should('have.length', 1); - cy.get('[data-role$=description]').should('have.length', 1); + // Check for description element (either article--description class or data-role attribute) + cy.get('.article--description, [data-role$=description]').should( + 'have.length.at.least', + 1 + ); }); it('links back to the version home page', function () { - cy.get('a.back').contains('Docs') - .should('have.length', 1) - .click(); + cy.get('a.back').contains('Docs').should('have.length', 1).click(); // Path should be the first two segments and trailing slash in $subject - cy.location('pathname') - .should('eq', subject.replace(/^(\/[^/]+\/[^/]+\/).*/, '$1')); + cy.location('pathname').should( + 'eq', + subject.replace(/^(\/[^/]+\/[^/]+\/).*/, '$1') + ); cy.get('h1').should('have.length', 1); }); it('contains valid internal links', function () { @@ -88,8 +86,7 @@ describe('API reference content', () => { // cy.request doesn't show in your browser's Developer Tools // because the request comes from Node, not from the browser. cy.request($a.attr('href')).its('status').should('eq', 200); - }); - + }); }); }); it('contains valid external links', function () { @@ -109,3 +106,449 @@ describe('API reference content', () => { }); }); }); + +/** + * API Reference Layout Tests + * Tests layout for InfluxDB 3 Core/Enterprise API documentation + */ +describe('API reference layout', () => { + const layoutSubjects = [ + '/influxdb3/core/api/write-data/', + '/influxdb3/enterprise/api/write-data/', + ]; + + layoutSubjects.forEach((subject) => { + describe(`${subject} layout`, () => { + 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(subject); + }); + + describe('Layout Structure', () => { + it('displays sidebar', () => { + cy.get('.sidebar').should('be.visible'); + }); + + it('displays API content area', () => { + cy.get('.api-content, .content-wrapper, .article--content').should( + 'exist' + ); + }); + + it('displays TOC on page', () => { + cy.get('.api-toc').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'); + }); + }); + }); + }); + }); +}); + +/** + * API Tag Page Tests + * Tests Hugo-native tag pages render operations correctly + */ +describe('API tag pages', () => { + const tagPages = [ + '/influxdb3/core/api/write-data/', + '/influxdb3/core/api/query-data/', + '/influxdb3/enterprise/api/write-data/', + ]; + + tagPages.forEach((page) => { + describe(`Tag page ${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 page title', () => { + cy.get('h1').should('exist'); + }); + + it('renders operations section', () => { + cy.get('.api-operations, .api-operations-section').should('exist'); + }); + + 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'); + }); + }); + + it('TOC contains operation links', () => { + cy.get('.api-toc-nav').should('exist'); + cy.get('.api-toc-link').should('have.length.at.least', 1); + }); + + it('TOC links have method badges', () => { + cy.get('.api-toc-link .api-method').should('have.length.at.least', 1); + }); + }); + }); +}); + +/** + * API Section Page Structure Tests + * Tests that API section pages show only tags (immediate children) + */ +describe('API section page structure', () => { + const sectionPages = ['/influxdb3/core/api/', '/influxdb3/enterprise/api/']; + + sectionPages.forEach((page) => { + describe(`Section page ${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 page title', () => { + cy.get('h1').should('contain', 'InfluxDB HTTP API'); + }); + + it('shows tag pages as children', () => { + cy.get('.children-links h3 a').should('have.length.at.least', 5); + }); + + it('does not show individual operations in content area', () => { + // Operations cards should not appear in the main content + cy.get('.article--content .api-operation-card').should('not.exist'); + }); + + it('has All endpoints link in navigation', () => { + cy.get('.sidebar a').contains('All endpoints').should('exist'); + }); + }); + }); +}); + +/** + * All Endpoints Page Tests + * Tests the "All endpoints" page shows all operations + */ +describe('All endpoints page', () => { + const allEndpointsPages = [ + '/influxdb3/core/api/all-endpoints/', + '/influxdb3/enterprise/api/all-endpoints/', + ]; + + allEndpointsPages.forEach((page) => { + describe(`All endpoints ${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 page title "All endpoints"', () => { + cy.get('h1').should('contain', 'All endpoints'); + }); + + it('shows v3 API section', () => { + cy.get('#v3-api').should('exist'); + }); + + it('displays operation cards', () => { + cy.get('.api-operation-card').should('have.length.at.least', 10); + }); + + it('operation cards have method badges', () => { + cy.get('.api-operation-card .api-method').should( + 'have.length.at.least', + 10 + ); + }); + + it('operation cards have path codes', () => { + cy.get('.api-operation-card .api-path').should( + 'have.length.at.least', + 10 + ); + }); + + it('operation cards link to tag pages with operation anchors', () => { + cy.get('.api-operation-card') + .first() + .should('have.attr', 'href') + .and('match', /\/api\/.*\/#operation\//); + }); + + it('is accessible from navigation', () => { + // Navigate back to section page + cy.get('.sidebar a').contains('InfluxDB HTTP API').click(); + // Then navigate to All endpoints + cy.get('.sidebar a').contains('All endpoints').click(); + cy.url().should('include', '/all-endpoints/'); + }); + }); + }); +}); + +/** + * 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 + */ +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); + }); + }); + }); + }); +}); diff --git a/data/products.yml b/data/products.yml index ec212bc84..406df0398 100644 --- a/data/products.yml +++ b/data/products.yml @@ -2,8 +2,8 @@ influxdb3_core: name: InfluxDB 3 Core altname: InfluxDB 3 Core namespace: influxdb3 - content_path: influxdb3/core - label_group: v3-monolith + api_path: /influxdb3/core/api/ + alt_link_key: core menu_category: self-managed versions: [core] list_order: 2 @@ -40,13 +40,13 @@ influxdb3_enterprise: name: InfluxDB 3 Enterprise altname: InfluxDB 3 Enterprise namespace: influxdb3 - content_path: influxdb3/enterprise - label_group: v3-monolith + api_path: /influxdb3/enterprise/api/ + alt_link_key: enterprise menu_category: self-managed versions: [enterprise] list_order: 2 latest: enterprise - latest_patch: 3.8.4 + latest_patch: 3.8.3 placeholder_host: localhost:8181 limits: database: 100 @@ -78,8 +78,6 @@ influxdb3_explorer: name: InfluxDB 3 Explorer altname: Explorer namespace: influxdb3_explorer - content_path: influxdb3/explorer - label_group: explorer menu_category: tools list_order: 1 latest: explorer @@ -96,8 +94,8 @@ influxdb3_cloud_serverless: name: InfluxDB Cloud Serverless altname: InfluxDB Cloud namespace: influxdb - content_path: influxdb3/cloud-serverless - label_group: v3-distributed + api_path: /influxdb3/cloud-serverless/api/ + alt_link_key: cloud-serverless menu_category: managed versions: [cloud-serverless] list_order: 2 @@ -131,8 +129,8 @@ influxdb3_cloud_dedicated: name: InfluxDB Cloud Dedicated altname: InfluxDB Cloud namespace: influxdb - content_path: influxdb3/cloud-dedicated - label_group: v3-distributed + api_path: /influxdb3/cloud-dedicated/api/ + alt_link_key: cloud-dedicated menu_category: managed versions: [cloud-dedicated] list_order: 3 @@ -164,8 +162,8 @@ influxdb3_clustered: name: InfluxDB Clustered altname: InfluxDB Clustered namespace: influxdb - content_path: influxdb3/clustered - label_group: v3-distributed + api_path: /influxdb3/clustered/api/ + alt_link_key: clustered menu_category: self-managed versions: [clustered] list_order: 3 @@ -200,12 +198,8 @@ influxdb: name__v1: InfluxDB OSS v1 altname: InfluxDB OSS namespace: influxdb - content_path: - v2: influxdb/v2 - v1: influxdb/v1 - label_group: - v2: v2 - v1: v1 + api_path: /influxdb/v2/api/ + alt_link_key: v2 succeeded_by: influxdb3_core menu_category: self-managed list_order: 1 @@ -216,7 +210,7 @@ influxdb: latest: v2.8 latest_patches: v2: 2.8.0 - v1: 1.12.3 + v1: 1.12.2 latest_cli: v2: 2.7.5 detector_config: @@ -251,8 +245,8 @@ influxdb_cloud: name__vcloud: InfluxDB Cloud (TSM) altname: InfluxDB Cloud namespace: influxdb - content_path: influxdb/cloud - label_group: v2-cloud + api_path: /influxdb/cloud/api/ + alt_link_key: cloud menu_category: managed versions: [cloud] list_order: 1 @@ -282,14 +276,12 @@ influxdb_cloud: telegraf: name: Telegraf namespace: telegraf - content_path: telegraf - label_group: telegraf menu_category: other list_order: 6 versions: [v1] - latest: v1.38 + latest: v1.37 latest_patches: - v1: 1.38.0 + v1: 1.37.3 ai_sample_questions: - How do I configure Telegraf for InfluxDB 3? - How do I write a custom Telegraf plugin? @@ -308,8 +300,6 @@ telegraf_controller: chronograf: name: Chronograf namespace: chronograf - content_path: chronograf - label_group: chronograf menu_category: other list_order: 7 versions: [v1] @@ -325,8 +315,6 @@ chronograf: kapacitor: name: Kapacitor namespace: kapacitor - content_path: kapacitor - label_group: kapacitor menu_category: other list_order: 7 versions: [v1] @@ -342,8 +330,6 @@ kapacitor: enterprise_influxdb: name: 'InfluxDB Enterprise v1' namespace: enterprise_influxdb - content_path: enterprise_influxdb - label_group: v1-enterprise menu_category: self-managed list_order: 5 versions: [v1] @@ -398,8 +384,6 @@ influxdb_cloud1: flux: name: Flux namespace: flux - content_path: flux - label_group: flux menu_category: languages list_order: 8 versions: [v0] diff --git a/docs/plans/2025-02-06-clustered-cloud-dedicated-api-structure-design.md b/docs/plans/2025-02-06-clustered-cloud-dedicated-api-structure-design.md new file mode 100644 index 000000000..4ed939b6a --- /dev/null +++ b/docs/plans/2025-02-06-clustered-cloud-dedicated-api-structure-design.md @@ -0,0 +1,154 @@ +# Clustered & Cloud Dedicated API Documentation Structure Design + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Unified API documentation for Clustered and Cloud Dedicated with separate downloadable specs for Data API and Management API. + +**Architecture:** Single nav menu combining tags from both API specs, with dual download buttons on landing page and context-aware buttons on tag pages. + +**Tech Stack:** Hugo templates, OpenAPI specs, existing API doc generator + +*** + +## Context + +Clustered and Cloud Dedicated have two distinct API planes: + +- **Data API** - Write and Query endpoints using database tokens +- **Management API** - Databases, Tables, Tokens endpoints using management tokens + +## Design Decisions + +### Navigation Structure + +Single `/api/` section with combined nav: + +``` +InfluxDB HTTP API +├── Quick start (conceptual) +├── Authentication (conceptual, covers both token types) +├── API compatibility (conceptual) +├── Common parameters (conceptual) +├── Headers (conceptual) +├── Response codes (conceptual) +├── Database tokens (Management API) +├── Databases (Management API) +├── Tables (Management API) +├── Ping (Data API) +├── Query data (Data API) +├── Write data (Data API) +└── All endpoints (combined) +``` + +### Download Buttons + +**Landing page:** Two buttons side-by-side + +- "Download Data API Spec" → `/openapi/influxdb-{product}-v2-data-api.yml` +- "Download Management API Spec" → `/openapi/influxdb-{product}-management-api.yml` + +**Tag pages:** Context-aware single button based on `staticFilePath` + +### Authentication Page + +Single unified page covering: + +- Token types table (Management vs Database) +- Authentication schemes table (Bearer, Token, Basic, Query string) +- Which endpoints use which token type +- Security schemes from OpenAPI spec + +### OpenAPI Spec Organization + +**`v2/ref.yml` (Data API):** + +- Contains all conceptual tags (Quick start, Authentication, etc.) +- Contains Data API operation tags (Ping, Query data, Write data) + +**`management/openapi.yml`:** + +- NO CHANGES to source file +- Contains only operation tags (Database tokens, Databases, Tables) + +Generator combines both specs into unified `articles.yml`. + +### Cleanup Required + +Remove old pages: + +- `/content/influxdb3/clustered/api/v2/_index.html` +- `/content/influxdb3/cloud-dedicated/api/v2/_index.html` +- `/content/influxdb3/*/api/admin-authentication-management-operations/` +- `/content/influxdb3/*/api/management-authentication-admin-operations/` + +*** + +## Implementation Tasks + +### Task 1: Update Authentication tag in Data API specs + +Update `api-docs/influxdb3/clustered/v2/ref.yml` and `api-docs/influxdb3/cloud-dedicated/v2/ref.yml`: + +- Revise Authentication tag description to cover both token types +- Include table of token types and which endpoints use them +- Keep `showSecuritySchemes: true` for security scheme rendering + +### Task 2: Update Quick start tag in Data API specs + +Update Quick start in both v2/ref.yml files: + +- Cover both Data and Management API getting started flow +- Show management token creation, then database/token setup, then write/query + +### Task 3: Add dual download buttons to API landing page + +Modify `layouts/api/list.html` or create partial: + +- Detect Clustered/Cloud Dedicated products +- Show two download buttons on section index pages +- Style buttons side-by-side + +### Task 4: Update tag page download button logic + +Modify `layouts/api/single.html` and/or `layouts/api/list.html`: + +- Detect API type from `staticFilePath` (contains `management-api` or `v2-data-api`) +- Show appropriate download button for the API + +### Task 5: Remove old v2 HTML pages + +Delete: + +- `content/influxdb3/clustered/api/v2/` +- `content/influxdb3/cloud-dedicated/api/v2/` + +### Task 6: Remove old authentication directories + +Delete leftover directories: + +- `content/influxdb3/clustered/api/admin-authentication-management-operations/` +- `content/influxdb3/clustered/api/management-authentication-admin-operations/` +- `content/influxdb3/cloud-dedicated/api/admin-authentication-management-operations/` +- `content/influxdb3/cloud-dedicated/api/management-authentication-admin-operations/` + +### Task 7: Regenerate API docs and verify + +Run `yarn build:api-docs` and verify: + +- Nav shows combined tags from both APIs +- Authentication page has unified content +- Download buttons work correctly +- Old pages are gone + +*** + +## Success Criteria + +- [ ] Single Authentication page covers both token types clearly +- [ ] Landing page shows two download buttons for Clustered/Cloud Dedicated +- [ ] Tag pages show context-appropriate download button +- [ ] Nav combines tags from both API specs +- [ ] Old v2 HTML pages removed +- [ ] Old duplicate authentication directories removed +- [ ] `yarn build:api-docs` succeeds +- [ ] Hugo builds without errors diff --git a/docs/plans/2026-01-07-api-reference-rapidoc-migration.md b/docs/plans/2026-01-07-api-reference-rapidoc-migration.md new file mode 100644 index 000000000..9d22a8da0 --- /dev/null +++ b/docs/plans/2026-01-07-api-reference-rapidoc-migration.md @@ -0,0 +1,184 @@ +# API Reference RapiDoc Migration Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Migrate all InfluxDB products from Redoc-based API reference to the new RapiDoc-based UI that's already working for influxdb3-core and influxdb3-enterprise. + +**Architecture:** The new API reference uses RapiDoc Mini to render OpenAPI specs. The generation script (`api-docs/scripts/generate-openapi-articles.ts`) processes OpenAPI specs and creates Hugo content pages with frontmatter that references spec files. Hugo layouts (`layouts/api/` and `layouts/api-operation/`) render these pages using the RapiDoc component. + +**Tech Stack:** TypeScript (generation scripts), Hugo templates, RapiDoc web component, OpenAPI 3.0 specs + +*** + +## Overview + +### Products to Migrate + +| Product | Status | Spec Location | Target Content Path | +| ----------------------- | ------- | ----------------------------------------------------------- | ----------------------------------------- | +| influxdb3-core | ✅ Done | `api-docs/influxdb3/core/v3/ref.yml` | `content/influxdb3/core/api/` | +| influxdb3-enterprise | ✅ Done | `api-docs/influxdb3/enterprise/v3/ref.yml` | `content/influxdb3/enterprise/api/` | +| cloud-dedicated | Partial | `api-docs/influxdb3/cloud-dedicated/management/openapi.yml` | `content/influxdb3/cloud-dedicated/api/` | +| cloud-serverless | Partial | `api-docs/influxdb3/cloud-serverless/v2/ref.yml` | `content/influxdb3/cloud-serverless/api/` | +| clustered | Partial | `api-docs/influxdb3/clustered/management/openapi.yml` | `content/influxdb3/clustered/api/` | +| cloud-v2 | Partial | `api-docs/influxdb/cloud/v2/ref.yml` | `content/influxdb/cloud/api/` | +| oss-v2 | Partial | `api-docs/influxdb/v2/v2/ref.yml` | `content/influxdb/v2/api/` | +| oss-v1 | Partial | `api-docs/influxdb/v1/v1/ref.yml` | `content/influxdb/v1/api/` | +| enterprise\_influxdb-v1 | Partial | `api-docs/enterprise_influxdb/v1/v1/ref.yml` | `content/enterprise_influxdb/v1/api/` | + +### Key Files + +- **Generation script:** `api-docs/scripts/generate-openapi-articles.ts` +- **Core conversion:** `api-docs/scripts/openapi-paths-to-hugo-data/index.ts` +- **Hugo layouts:** `layouts/api/`, `layouts/api-operation/` +- **RapiDoc component:** `assets/js/components/rapidoc-mini.ts` +- **Styles:** `assets/styles/layouts/_api-layout.scss` + +*** + +**API Tag Page Consolidation Complete** + +The API documentation structure has been refactored from individual operation pages to consolidated tag pages: + +1. **Before**: Each API operation had its own page (e.g., `/api/v3/configure/distinct_cache/`) +2. **After**: All operations for a tag are rendered inline on the tag page (e.g., `/api/cache-data/#post-/api/v3/configure/distinct_cache`) + +**Key implementation details:** + +- Server-side TOC generated from frontmatter `operations` array using Hugo templates +- `safeURL` filter prevents Hugo from URL-encoding anchor slashes +- JavaScript `api-toc.ts` detects pre-rendered TOC and preserves it +- RapiDoc's `scrollToPath()` method handles TOC click navigation to shadow DOM elements +- `goto-path` attribute initializes RapiDoc to scroll to operation from URL hash on page load +- `update-route="true"` enables RapiDoc to update URL hash as user navigates + +See [API tag pages design](2026-01-21-api-tag-pages-design.md) for link anchor patterns and route information. + +## Fix all InfluxDB products + +These products already have generated content but may need spec adjustments and testing. + +### Task 1.1: Verify API Generation + +**Files:** + +- Check: `content/influxdb3/cloud-dedicated/api/_index.md` +- Check: `api-docs/influxdb3/cloud-dedicated/management/openapi.yml` +- Verify: `static/openapi/influxdb3-cloud-dedicated/` + +**Step 1: Check existing generated content** + +```bash +ls -la content/influxdb3/cloud-dedicated/api/ +cat content/influxdb3/cloud-dedicated/api/_index.md +``` + +Expected: Should see `_index.md` and subdirectories for each tag. + +**Step 2: Verify OpenAPI spec exists and is valid** + +```bash +head -50 api-docs/influxdb3/cloud-dedicated/management/openapi.yml +``` + +Expected: Valid OpenAPI 3.x spec with `openapi:`, `info:`, `paths:` sections. + +**Step 3: Run generation** + +```bash +yarn build:api-docs +``` + +Or for just this product: + +```bash +node api-docs/scripts/dist/generate-openapi-articles.js cloud-dedicated +``` + +**Step 4: Start Hugo and verify pages render** + +```bash +npx hugo server --port 1315 +``` + +Visit the product URL--for example: + +Expected: API reference pages render with RapiDoc component showing operations. + +**Step 5: Check for console errors** + +Open browser DevTools, verify no JavaScript errors related to RapiDoc. + +**Step 6: Commit if working** + +```bash +git add content/influxdb3/cloud-dedicated/api/ +git add static/openapi/influxdb3-cloud-dedicated/ +git add data/article-data/influxdb3/cloud-dedicated/ +git commit -m "feat(api): generate cloud-dedicated API reference with RapiDoc" +``` + +### How to generate API reference articles + +**Step 1: Rebuild TypeScript** + +```bash +cd api-docs/scripts && yarn build +``` + +Or from root: + +```bash +tsc --project api-docs/scripts/tsconfig.json +``` + +**Step 2: Test compilation succeeded** + +```bash +node api-docs/scripts/dist/generate-openapi-articles.js --help +``` + +**Step 3: Commit the config change** + +```bash +git add api-docs/scripts/generate-openapi-articles.ts +git commit -m "feat(api): enable cloud-v2 product config for RapiDoc migration" +``` + +*** + +## Verification Checklist + +Before considering migration complete: + +- [ ] All product API pages render without errors +- [ ] RapiDoc "Try It Out" works for each product +- [ ] Mobile responsive layout works correctly +- [ ] Navigation menus updated +- [ ] Old URLs redirect to new locations +- [ ] E2E tests pass +- [ ] No console errors in browser DevTools +- [ ] Links validation passes + +*** + +## Rollback Plan + +If issues are found: + +1. Revert the product config changes in `generate-openapi-articles.ts` +2. Remove generated content directories +3. Restore original navigation files from git history + +```bash +git checkout HEAD~N -- content/influxdb/cloud/reference/api/ +git checkout HEAD~N -- api-docs/scripts/generate-openapi-articles.ts +``` + +*** + +## Notes + +- The `useTagBasedGeneration` option creates pages organized by OpenAPI tags (used for influxdb3 products) +- The path-based generation creates pages organized by API paths (used for v2 products) +- The `skipParentMenu` option prevents duplicate menu entries when existing reference pages have menus diff --git a/docs/plans/2026-01-21-api-tag-pages-design.md b/docs/plans/2026-01-21-api-tag-pages-design.md new file mode 100644 index 000000000..ddf6c6d6a --- /dev/null +++ b/docs/plans/2026-01-21-api-tag-pages-design.md @@ -0,0 +1,145 @@ +# API Tag Pages Design + +## Overview + +Consolidate API documentation onto tag pages, where each tag page displays all operations for that tag using RapiDoc. This replaces the previous path-based page structure. + +## Goals + +1. Keep tag-based navigation in the left sidebar +2. Remove operations as children of tags in the left sidebar +3. Each tag page displays all RapiDoc renderings for operations in that tag +4. "On this page" TOC links to Overview and each operation +5. No frame/internal scrolling - page scrolls naturally as one document +6. Consistent styling with existing implementation +7. Clear visual separation between operations + +## URL Structure + +- **Tag page:** `/influxdb3/core/api/cache-data/` +- **Operation anchor:** `/influxdb3/core/api/cache-data/#post-/api/v3/configure/distinct_cache` + +## RapiDoc Anchor Reference + +| Feature | Format/Value | +| ------------------------------ | ---------------------------------------------------------------------------------- | +| Anchor format | `#{method}-{path}` (e.g., `#post-/api/v3/configure/distinct_cache`) | +| `goto-path` attribute | Navigate to operation on load: `goto-path="post-/api/v3/configure/distinct_cache"` | +| `scrollToPath(path)` method | Programmatic navigation | +| `update-route` (default: true) | Updates URL hash as user scrolls | +| `route-prefix` (default: #) | Hash prefix for routes | +| Built-in anchors | `#overview`, `#servers`, `#auth`, `#operations-top` | + +## Page Layout + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Header / Top Nav │ +├──────────┬─────────────────────────────────────┬────────────┤ +│ │ │ │ +│ Left │ Main Content │ On This │ +│ Sidebar │ │ Page TOC │ +│ (nav) │ ┌─────────────────────────────┐ │ │ +│ │ │

Cache data

│ │ Overview │ +│ │ │

Tag description...

│ │ POST ... │ +│ │ └─────────────────────────────┘ │ DELETE .. │ +│ │ │ POST ... │ +│ │ ┌─────────────────────────────┐ │ DELETE .. │ +│ │ │ RapiDoc (full height, │ │ │ +│ │ │ no internal scroll) │ │ │ +│ │ │ │ │ │ +│ │ │ - POST distinct_cache │ │ │ +│ │ │ - DELETE distinct_cache │ │ │ +│ │ │ - POST last_cache │ │ │ +│ │ │ - DELETE last_cache │ │ │ +│ │ │ │ │ │ +│ │ └─────────────────────────────┘ │ │ +│ │ │ │ +├──────────┴─────────────────────────────────────┴────────────┤ +│ Footer │ +└─────────────────────────────────────────────────────────────┘ +``` + +## RapiDoc Configuration + +- `render-style="read"` - Linear document flow (no internal scrolling) +- `spec-url` - Tag-specific spec (e.g., `/openapi/influxdb3-core/tags/tags/influxdb3-core-cache-data.yaml`) +- `update-route="true"` - URL updates as user navigates (default) +- No fixed height on container - expands to fit content + +## "On This Page" TOC + +Generated server-side from frontmatter `operations` array: + +```yaml +operations: + - operationId: PostConfigureDistinctCache + method: POST + path: /api/v3/configure/distinct_cache + summary: Create distinct cache + - operationId: DeleteConfigureDistinctCache + method: DELETE + path: /api/v3/configure/distinct_cache + summary: Delete distinct cache +``` + +TOC output: + +``` +ON THIS PAGE +- Overview +- POST /api/v3/configure/distinct_cache +- DELETE /api/v3/configure/distinct_cache +- POST /api/v3/configure/last_cache +- DELETE /api/v3/configure/last_cache +``` + +Links use `#{method}-{path}` format matching RapiDoc anchors. + +## Hash Navigation + +1. On page load, JS reads `window.location.hash` +2. If hash present, set RapiDoc's `goto-path` attribute (without the `#`) +3. RapiDoc's default `update-route=true` updates URL as user scrolls +4. Native URL sharing works + +## Files to Modify + +### Layouts + +- `layouts/api/list.html` - Embed RapiDoc instead of operation cards grid +- `layouts/partials/api/rapidoc-tag.html` - New partial for tag-level RapiDoc + +### JavaScript + +- `assets/js/components/rapidoc-mini.ts` - Add hash-based `goto-path` initialization + +### Remove/Deprecate + +- `layouts/api-path/path.html` - Path page layout +- `layouts/partials/api/rapidoc-path.html` - Path partial +- Generated path pages in `content/influxdb3/*/api/v*/` + +### Keep + +- Tag-specific spec files (`static/openapi/influxdb3-core/tags/tags/`) +- Generation script for tag pages and article data +- All endpoints page + +### Generation Script + +- Remove `generatePathPages()` function +- Keep tag page generation +- Ensure frontmatter `operations` array is complete for TOC + +## Development Scope + +Focus on `influxdb3/core` first, then migrate other products. + +## Testing + +- Verify tag pages load with all operations rendered +- Test hash navigation (direct URL, TOC clicks, browser back/forward) +- Verify no internal scrolling - page flows naturally +- Check visual separation between operations +- Test "On this page" TOC links diff --git a/docs/plans/2026-02-04-api-link-migration-design.md b/docs/plans/2026-02-04-api-link-migration-design.md new file mode 100644 index 000000000..4927e44b5 --- /dev/null +++ b/docs/plans/2026-02-04-api-link-migration-design.md @@ -0,0 +1,174 @@ +# API Link Migration: Redoc to RapiDoc Anchors + +## Overview + +Migrate all internal API operation links from Redoc's `#operation/{operationId}` format to RapiDoc's native `#{method}-{path}` format. + +## Background + +The RapiDoc migration changes how anchor links work for API operations: + +| Source | Pattern | Example | +|--------|---------|---------| +| **Redoc (old)** | `#operation/{operationId}` | `#operation/PostTasks` | +| **RapiDoc (new)** | `#{method}-{path}` | `#post-/api/v2/tasks` | + +**Scope:** 237 links across 111 content files use the Redoc pattern. + +**Constraint:** Don't modify source OpenAPI specs—transformation happens at the link level only. + +## Goals + +1. **Prevent 404s** for external links to API pages (base URL stability) +2. **Clean migration** of all internal links to RapiDoc's native format +3. **Validation** via link-checker after migration + +## Non-Goals + +- Backward compatibility for fragment identifiers (URL fragments are client-side only; server redirects can't translate them) +- External links with old `#operation/` fragments will land on the correct page but won't auto-scroll + +## URL Structure + +**API page URLs remain stable:** +- `/influxdb/cloud/api/` — All endpoints page +- `/influxdb3/core/api/` — All endpoints page +- `/influxdb3/core/api/{tag-name}/` — Tag page + +**Anchor format changes:** +- Old: `/influxdb/cloud/api/#operation/PostTasks` +- New: `/influxdb/cloud/api/#post-/api/v2/tasks` + +## RapiDoc Anchor Format + +RapiDoc uses `#{method}-{path}` with these conventions: + +- Method is lowercase: `post`, `get`, `delete`, `put`, `patch` +- Path parameters `{param}` become `-param-`: `/tasks/{taskID}` → `/tasks/-taskID-` +- Slashes in fragments are valid per RFC 3986 + +**Examples:** +``` +#get-/api/v2/tasks +#post-/api/v2/write +#delete-/api/v2/tasks/-taskID- +#get-/api/v2/tasks/-taskID-/runs/-runID- +``` + +## Migration Script Design + +### Location + +`helper-scripts/migrate-api-links.js` (one-time migration tool, plain JS) + +### Algorithm + +**Step 1: Build lookup table from OpenAPI specs** + +Key by product to handle duplicate operationIds across specs: + +```json +{ + "influxdb3/cloud-dedicated": { + "PostWrite": "post-/api/v2/write", + "GetDatabaseTokens": "get-/api/v0/accounts/-accountId-/clusters/-clusterId-/tokens" + }, + "influxdb3/core": { + "PostWrite": "post-/api/v3/write" + }, + "influxdb/cloud": { + "PostTasks": "post-/api/v2/tasks", + "GetTasksID": "get-/api/v2/tasks/-taskID-" + } +} +``` + +**Step 2: Scan and transform content files** + +``` +For each .md file in content/: + Find all patterns: #operation/(\w+) + Extract product from link URL path + Look up operationId in product's mapping + Replace with RapiDoc anchor format + Flag unmapped operationIds for manual review +``` + +**Step 3: Report** + +- Files modified +- Links updated (count) +- Unmapped operationIds (manual review needed) +- Dry-run mode available + +### Edge Cases + +| Case | Example | Handling | +|------|---------|----------| +| Path parameters | `{taskID}` | Replace with `-taskID-` in anchor | +| Multiple params | `/tasks/{taskID}/runs/{runID}` | Replace all params | +| Missing operationId | Path exists but no operationId in spec | Flag for manual review | +| Deprecated operations | Link to removed endpoint | Flag as potentially broken | + +### Usage + +```bash +# Dry-run (report only, no changes) +node helper-scripts/migrate-api-links.js --dry-run + +# Execute migration +node helper-scripts/migrate-api-links.js + +# Review changes +git diff content/ +``` + +## Validation + +### Pre-migration + +Verify API page URLs are stable: +- Check `_index.md` files have `aliases:` if paths changed +- Confirm no 404s for existing API base paths + +### Post-migration + +```bash +# Build site +npx hugo --quiet + +# Run link-checker on full site +link-checker check public/ +``` + +## Rollback + +Git provides easy rollback: + +```bash +git checkout -- content/ +``` + +## Files to Create/Modify + +### New Files + +- `helper-scripts/migrate-api-links.js` — Migration script + +### Modified Files + +- ~111 content files containing API operation links + +## Testing Checklist + +- [ ] Dry-run reports expected changes +- [ ] All operationIds map successfully (or flagged for review) +- [ ] Links transform to correct RapiDoc format +- [ ] Hugo build succeeds after migration +- [ ] Link-checker passes on full site +- [ ] Spot-check: anchors navigate to correct operations in browser + +## Related Documents + +- [API Tag Pages Design](2026-01-21-api-tag-pages-design.md) +- [API Reference RapiDoc Migration Plan](2026-01-07-api-reference-rapidoc-migration.md) diff --git a/docs/plans/2026-02-04-api-link-migration-implementation.md b/docs/plans/2026-02-04-api-link-migration-implementation.md new file mode 100644 index 000000000..e8f0d2888 --- /dev/null +++ b/docs/plans/2026-02-04-api-link-migration-implementation.md @@ -0,0 +1,605 @@ +# API Link Migration Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Migrate all 237 internal API links from Redoc `#operation/{operationId}` format to RapiDoc `#{method}-{path}` format. + +**Architecture:** One-time Node.js script that (1) parses OpenAPI specs to build operationId→anchor mapping, (2) scans content files for `#operation/` links, (3) replaces with RapiDoc anchors using the mapping. + +**Tech Stack:** Node.js, js-yaml (already in dependencies), glob (already in dependencies) + +--- + +## Spec Files → Product URL Mapping + +| Spec File | Product URL Prefix | +|-----------|-------------------| +| `api-docs/influxdb/cloud/v2/ref.yml` | `/influxdb/cloud/api/` | +| `api-docs/influxdb/v2/v2/ref.yml` | `/influxdb/v2/api/` | +| `api-docs/influxdb/v1/v1/ref.yml` | `/influxdb/v1/api/` | +| `api-docs/enterprise_influxdb/v1/v1/ref.yml` | `/enterprise_influxdb/v1/api/` | +| `api-docs/influxdb3/core/v3/ref.yml` | `/influxdb3/core/api/` | +| `api-docs/influxdb3/enterprise/v3/ref.yml` | `/influxdb3/enterprise/api/` | +| `api-docs/influxdb3/cloud-dedicated/v2/ref.yml` | `/influxdb3/cloud-dedicated/api/` | +| `api-docs/influxdb3/cloud-dedicated/management/openapi.yml` | `/influxdb3/cloud-dedicated/api/management/` | +| `api-docs/influxdb3/cloud-serverless/v2/ref.yml` | `/influxdb3/cloud-serverless/api/` | +| `api-docs/influxdb3/clustered/v2/ref.yml` | `/influxdb3/clustered/api/` | +| `api-docs/influxdb3/clustered/management/openapi.yml` | `/influxdb3/clustered/api/management/` | + +--- + +## Task 1: Create Migration Script Skeleton + +**Files:** +- Create: `helper-scripts/migrate-api-links.js` + +**Step 1: Create the script with CLI setup** + +```javascript +#!/usr/bin/env node +/** + * migrate-api-links.js + * + * One-time migration script to convert Redoc API links to RapiDoc format. + * + * Usage: + * node helper-scripts/migrate-api-links.js --dry-run # Preview changes + * node helper-scripts/migrate-api-links.js # Execute migration + */ + +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); +const { glob } = require('glob'); + +// CLI arguments +const args = process.argv.slice(2); +const DRY_RUN = args.includes('--dry-run'); +const VERBOSE = args.includes('--verbose'); + +// Paths +const ROOT_DIR = path.resolve(__dirname, '..'); +const CONTENT_DIR = path.join(ROOT_DIR, 'content'); +const API_DOCS_DIR = path.join(ROOT_DIR, 'api-docs'); + +// Spec file → product URL mapping +const SPEC_MAPPINGS = [ + { spec: 'influxdb/cloud/v2/ref.yml', urlPrefix: '/influxdb/cloud/api/' }, + { spec: 'influxdb/v2/v2/ref.yml', urlPrefix: '/influxdb/v2/api/' }, + { spec: 'influxdb/v1/v1/ref.yml', urlPrefix: '/influxdb/v1/api/' }, + { spec: 'enterprise_influxdb/v1/v1/ref.yml', urlPrefix: '/enterprise_influxdb/v1/api/' }, + { spec: 'influxdb3/core/v3/ref.yml', urlPrefix: '/influxdb3/core/api/' }, + { spec: 'influxdb3/enterprise/v3/ref.yml', urlPrefix: '/influxdb3/enterprise/api/' }, + { spec: 'influxdb3/cloud-dedicated/v2/ref.yml', urlPrefix: '/influxdb3/cloud-dedicated/api/' }, + { spec: 'influxdb3/cloud-dedicated/management/openapi.yml', urlPrefix: '/influxdb3/cloud-dedicated/api/management/' }, + { spec: 'influxdb3/cloud-serverless/v2/ref.yml', urlPrefix: '/influxdb3/cloud-serverless/api/' }, + { spec: 'influxdb3/clustered/v2/ref.yml', urlPrefix: '/influxdb3/clustered/api/' }, + { spec: 'influxdb3/clustered/management/openapi.yml', urlPrefix: '/influxdb3/clustered/api/management/' }, +]; + +console.log(`API Link Migration Script`); +console.log(`Mode: ${DRY_RUN ? 'DRY RUN (no changes)' : 'EXECUTE'}\n`); +``` + +**Step 2: Make it executable and test** + +Run: +```bash +chmod +x helper-scripts/migrate-api-links.js +node helper-scripts/migrate-api-links.js --dry-run +``` + +Expected: Script runs and prints header without errors. + +**Step 3: Commit** + +```bash +git add helper-scripts/migrate-api-links.js +git commit -m "feat(api): add migration script skeleton" +``` + +--- + +## Task 2: Build OperationId Lookup Table + +**Files:** +- Modify: `helper-scripts/migrate-api-links.js` + +**Step 1: Add function to parse spec and extract operationIds** + +Add after the SPEC_MAPPINGS constant: + +```javascript +/** + * Convert path parameters from {param} to -param- (RapiDoc format) + */ +function convertPathParams(path) { + return path.replace(/\{([^}]+)\}/g, '-$1-'); +} + +/** + * Build RapiDoc anchor from method and path + * Format: {method}-{path} with {param} → -param- + */ +function buildAnchor(method, pathStr) { + const convertedPath = convertPathParams(pathStr); + return `${method.toLowerCase()}-${convertedPath}`; +} + +/** + * Parse OpenAPI spec and extract operationId → anchor mapping + */ +function parseSpec(specPath) { + const mapping = {}; + + try { + const content = fs.readFileSync(specPath, 'utf8'); + const spec = yaml.load(content); + + if (!spec.paths) { + console.warn(` Warning: No paths in ${specPath}`); + return mapping; + } + + for (const [pathStr, pathItem] of Object.entries(spec.paths)) { + const methods = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head']; + + for (const method of methods) { + const operation = pathItem[method]; + if (operation && operation.operationId) { + const anchor = buildAnchor(method, pathStr); + mapping[operation.operationId] = anchor; + + if (VERBOSE) { + console.log(` ${operation.operationId} → #${anchor}`); + } + } + } + } + } catch (error) { + console.error(` Error parsing ${specPath}: ${error.message}`); + } + + return mapping; +} + +/** + * Build complete lookup table from all specs + * Returns: { urlPrefix: { operationId: anchor } } + */ +function buildLookupTable() { + const lookup = {}; + + console.log('Building operationId lookup table...\n'); + + for (const { spec, urlPrefix } of SPEC_MAPPINGS) { + const specPath = path.join(API_DOCS_DIR, spec); + + if (!fs.existsSync(specPath)) { + console.warn(` Skipping missing spec: ${spec}`); + continue; + } + + console.log(` Processing: ${spec}`); + const mapping = parseSpec(specPath); + lookup[urlPrefix] = mapping; + console.log(` Found ${Object.keys(mapping).length} operations`); + } + + console.log(''); + return lookup; +} + +// Test: Build and display lookup table +const lookupTable = buildLookupTable(); +console.log('Lookup table built successfully.\n'); +``` + +**Step 2: Test lookup table generation** + +Run: +```bash +node helper-scripts/migrate-api-links.js --dry-run --verbose 2>&1 | head -50 +``` + +Expected: See operationId mappings printed for each spec. + +**Step 3: Commit** + +```bash +git add helper-scripts/migrate-api-links.js +git commit -m "feat(api): add operationId lookup table generation" +``` + +--- + +## Task 3: Add Content File Scanner + +**Files:** +- Modify: `helper-scripts/migrate-api-links.js` + +**Step 1: Add function to find and parse links** + +Add after buildLookupTable function: + +```javascript +/** + * Find all #operation/ links in a file + * Returns array of { match, operationId, urlPath, fullUrl } + */ +function findOperationLinks(content) { + const links = []; + // Match patterns like: /influxdb/cloud/api/#operation/PostTasks + // or /influxdb3/cloud-dedicated/api/management/#operation/CreateDatabaseToken + const regex = /(\/[a-z0-9_/-]+\/api(?:\/management)?(?:\/[a-z0-9-]*)?\/)#operation\/(\w+)/g; + + let match; + while ((match = regex.exec(content)) !== null) { + links.push({ + match: match[0], + urlPath: match[1], + operationId: match[2], + }); + } + + return links; +} + +/** + * Find the best matching URL prefix for a given URL path + */ +function findUrlPrefix(urlPath, lookup) { + // Sort by length descending to match most specific first + const prefixes = Object.keys(lookup).sort((a, b) => b.length - a.length); + + for (const prefix of prefixes) { + if (urlPath.startsWith(prefix) || urlPath === prefix.slice(0, -1)) { + return prefix; + } + } + + return null; +} + +/** + * Scan content directory for files with #operation/ links + */ +async function scanContentFiles(lookup) { + console.log('Scanning content files for #operation/ links...\n'); + + const files = await glob('**/*.md', { cwd: CONTENT_DIR }); + const results = { + filesWithLinks: [], + totalLinks: 0, + unmapped: [], + }; + + for (const file of files) { + const filePath = path.join(CONTENT_DIR, file); + const content = fs.readFileSync(filePath, 'utf8'); + const links = findOperationLinks(content); + + if (links.length > 0) { + const fileResult = { + file, + links: [], + }; + + for (const link of links) { + const urlPrefix = findUrlPrefix(link.urlPath, lookup); + + if (!urlPrefix) { + results.unmapped.push({ file, ...link, reason: 'No matching URL prefix' }); + continue; + } + + const productLookup = lookup[urlPrefix]; + const anchor = productLookup[link.operationId]; + + if (!anchor) { + results.unmapped.push({ file, ...link, reason: 'OperationId not found in spec' }); + continue; + } + + fileResult.links.push({ + ...link, + urlPrefix, + newAnchor: anchor, + oldLink: `${link.urlPath}#operation/${link.operationId}`, + newLink: `${link.urlPath}#${anchor}`, + }); + } + + if (fileResult.links.length > 0) { + results.filesWithLinks.push(fileResult); + results.totalLinks += fileResult.links.length; + } + } + } + + return results; +} +``` + +**Step 2: Add main execution and reporting** + +Replace the test code at the bottom with: + +```javascript +async function main() { + // Build lookup table + const lookupTable = buildLookupTable(); + + // Scan content files + const results = await scanContentFiles(lookupTable); + + // Report findings + console.log('=== SCAN RESULTS ===\n'); + console.log(`Files with links: ${results.filesWithLinks.length}`); + console.log(`Total links to migrate: ${results.totalLinks}`); + console.log(`Unmapped links: ${results.unmapped.length}\n`); + + if (VERBOSE && results.filesWithLinks.length > 0) { + console.log('Links to migrate:'); + for (const { file, links } of results.filesWithLinks) { + console.log(`\n ${file}:`); + for (const link of links) { + console.log(` ${link.oldLink}`); + console.log(` → ${link.newLink}`); + } + } + } + + if (results.unmapped.length > 0) { + console.log('\n=== UNMAPPED LINKS (require manual review) ===\n'); + for (const item of results.unmapped) { + console.log(` ${item.file}:`); + console.log(` ${item.match}`); + console.log(` Reason: ${item.reason}\n`); + } + } + + if (DRY_RUN) { + console.log('\n[DRY RUN] No files modified. Run without --dry-run to apply changes.'); + } +} + +main().catch(console.error); +``` + +**Step 3: Test scanner** + +Run: +```bash +node helper-scripts/migrate-api-links.js --dry-run +``` + +Expected: See count of files and links found, plus any unmapped links. + +**Step 4: Commit** + +```bash +git add helper-scripts/migrate-api-links.js +git commit -m "feat(api): add content file scanner for operation links" +``` + +--- + +## Task 4: Add Link Replacement Logic + +**Files:** +- Modify: `helper-scripts/migrate-api-links.js` + +**Step 1: Add replacement function** + +Add before the main function: + +```javascript +/** + * Replace operation links in a file + * Returns the modified content + */ +function replaceLinks(content, links) { + let modified = content; + + for (const link of links) { + // Replace all occurrences of this specific link + modified = modified.split(link.oldLink).join(link.newLink); + } + + return modified; +} + +/** + * Apply migrations to files + */ +async function applyMigrations(results) { + console.log('\n=== APPLYING MIGRATIONS ===\n'); + + let filesModified = 0; + let linksReplaced = 0; + + for (const { file, links } of results.filesWithLinks) { + const filePath = path.join(CONTENT_DIR, file); + const originalContent = fs.readFileSync(filePath, 'utf8'); + const modifiedContent = replaceLinks(originalContent, links); + + if (originalContent !== modifiedContent) { + fs.writeFileSync(filePath, modifiedContent, 'utf8'); + filesModified++; + linksReplaced += links.length; + console.log(` ✓ ${file} (${links.length} links)`); + } + } + + console.log(`\nMigration complete: ${filesModified} files modified, ${linksReplaced} links replaced.`); +} +``` + +**Step 2: Update main function to apply changes** + +Update the main function to call applyMigrations when not in dry-run mode: + +```javascript +async function main() { + // Build lookup table + const lookupTable = buildLookupTable(); + + // Scan content files + const results = await scanContentFiles(lookupTable); + + // Report findings + console.log('=== SCAN RESULTS ===\n'); + console.log(`Files with links: ${results.filesWithLinks.length}`); + console.log(`Total links to migrate: ${results.totalLinks}`); + console.log(`Unmapped links: ${results.unmapped.length}\n`); + + if (VERBOSE && results.filesWithLinks.length > 0) { + console.log('Links to migrate:'); + for (const { file, links } of results.filesWithLinks) { + console.log(`\n ${file}:`); + for (const link of links) { + console.log(` ${link.oldLink}`); + console.log(` → ${link.newLink}`); + } + } + } + + if (results.unmapped.length > 0) { + console.log('\n=== UNMAPPED LINKS (require manual review) ===\n'); + for (const item of results.unmapped) { + console.log(` ${item.file}:`); + console.log(` ${item.match}`); + console.log(` Reason: ${item.reason}\n`); + } + } + + // Apply migrations if not dry-run + if (DRY_RUN) { + console.log('\n[DRY RUN] No files modified. Run without --dry-run to apply changes.'); + } else if (results.filesWithLinks.length > 0) { + await applyMigrations(results); + } else { + console.log('\nNo links to migrate.'); + } +} + +main().catch(console.error); +``` + +**Step 3: Test dry-run shows expected changes** + +Run: +```bash +node helper-scripts/migrate-api-links.js --dry-run --verbose 2>&1 | head -100 +``` + +Expected: See specific link transformations listed. + +**Step 4: Commit script completion** + +```bash +git add helper-scripts/migrate-api-links.js +git commit -m "feat(api): complete migration script with replacement logic" +``` + +--- + +## Task 5: Execute Migration + +**Step 1: Final dry-run review** + +Run: +```bash +node helper-scripts/migrate-api-links.js --dry-run +``` + +Review the output. Verify: +- Link count matches expectations (~237 links) +- No critical unmapped links +- Transformations look correct + +**Step 2: Execute migration** + +Run: +```bash +node helper-scripts/migrate-api-links.js +``` + +Expected: Files modified, links replaced. + +**Step 3: Review changes** + +Run: +```bash +git diff content/ | head -200 +``` + +Verify transformations look correct (spot check a few). + +**Step 4: Commit migrated content** + +```bash +git add content/ +git commit -m "refactor(api): migrate operation links to RapiDoc anchor format + +Migrated ~237 links from #operation/{operationId} to #{method}-{path} format +for RapiDoc compatibility." +``` + +--- + +## Task 6: Validate with Link-Checker + +**Step 1: Build Hugo site** + +Run: +```bash +npx hugo --quiet +``` + +Expected: Build succeeds without errors. + +**Step 2: Run link-checker** + +Run: +```bash +link-checker check public/ +``` + +Or if link-checker isn't installed globally: +```bash +# Map changed content files to HTML and check +git diff --name-only HEAD~1 HEAD | grep '\.md$' | head -20 | \ + xargs -I {} link-checker map {} | \ + xargs link-checker check +``` + +Expected: No broken links related to API anchors. + +**Step 3: Manual spot-check in browser** + +1. Start Hugo server: `npx hugo server` +2. Visit a page with migrated links +3. Click API links and verify they navigate to correct operations + +**Step 4: Final commit if any fixes needed** + +If link-checker found issues, fix and commit: +```bash +git add content/ +git commit -m "fix(api): correct link migration issues found by link-checker" +``` + +--- + +## Summary + +| Task | Description | Output | +|------|-------------|--------| +| 1 | Script skeleton | `helper-scripts/migrate-api-links.js` | +| 2 | Lookup table generation | operationId → anchor mapping | +| 3 | Content file scanner | Find all `#operation/` links | +| 4 | Replacement logic | Transform links in place | +| 5 | Execute migration | ~237 links migrated | +| 6 | Validate | Link-checker passes | diff --git a/docs/plans/2026-02-04-v1-api-deduplication-design.md b/docs/plans/2026-02-04-v1-api-deduplication-design.md new file mode 100644 index 000000000..4fb66b9b3 --- /dev/null +++ b/docs/plans/2026-02-04-v1-api-deduplication-design.md @@ -0,0 +1,93 @@ +# InfluxDB v1 API Consistency Design + +**Date:** 2026-02-04 +**Goal:** Make InfluxDB v1 API specs consistent with other products by using the same Redocly-based overlay approach. + +## Current State + +- `api-docs/influxdb/v1/v1/ref.yml` - Complete standalone spec (OSS) +- `api-docs/enterprise_influxdb/v1/v1/ref.yml` - Complete standalone spec (Enterprise) +- Not using Redocly decorators or content overlays +- Not integrated with `getswagger.sh` + +## Target State + +- Both v1 products use `.config.yml` and `content/` overlays like other products +- Integrated with `getswagger.sh` and Redocly decorator pipeline +- Remove unused tag-groups decorator (not used by RapiDoc) + +## Design Decisions + +1. **Keep both specs as complete, standalone files** - Accept duplication for simplicity +2. **Use overlays for info and servers only** - Paths stay in each `ref.yml` +3. **Remove tag-groups entirely** - Not used by RapiDoc UI + +## Implementation + +### 1. Directory Structure + +``` +api-docs/ + influxdb/ + v1/ + .config.yml # Redocly config + v1/ + content/ + info.yml # OSS info overlay + servers.yml # OSS servers overlay + ref.yml # Complete OSS spec (exists) + enterprise_influxdb/ + v1/ + .config.yml # Redocly config + v1/ + content/ + info.yml # Enterprise info overlay + servers.yml # Enterprise servers overlay + ref.yml # Complete Enterprise spec (exists) +``` + +### 2. Redocly Decorator Changes + +**Remove (unused with RapiDoc):** + +- `openapi/plugins/decorators/tags/set-tag-groups.cjs` +- `tag-groups.yml` loading from `docs-content.cjs` +- `set-tag-groups` references in `docs-plugin.cjs` +- All `content/tag-groups.yml` files across products + +**Keep:** + +- `set-info.cjs` - merges info.yml overlay +- `set-servers.cjs` - merges servers.yml overlay +- `replace-shortcodes.cjs` - handles doc URL placeholders + +### 3. getswagger.sh Changes + +Add functions: + +```bash +function updateOSSV1 { + postProcess influxdb/v1/v1/ref.yml 'influxdb/v1/.config.yml' 'v1@1' +} + +function updateEnterpriseV1 { + postProcess enterprise_influxdb/v1/v1/ref.yml 'enterprise_influxdb/v1/.config.yml' 'v1@1' +} +``` + +## Tasks + +1. [x] Create `influxdb/v1/.config.yml` +2. [x] Create `influxdb/v1/v1/content/info.yml` +3. [x] Create `influxdb/v1/v1/content/servers.yml` +4. [x] Create `enterprise_influxdb/v1/.config.yml` +5. [x] Create `enterprise_influxdb/v1/v1/content/info.yml` +6. [x] Create `enterprise_influxdb/v1/v1/content/servers.yml` +7. [x] Remove tag-groups decorator and all tag-groups.yml files +8. [x] Add `updateOSSV1()` and `updateEnterpriseV1()` to getswagger.sh +9. [x] Test: Run getswagger.sh for both v1 products +10. [x] Test: Verify API pages render correctly + +## Completed: 2026-02-04 + +All tasks completed successfully. The v1 products now use the same Redocly overlay pattern as other products. diff --git a/docs/plans/2026-02-13-hugo-native-api-migration.md b/docs/plans/2026-02-13-hugo-native-api-migration.md new file mode 100644 index 000000000..99bb7616a --- /dev/null +++ b/docs/plans/2026-02-13-hugo-native-api-migration.md @@ -0,0 +1,344 @@ +# 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 ✅ COMPLETED + +**Priority:** Medium | **Status:** Completed 2026-02-13 + +Remove RapiDoc-specific styles, JavaScript, and references from the codebase. + +**Files modified:** + +- `assets/styles/layouts/_api-layout.scss` - Removed \~40 lines of `rapi-doc::part()` CSS selectors +- `assets/styles/layouts/_api-overrides.scss` - Updated comment header +- `assets/styles/layouts/_api-security-schemes.scss` - Removed \~290 lines of dead auth modal styles +- `assets/js/main.js` - Removed dead `api-auth-input` import and registration +- `assets/js/components/api-toc.ts` - Removed RapiDoc-specific code and updated comments + +**Files deleted:** + +- `static/css/rapidoc-custom.css` - Unused static CSS file + +**Changes:** + +1. ✅ Removed `rapi-doc` container styling and `::part()` selectors from `_api-layout.scss` +2. ✅ Removed dead auth modal section from `_api-security-schemes.scss` (was for RapiDoc "Try it" integration) +3. ✅ Removed `api-auth-input` dead import from `main.js` (component file was already deleted) +4. ✅ Removed `setupRapiDocNavigation()` dead function and references from `api-toc.ts` +5. ✅ Updated comments throughout to remove RapiDoc mentions +6. ✅ Rebuilt `api-docs/scripts/dist/` to update compiled JavaScript + +**Architecture decision:** Kept operation styles separate from layout styles for cleaner separation of concerns: + +- `_api-layout.scss` handles page structure and navigation +- `_api-operations.scss` handles operation/schema component rendering (renamed from `_api-hugo-native.scss`) + +*** + +### Task 7: Fix Generation Script for Clean Regeneration ✅ COMPLETED + +**Priority:** Medium | **Status:** Completed 2026-02-17 + +Added clean regeneration to prevent stale files from accumulating when tags are renamed or removed. + +**Files modified:** + +- `api-docs/scripts/generate-openapi-articles.ts` - Added cleanup functions and CLI flags + +**Implementation:** + +1. ✅ Added `--no-clean` flag to skip cleanup (default is to clean) +2. ✅ Added `--dry-run` flag to preview what would be deleted +3. ✅ Added `getCleanupPaths()` function to identify directories/files to clean +4. ✅ Added `cleanProductOutputs()` function to delete directories and files +5. ✅ Added `showDryRunPreview()` function for dry-run output +6. ✅ Integrated cleanup into `processProduct()` (runs before generation) +7. ✅ Updated script header documentation with new usage examples + +**Cleaned directories per product:** + +- `static/openapi/{staticDirName}/` - Tag specs +- `static/openapi/{staticDirName}-*.yml` and `.json` - Root specs +- `data/article_data/influxdb/{productKey}/` - Article data +- `content/{pagesDir}/api/` - Content pages + +**Design:** See `plans/2026-02-17-api-clean-regeneration-design.md` + +*** + +### Task 8: Apply Cache Data tag split to InfluxDB 3 Enterprise + +**Priority:** Medium + +Apply the same tag split done for Core. + +**Files to modify:** + +- `api-docs/influxdb3/enterprise/v3/ref.yml` + +**Changes:** + +1. Replace "Cache data" tag with "Cache distinct values" and "Cache last value" tags +2. Update operation tag references +3. Update x-tagGroups references +4. Regenerate: `sh api-docs/generate-api-docs.sh` + +*** + +### Task 9: Migrate remaining products to Hugo-native + +**Priority:** Medium + +After the infrastructure is in place, migrate remaining products. + +**Products:** + +- [ ] cloud-dedicated (management API) +- [ ] cloud-serverless +- [ ] clustered (management API) +- [ ] cloud-v2 +- [ ] oss-v2 +- [ ] oss-v1 + +**For each product:** + +1. Review tag structure in OpenAPI spec +2. Add `x-influxdata-related` links where appropriate +3. Clean and regenerate +4. Verify all tag pages render correctly + +*** + +## Key Files Reference + +**Hugo-Native Templates (after migration):** + +- `layouts/partials/api/tag-renderer.html` - Main tag page renderer +- `layouts/partials/api/operation.html` - Individual operation renderer +- `layouts/partials/api/parameters.html` - Parameters section +- `layouts/partials/api/parameter-row.html` - Single parameter row +- `layouts/partials/api/request-body.html` - Request body section +- `layouts/partials/api/schema.html` - JSON schema renderer +- `layouts/partials/api/responses.html` - Response section + +**Layouts:** + +- `layouts/api/list.html` - Tag page layout (Hugo-native only) +- `layouts/api/section.html` - API section page layout +- `layouts/api/all-endpoints.html` - All endpoints page layout + +**Styles:** + +- `assets/styles/layouts/_api-layout.scss` - Consolidated API styles + +**Generation:** + +- `api-docs/scripts/generate-openapi-articles.ts` - Main generation script +- `api-docs/scripts/openapi-paths-to-hugo-data/index.ts` - OpenAPI processing + +*** + +## Verification Checklist + +Before considering migration complete for each product: + +- [ ] All tag pages render without errors +- [ ] Operation details (parameters, request body, responses) display correctly +- [ ] Schema references resolve and render +- [ ] `x-influxdata-related` links appear at page bottom +- [ ] Navigation shows correct tag structure +- [ ] Mobile responsive layout works +- [ ] No console errors in browser DevTools +- [ ] "On this page" TOC links work correctly +- [ ] Cypress tests pass +- [ ] No RapiDoc references remain in codebase + +## Files to Delete (Summary) + +**Already deleted (Tasks 1-3):** + +- ✅ `layouts/partials/api/rapidoc.html` +- ✅ `layouts/partials/api/rapidoc-tag.html` +- ✅ `layouts/partials/api/rapidoc-mini.html` +- ✅ `layouts/partials/api/hugo-native/` (entire directory - 7 files moved to parent) +- ✅ `assets/js/components/api-rapidoc.ts` +- ✅ `assets/js/components/rapidoc-mini.ts` + +**Still to review (Task 6):** + +- `assets/styles/layouts/_api-overrides.scss` (if RapiDoc-only) + +*** + +## Migration Findings + +### Completed Work Summary (Tasks 1-5) + +**Infrastructure changes:** + +- Hugo-native templates are now the default (no feature flag required) +- All RapiDoc code removed from layouts and JavaScript +- Generation scripts cleaned up (\~200 lines of dead code removed) +- Cypress tests simplified (no more shadow DOM piercing) + +**Key discoveries:** + +1. The `useHugoNative` flag did not exist in the codebase - operation page generation was already disabled +2. The `generatePathPages` function was dead code that could be safely removed +3. RapiDoc Mini tests were \~160 lines that are no longer needed +4. Hugo build and TypeScript compilation both pass after all changes + +**Verification status:** + +- ✅ Hugo build: `npx hugo --quiet` passes +- ✅ TypeScript: `yarn build:ts` passes +- ⏳ Cypress tests: Need to run `yarn test:e2e` to verify new tests pass +- ⏳ Visual review: Need to check pages render correctly in browser + +### Remaining Work (Tasks 6-9) + +1. **Task 6 (styles)**: Review and consolidate SCSS files +2. **Task 7 (clean regeneration)**: Add `--clean` flag to generation scripts +3. **Task 8 (Enterprise tags)**: Split Cache Data tag in Enterprise spec +4. **Task 9 (product migration)**: Apply to remaining 6 products diff --git a/docs/plans/2026-02-17-api-clean-regeneration-design.md b/docs/plans/2026-02-17-api-clean-regeneration-design.md new file mode 100644 index 000000000..06e62e6cc --- /dev/null +++ b/docs/plans/2026-02-17-api-clean-regeneration-design.md @@ -0,0 +1,160 @@ +# API Clean Regeneration Design + +**Goal:** Add clean regeneration to `generate-openapi-articles.ts` to prevent stale files from accumulating when tags are renamed or removed. + +**Problem:** When OpenAPI tags are renamed (e.g., "Cache data" → "Cache distinct values" + "Cache last value"), old generated files persist alongside new ones, causing navigation confusion and stale content. + +*** + +## CLI Interface + +**New flags:** + +| Flag | Description | +| ------------ | ---------------------------------------------------- | +| `--no-clean` | Skip directory cleanup (preserve existing files) | +| `--dry-run` | Show what would be deleted without actually deleting | + +**Behavior:** + +- Default is to clean before generating (no flag needed) +- `--dry-run` implies `--no-clean` (shows deletions but doesn't execute or generate) +- Existing flags (`--validate-links`, `--skip-fetch`) continue to work + +**Usage examples:** + +```bash +# Default: clean and regenerate all products +node generate-openapi-articles.js + +# Clean and regenerate specific product +node generate-openapi-articles.js influxdb3_core + +# Preview what would be deleted +node generate-openapi-articles.js --dry-run + +# Preserve existing files (legacy behavior) +node generate-openapi-articles.js --no-clean +``` + +*** + +## Directories Cleaned Per Product + +For each product (e.g., `influxdb3_core`), the following are cleaned: + +| Location | Pattern | Example | +| ------------- | -------------------------------------------------- | -------------------------------------------- | +| Tag specs | `static/openapi/{staticDirName}/` | `static/openapi/influxdb3-core/` | +| Root specs | `static/openapi/{staticDirName}-*.yml` and `.json` | `static/openapi/influxdb3-core-ref.yml` | +| Article data | `data/article_data/influxdb/{productKey}/` | `data/article_data/influxdb/influxdb3_core/` | +| Content pages | `content/{pagesDir}/api/` | `content/influxdb3/core/api/` | + +**Boundaries:** + +- Only cleans the `api/` subdirectory within content, not the entire product +- Only cleans files matching the product's `staticDirName` pattern +- Never touches other products' files +- Multi-spec products (cloud-dedicated, clustered) clean all spec variants + +*** + +## Dry-Run Output Format + +``` +$ node generate-openapi-articles.js influxdb3_core --dry-run + +DRY RUN: Would clean the following for influxdb3_core: + +Directories to remove: + - static/openapi/influxdb3-core/ + - data/article_data/influxdb/influxdb3_core/ + - content/influxdb3/core/api/ + +Files to remove: + - static/openapi/influxdb3-core-ref.yml + - static/openapi/influxdb3-core-ref.json + +Summary: 3 directories, 2 files would be removed + +Skipping generation (dry-run mode). +``` + +*** + +## Code Structure + +**File modified:** `api-docs/scripts/generate-openapi-articles.ts` + +**New CLI flag parsing:** + +```typescript +const noClean = process.argv.includes('--no-clean'); +const dryRun = process.argv.includes('--dry-run'); +``` + +**New functions:** + +```typescript +/** + * Get all paths that would be cleaned for a product + */ +function getCleanupPaths(productKey: string, config: ProductConfig): { + directories: string[]; + files: string[]; +} + +/** + * Clean output directories for a product before regeneration + */ +function cleanProductOutputs(productKey: string, config: ProductConfig): void + +/** + * Display dry-run preview of what would be cleaned + */ +function showDryRunPreview(productKey: string, config: ProductConfig): void +``` + +**Changes to `processProduct()`:** + +```typescript +function processProduct(productKey: string, config: ProductConfig): void { + // Clean before generating (unless --no-clean or --dry-run) + if (!noClean && !dryRun) { + cleanProductOutputs(productKey, config); + } + + // Existing generation logic... +} +``` + +**Changes to `main()`:** + +```typescript +function main(): void { + // Handle dry-run mode + if (dryRun) { + productsToProcess.forEach((productKey) => { + showDryRunPreview(productKey, productConfigs[productKey]); + }); + console.log('\nDry run complete. No files were modified.'); + return; // Exit without generating + } + + // Existing processing logic... +} +``` + +**No changes to:** `openapi-paths-to-hugo-data/index.ts` + +*** + +## Verification + +After implementation: + +1. Run `--dry-run` and verify output matches expected format +2. Run without flags and verify old files are removed +3. Run with `--no-clean` and verify files are preserved +4. Verify Hugo build passes after clean regeneration +5. Verify no stale tag pages appear in navigation diff --git a/docs/plans/2026-02-17-api-clean-regeneration-implementation.md b/docs/plans/2026-02-17-api-clean-regeneration-implementation.md new file mode 100644 index 000000000..723b140d6 --- /dev/null +++ b/docs/plans/2026-02-17-api-clean-regeneration-implementation.md @@ -0,0 +1,519 @@ +# API Clean Regeneration Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add `--no-clean` and `--dry-run` flags to `generate-openapi-articles.ts` so stale files are automatically removed before regeneration. + +**Architecture:** Delete-and-regenerate approach. Before processing each product, remove its output directories (static specs, article data, content pages), then generate fresh. Default behavior is to clean; `--no-clean` preserves existing files. + +**Tech Stack:** TypeScript, Node.js fs module, existing generation script + +**Design:** See `plans/2026-02-17-api-clean-regeneration-design.md` + +*** + +## Task 1: Add CLI Flag Parsing + +**Files:** + +- Modify: `api-docs/scripts/generate-openapi-articles.ts:88-89` + +**Step 1: Add flag constants after existing flags** + +Find the existing CLI flags section (around line 88): + +```typescript +// CLI flags +const validateLinks = process.argv.includes('--validate-links'); +const skipFetch = process.argv.includes('--skip-fetch'); +``` + +Add new flags below: + +```typescript +// CLI flags +const validateLinks = process.argv.includes('--validate-links'); +const skipFetch = process.argv.includes('--skip-fetch'); +const noClean = process.argv.includes('--no-clean'); +const dryRun = process.argv.includes('--dry-run'); +``` + +**Step 2: Verify TypeScript compiles** + +Run: `yarn build:ts` +Expected: Compiles without errors + +**Step 3: Commit** + +```bash +git add api-docs/scripts/generate-openapi-articles.ts +git commit -m "feat(api): add --no-clean and --dry-run CLI flags" +``` + +*** + +## Task 2: Add getCleanupPaths Function + +**Files:** + +- Modify: `api-docs/scripts/generate-openapi-articles.ts` + +**Step 1: Add getCleanupPaths function after getStaticDirName function (around line 170)** + +```typescript +/** + * Get all paths that would be cleaned for a product + * + * @param productKey - Product identifier (e.g., 'influxdb3_core') + * @param config - Product configuration + * @returns Object with directories and files arrays + */ +function getCleanupPaths( + productKey: string, + config: ProductConfig +): { directories: string[]; files: string[] } { + const staticDirName = getStaticDirName(productKey); + const staticPath = path.join(DOCS_ROOT, 'static/openapi'); + + const directories: string[] = []; + const files: string[] = []; + + // Tag specs directory: static/openapi/{staticDirName}/ + const tagSpecsDir = path.join(staticPath, staticDirName); + if (fs.existsSync(tagSpecsDir)) { + directories.push(tagSpecsDir); + } + + // Article data directory: data/article_data/influxdb/{productKey}/ + const articleDataDir = path.join( + DOCS_ROOT, + `data/article_data/influxdb/${productKey}` + ); + if (fs.existsSync(articleDataDir)) { + directories.push(articleDataDir); + } + + // Content pages directory: content/{pagesDir}/api/ + const contentApiDir = path.join(config.pagesDir, 'api'); + if (fs.existsSync(contentApiDir)) { + directories.push(contentApiDir); + } + + // Root spec files: static/openapi/{staticDirName}-*.yml and .json + if (fs.existsSync(staticPath)) { + const staticFiles = fs.readdirSync(staticPath); + const pattern = new RegExp(`^${staticDirName}-.*\\.(yml|json)$`); + staticFiles + .filter((f) => pattern.test(f)) + .forEach((f) => { + files.push(path.join(staticPath, f)); + }); + } + + return { directories, files }; +} +``` + +**Step 2: Verify TypeScript compiles** + +Run: `yarn build:ts` +Expected: Compiles without errors + +**Step 3: Commit** + +```bash +git add api-docs/scripts/generate-openapi-articles.ts +git commit -m "feat(api): add getCleanupPaths function" +``` + +*** + +## Task 3: Add cleanProductOutputs Function + +**Files:** + +- Modify: `api-docs/scripts/generate-openapi-articles.ts` + +**Step 1: Add cleanProductOutputs function after getCleanupPaths** + +```typescript +/** + * Clean output directories for a product before regeneration + * + * @param productKey - Product identifier + * @param config - Product configuration + */ +function cleanProductOutputs(productKey: string, config: ProductConfig): void { + const { directories, files } = getCleanupPaths(productKey, config); + + // Remove directories recursively + for (const dir of directories) { + console.log(`🧹 Removing directory: ${dir}`); + fs.rmSync(dir, { recursive: true, force: true }); + } + + // Remove individual files + for (const file of files) { + console.log(`🧹 Removing file: ${file}`); + fs.unlinkSync(file); + } + + const total = directories.length + files.length; + if (total > 0) { + console.log( + `✓ Cleaned ${directories.length} directories, ${files.length} files for ${productKey}` + ); + } +} +``` + +**Step 2: Verify TypeScript compiles** + +Run: `yarn build:ts` +Expected: Compiles without errors + +**Step 3: Commit** + +```bash +git add api-docs/scripts/generate-openapi-articles.ts +git commit -m "feat(api): add cleanProductOutputs function" +``` + +*** + +## Task 4: Add showDryRunPreview Function + +**Files:** + +- Modify: `api-docs/scripts/generate-openapi-articles.ts` + +**Step 1: Add showDryRunPreview function after cleanProductOutputs** + +```typescript +/** + * Display dry-run preview of what would be cleaned + * + * @param productKey - Product identifier + * @param config - Product configuration + */ +function showDryRunPreview(productKey: string, config: ProductConfig): void { + const { directories, files } = getCleanupPaths(productKey, config); + + console.log(`\nDRY RUN: Would clean the following for ${productKey}:\n`); + + if (directories.length > 0) { + console.log('Directories to remove:'); + directories.forEach((dir) => console.log(` - ${dir}`)); + } + + if (files.length > 0) { + console.log('\nFiles to remove:'); + files.forEach((file) => console.log(` - ${file}`)); + } + + if (directories.length === 0 && files.length === 0) { + console.log(' (no files to clean)'); + } + + console.log( + `\nSummary: ${directories.length} directories, ${files.length} files would be removed` + ); +} +``` + +**Step 2: Verify TypeScript compiles** + +Run: `yarn build:ts` +Expected: Compiles without errors + +**Step 3: Commit** + +```bash +git add api-docs/scripts/generate-openapi-articles.ts +git commit -m "feat(api): add showDryRunPreview function" +``` + +*** + +## Task 5: Integrate Cleanup into processProduct + +**Files:** + +- Modify: `api-docs/scripts/generate-openapi-articles.ts:1129-1135` + +**Step 1: Add cleanup call at the start of processProduct function** + +Find the beginning of `processProduct` function (around line 1129): + +```typescript +function processProduct(productKey: string, config: ProductConfig): void { + console.log('\n' + '='.repeat(80)); + console.log(`Processing ${config.description || productKey}`); + console.log('='.repeat(80)); +``` + +Add cleanup after the header output: + +```typescript +function processProduct(productKey: string, config: ProductConfig): void { + console.log('\n' + '='.repeat(80)); + console.log(`Processing ${config.description || productKey}`); + console.log('='.repeat(80)); + + // Clean output directories before regeneration (unless --no-clean) + if (!noClean && !dryRun) { + cleanProductOutputs(productKey, config); + } +``` + +**Step 2: Verify TypeScript compiles** + +Run: `yarn build:ts` +Expected: Compiles without errors + +**Step 3: Commit** + +```bash +git add api-docs/scripts/generate-openapi-articles.ts +git commit -m "feat(api): integrate cleanup into processProduct" +``` + +*** + +## Task 6: Add Dry-Run Mode to main Function + +**Files:** + +- Modify: `api-docs/scripts/generate-openapi-articles.ts:1307-1346` + +**Step 1: Add dry-run handling after product validation in main()** + +Find the section after product validation (around line 1340): + +```typescript + // Validate product keys + const invalidProducts = productsToProcess.filter( + (key) => !productConfigs[key] + ); + if (invalidProducts.length > 0) { + // ... error handling ... + } + + // Process each product + productsToProcess.forEach((productKey) => { +``` + +Add dry-run handling before the forEach: + +```typescript + // Validate product keys + const invalidProducts = productsToProcess.filter( + (key) => !productConfigs[key] + ); + if (invalidProducts.length > 0) { + // ... error handling ... + } + + // Handle dry-run mode + if (dryRun) { + console.log('\n📋 DRY RUN MODE - No files will be modified\n'); + productsToProcess.forEach((productKey) => { + showDryRunPreview(productKey, productConfigs[productKey]); + }); + console.log('\nDry run complete. No files were modified.'); + return; + } + + // Process each product + productsToProcess.forEach((productKey) => { +``` + +**Step 2: Verify TypeScript compiles** + +Run: `yarn build:ts` +Expected: Compiles without errors + +**Step 3: Commit** + +```bash +git add api-docs/scripts/generate-openapi-articles.ts +git commit -m "feat(api): add dry-run mode to main function" +``` + +*** + +## Task 7: Update Script Header Documentation + +**Files:** + +- Modify: `api-docs/scripts/generate-openapi-articles.ts:1-21` + +**Step 1: Update the usage documentation in the file header** + +Find the header comment block and update: + +```typescript +#!/usr/bin/env node +/** + * Generate OpenAPI Articles Script + * + * Generates Hugo data files and content pages from OpenAPI specifications + * for all InfluxDB products. + * + * This script: + * 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 # 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 + */ +``` + +**Step 2: Verify TypeScript compiles** + +Run: `yarn build:ts` +Expected: Compiles without errors + +**Step 3: Commit** + +```bash +git add api-docs/scripts/generate-openapi-articles.ts +git commit -m "docs(api): update script header with new flags" +``` + +*** + +## Task 8: Rebuild Compiled JavaScript + +**Files:** + +- Modify: `api-docs/scripts/dist/generate-openapi-articles.js` (generated) + +**Step 1: Rebuild TypeScript** + +Run: `yarn build:ts` +Expected: Compiles without errors, updates dist/ files + +**Step 2: Verify the compiled output includes new functions** + +Run: `grep -n "getCleanupPaths\|cleanProductOutputs\|showDryRunPreview\|noClean\|dryRun" api-docs/scripts/dist/generate-openapi-articles.js | head -10` +Expected: Shows line numbers where new code appears + +**Step 3: Commit compiled output** + +```bash +git add api-docs/scripts/dist/ +git commit -m "build(api): rebuild compiled generation scripts" +``` + +*** + +## Task 9: Manual Testing + +**Step 1: Test dry-run mode** + +Run: `node api-docs/scripts/dist/generate-openapi-articles.js influxdb3_core --dry-run` + +Expected output format: + +``` +📋 DRY RUN MODE - No files will be modified + +DRY RUN: Would clean the following for influxdb3_core: + +Directories to remove: + - static/openapi/influxdb3-core + - data/article_data/influxdb/influxdb3_core + - content/influxdb3/core/api + +Files to remove: + - static/openapi/influxdb3-core-ref.yml + - static/openapi/influxdb3-core-ref.json + +Summary: 3 directories, 2 files would be removed + +Dry run complete. No files were modified. +``` + +**Step 2: Verify files were NOT deleted after dry-run** + +Run: `ls content/influxdb3/core/api/` +Expected: Directory still exists with content + +**Step 3: Test actual clean regeneration** + +Run: `node api-docs/scripts/dist/generate-openapi-articles.js influxdb3_core --skip-fetch` +Expected: Shows cleanup messages, then regenerates successfully + +**Step 4: Verify Hugo build passes** + +Run: `npx hugo --quiet` +Expected: Builds without errors + +**Step 5: Test --no-clean flag preserves files** + +First, create a marker file: + +```bash +touch content/influxdb3/core/api/MARKER_FILE.md +``` + +Run: `node api-docs/scripts/dist/generate-openapi-articles.js influxdb3_core --skip-fetch --no-clean` + +Verify marker still exists: + +```bash +ls content/influxdb3/core/api/MARKER_FILE.md +``` + +Expected: File exists + +Clean up marker: + +```bash +rm content/influxdb3/core/api/MARKER_FILE.md +``` + +*** + +## Task 10: Update Migration Plan + +**Files:** + +- Modify: `plans/2026-02-13-hugo-native-api-migration.md` + +**Step 1: Mark Task 7 as completed** + +Update the task status from planned to completed. + +**Step 2: Commit** + +```bash +git add plans/2026-02-13-hugo-native-api-migration.md +git commit -m "docs(plan): mark Task 7 (clean regeneration) as completed" +``` + +*** + +## Verification Checklist + +Before considering complete: + +- [ ] `yarn build:ts` compiles without errors +- [ ] `--dry-run` shows expected output format +- [ ] `--dry-run` does NOT delete any files +- [ ] Default mode (no flags) cleans before regenerating +- [ ] `--no-clean` preserves existing files +- [ ] `npx hugo --quiet` builds successfully after regeneration +- [ ] All new code is committed diff --git a/docs/plans/2026-03-07-api-code-samples-design.md b/docs/plans/2026-03-07-api-code-samples-design.md new file mode 100644 index 000000000..1ef32cc2b --- /dev/null +++ b/docs/plans/2026-03-07-api-code-samples-design.md @@ -0,0 +1,125 @@ +# API Code Samples & Ask AI Integration Plan + +## Scope + +This plan covers: + +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 + +**Out of scope** (separate plans): + +- 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) + +*** + +## 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. + +The existing `influxdb-url.js` automatically replaces the default placeholder host in `
` elements with the user's custom URL. No new JavaScript is needed for URL personalization.
+
+### Operation layout order (revised)
+
+1. Header (method + path + summary)
+2. Description
+3. Parameters
+4. Request Body
+5. **Example (curl + Ask AI)** — new
+6. Responses
+
+***
+
+## curl Example Generation
+
+### Partial: `layouts/partials/api/code-sample.html`
+
+Receives the operation definition (`$opDef`), spec (`$spec`), and operation metadata from `operation.html`. Constructs a curl command:
+
+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.
+
+### 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
+
+  Ask AI about this example
+
+```
+
+***
+
+## Client Library Related Link
+
+The generation script adds a related link to `/influxdb3/{product}/reference/client-libraries/v3/` for all InfluxDB 3 product tag pages.
+
+**InfluxDB 3 products** (identified by `pagesDir` containing `influxdb3/`):
+
+- `influxdb3_core`
+- `influxdb3_enterprise`
+- `cloud-dedicated`
+- `cloud-serverless`
+- `clustered`
+
+**Excluded** (future plan with v2 client library links):
+
+- `cloud-v2`, `oss-v2`, `oss-v1`, `enterprise-v1`
+
+The `{product}` segment is derived from the `pagesDir` (e.g., `content/influxdb3/core` yields `core`).
+
+***
+
+## File Changes
+
+### New files
+
+| 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 |
+
+### Modified files
+
+| 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 |
+
+### Not modified
+
+| 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    |
+
+***
+
+## Verification
+
+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)
diff --git a/docs/plans/TESTING.md b/docs/plans/TESTING.md
new file mode 100644
index 000000000..7b2888650
--- /dev/null
+++ b/docs/plans/TESTING.md
@@ -0,0 +1,77 @@
+# API Reference Testing Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Validate Hugo-native API reference pages render correctly and all tests pass.
+
+**Architecture:** Hugo-native rendering uses standard HTML without shadow DOM, making tests simpler. No RapiDoc web components - operations are rendered server-side by Hugo templates.
+
+**Tech Stack:** Hugo templates, SCSS, Cypress
+
+***
+
+## Test Structure
+
+The API reference tests validate:
+
+1. **API index pages** - Main API landing pages load correctly
+2. **API tag pages** - Tag pages render operations with parameters/responses
+3. **Section structure** - Section pages list tag children correctly
+4. **All endpoints** - All endpoints page shows all operations
+5. **Layout** - 3-column layout with sidebar, content, and TOC
+
+## Running Tests
+
+### Quick validation
+
+```bash
+# Build site
+yarn hugo --quiet
+
+# Start server
+yarn hugo server &
+
+# Test pages load
+curl -s -o /dev/null -w "%{http_code}" http://localhost:1313/influxdb3/core/api/
+# Expected: 200
+
+# Run Cypress tests (for example, for InfluxDB 3 Core)
+node cypress/support/run-e2e-specs.js --spec "cypress/e2e/content/api-reference.cy.js" content/influxdb3/core/api/_index.md
+
+# Stop server
+pkill -f "hugo server"
+```
+
+### Full test suite
+
+```bash
+node cypress/support/run-e2e-specs.js --spec "cypress/e2e/content/api-reference.cy.js"
+```
+
+## Test Selectors (Hugo-Native)
+
+Since Hugo-native uses standard HTML, tests use simple CSS selectors:
+
+| Element          | Selector            |
+| ---------------- | ------------------- |
+| Page title       | `h1`                |
+| Operation        | `.api-operation`    |
+| Method badge     | `.api-method`       |
+| Path             | `.api-path`         |
+| Parameters table | `.api-parameters`   |
+| Request body     | `.api-request-body` |
+| Responses        | `.api-responses`    |
+| TOC              | `.api-toc`          |
+| Related links    | `.article--related` |
+
+## Expected Test Coverage
+
+- [ ] API index pages (Core, Enterprise, Cloud Dedicated, Clustered, Cloud Serverless)
+- [ ] Tag pages render operations
+- [ ] Parameters display correctly
+- [ ] Request body sections display
+- [ ] Response sections display
+- [ ] TOC links work
+- [ ] All endpoints page lists operations
+- [ ] Section pages list tags
+- [ ] Links are valid
diff --git a/helper-scripts/migrate-api-links.cjs b/helper-scripts/migrate-api-links.cjs
new file mode 100755
index 000000000..98ed624a1
--- /dev/null
+++ b/helper-scripts/migrate-api-links.cjs
@@ -0,0 +1,331 @@
+#!/usr/bin/env node
+/**
+ * migrate-api-links.js
+ *
+ * One-time migration script to convert Redoc API links to RapiDoc format.
+ *
+ * Usage:
+ *   node helper-scripts/migrate-api-links.js --dry-run  # Preview changes
+ *   node helper-scripts/migrate-api-links.js            # Execute migration
+ */
+
+const fs = require('fs');
+const path = require('path');
+const yaml = require('js-yaml');
+const { glob } = require('glob');
+
+// CLI arguments
+const args = process.argv.slice(2);
+const DRY_RUN = args.includes('--dry-run');
+const VERBOSE = args.includes('--verbose');
+
+// Paths
+const ROOT_DIR = path.resolve(__dirname, '..');
+const CONTENT_DIR = path.join(ROOT_DIR, 'content');
+const API_DOCS_DIR = path.join(ROOT_DIR, 'api-docs');
+
+// Spec file → product URL mapping
+const SPEC_MAPPINGS = [
+  { spec: 'influxdb/cloud/v2/ref.yml', urlPrefix: '/influxdb/cloud/api/' },
+  { spec: 'influxdb/v2/v2/ref.yml', urlPrefix: '/influxdb/v2/api/' },
+  { spec: 'influxdb/v1/v1/ref.yml', urlPrefix: '/influxdb/v1/api/' },
+  { spec: 'enterprise_influxdb/v1/v1/ref.yml', urlPrefix: '/enterprise_influxdb/v1/api/' },
+  { spec: 'influxdb3/core/v3/ref.yml', urlPrefix: '/influxdb3/core/api/' },
+  { spec: 'influxdb3/enterprise/v3/ref.yml', urlPrefix: '/influxdb3/enterprise/api/' },
+  { spec: 'influxdb3/cloud-dedicated/v2/ref.yml', urlPrefix: '/influxdb3/cloud-dedicated/api/' },
+  { spec: 'influxdb3/cloud-dedicated/management/openapi.yml', urlPrefix: '/influxdb3/cloud-dedicated/api/management/' },
+  { spec: 'influxdb3/cloud-serverless/v2/ref.yml', urlPrefix: '/influxdb3/cloud-serverless/api/' },
+  { spec: 'influxdb3/clustered/v2/ref.yml', urlPrefix: '/influxdb3/clustered/api/' },
+  { spec: 'influxdb3/clustered/management/openapi.yml', urlPrefix: '/influxdb3/clustered/api/management/' },
+];
+
+// Version placeholder mappings for shared content
+// Maps /version/ placeholder URLs to representative specs for operationId lookup
+const VERSION_PLACEHOLDER_MAPPINGS = [
+  // InfluxDB 3 v3 API (core/enterprise share same operationIds)
+  { pattern: /^\/influxdb3\/version\/api\/v3\//, lookupPrefix: '/influxdb3/core/api/' },
+  // InfluxDB 3 reference path variant
+  { pattern: /^\/influxdb3\/[^/]+\/reference\/api\/v3\//, lookupPrefix: '/influxdb3/core/api/' },
+  // InfluxDB v2 API - use v2 (OSS) as it has more operations than cloud (replication, etc.)
+  { pattern: /^\/influxdb\/version\/api\/v2\//, lookupPrefix: '/influxdb/v2/api/' },
+  { pattern: /^\/influxdb\/version\/api\/v1\//, lookupPrefix: '/influxdb/v2/api/' },  // v1 compat is in v2 spec
+  { pattern: /^\/influxdb\/version\/api\//, lookupPrefix: '/influxdb/v2/api/' },
+  // InfluxDB 3 version placeholder (generic)
+  { pattern: /^\/influxdb3\/version\/api\//, lookupPrefix: '/influxdb3/cloud-serverless/api/' },
+];
+
+/**
+ * Convert path parameters from {param} to -param- (RapiDoc format)
+ */
+function convertPathParams(path) {
+  return path.replace(/\{([^}]+)\}/g, '-$1-');
+}
+
+/**
+ * Build RapiDoc anchor from method and path
+ * Format: {method}-{path} with {param} → -param-
+ */
+function buildAnchor(method, pathStr) {
+  const convertedPath = convertPathParams(pathStr);
+  return `${method.toLowerCase()}-${convertedPath}`;
+}
+
+/**
+ * Parse OpenAPI spec and extract operationId → anchor mapping
+ */
+function parseSpec(specPath) {
+  const mapping = {};
+
+  try {
+    const content = fs.readFileSync(specPath, 'utf8');
+    const spec = yaml.load(content);
+
+    if (!spec.paths) {
+      console.warn(`  Warning: No paths in ${specPath}`);
+      return mapping;
+    }
+
+    for (const [pathStr, pathItem] of Object.entries(spec.paths)) {
+      const methods = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head'];
+
+      for (const method of methods) {
+        const operation = pathItem[method];
+        if (operation && operation.operationId) {
+          const anchor = buildAnchor(method, pathStr);
+          mapping[operation.operationId] = anchor;
+
+          if (VERBOSE) {
+            console.log(`    ${operation.operationId} → #${anchor}`);
+          }
+        }
+      }
+    }
+  } catch (error) {
+    console.error(`  Error parsing ${specPath}: ${error.message}`);
+  }
+
+  return mapping;
+}
+
+/**
+ * Build complete lookup table from all specs
+ * Returns: { urlPrefix: { operationId: anchor } }
+ */
+function buildLookupTable() {
+  const lookup = {};
+
+  console.log('Building operationId lookup table...\n');
+
+  for (const { spec, urlPrefix } of SPEC_MAPPINGS) {
+    const specPath = path.join(API_DOCS_DIR, spec);
+
+    if (!fs.existsSync(specPath)) {
+      console.warn(`  Skipping missing spec: ${spec}`);
+      continue;
+    }
+
+    console.log(`  Processing: ${spec}`);
+    const mapping = parseSpec(specPath);
+    lookup[urlPrefix] = mapping;
+    console.log(`    Found ${Object.keys(mapping).length} operations`);
+  }
+
+  console.log('');
+  return lookup;
+}
+
+/**
+ * Find all #operation/ links in a file
+ * Returns array of { match, operationId, urlPath, fullUrl }
+ */
+function findOperationLinks(content) {
+  const links = [];
+  // Match patterns like: /influxdb/cloud/api/#operation/PostTasks
+  // or /influxdb3/cloud-dedicated/api/management/#operation/CreateDatabaseToken
+  const regex = /(\/[a-z0-9_/-]+\/api(?:\/management)?(?:\/[a-z0-9-]*)?\/)#operation\/(\w+)/g;
+
+  let match;
+  while ((match = regex.exec(content)) !== null) {
+    links.push({
+      match: match[0],
+      urlPath: match[1],
+      operationId: match[2],
+    });
+  }
+
+  return links;
+}
+
+/**
+ * Find the best matching URL prefix for a given URL path
+ * Also handles /version/ placeholders in shared content
+ */
+function findUrlPrefix(urlPath, lookup) {
+  // Sort by length descending to match most specific first
+  const prefixes = Object.keys(lookup).sort((a, b) => b.length - a.length);
+
+  for (const prefix of prefixes) {
+    if (urlPath.startsWith(prefix) || urlPath === prefix.slice(0, -1)) {
+      return prefix;
+    }
+  }
+
+  // Check version placeholder mappings for shared content
+  for (const { pattern, lookupPrefix } of VERSION_PLACEHOLDER_MAPPINGS) {
+    if (pattern.test(urlPath)) {
+      if (VERBOSE) {
+        console.log(`    Mapped ${urlPath} → ${lookupPrefix} (version placeholder)`);
+      }
+      return lookupPrefix;
+    }
+  }
+
+  return null;
+}
+
+/**
+ * Scan content directory for files with #operation/ links
+ */
+async function scanContentFiles(lookup) {
+  console.log('Scanning content files for #operation/ links...\n');
+
+  const files = await glob('**/*.md', { cwd: CONTENT_DIR });
+  const results = {
+    filesWithLinks: [],
+    totalLinks: 0,
+    unmapped: [],
+  };
+
+  for (const file of files) {
+    const filePath = path.join(CONTENT_DIR, file);
+    const content = fs.readFileSync(filePath, 'utf8');
+    const links = findOperationLinks(content);
+
+    if (links.length > 0) {
+      const fileResult = {
+        file,
+        links: [],
+      };
+
+      for (const link of links) {
+        const urlPrefix = findUrlPrefix(link.urlPath, lookup);
+
+        if (!urlPrefix) {
+          results.unmapped.push({ file, ...link, reason: 'No matching URL prefix' });
+          continue;
+        }
+
+        const productLookup = lookup[urlPrefix];
+        const anchor = productLookup[link.operationId];
+
+        if (!anchor) {
+          results.unmapped.push({ file, ...link, reason: 'OperationId not found in spec' });
+          continue;
+        }
+
+        fileResult.links.push({
+          ...link,
+          urlPrefix,
+          newAnchor: anchor,
+          oldLink: `${link.urlPath}#operation/${link.operationId}`,
+          newLink: `${link.urlPath}#${anchor}`,
+        });
+      }
+
+      if (fileResult.links.length > 0) {
+        results.filesWithLinks.push(fileResult);
+        results.totalLinks += fileResult.links.length;
+      }
+    }
+  }
+
+  return results;
+}
+
+/**
+ * Replace operation links in a file
+ * Returns the modified content
+ */
+function replaceLinks(content, links) {
+  let modified = content;
+
+  for (const link of links) {
+    // Replace all occurrences of this specific link
+    modified = modified.split(link.oldLink).join(link.newLink);
+  }
+
+  return modified;
+}
+
+/**
+ * Apply migrations to files
+ */
+async function applyMigrations(results) {
+  console.log('\n=== APPLYING MIGRATIONS ===\n');
+
+  let filesModified = 0;
+  let linksReplaced = 0;
+
+  for (const { file, links } of results.filesWithLinks) {
+    const filePath = path.join(CONTENT_DIR, file);
+    const originalContent = fs.readFileSync(filePath, 'utf8');
+    const modifiedContent = replaceLinks(originalContent, links);
+
+    if (originalContent !== modifiedContent) {
+      fs.writeFileSync(filePath, modifiedContent, 'utf8');
+      filesModified++;
+      linksReplaced += links.length;
+      console.log(`  ✓ ${file} (${links.length} links)`);
+    }
+  }
+
+  console.log(`\nMigration complete: ${filesModified} files modified, ${linksReplaced} links replaced.`);
+}
+
+async function main() {
+  console.log(`API Link Migration Script`);
+  console.log(`Mode: ${DRY_RUN ? 'DRY RUN (no changes)' : 'EXECUTE'}\n`);
+
+  // Build lookup table
+  const lookupTable = buildLookupTable();
+
+  // Scan content files
+  const results = await scanContentFiles(lookupTable);
+
+  // Report findings
+  console.log('=== SCAN RESULTS ===\n');
+  console.log(`Files with links: ${results.filesWithLinks.length}`);
+  console.log(`Total links to migrate: ${results.totalLinks}`);
+  console.log(`Unmapped links: ${results.unmapped.length}\n`);
+
+  if (VERBOSE && results.filesWithLinks.length > 0) {
+    console.log('Links to migrate:');
+    for (const { file, links } of results.filesWithLinks) {
+      console.log(`\n  ${file}:`);
+      for (const link of links) {
+        console.log(`    ${link.oldLink}`);
+        console.log(`    → ${link.newLink}`);
+      }
+    }
+  }
+
+  if (results.unmapped.length > 0) {
+    console.log('\n=== UNMAPPED LINKS (require manual review) ===\n');
+    for (const item of results.unmapped) {
+      console.log(`  ${item.file}:`);
+      console.log(`    ${item.match}`);
+      console.log(`    Reason: ${item.reason}\n`);
+    }
+  }
+
+  // Apply migrations if not dry-run
+  if (DRY_RUN) {
+    console.log('\n[DRY RUN] No files modified. Run without --dry-run to apply changes.');
+  } else if (results.filesWithLinks.length > 0) {
+    await applyMigrations(results);
+  } else {
+    console.log('\nNo links to migrate.');
+  }
+}
+
+main().catch(console.error);
diff --git a/layouts/_default/LLMS-TXT-README.md b/layouts/LLMS-TXT-README.md
similarity index 63%
rename from layouts/_default/LLMS-TXT-README.md
rename to layouts/LLMS-TXT-README.md
index 0535d8f19..3d3900424 100644
--- a/layouts/_default/LLMS-TXT-README.md
+++ b/layouts/LLMS-TXT-README.md
@@ -8,26 +8,39 @@ The llms.txt format helps LLMs discover and understand documentation structure.
 
 ## Template Files
 
-### `index.llms.txt`
-- **Location**: `/layouts/index.llms.txt`
+### `index.llmstxt.txt`
+
+- **Location**: `/layouts/index.llmstxt.txt`
 - **Output**: `/llms.txt` (site-level)
 - **Type**: Hugo template
 - **Purpose**: Primary entry point for LLM discovery
-- **Content**: Dynamically generated from `data/products.yml` with:
-  - Product descriptions from data files
-  - Organized by product category
-  - Conditional rendering for optional products
+- **Content**: Hardcoded curated list of major product sections with:
+  - Direct links to product documentation
+  - Product descriptions
+  - Organized by product category (InfluxDB 3, InfluxDB 2, InfluxDB 1, Tools)
 
-### `section.llms.txt`
-- **Location**: `/layouts/_default/section.llms.txt`
-- **Output**: Product/section-level llms.txt files (e.g., `/influxdb3/core/llms.txt`)
-- **Type**: Hugo template
-- **Purpose**: Provide curated navigation for specific products/sections
+### `landing-influxdb.llms.txt`
+
+- **Location**: `/layouts/section/landing-influxdb.llms.txt`
+- **Output**: Section-level llms.txt files (e.g., `/influxdb3/core/llms.txt`)
+- **Type**: Hugo template (for `landing-influxdb` layout type)
+- **Purpose**: Provide curated navigation for specific products/sections with landing-influxdb layout
 - **Content**: Dynamically generated from:
   - Product metadata from `data/products.yml`
   - Section content and child pages
   - Page descriptions
 
+### `landing-influxdb.llmstxt.txt`
+
+- **Location**: `/layouts/_default/landing-influxdb.llmstxt.txt`
+- **Output**: Landing page llms.txt files
+- **Type**: Hugo template (for `landing-influxdb` layout type in \_default)
+- **Purpose**: Generate llms.txt for landing pages
+- **Content**: Dynamically generated from:
+  - Product metadata from `data/products.yml`
+  - Page title and description
+  - Child pages list
+
 ## Hugo Configuration
 
 In `config/_default/hugo.yml`:
@@ -58,39 +71,47 @@ After building with `hugo`:
 
 ```
 public/
-├── llms.txt                              # Site-level discovery file
+├── llms.txt                              # Site-level discovery file (from index.llmstxt.txt)
 ├── influxdb3/
 │   ├── core/
-│   │   ├── llms.txt                      # InfluxDB 3 Core product index
-│   │   ├── get-started/
-│   │   │   └── llms.txt                  # Section-level index
-│   │   └── query-data/
-│   │       └── llms.txt                  # Section-level index
+│   │   └── llms.txt                      # InfluxDB 3 Core product index (landing-influxdb layout)
 │   ├── cloud-dedicated/
-│   │   └── llms.txt                      # Cloud Dedicated product index
-│   └── cloud-serverless/
-│       └── llms.txt                      # Cloud Serverless product index
+│   │   └── llms.txt                      # Cloud Dedicated product index (landing-influxdb layout)
+│   ├── cloud-serverless/
+│   │   └── llms.txt                      # Cloud Serverless product index (landing-influxdb layout)
+│   └── clustered/
+│       └── llms.txt                      # Clustered product index (landing-influxdb layout)
+├── influxdb/
+│   ├── v2/
+│   │   └── llms.txt                      # InfluxDB v2 product index (landing-influxdb layout)
+│   └── cloud/
+│       └── llms.txt                      # InfluxDB Cloud TSM index (landing-influxdb layout)
 ├── telegraf/
 │   └── v1/
-│       └── llms.txt                      # Telegraf product index
+│       └── llms.txt                      # Telegraf product index (landing-influxdb layout)
 └── flux/
     └── v0/
-        └── llms.txt                      # Flux product index
+        └── llms.txt                      # Flux product index (landing-influxdb layout)
 ```
 
+Note: llms.txt files are only generated for pages with the `landing-influxdb` layout type and for the site root.
+
 ## llmstxt.org Specification Compliance
 
 ### Required Elements
+
 - ✅ **H1 header**: Product or section name
 - ✅ **Curated links**: Not exhaustive - intentionally selective
 
 ### Optional Elements
+
 - ✅ **Blockquote summary**: Brief product/section description
 - ✅ **Content paragraphs**: Additional context (NO headings allowed)
 - ✅ **H2-delimited sections**: Organize links by category
 - ✅ **Link format**: `[Title](url): Description`
 
 ### Key Rules
+
 1. **H1 is required** - Only the product/section name
 2. **Content sections cannot have headings** - Use paragraphs only
 3. **Curate, don't list everything** - Be selective with links
@@ -101,24 +122,25 @@ public/
 
 ### For Site-Level (/llms.txt)
 
-Edit `/layouts/index.llms.txt` directly. This file is hardcoded for precise curation of top-level products.
+Edit `/layouts/index.llmstxt.txt` directly. This file is hardcoded for precise curation of top-level products.
 
-### For Product/Section-Level
+### For Product/Section-Level (landing-influxdb layout)
 
-The `/layouts/_default/section.llms.txt` template automatically generates llms.txt files for all sections.
+The `/layouts/section/landing-influxdb.llms.txt` template automatically generates llms.txt files for pages with the `landing-influxdb` layout type.
 
 **To customize a specific product's llms.txt:**
 
 1. Create a product-specific template following Hugo's lookup order:
    ```
-   layouts/influxdb3/core/section.llms.txt  # Specific to Core
-   layouts/influxdb3/section.llms.txt       # All InfluxDB 3 products
-   layouts/_default/section.llms.txt        # Default for all
+   layouts/influxdb3/core/landing-influxdb.llms.txt      # Specific to Core
+   layouts/influxdb3/landing-influxdb.llms.txt           # All InfluxDB 3 products
+   layouts/section/landing-influxdb.llms.txt             # Default for all sections
    ```
 
 2. **Example: Custom template for InfluxDB 3 Core**
 
-   Create `/layouts/influxdb3/core/section.llms.txt`:
+   Create `/layouts/influxdb3/core/landing-influxdb.llms.txt`:
+
    ```
    # InfluxDB 3 Core
 
@@ -137,6 +159,10 @@ The `/layouts/_default/section.llms.txt` template automatically generates llms.t
    - [Query with SQL](/influxdb3/core/query-data/sql/): SQL query guide
    ```
 
+### For Landing Pages in \_default
+
+The `/layouts/_default/landing-influxdb.llmstxt.txt` template generates llms.txt for landing pages that use the default layout lookup.
+
 ### Using Product Metadata from data/products.yml
 
 The template accesses product metadata:
@@ -206,7 +232,7 @@ llms.txt files are automatically generated during:
 
 ### Updating Site-Level llms.txt
 
-Edit `/layouts/index.llms.txt` to add/remove product links.
+Edit `/layouts/index.llmstxt.txt` to add/remove product links.
 
 ### Troubleshooting
 
@@ -223,4 +249,10 @@ Edit `/layouts/index.llms.txt` to add/remove product links.
 
 - [llmstxt.org specification](https://llmstxt.org/)
 - [Hugo output formats](https://gohugo.io/templates/output-formats/)
-- [InfluxData products.yml](../../data/products.yml)
+- [InfluxData products.yml](../data/products.yml)
+
+## Current Template Files
+
+- `/layouts/index.llmstxt.txt` - Root site llms.txt generator
+- `/layouts/section/landing-influxdb.llms.txt` - Section-level llms.txt for landing-influxdb layout
+- `/layouts/_default/landing-influxdb.llmstxt.txt` - Default landing page llms.txt generator
diff --git a/layouts/_default/api.html b/layouts/_default/api.html
index 68a09ae66..42987cb05 100644
--- a/layouts/_default/api.html
+++ b/layouts/_default/api.html
@@ -1 +1,111 @@
-{{ .Content }}
+{{/* 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
+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. */}}
+
+{{/* Extract product and version from URL path for download buttons */}}
+{{/* Example: /influxdb3/clustered/api/ → ["", "influxdb3", "clustered", "api", ""] */}}
+{{ $pathParts := split .RelPermalink "/" }}
+{{ $version := "" }}
+{{ if ge (len $pathParts) 3 }}
+  {{ $version = index $pathParts 2 }}
+{{ end }}
+
+{{/* Section pages without staticFilePath render content directly */}} {{ if and .IsSection (not .Params.staticFilePath)
+}} {{ partial "header.html" . }} {{ partial "topnav.html" . }}
+
+
+ {{ partial "sidebar.html" . }} + +
+
+
+
+

{{ .Title }}

+ {{ with .Description }} +

{{ . }}

+ {{ end }} +
+ + {{/* Dual download buttons for Clustered and Cloud Dedicated */}} + {{ if or (eq $version "clustered") (eq $version "cloud-dedicated") }} + + {{ end }} + + {{/* SECTION INDEX - Show page content then children listing */}} {{ + with .Content }} +
{{ . }}
+ {{ end }} {{/* Always show tag pages from article data */}} {{ partial + "api/section-children.html" . }} {{ partial "article/related.html" . }} +
+
+ + +
+
+ +{{ partial "footer.html" . }} {{ else }} {{/* Pages with staticFilePath +(operation pages) use Hugo-native renderer */}} {{ partial "header.html" . }} {{ +partial "topnav.html" . }} + +
+ {{ partial "sidebar.html" . }} +
+
+
+

{{ .Title }}

+ {{ with .Description }} +

{{ . }}

+ {{ end }} +
+ + {{/* Render API documentation using the configured renderer */}} {{ + partial "api/renderer.html" . }} +
+ +
+
+ +{{ partial "footer.html" . }} {{ end }} + + diff --git a/layouts/api/all-endpoints.html b/layouts/api/all-endpoints.html new file mode 100644 index 000000000..b32fe8613 --- /dev/null +++ b/layouts/api/all-endpoints.html @@ -0,0 +1,48 @@ +{{/* + All Endpoints Layout + + Shows all API operations on a single page, grouped by version and sorted by path. + Used for /api/all-endpoints/ pages. + + Uses data from: + - data/article_data/influxdb/{product}/articles.yml +*/}} + +{{ partial "header.html" . }} +{{ partial "topnav.html" . }} + +
+ {{ partial "sidebar.html" . }} + +
+
+
+
+

{{ .Title }}

+ {{ with .Description }} +

{{ . }}

+ {{ end }} +
+ + {{ with .Content }} +
+ {{ . }} +
+ {{ end }} + + {{/* Get all operations from article data */}} + {{ partial "api/all-endpoints-list.html" . }} + + {{ partial "article/related.html" . }} + +
+
+ + +
+
+ +{{ partial "footer.html" . }} diff --git a/layouts/api/list.html b/layouts/api/list.html new file mode 100644 index 000000000..452355078 --- /dev/null +++ b/layouts/api/list.html @@ -0,0 +1,200 @@ +{{/* 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. +*/}} + + +{{ partial "header.html" . }} {{ partial "topnav.html" . }} + +
+ {{ partial "sidebar.html" . }} + +
+
+
+
+

{{ .Title }}

+ {{/* Only show description in header for section index pages */}} + {{ if not (isset .Params "tag") }} + {{ with .Description }} +

{{ . }}

+ {{ end }} + {{ end }} +
+ + {{ $hasTag := isset .Params "tag" }} + {{ if not $hasTag }} + {{/* SECTION INDEX - Show intro content then tag-based children */}} + + {{ with .Content }} +
{{ . }}
+ {{ end }} + {{ partial "api/section-children.html" . }} + + {{ else }} + {{/* TAG PAGE - Show operations or conceptual content */}} + {{ $isConceptual := .Params.isConceptual | default false }} + {{ if $isConceptual }} +
+ {{ with .Content }} {{ . }} {{ else }} + {{ with .Params.tagDescription }}{{ . | markdownify }}{{ end }} + {{ end }} +
+ + {{ else }} + {{/* Operational Page - Show all operations */}} + + {{/* Download OpenAPI spec button — uses specDownloadPath from frontmatter */}} + {{ with .Params.specDownloadPath }} + + {{ end }} + + {{/* Hugo page content if any (for custom intro content) */}} + {{ with .Content }} +
{{ . }}
+ {{ end }} + + {{/* Render operations using Hugo-native templates */}} + {{ with .Params.staticFilePath }} +
+ {{ partial "api/tag-renderer.html" $ }} +
+ {{ end }} {{ end }} {{ end }} {{ partial "article/related.html" . }} +
+
+ + {{/* ON THIS PAGE TOC - Generated from operations array */}} {{ $operations + := .Params.operations }} {{ $hasTag := isset .Params "tag" }} {{ + $isConceptual := .Params.isConceptual | default false }} {{ if and $hasTag + (not $isConceptual) }} + + {{ else }} + + {{ end }} +
+
+ +{{ partial "footer.html" . }} + + diff --git a/layouts/api/section.html b/layouts/api/section.html new file mode 100644 index 000000000..84c5e298f --- /dev/null +++ b/layouts/api/section.html @@ -0,0 +1,86 @@ +{{/* API Documentation Section Layout Used for API section index pages (e.g., +/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 */}} +{{/* Example: /influxdb3/clustered/api/ → ["", "influxdb3", "clustered", "api", ""] */}} +{{ $pathParts := split .RelPermalink "/" }} +{{ $version := "" }} +{{ if ge (len $pathParts) 3 }} + {{ $version = index $pathParts 2 }} +{{ end }} + +{{ partial "header.html" . }} {{ partial "topnav.html" . }} + +
+ {{ partial "sidebar.html" . }} + +
+
+
+
+

{{ .Title }}

+ {{ with .Description }} +

{{ . }}

+ {{ end }} +
+ + {{/* Dual download buttons for Clustered and Cloud Dedicated */}} + {{ if or (eq $version "clustered") (eq $version "cloud-dedicated") }} + + {{ end }} + + {{/* SECTION INDEX - Show intro content then tag-based children */}} {{ + with .Content }} +
{{ . }}
+ {{ end }} {{/* Always show tag pages from article data */}} {{ partial + "api/section-children.html" . }} {{ partial "article/related.html" . }} +
+
+ + +
+
+ +{{ partial "footer.html" . }} + + diff --git a/layouts/api/single.html b/layouts/api/single.html new file mode 100644 index 000000000..28a11e052 --- /dev/null +++ b/layouts/api/single.html @@ -0,0 +1,152 @@ +{{/* + API Documentation Single Page Layout + + Used for: + - Conceptual pages (isConceptual: true) like Authentication, Quick start + - Individual operation pages (legacy - being phased out) + + For conceptual pages: + - Shows Hugo content or tagDescription markdown + + Required frontmatter: + - title: Page title + - isConceptual: true (for conceptual pages) +*/}} + +{{ partial "header.html" . }} +{{ partial "topnav.html" . }} + +
+ {{/* Left: Existing Hugo sidebar (includes API nav via sidebar.html) */}} + {{ partial "sidebar.html" . }} + + {{/* Center + Right: Content and TOC */}} +
+
+
+
+
+
+ {{/* For operation pages, show method badge with title */}} + {{ with .Params.method }} +
+ {{ upper . }} +

{{ $.Title }}

+
+ {{ with $.Params.path }} + {{ . }} + {{ end }} + {{ else }} +

{{ .Title }}

+ {{ end }} + + {{/* Summary/Description - skip for conceptual pages (shown in content section) */}} + {{ if not (.Params.isConceptual | default false) }} + {{ with .Params.summary }} +

{{ . | markdownify }}

+ {{ else }} + {{ with .Description }} +

{{ . | markdownify }}

+ {{ end }} + {{ end }} + {{ end }} +
+ + {{/* 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 }} + + {{ else }} + {{ $specPath := printf "/openapi/%s-v2-data-api.yml" $productName }} + + {{ end }} + {{ else }} + {{/* Single-spec products - existing behavior */}} + {{ $completeSpecPath := printf "/openapi/%s.yml" $productName }} + + {{ end }} + {{ end }} +
+
+ + {{ $isConceptual := .Params.isConceptual | default false }} + + {{ if $isConceptual }} + {{/* Conceptual Page - Show content directly */}} +
+ {{ with .Content }} + {{ . }} + {{ else }} + {{ with .Params.tagDescription }} + {{ . | markdownify }} + {{ end }} + {{ end }} +
+ + {{/* Security Schemes from OpenAPI spec (only show if showSecuritySchemes: true) */}} + {{ if .Params.showSecuritySchemes }} + {{ partial "api/security-schemes.html" . }} + {{ end }} + {{ else }} + {{/* 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 }} +
+ {{ . }} +
+ {{ end }} + + {{ end }} + + {{/* Related documentation links */}} + {{ partial "article/related.html" . }} + +
+
+ + {{/* Right: Page TOC - "ON THIS PAGE" */}} + +
+
+ +{{ partial "footer.html" . }} diff --git a/layouts/partials/api/all-endpoints-list.html b/layouts/partials/api/all-endpoints-list.html new file mode 100644 index 000000000..5bab0333a --- /dev/null +++ b/layouts/partials/api/all-endpoints-list.html @@ -0,0 +1,221 @@ +{{/* + All Endpoints List + + Renders all API operations grouped by version (v3, v2, v1) and sorted by path. + Links point to tag pages with hash anchors (e.g., /api/cache-data/#operation/PostConfigureDistinctCache) + Excludes conceptual/trait tag operations. + + Uses frontmatter params: + - articleDataKey: product data key (e.g., 'influxdb3-core') + - articleSection: section slug (e.g., 'api' or 'management-api') +*/}} +{{ $currentPage := . }} + +{{/* Read data key and section from frontmatter */}} +{{ $dataKey := .Params.articleDataKey | default "" }} +{{ $section := .Params.articleSection | default "" }} + +{{/* Get article data using frontmatter-driven lookup */}} +{{ $articles := slice }} +{{ 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 }} + {{ end }} +{{ end }} + +{{/* Build a map of tag name -> article path for URL lookups */}} +{{ $tagToPath := dict }} +{{ range $articles }} + {{ if and (reflect.IsMap .) (isset . "fields") }} + {{ $fields := index . "fields" }} + {{ if reflect.IsMap $fields }} + {{ if isset $fields "tag" }} + {{ $tag := index $fields "tag" }} + {{ $path := index . "path" }} + {{ $tagToPath = merge $tagToPath (dict $tag $path) }} + {{ end }} + {{ end }} + {{ end }} +{{ end }} + +{{/* Collect all operations from non-conceptual articles */}} +{{ $allOperations := slice }} +{{ range $articles }} + {{ if and (reflect.IsMap .) (isset . "fields") }} + {{ $fields := index . "fields" }} + {{ if reflect.IsMap $fields }} + {{ $isConceptual := false }} + {{ if isset $fields "isConceptual" }} + {{ $isConceptual = index $fields "isConceptual" }} + {{ end }} + {{ if not $isConceptual }} + {{ if isset $fields "operations" }} + {{ $tag := index $fields "tag" }} + {{ $articlePath := index . "path" }} + {{ range index $fields "operations" }} + {{ $allOperations = $allOperations | append (dict "op" . "tag" $tag "articlePath" $articlePath) }} + {{ end }} + {{ end }} + {{ end }} + {{ end }} + {{ end }} +{{ end }} + +{{ if gt (len $allOperations) 0 }} + {{/* Group operations by API version prefix */}} + {{ $v3Ops := slice }} + {{ $v2Ops := slice }} + {{ $v1Ops := slice }} + {{ $otherOps := slice }} + + {{ range $allOperations }} + {{ $path := .op.path }} + {{ if hasPrefix $path "/api/v3" }} + {{ $v3Ops = $v3Ops | append . }} + {{ else if hasPrefix $path "/api/v2" }} + {{ $v2Ops = $v2Ops | append . }} + {{ else if or (hasPrefix $path "/api/v1") (hasPrefix $path "/health") (hasPrefix $path "/ping") (hasPrefix $path "/metrics") (hasPrefix $path "/query") (hasPrefix $path "/write") }} + {{ $v1Ops = $v1Ops | append . }} + {{ else }} + {{ $otherOps = $otherOps | append . }} + {{ end }} + {{ end }} + + {{/* Sort each group by path then method */}} + {{ $sortV3 := slice }} + {{ range $v3Ops }} + {{ $sortKey := printf "%s %s" .op.path (upper .op.method) }} + {{ $sortV3 = $sortV3 | append (dict "sortKey" $sortKey "data" .) }} + {{ end }} + {{ $sortV3 = sort $sortV3 "sortKey" }} + + {{ $sortV2 := slice }} + {{ range $v2Ops }} + {{ $sortKey := printf "%s %s" .op.path (upper .op.method) }} + {{ $sortV2 = $sortV2 | append (dict "sortKey" $sortKey "data" .) }} + {{ end }} + {{ $sortV2 = sort $sortV2 "sortKey" }} + + {{ $sortV1 := slice }} + {{ range $v1Ops }} + {{ $sortKey := printf "%s %s" .op.path (upper .op.method) }} + {{ $sortV1 = $sortV1 | append (dict "sortKey" $sortKey "data" .) }} + {{ end }} + {{ $sortV1 = sort $sortV1 "sortKey" }} + + {{/* Render v3 API endpoints */}} + {{ if gt (len $sortV3) 0 }} +
+

v3 API

+
+ {{ range $sortV3 }} + {{ $op := .data.op }} + {{ $articlePath := .data.articlePath }} + {{/* Build tag page URL with hash anchor: operation/{operationId} */}} + {{/* 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 }} + + {{ upper $op.method }} + {{ $op.path }} + {{ $op.summary }} + + {{ end }} +
+
+ {{ end }} + + {{/* Render v2-compatible endpoints */}} + {{ if gt (len $sortV2) 0 }} +
+

v2-compatible API

+
+ {{ range $sortV2 }} + {{ $op := .data.op }} + {{ $articlePath := .data.articlePath }} + {{/* Build tag page URL with hash anchor: operation/{operationId} */}} + {{/* 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 }} + + {{ upper $op.method }} + {{ $op.path }} + {{ $op.summary }} + {{ with $op.compatVersion }}{{ . }}{{ end }} + + {{ end }} +
+
+ {{ end }} + + {{/* Render v1-compatible endpoints */}} + {{ if gt (len $sortV1) 0 }} +
+

v1-compatible API

+
+ {{ range $sortV1 }} + {{ $op := .data.op }} + {{ $articlePath := .data.articlePath }} + {{/* Build tag page URL with hash anchor: operation/{operationId} */}} + {{/* 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 }} + + {{ upper $op.method }} + {{ $op.path }} + {{ $op.summary }} + {{ with $op.compatVersion }}{{ . }}{{ end }} + + {{ end }} +
+
+ {{ end }} + + {{/* Render Management API endpoints (paths not matching v1/v2/v3 patterns) */}} + {{ if gt (len $otherOps) 0 }} + {{ $sortOther := slice }} + {{ range $otherOps }} + {{ $sortKey := printf "%s %s" .op.path (upper .op.method) }} + {{ $sortOther = $sortOther | append (dict "sortKey" $sortKey "data" .) }} + {{ end }} + {{ $sortOther = sort $sortOther "sortKey" }} +
+

Management API

+
+ {{ range $sortOther }} + {{ $op := .data.op }} + {{ $articlePath := .data.articlePath }} + {{/* Build tag page URL with hash anchor: operation/{operationId} */}} + {{/* 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 }} + + {{ upper $op.method }} + {{ $op.path }} + {{ $op.summary }} + + {{ end }} +
+
+ {{ end }} +{{ else }} +

No endpoints available.

+{{ end }} diff --git a/layouts/partials/api/code-sample.html b/layouts/partials/api/code-sample.html new file mode 100644 index 000000000..c8787de74 --- /dev/null +++ b/layouts/partials/api/code-sample.html @@ -0,0 +1,235 @@ +{{/* + 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
 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 --- */}}
+
+
+ Example request + + Ask AI about this + +
+
+
{{ $curlCommand }}
+
+
diff --git a/layouts/partials/api/normalize-path.html b/layouts/partials/api/normalize-path.html new file mode 100644 index 000000000..7f3735a64 --- /dev/null +++ b/layouts/partials/api/normalize-path.html @@ -0,0 +1,22 @@ +{{/* + Normalize API path for URL generation + + Transforms an API path to a URL-friendly slug: + 1. Strips leading "/api" prefix (parent directory provides /api/) + 2. Adds v1/ prefix for paths without a version (e.g., /write → v1/write) + 3. Strips leading "/" to avoid double slashes + 4. Removes curly braces from path parameters (e.g., {db} → db) + + Parameters: + - path: The API path (e.g., "/write", "/api/v3/engine/{request_path}") + + Returns: URL-safe path slug (e.g., "v1/write", "v3/engine/request_path") +*/}} +{{ $path := . | strings.TrimPrefix "/api" }} +{{ if not (findRE `^/v\d+/` $path) }} + {{ $path = printf "/v1%s" $path }} +{{ end }} +{{ $path = $path | strings.TrimPrefix "/" }} +{{/* Remove curly braces from path parameters */}} +{{ $path = replaceRE `[{}]` "" $path }} +{{ return $path }} diff --git a/layouts/partials/api/operation.html b/layouts/partials/api/operation.html new file mode 100644 index 000000000..12fb7881a --- /dev/null +++ b/layouts/partials/api/operation.html @@ -0,0 +1,69 @@ +{{/* + Hugo-Native Operation Renderer + + Renders a single API operation with parameters, request body, and responses. + Styled to match docusaurus-openapi aesthetic. + + Params: + - operation: Map with method, path, summary, operationId + - spec: The full OpenAPI spec object + - context: The page context for URL generation +*/}} + +{{ $operation := .operation }} +{{ $spec := .spec }} +{{ $method := lower $operation.method }} +{{ $path := $operation.path }} +{{ $operationId := $operation.operationId }} + +{{/* Find the operation definition in the spec */}} +{{ $pathDef := index $spec.paths $path }} +{{ $opDef := dict }} +{{ if $pathDef }} + {{ $opDef = index $pathDef $method | default dict }} +{{ end }} + +{{/* Generate anchor ID matching Redocly operation/{operationId} format */}} +{{ $anchorId := printf "operation/%s" $operationId }} + +
+ {{/* Operation Header */}} +
+
+ {{ upper $method }} + {{ $path }} +
+

{{ $operation.summary }}

+
+ + {{/* Operation Description */}} + {{ with $opDef.description }} +
+ {{ . | markdownify }} +
+ {{ end }} + + {{/* Parameters Section */}} + {{ $params := $opDef.parameters | default slice }} + {{ if gt (len $params) 0 }} + {{ partial "api/parameters.html" (dict "parameters" $params "spec" $spec) }} + {{ end }} + + {{/* Request Body Section */}} + {{ with $opDef.requestBody }} + {{ 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) }} + {{ end }} +
diff --git a/layouts/partials/api/parameter-row.html b/layouts/partials/api/parameter-row.html new file mode 100644 index 000000000..5f34125a9 --- /dev/null +++ b/layouts/partials/api/parameter-row.html @@ -0,0 +1,65 @@ +{{/* + Hugo-Native Parameter Row Renderer + + Renders a single parameter with name, type, required badge, and description. + + Params: + - param: Parameter object with name, schema, required, description + - spec: The full OpenAPI spec object for resolving schema $ref +*/}} + +{{ $param := .param }} +{{ $spec := .spec }} + +{{ $name := $param.name }} +{{ $required := $param.required | default false }} +{{ $description := $param.description | default "" }} + +{{/* Resolve schema type */}} +{{ $schema := $param.schema | default dict }} +{{ $type := $schema.type | default "string" }} +{{ $format := $schema.format | default "" }} +{{ $enum := $schema.enum | default slice }} +{{ $default := $schema.default }} + +{{/* Build type display string */}} +{{ $typeDisplay := $type }} +{{ if $format }} + {{ $typeDisplay = printf "%s <%s>" $type $format }} +{{ end }} + +
+
+
+ {{ $name }} + {{ if $required }} + required + {{ end }} + {{ $typeDisplay }} +
+ + {{ if $description }} +
+ {{ $description | markdownify }} +
+ {{ end }} + + {{/* Show enum values if present */}} + {{ if gt (len $enum) 0 }} +
+ Allowed values: + {{ range $i, $val := $enum }} + {{ if $i }}, {{ end }}{{ $val }} + {{ end }} +
+ {{ end }} + + {{/* Show default value if present */}} + {{ if $default }} +
+ Default: + {{ $default }} +
+ {{ end }} +
+
diff --git a/layouts/partials/api/parameters.html b/layouts/partials/api/parameters.html new file mode 100644 index 000000000..4285feafd --- /dev/null +++ b/layouts/partials/api/parameters.html @@ -0,0 +1,85 @@ +{{/* + Hugo-Native Parameters Renderer + + Renders a table of API operation parameters (query, path, header). + Resolves $ref references to component parameters. + + Params: + - parameters: Array of parameter objects + - spec: The full OpenAPI spec object for resolving $ref +*/}} + +{{ $parameters := .parameters }} +{{ $spec := .spec }} + +{{/* Resolve $ref parameters and group by location */}} +{{ $queryParams := slice }} +{{ $pathParams := slice }} +{{ $headerParams := slice }} + +{{ range $parameters }} + {{ $param := . }} + + {{/* Resolve $ref if present */}} + {{ if isset . "$ref" }} + {{ $refPath := index . "$ref" }} + {{/* Parse ref like "#/components/parameters/db" */}} + {{ $refParts := split $refPath "/" }} + {{ if ge (len $refParts) 4 }} + {{ $paramName := index $refParts 3 }} + {{ with index $spec.components.parameters $paramName }} + {{ $param = . }} + {{ end }} + {{ end }} + {{ end }} + + {{/* Group by 'in' location */}} + {{ $location := $param.in | default "query" }} + {{ if eq $location "query" }} + {{ $queryParams = $queryParams | append $param }} + {{ else if eq $location "path" }} + {{ $pathParams = $pathParams | append $param }} + {{ else if eq $location "header" }} + {{ $headerParams = $headerParams | append $param }} + {{ end }} +{{ end }} + +
+

Parameters

+ + {{/* Path Parameters */}} + {{ if gt (len $pathParams) 0 }} +
+
Path parameters
+
+ {{ range $pathParams }} + {{ partial "api/parameter-row.html" (dict "param" . "spec" $spec) }} + {{ end }} +
+
+ {{ end }} + + {{/* Query Parameters */}} + {{ if gt (len $queryParams) 0 }} +
+
Query parameters
+
+ {{ range $queryParams }} + {{ partial "api/parameter-row.html" (dict "param" . "spec" $spec) }} + {{ end }} +
+
+ {{ end }} + + {{/* Header Parameters */}} + {{ if gt (len $headerParams) 0 }} +
+
Header parameters
+
+ {{ range $headerParams }} + {{ partial "api/parameter-row.html" (dict "param" . "spec" $spec) }} + {{ end }} +
+
+ {{ end }} +
diff --git a/layouts/partials/api/renderer.html b/layouts/partials/api/renderer.html new file mode 100644 index 000000000..d2b32f308 --- /dev/null +++ b/layouts/partials/api/renderer.html @@ -0,0 +1,12 @@ +{{/* + API Renderer + + 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/tag-renderer.html" . }} diff --git a/layouts/partials/api/request-body.html b/layouts/partials/api/request-body.html new file mode 100644 index 000000000..9663c8d56 --- /dev/null +++ b/layouts/partials/api/request-body.html @@ -0,0 +1,60 @@ +{{/* + Hugo-Native Request Body Renderer + + Renders the request body section including schema properties. + + Params: + - requestBody: OpenAPI requestBody object + - spec: The full OpenAPI spec object for resolving $ref +*/}} + +{{ $requestBody := .requestBody }} +{{ $spec := .spec }} + +{{ $required := $requestBody.required | default false }} +{{ $description := $requestBody.description | default "" }} + +{{/* Get content schema - typically application/json */}} +{{ $content := $requestBody.content | default dict }} +{{ $jsonContent := index $content "application/json" | default dict }} +{{ $schema := $jsonContent.schema | default dict }} + +{{/* Resolve $ref if present */}} +{{ $resolvedSchema := $schema }} +{{ if isset $schema "$ref" }} + {{ $refPath := index $schema "$ref" }} + {{/* Parse ref like "#/components/schemas/DistinctCacheCreateRequest" */}} + {{ $refParts := split $refPath "/" }} + {{ if ge (len $refParts) 4 }} + {{ $schemaName := index $refParts 3 }} + {{ with index $spec.components.schemas $schemaName }} + {{ $resolvedSchema = . }} + {{ end }} + {{ end }} +{{ end }} + +
+

+ Request body + {{ if $required }} + required + {{ end }} +

+ + {{ if $description }} +
+ {{ $description | markdownify }} +
+ {{ end }} + + {{/* Content type indicator */}} +
+ Content-Type: + application/json +
+ + {{/* Render schema properties */}} + {{ with $resolvedSchema }} + {{ partial "api/schema.html" (dict "schema" . "spec" $spec "level" 0) }} + {{ end }} +
diff --git a/layouts/partials/api/responses.html b/layouts/partials/api/responses.html new file mode 100644 index 000000000..3973a431d --- /dev/null +++ b/layouts/partials/api/responses.html @@ -0,0 +1,79 @@ +{{/* + Hugo-Native Responses Renderer + + Renders the responses section for an API operation. + Shows status codes, descriptions, and response schemas. + + Params: + - responses: Map of status codes to response objects + - spec: The full OpenAPI spec object for resolving $ref +*/}} + +{{ $responses := .responses }} +{{ $spec := .spec }} + +
+

Responses

+ +
+ {{ range $statusCode, $response := $responses }} + {{/* Resolve $ref if present */}} + {{ $resolvedResponse := $response }} + {{ if isset $response "$ref" }} + {{ $refPath := index $response "$ref" }} + {{ $refParts := split $refPath "/" }} + {{ if ge (len $refParts) 4 }} + {{ $responseName := index $refParts 3 }} + {{ with index $spec.components.responses $responseName }} + {{ $resolvedResponse = . }} + {{ end }} + {{ end }} + {{ end }} + + {{ $description := $resolvedResponse.description | default "" }} + {{ $content := $resolvedResponse.content | default dict }} + + {{/* Determine status category for styling */}} + {{ $statusCategory := "info" }} + {{ if hasPrefix $statusCode "2" }} + {{ $statusCategory = "success" }} + {{ else if hasPrefix $statusCode "3" }} + {{ $statusCategory = "redirect" }} + {{ else if hasPrefix $statusCode "4" }} + {{ $statusCategory = "client-error" }} + {{ else if hasPrefix $statusCode "5" }} + {{ $statusCategory = "server-error" }} + {{ end }} + +
+
+ {{ $statusCode }} + {{ $description }} +
+ + {{/* Response body schema if present */}} + {{ with $content }} + {{ $jsonContent := index . "application/json" | default dict }} + {{ with $jsonContent.schema }} + {{/* Resolve schema $ref if present */}} + {{ $resolvedSchema := . }} + {{ if isset . "$ref" }} + {{ $refPath := index . "$ref" }} + {{ $refParts := split $refPath "/" }} + {{ if ge (len $refParts) 4 }} + {{ $schemaName := index $refParts 3 }} + {{ with index $spec.components.schemas $schemaName }} + {{ $resolvedSchema = . }} + {{ end }} + {{ end }} + {{ end }} + +
+ {{ partial "api/schema.html" (dict "schema" $resolvedSchema "spec" $spec "level" 0) }} +
+ {{ end }} + {{ end }} +
+ {{ end }} +
+
diff --git a/layouts/partials/api/schema.html b/layouts/partials/api/schema.html new file mode 100644 index 000000000..29d1eb704 --- /dev/null +++ b/layouts/partials/api/schema.html @@ -0,0 +1,117 @@ +{{/* + Hugo-Native Schema Renderer + + Renders a JSON schema as a property table with nested object support. + Similar to docusaurus-openapi's schema tables. + + Params: + - schema: OpenAPI schema object + - spec: The full OpenAPI spec object for resolving $ref + - level: Nesting level (0 = root) +*/}} + +{{ $schema := .schema }} +{{ $spec := .spec }} +{{ $level := .level | default 0 }} + +{{ $type := $schema.type | default "object" }} +{{ $properties := $schema.properties | default dict }} +{{ $required := $schema.required | default slice }} +{{ $example := $schema.example }} + +{{/* Convert required slice to map for easy lookup */}} +{{ $requiredMap := dict }} +{{ range $required }} + {{ $requiredMap = merge $requiredMap (dict . true) }} +{{ end }} + +
+ {{ if gt (len $properties) 0 }} +
+ {{ range $propName, $propSchema := $properties }} + {{ $isRequired := index $requiredMap $propName | default false }} + {{ $propType := $propSchema.type | default "string" }} + {{ $propDescription := $propSchema.description | default "" }} + {{ $propFormat := $propSchema.format | default "" }} + {{ $propEnum := $propSchema.enum | default slice }} + {{ $propDefault := $propSchema.default }} + {{ $propExample := $propSchema.example }} + + {{/* Build type display */}} + {{ $typeDisplay := $propType }} + {{ if eq $propType "array" }} + {{ $itemsType := "object" }} + {{ with $propSchema.items }} + {{ $itemsType = .type | default "object" }} + {{ end }} + {{ $typeDisplay = printf "%s[]" $itemsType }} + {{ else if $propFormat }} + {{ $typeDisplay = printf "%s <%s>" $propType $propFormat }} + {{ end }} + +
+
+ {{ $propName }} + {{ if $isRequired }} + required + {{ end }} + {{ $typeDisplay }} +
+ + {{ if $propDescription }} +
+ {{ $propDescription | markdownify }} +
+ {{ end }} + + {{/* Enum values */}} + {{ if gt (len $propEnum) 0 }} +
+ Allowed: + {{ range $i, $val := $propEnum }} + {{ if $i }}, {{ end }}{{ $val }} + {{ end }} +
+ {{ end }} + + {{/* Default value */}} + {{ if $propDefault }} +
+ Default: + {{ $propDefault }} +
+ {{ end }} + + {{/* Example value */}} + {{ if $propExample }} +
+ Example: + {{ jsonify $propExample }} +
+ {{ end }} + + {{/* Nested object/array rendering (limit depth to prevent infinite loops) */}} + {{ if and (eq $propType "object") (lt $level 2) }} + {{ with $propSchema.properties }} + {{ 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/schema.html" (dict "schema" . "spec" $spec "level" (add $level 1)) }} + {{ end }} + {{ end }} + {{ end }} +
+ {{ end }} +
+ {{ end }} + + {{/* Show example at schema level */}} + {{ if and $example (eq $level 0) }} +
+ Example request body +
{{ jsonify (dict "indent" "  ") $example }}
+
+ {{ end }} +
diff --git a/layouts/partials/api/section-children.html b/layouts/partials/api/section-children.html new file mode 100644 index 000000000..c217ce5b5 --- /dev/null +++ b/layouts/partials/api/section-children.html @@ -0,0 +1,112 @@ +{{/* + API Section Children + + Renders tag pages from article data as a children list. + Sort order: conceptual tags (traitTags) first, then other tags alphabetically. + + 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 := . }} + +{{/* Read data key and section from frontmatter */}} +{{ $dataKey := .Params.articleDataKey | default "" }} +{{ $section := .Params.articleSection | default "" }} + +{{/* Get article data using frontmatter-driven lookup */}} +{{ $articles := slice }} +{{ 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 }} + {{ end }} +{{ end }} + +{{/* Separate conceptual (traitTag) and non-conceptual articles */}} +{{ $conceptualArticles := slice }} +{{ $operationArticles := slice }} + +{{ range $articles }} + {{ if and (reflect.IsMap .) (isset . "fields") }} + {{ $fields := index . "fields" }} + {{ if reflect.IsMap $fields }} + {{ $isConceptual := false }} + {{ if isset $fields "isConceptual" }} + {{ $isConceptual = index $fields "isConceptual" }} + {{ end }} + {{ if $isConceptual }} + {{ $conceptualArticles = $conceptualArticles | append . }} + {{ else }} + {{ $operationArticles = $operationArticles | append . }} + {{ end }} + {{ end }} + {{ end }} +{{ end }} + +{{/* Sort each group by weight (default 100), then alphabetically by tag name */}} +{{ $conceptualArticles = sort $conceptualArticles "fields.weight" }} +{{ $operationArticles = sort $operationArticles "fields.weight" }} + +{{/* Combine: conceptual first, then operations */}} +{{ $sortedArticles := $conceptualArticles | append $operationArticles }} + +{{/* Also include static API pages (HTML files) not in article data */}} +{{/* These are compatibility API pages like v1-compatibility, v2, management */}} +{{ $staticApiPages := slice }} +{{ range $currentPage.Pages }} + {{/* Skip pages that are in article data (have tag param) or are all-endpoints */}} + {{ if and (not (isset .Params "tag")) (not .Params.isAllEndpoints) }} + {{ $staticApiPages = $staticApiPages | append . }} + {{ end }} +{{ end }} +{{ $staticApiPages = sort $staticApiPages "Weight" }} + + diff --git a/layouts/partials/api/security-schemes.html b/layouts/partials/api/security-schemes.html new file mode 100644 index 000000000..87210f3a3 --- /dev/null +++ b/layouts/partials/api/security-schemes.html @@ -0,0 +1,68 @@ +{{/* + Security Schemes Display + + Renders OpenAPI security schemes as human-readable documentation. + Extracts securitySchemes from the referenced OpenAPI spec file. + + Design principles: + - Each scheme gets an h2 heading (appears in "On this page" TOC) + - Focus on human-readable descriptions, not OpenAPI schema details + - Technical details (type, scheme, in) are omitted - they're for machines + - Descriptions should include usage examples + + Required page params: + - staticFilePath: Path to the OpenAPI specification file +*/}} + +{{ $specPath := .Params.staticFilePath }} +{{ if $specPath }} + {{/* Load the OpenAPI spec file from static directory */}} + {{ $fullPath := printf "static%s" $specPath }} + {{ $specContent := readFile $fullPath }} + {{ if $specContent }} + {{ $spec := transform.Unmarshal $specContent }} + {{ with $spec.components.securitySchemes }} +
+ {{ range $name, $scheme := . }} +
+ {{/* Human-friendly title from scheme name */}} + {{ $title := $name }} + {{/* Convert common scheme names to readable titles */}} + {{/* Short names (v1 specs) */}} + {{ if eq $name "BasicAuth" }}{{ $title = "Basic Authentication" }}{{ end }} + {{ if eq $name "TokenAuth" }}{{ $title = "Token Authentication" }}{{ end }} + {{ if eq $name "QueryAuth" }}{{ $title = "Query String Authentication" }}{{ end }} + {{ if eq $name "BearerAuth" }}{{ $title = "Bearer Token Authentication" }}{{ end }} + {{ if eq $name "ApiKeyAuth" }}{{ $title = "API Key Authentication" }}{{ end }} + {{/* Long names (v2+ specs) */}} + {{ if eq $name "BasicAuthentication" }}{{ $title = "Basic Authentication" }}{{ end }} + {{ if eq $name "TokenAuthentication" }}{{ $title = "Token Authentication" }}{{ end }} + {{ if eq $name "QuerystringAuthentication" }}{{ $title = "Query String Authentication" }}{{ end }} + +

{{ $title }}

+ + {{/* Description is the primary content - should include usage examples */}} + {{ with $scheme.description }} +
+ {{ . | markdownify }} +
+ {{ else }} + {{/* Fallback descriptions when OpenAPI spec doesn't provide one */}} +
+ {{ if eq $scheme.type "http" }} + {{ if eq $scheme.scheme "basic" }} +

Use HTTP Basic Authentication by including your credentials in the request.

+ {{ else if eq $scheme.scheme "bearer" }} +

Include a bearer token in the Authorization header.

+ {{ end }} + {{ else if eq $scheme.type "apiKey" }} +

Pass your API key {{ if eq $scheme.in "header" }}in the {{ $scheme.name }} header{{ else if eq $scheme.in "query" }}as the {{ $scheme.name }} query parameter{{ end }}.

+ {{ end }} +
+ {{ end }} +
+ {{ end }} +
+ {{ end }} + {{ end }} +{{ end }} diff --git a/layouts/partials/api/tag-renderer.html b/layouts/partials/api/tag-renderer.html new file mode 100644 index 000000000..828b46bf1 --- /dev/null +++ b/layouts/partials/api/tag-renderer.html @@ -0,0 +1,68 @@ +{{/* + Tag Page Renderer + + Renders all operations for a tag page using Hugo templates. + Parses the OpenAPI spec file and renders each operation natively. + + Required page params: + - staticFilePath: Path to the OpenAPI specification file (YAML) + - operations: Array of operation metadata from frontmatter + + Usage: + {{ partial "api/tag-renderer.html" . }} +*/}} + +{{ $page := . }} +{{ $specPath := .Params.staticFilePath }} +{{ $operations := .Params.operations | default slice }} + +{{/* Load and parse the OpenAPI spec from static/ directory */}} +{{ $spec := dict }} +{{ if $specPath }} + {{/* Build path to static file (staticFilePath has leading slash, e.g. /openapi/...) */}} + {{ $staticFile := printf "static%s" $specPath }} + + {{/* Use os.ReadFile (Hugo 0.121+) to read from static directory */}} + {{ with os.ReadFile $staticFile }} + {{ $spec = . | transform.Unmarshal }} + {{ else }} + {{/* Fallback: try unmounted resources (for assets mount configuration) */}} + {{ $cleanPath := strings.TrimPrefix "/" $specPath }} + {{ with resources.Get $cleanPath }} + {{ $spec = .Content | transform.Unmarshal }} + {{ end }} + {{ end }} +{{ end }} + +{{/* Tag description from spec */}} +{{ $tagDescription := "" }} +{{ $tagName := .Params.tag | default "" }} +{{ range $spec.tags }} + {{ if eq .name $tagName }} + {{ $tagDescription = .description | default "" }} + {{ end }} +{{ end }} + +
+ {{/* Tag Overview/Description */}} + {{ if $tagDescription }} +
+
+ {{ $tagDescription | markdownify }} +
+
+ {{ end }} + + {{/* Operations List */}} +
+ {{ range $operations }} + {{ partial "api/operation.html" (dict + "operation" . + "spec" $spec + "context" $page + ) }} + {{ end }} +
+ + {{/* Related links rendered via frontmatter + article/related.html */}} +
diff --git a/layouts/partials/article/related.html b/layouts/partials/article/related.html index 184b7f6dd..41becf349 100644 --- a/layouts/partials/article/related.html +++ b/layouts/partials/article/related.html @@ -1,32 +1,25 @@ -{{ $scratch := newScratch }} {{ if .Params.related }}