From da077a21b4f14dbb7d9f755dbe18bc5a3bc77d1e Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Fri, 6 Feb 2026 13:48:31 -0600 Subject: [PATCH] refactor(api): move Related guides to x-influxdatadocs-related field - Add x-influxdatadocs-related support in TypeScript for tags and operations - Remove inline "Related guides" sections from OpenAPI descriptions - Move related links to structured x-influxdatadocs-related arrays - Add weight support for Quick start pages (weight: 1) to appear first in nav - Update clustered and cloud-dedicated v2/ref.yml specs --- api-docs/influxdb3/cloud-dedicated/v2/ref.yml | 64 +- api-docs/influxdb3/clustered/v2/ref.yml | 62 +- .../scripts/dist/generate-openapi-articles.js | 1853 ++++++++--------- .../dist/openapi-paths-to-hugo-data/index.js | 1355 ++++++------ api-docs/scripts/generate-openapi-articles.ts | 7 +- .../openapi-paths-to-hugo-data/index.ts | 45 +- .../api/quick-start/_index.md | 2 +- 7 files changed, 1624 insertions(+), 1764 deletions(-) diff --git a/api-docs/influxdb3/cloud-dedicated/v2/ref.yml b/api-docs/influxdb3/cloud-dedicated/v2/ref.yml index 1cc195922..905eb8be2 100644 --- a/api-docs/influxdb3/cloud-dedicated/v2/ref.yml +++ b/api-docs/influxdb3/cloud-dedicated/v2/ref.yml @@ -127,12 +127,10 @@ tags: using **InfluxQL** and retrieve data in **CSV** or **JSON** format. - The `/api/v2/query` endpoint can't query InfluxDB 3 Cloud Dedicated. - _Flight + gRPC_ clients can query using **SQL** or **InfluxQL** and retrieve data in **Arrow** format. - - #### Related guides - - - [Get started querying InfluxDB](/influxdb3/cloud-dedicated/get-started/query/) - - [Execute queries](/influxdb3/cloud-dedicated/query-data/execute-queries/) name: Query data + x-influxdatadocs-related: + - /influxdb3/cloud-dedicated/get-started/query/ + - /influxdb3/cloud-dedicated/query-data/execute-queries/ - description: | Get started with the InfluxDB 3 Cloud Dedicated API: @@ -302,14 +300,12 @@ paths: - Use the [`/write` endpoint](#operation/PostLegacyWrite) for [InfluxDB v1 parameter compatibility](/influxdb3/cloud-dedicated/guides/api-compatibility/v1/). - Use the [`/api/v2/write` endpoint](#operation/PostWrite) for [InfluxDB v2 parameter compatibility](/influxdb3/cloud-dedicated/guides/api-compatibility/v2/). - - #### Related guides - - - [Get started writing data](/influxdb3/cloud-dedicated/get-started/write/) - - [Write data](/influxdb3/cloud-dedicated/write-data/) - - [Best practices for writing data](/influxdb3/cloud-dedicated/write-data/best-practices/) - - [Troubleshoot issues writing data](/influxdb3/cloud-dedicated/write-data/troubleshoot/) operationId: PostWrite + x-influxdatadocs-related: + - /influxdb3/cloud-dedicated/get-started/write/ + - /influxdb3/cloud-dedicated/write-data/ + - /influxdb3/cloud-dedicated/write-data/best-practices/ + - /influxdb3/cloud-dedicated/write-data/troubleshoot/ parameters: - $ref: '#/components/parameters/TraceSpan' - description: | @@ -357,10 +353,6 @@ paths: - Returns only `application/json` for format and limit errors. - Returns only `text/html` for some quota limit errors. - - #### Related guides - - - [Troubleshoot issues writing data](/influxdb3/cloud-dedicated/write-data/troubleshoot/) in: header name: Accept schema: @@ -422,10 +414,6 @@ paths: 1. Use [gzip](https://www.gzip.org/) to compress the line protocol data. 2. In your request, send the compressed data and the `Content-Encoding: gzip` header. - - #### Related guides - - - [Best practices for optimizing writes](/influxdb3/cloud-dedicated/write-data/best-practices/optimize-writes/) required: true responses: '201': @@ -642,13 +630,11 @@ paths: - Use the [`/write` endpoint](#operation/PostLegacyWrite) for [InfluxDB v1 parameter compatibility](/influxdb3/cloud-dedicated/guides/api-compatibility/v1/). - Use the [`/api/v2/write` endpoint](#operation/PostWrite) for [InfluxDB v2 parameter compatibility](/influxdb3/cloud-dedicated/guides/api-compatibility/v2/). - - #### Related guides - - - [Get started writing data](/influxdb3/cloud-dedicated/get-started/write/) - - [Write data](/influxdb3/cloud-dedicated/write-data/) - - [Best practices for writing data](/influxdb3/cloud-dedicated/write-data/best-practices/) - - [Troubleshoot issues writing data](/influxdb3/cloud-dedicated/write-data/troubleshoot/) + x-influxdatadocs-related: + - /influxdb3/cloud-dedicated/get-started/write/ + - /influxdb3/cloud-dedicated/write-data/ + - /influxdb3/cloud-dedicated/write-data/best-practices/ + - /influxdb3/cloud-dedicated/write-data/troubleshoot/ parameters: - $ref: '#/components/parameters/TraceSpan' - description: The InfluxDB 1.x username to authenticate the request. @@ -1086,9 +1072,7 @@ components: Annotation rows to include in the results. An _annotation_ is metadata associated with an object (column) in the data model. - #### Related guides - - - See [Annotated CSV annotations](/influxdb3/cloud-dedicated/reference/syntax/annotated-csv/#annotations) for examples and more information. + See [Annotated CSV annotations](/influxdb3/cloud-dedicated/reference/syntax/annotated-csv/#annotations) for examples and more information. For more information about **annotations** in tabular data, see [W3 metadata vocabulary for tabular data](https://www.w3.org/TR/2015/REC-tabular-data-model-20151217/#columns). @@ -1852,9 +1836,7 @@ components: - Doesn't use `shardGroupDurationsSeconds`. - #### Related guides - - - InfluxDB [shards and shard groups](/influxdb3/cloud-dedicated/reference/internals/shards/) + For more information, see [shards and shard groups](/influxdb3/cloud-dedicated/reference/internals/shards/). format: int64 type: integer type: @@ -2018,10 +2000,8 @@ components: - **`DATABASE_NAME`**: your InfluxDB 3 Cloud Dedicated database - **`DATABASE_TOKEN`**: a database token with sufficient permissions to the database - #### Related guides - - - [Authenticate v1 API requests](/influxdb3/cloud-dedicated/guides/api-compatibility/v1/) - - [Manage tokens](/influxdb3/cloud-dedicated/admin/tokens/) + For more information, see [Authenticate v1 API requests](/influxdb3/cloud-dedicated/guides/api-compatibility/v1/) + and [Manage tokens](/influxdb3/cloud-dedicated/admin/tokens/). QuerystringAuthentication: type: apiKey in: query @@ -2074,10 +2054,8 @@ components: - **`DATABASE_NAME`**: the database to query - **`DATABASE_TOKEN`**: a database token with sufficient permissions to the database - #### Related guides - - - [Authenticate v1 API requests](/influxdb3/cloud-dedicated/guides/api-compatibility/v1/) - - [Manage tokens](/influxdb3/cloud-dedicated/admin/tokens/) + For more information, see [Authenticate v1 API requests](/influxdb3/cloud-dedicated/guides/api-compatibility/v1/) + and [Manage tokens](/influxdb3/cloud-dedicated/admin/tokens/). BearerAuthentication: type: http scheme: bearer @@ -2134,9 +2112,7 @@ components: --data-binary 'home,room=kitchen temp=72 1463683075' ``` - ### Related guides - - - [Manage tokens](/influxdb3/cloud-dedicated/admin/tokens/) + For more information, see [Manage tokens](/influxdb3/cloud-dedicated/admin/tokens/). in: header name: Authorization type: apiKey diff --git a/api-docs/influxdb3/clustered/v2/ref.yml b/api-docs/influxdb3/clustered/v2/ref.yml index 78c037383..1de84b46a 100644 --- a/api-docs/influxdb3/clustered/v2/ref.yml +++ b/api-docs/influxdb3/clustered/v2/ref.yml @@ -127,12 +127,10 @@ tags: using **InfluxQL** and retrieve data in **CSV** or **JSON** format. - The `/api/v2/query` endpoint can't query InfluxDB 3 Clustered. - _Flight + gRPC_ clients can query using **SQL** or **InfluxQL** and retrieve data in **Arrow** format. - - #### Related guides - - - [Get started querying InfluxDB](/influxdb3/clustered/get-started/query/) - - [Execute queries](/influxdb3/clustered/query-data/execute-queries/) name: Query data + x-influxdatadocs-related: + - /influxdb3/clustered/get-started/query/ + - /influxdb3/clustered/query-data/execute-queries/ - description: | Get started with the InfluxDB 3 Clustered API: @@ -283,14 +281,12 @@ paths: To ensure that InfluxDB Cloud handles writes in the order you request them, wait for a success response (HTTP `2xx` status code) before you send the next request. - - #### Related guides - - - [Get started writing data](/influxdb3/clustered/get-started/write/) - - [Write data](/influxdb3/clustered/write-data/) - - [Best practices for writing data](/influxdb3/clustered/write-data/best-practices/) - - [Troubleshoot issues writing data](/influxdb3/clustered/write-data/troubleshoot/) operationId: PostWrite + x-influxdatadocs-related: + - /influxdb3/clustered/get-started/write/ + - /influxdb3/clustered/write-data/ + - /influxdb3/clustered/write-data/best-practices/ + - /influxdb3/clustered/write-data/troubleshoot/ parameters: - $ref: '#/components/parameters/TraceSpan' - description: | @@ -336,10 +332,6 @@ paths: - Returns only `application/json` for format and limit errors. - Returns only `text/html` for some quota limit errors. - - #### Related guides - - - [Troubleshoot issues writing data](/influxdb3/clustered/write-data/troubleshoot/) in: header name: Accept schema: @@ -401,10 +393,6 @@ paths: 1. Use [gzip](https://www.gzip.org/) to compress the line protocol data. 2. In your request, send the compressed data and the `Content-Encoding: gzip` header. - - #### Related guides - - - [Best practices for optimizing writes](/influxdb3/clustered/write-data/best-practices/optimize-writes/) required: true responses: '204': @@ -727,15 +715,13 @@ paths: To ensure that InfluxDB handles writes in the order you request them, wait for a success response (HTTP `2xx` status code) before you send the next request. - - #### Related guides - - - [Write data with the InfluxDB API](/influxdb3/clustered/get-started/write/) - - [Optimize writes to InfluxDB](/influxdb3/clustered/write-data/best-practices/optimize-writes/) - - [Troubleshoot issues writing data](/influxdb3/clustered/write-data/troubleshoot/) summary: Write data using the InfluxDB v1 HTTP API tags: - Write data + x-influxdatadocs-related: + - /influxdb3/clustered/get-started/write/ + - /influxdb3/clustered/write-data/best-practices/optimize-writes/ + - /influxdb3/clustered/write-data/troubleshoot/ components: parameters: TraceSpan: @@ -1064,9 +1050,7 @@ components: Annotation rows to include in the results. An _annotation_ is metadata associated with an object (column) in the data model. - #### Related guides - - - See [Annotated CSV annotations](/influxdb3/clustered/reference/syntax/annotated-csv/#annotations) for examples and more information. + See [Annotated CSV annotations](/influxdb3/clustered/reference/syntax/annotated-csv/#annotations) for examples and more information. For more information about **annotations** in tabular data, see [W3 metadata vocabulary for tabular data](https://www.w3.org/TR/2015/REC-tabular-data-model-20151217/#columns). @@ -1830,9 +1814,7 @@ components: - Doesn't use `shardGroupDurationsSeconds`. - #### Related guides - - - InfluxDB [shards and shard groups](/influxdb3/clustered/reference/internals/shards/) + For more information, see [shards and shard groups](/influxdb3/clustered/reference/internals/shards/). format: int64 type: integer type: @@ -2002,10 +1984,8 @@ components: - **`DATABASE_NAME`**: your InfluxDB 3 Clustered database - **`DATABASE_TOKEN`**: a database token with sufficient permissions to the database - #### Related guides - - - [Authenticate v1 API requests](/influxdb3/clustered/guides/api-compatibility/v1/) - - [Manage tokens](/influxdb3/clustered/admin/tokens/) + For more information, see [Authenticate v1 API requests](/influxdb3/clustered/guides/api-compatibility/v1/) + and [Manage tokens](/influxdb3/clustered/admin/tokens/). QuerystringAuthentication: type: apiKey in: query @@ -2058,10 +2038,8 @@ components: - **`DATABASE_NAME`**: the database to query - **`DATABASE_TOKEN`**: a database token with sufficient permissions to the database - #### Related guides - - - [Authenticate v1 API requests](/influxdb3/clustered/guides/api-compatibility/v1/) - - [Manage tokens](/influxdb3/clustered/admin/tokens/) + For more information, see [Authenticate v1 API requests](/influxdb3/clustered/guides/api-compatibility/v1/) + and [Manage tokens](/influxdb3/clustered/admin/tokens/). BearerAuthentication: type: http scheme: bearer @@ -2118,9 +2096,7 @@ components: --data-binary 'home,room=kitchen temp=72 1463683075' ``` - ### Related guides - - - [Manage tokens](/influxdb3/clustered/admin/tokens/) + For more information, see [Manage tokens](/influxdb3/clustered/admin/tokens/). in: header name: Authorization type: apiKey diff --git a/api-docs/scripts/dist/generate-openapi-articles.js b/api-docs/scripts/dist/generate-openapi-articles.js index e09575543..36e9697fd 100644 --- a/api-docs/scripts/dist/generate-openapi-articles.js +++ b/api-docs/scripts/dist/generate-openapi-articles.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -'use strict'; +"use strict"; /** * Generate OpenAPI Articles Script * @@ -20,55 +20,41 @@ * * @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 (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) - for (var k in mod) - if (k !== 'default' && Object.prototype.hasOwnProperty.call(mod, k)) - __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; - }; -Object.defineProperty(exports, '__esModule', { value: true }); -exports.LINK_PATTERN = - exports.MARKDOWN_FIELDS = - exports.productConfigs = - void 0; +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 = exports.productConfigs = void 0; exports.processProduct = processProduct; exports.generateDataFromOpenAPI = generateDataFromOpenAPI; exports.generatePagesFromArticleData = generatePagesFromArticleData; @@ -76,9 +62,9 @@ exports.deriveProductPath = deriveProductPath; exports.transformDocLinks = transformDocLinks; exports.validateDocLinks = validateDocLinks; exports.resolveContentPath = resolveContentPath; -const child_process_1 = require('child_process'); -const path = __importStar(require('path')); -const fs = __importStar(require('fs')); +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'); // Calculate the relative paths @@ -93,22 +79,22 @@ const skipFetch = process.argv.includes('--skip-fetch'); * The alt_link_key matches what the Hugo product-selector template expects */ 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 [key, 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); + 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(); } - } - return apiProducts; + const productsContent = fs.readFileSync(productsFile, 'utf8'); + const products = yaml.load(productsContent); + const apiProducts = new Map(); + for (const [key, 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); + } + } + return apiProducts; } // Load API products at module initialization const apiProductsMap = loadApiProducts(); @@ -120,19 +106,20 @@ const apiProductsMap = loadApiProducts(); * @throws Exits process with code 1 on error */ function execCommand(command, description) { - try { - if (description) { - console.log(`\n${description}...`); + try { + if (description) { + console.log(`\n${description}...`); + } + console.log(`Executing: ${command}\n`); + (0, child_process_1.execSync)(command, { stdio: 'inherit' }); } - 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); + catch (error) { + console.error(`\n❌ Error executing command: ${command}`); + if (error instanceof Error) { + console.error(error.message); + } + process.exit(1); } - process.exit(1); - } } /** * Generate a clean static directory name from a product key. @@ -142,12 +129,12 @@ function execCommand(command, description) { * @returns Clean directory name (e.g., 'influxdb-cloud-v2', 'influxdb3-core') */ function getStaticDirName(productKey) { - // For influxdb3_* products, convert underscore to hyphen and don't add prefix - if (productKey.startsWith('influxdb3_')) { - return productKey.replace('_', '-'); - } - // For other products, add 'influxdb-' prefix - return `influxdb-${productKey}`; + // For influxdb3_* products, convert underscore to hyphen and don't add prefix + if (productKey.startsWith('influxdb3_')) { + return productKey.replace('_', '-'); + } + // For other products, add 'influxdb-' prefix + return `influxdb-${productKey}`; } /** * Generate Hugo data files from OpenAPI specification @@ -157,14 +144,14 @@ function getStaticDirName(productKey) { * @param articleOutPath - Output path for article metadata */ function generateDataFromOpenAPI(specFile, dataOutPath, articleOutPath) { - if (!fs.existsSync(dataOutPath)) { - fs.mkdirSync(dataOutPath, { recursive: true }); - } - openapiPathsToHugo.generateHugoData({ - dataOutPath, - articleOutPath, - specFile, - }); + if (!fs.existsSync(dataOutPath)) { + fs.mkdirSync(dataOutPath, { recursive: true }); + } + openapiPathsToHugo.generateHugoData({ + dataOutPath, + articleOutPath, + specFile, + }); } /** * Generate Hugo content pages from article data @@ -175,135 +162,117 @@ function generateDataFromOpenAPI(specFile, dataOutPath, articleOutPath) { * @param options - Generation options */ function generatePagesFromArticleData(options) { - 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); - 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 }); + 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; } - 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 = { - 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 = `--- + // Read articles data + 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; + } + // 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 = { + 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}`); + 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 = { - 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 = `--- + // 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 = { + 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}` - ); + fs.writeFileSync(pageFile, pageContent); + } + console.log(`✓ Generated ${data.articles.length} content pages in ${contentPath}`); } /** * Generate Hugo content pages from tag-based article data @@ -315,193 +284,177 @@ ${yaml.dump(frontmatter)}--- * @param options - Generation options */ function generateTagPagesFromArticleData(options) { - 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); - 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 = { - 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', - }, - }; + 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; } - // Add alt_links for cross-product API navigation - if (apiProductsMap.size > 0) { - const altLinks = {}; - apiProductsMap.forEach((apiPath, productName) => { - altLinks[productName] = apiPath; - }); - parentFrontmatter.alt_links = altLinks; + // Read articles data + 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; } - // Build page content with intro paragraph and children listing - const introText = apiDescription.replace( - 'InfluxDB', - '{{% product-name %}}' - ); - const parentContent = `--- + // 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 = { + 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 = {}; + 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 = { - 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', - }, + 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 = { + title: 'All endpoints', + description: `View all API endpoints sorted by path.`, + type: 'api', + layout: 'all-endpoints', + weight: 999, + isAllEndpoints: true, }; - } - // Add alt_links for cross-product API navigation - if (apiProductsMap.size > 0) { - const altLinks = {}; - apiProductsMap.forEach((apiPath, productName) => { - altLinks[productName] = apiPath; - }); - allEndpointsFrontmatter.alt_links = altLinks; - } - const allEndpointsContent = `--- + // 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 = {}; + 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; - const frontmatter = { - title, - description: article.fields.description || `API reference for ${title}`, - type: 'api', - layout: isConceptual ? 'single' : 'list', - staticFilePath: article.fields.staticFilePath, - weight: 100, - // 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 alt_links for cross-product API navigation - if (apiProductsMap.size > 0) { - const altLinks = {}; - apiProductsMap.forEach((apiPath, productName) => { - altLinks[productName] = apiPath; - }); - frontmatter.alt_links = altLinks; - } - const pageContent = `--- + 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 = { + 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 alt_links for cross-product API navigation + 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}` - ); - // NOTE: Path page generation is disabled - all operations are now displayed - // inline on tag pages using RapiDoc with hash-based navigation for deep linking. - // The tag pages render all operations in a single scrollable view with a - // server-side generated TOC for quick navigation. - // - // Previously this generated individual pages per API path: - // generatePathPages({ articlesPath, contentPath, pathSpecFiles }); + 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 RapiDoc with hash-based navigation for deep linking. + // The tag pages render all operations in a single scrollable view with a + // server-side generated TOC for quick navigation. + // + // Previously this generated individual pages per API path: + // generatePathPages({ articlesPath, contentPath, pathSpecFiles }); } /** * Convert API path to URL-safe slug with normalized version prefix @@ -523,18 +476,18 @@ ${yaml.dump(frontmatter)}--- * @returns URL-safe path slug with version prefix (e.g., "v1/write", "v3/configure/database") */ function apiPathToSlug(apiPath) { - // Remove leading "/api" prefix if present - let normalizedPath = apiPath.replace(/^\/api/, ''); - // Remove leading slash - normalizedPath = normalizedPath.replace(/^\//, ''); - // If path doesn't start with version prefix, add v1/ - if (!/^v\d+\//.test(normalizedPath)) { - normalizedPath = `v1/${normalizedPath}`; - } - // Remove curly braces from path parameters (e.g., {db} → db) - // to avoid URL encoding issues in Hugo - normalizedPath = normalizedPath.replace(/[{}]/g, ''); - return normalizedPath; + // Remove leading "/api" prefix if present + let normalizedPath = apiPath.replace(/^\/api/, ''); + // Remove leading slash + normalizedPath = normalizedPath.replace(/^\//, ''); + // If path doesn't start with version prefix, add v1/ + if (!/^v\d+\//.test(normalizedPath)) { + normalizedPath = `v1/${normalizedPath}`; + } + // Remove curly braces from path parameters (e.g., {db} → db) + // to avoid URL encoding issues in Hugo + normalizedPath = normalizedPath.replace(/[{}]/g, ''); + return normalizedPath; } /** Method sort order for consistent display */ const METHOD_ORDER = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']; @@ -551,110 +504,110 @@ const METHOD_ORDER = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']; * @param options - Generation options */ function generatePathPages(options) { - const { articlesPath, contentPath, pathSpecFiles } = options; - const yaml = require('js-yaml'); - const articlesFile = path.join(articlesPath, 'articles.yml'); - if (!fs.existsSync(articlesFile)) { - console.warn(`⚠️ Articles file not found: ${articlesFile}`); - return; - } - // Read articles data - const articlesContent = fs.readFileSync(articlesFile, 'utf8'); - const data = yaml.load(articlesContent); - if (!data.articles || !Array.isArray(data.articles)) { - console.warn(`⚠️ No articles found in ${articlesFile}`); - return; - } - // Collect all operations and group by API path - const pathOperations = new Map(); - // Process each article (tag) and collect operations by path - for (const article of data.articles) { - // Skip conceptual articles (they don't have operations) - if (article.fields.isConceptual) { - continue; + const { articlesPath, contentPath, pathSpecFiles } = options; + const yaml = require('js-yaml'); + const articlesFile = path.join(articlesPath, 'articles.yml'); + if (!fs.existsSync(articlesFile)) { + console.warn(`⚠️ Articles file not found: ${articlesFile}`); + return; } - const operations = article.fields.operations || []; - const tagSpecFile = article.fields.staticFilePath; - const tagName = article.fields.tag || article.fields.name || ''; - for (const op of operations) { - const existing = pathOperations.get(op.path); - if (existing) { - // Add operation to existing path group - existing.operations.push(op); - } else { - // Create new path group - pathOperations.set(op.path, { - operations: [op], - tagSpecFile, - tagName, + // Read articles data + 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; + } + // Collect all operations and group by API path + const pathOperations = new Map(); + // Process each article (tag) and collect operations by path + for (const article of data.articles) { + // Skip conceptual articles (they don't have operations) + if (article.fields.isConceptual) { + continue; + } + const operations = article.fields.operations || []; + const tagSpecFile = article.fields.staticFilePath; + const tagName = article.fields.tag || article.fields.name || ''; + for (const op of operations) { + const existing = pathOperations.get(op.path); + if (existing) { + // Add operation to existing path group + existing.operations.push(op); + } + else { + // Create new path group + pathOperations.set(op.path, { + operations: [op], + tagSpecFile, + tagName, + }); + } + } + } + let pathCount = 0; + // Generate a page for each unique API path + for (const [apiPath, pathData] of pathOperations) { + // Build page path: api/{path}/ + // e.g., /api/v3/configure/database -> api/v3/configure/database/ + const pathSlug = apiPathToSlug(apiPath); + // Only add 'api/' prefix if the path doesn't already start with 'api/' + const basePath = pathSlug.startsWith('api/') ? pathSlug : `api/${pathSlug}`; + const pathDir = path.join(contentPath, basePath); + const pathFile = path.join(pathDir, '_index.md'); + // Create directory if needed + if (!fs.existsSync(pathDir)) { + fs.mkdirSync(pathDir, { recursive: true }); + } + // Sort operations by method order + const sortedOperations = [...pathData.operations].sort((a, b) => { + const aIndex = METHOD_ORDER.indexOf(a.method.toUpperCase()); + const bIndex = METHOD_ORDER.indexOf(b.method.toUpperCase()); + return (aIndex === -1 ? 999 : aIndex) - (bIndex === -1 ? 999 : bIndex); }); - } - } - } - let pathCount = 0; - // Generate a page for each unique API path - for (const [apiPath, pathData] of pathOperations) { - // Build page path: api/{path}/ - // e.g., /api/v3/configure/database -> api/v3/configure/database/ - const pathSlug = apiPathToSlug(apiPath); - // Only add 'api/' prefix if the path doesn't already start with 'api/' - const basePath = pathSlug.startsWith('api/') ? pathSlug : `api/${pathSlug}`; - const pathDir = path.join(contentPath, basePath); - const pathFile = path.join(pathDir, '_index.md'); - // Create directory if needed - if (!fs.existsSync(pathDir)) { - fs.mkdirSync(pathDir, { recursive: true }); - } - // Sort operations by method order - const sortedOperations = [...pathData.operations].sort((a, b) => { - const aIndex = METHOD_ORDER.indexOf(a.method.toUpperCase()); - const bIndex = METHOD_ORDER.indexOf(b.method.toUpperCase()); - return (aIndex === -1 ? 999 : aIndex) - (bIndex === -1 ? 999 : bIndex); - }); - // Use first operation's summary or construct from methods - const methods = sortedOperations.map((op) => op.method.toUpperCase()); - const title = - sortedOperations.length === 1 && sortedOperations[0].summary - ? sortedOperations[0].summary - : `${apiPath}`; - // Determine spec file - use path-specific spec if available - const pathSpecFile = pathSpecFiles?.get(apiPath); - const specFile = pathSpecFile || pathData.tagSpecFile; - const frontmatter = { - title, - description: `API reference for ${apiPath} - ${methods.join(', ')}`, - type: 'api-path', - layout: 'path', - // RapiDoc configuration - specFile, - apiPath, - // Include all operations for TOC generation - operations: sortedOperations.map((op) => ({ - operationId: op.operationId, - method: op.method, - path: op.path, - summary: op.summary, - ...(op.compatVersion && { compatVersion: op.compatVersion }), - })), - tag: pathData.tagName, - }; - // Collect related links from all operations - const relatedLinks = []; - for (const op of sortedOperations) { - if (op.externalDocs?.url && !relatedLinks.includes(op.externalDocs.url)) { - relatedLinks.push(op.externalDocs.url); - } - } - if (relatedLinks.length > 0) { - frontmatter.related = relatedLinks; - } - const pageContent = `--- + // Use first operation's summary or construct from methods + const methods = sortedOperations.map((op) => op.method.toUpperCase()); + const title = sortedOperations.length === 1 && sortedOperations[0].summary + ? sortedOperations[0].summary + : `${apiPath}`; + // Determine spec file - use path-specific spec if available + const pathSpecFile = pathSpecFiles?.get(apiPath); + const specFile = pathSpecFile || pathData.tagSpecFile; + const frontmatter = { + title, + description: `API reference for ${apiPath} - ${methods.join(', ')}`, + type: 'api-path', + layout: 'path', + // RapiDoc configuration + specFile, + apiPath, + // Include all operations for TOC generation + operations: sortedOperations.map((op) => ({ + operationId: op.operationId, + method: op.method, + path: op.path, + summary: op.summary, + ...(op.compatVersion && { compatVersion: op.compatVersion }), + })), + tag: pathData.tagName, + }; + // Collect related links from all operations + const relatedLinks = []; + for (const op of sortedOperations) { + if (op.externalDocs?.url && !relatedLinks.includes(op.externalDocs.url)) { + relatedLinks.push(op.externalDocs.url); + } + } + if (relatedLinks.length > 0) { + frontmatter.related = relatedLinks; + } + const pageContent = `--- ${yaml.dump(frontmatter)}--- `; - fs.writeFileSync(pathFile, pageContent); - pathCount++; - } - console.log(`✓ Generated ${pathCount} path pages in ${contentPath}/api/`); + fs.writeFileSync(pathFile, pageContent); + pathCount++; + } + console.log(`✓ Generated ${pathCount} path pages in ${contentPath}/api/`); } /** * Merge article data from multiple specs into a single articles.yml @@ -666,76 +619,71 @@ ${yaml.dump(frontmatter)}--- * @param outputPath - Path to write the merged articles.yml */ function mergeArticleData(articlesFiles, outputPath) { - 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 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); + 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 + if (article.fields.related && article.fields.related.length > 0) { + const existingRelated = existing.fields.related || []; + const newRelated = article.fields.related.filter((r) => !existingRelated.includes(r)); + 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))); + } + } } - const content = fs.readFileSync(file, 'utf8'); - const data = yaml.load(content); - if (!data.articles || !Array.isArray(data.articles)) { - console.warn(`⚠️ No articles found in ${file}`); - continue; + // Convert map to array and write + const mergedData = { + articles: Array.from(mergedArticles.values()), + }; + // Ensure output directory exists + const outputDir = path.dirname(outputPath); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); } - 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 - if (article.fields.related && article.fields.related.length > 0) { - const existingRelated = existing.fields.related || []; - const newRelated = article.fields.related.filter( - (r) => !existingRelated.includes(r) - ); - 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 = { - 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}` - ); + // 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 @@ -743,115 +691,109 @@ function mergeArticleData(articlesFiles, outputPath) { * Maps product identifiers to their OpenAPI specs and content directories */ const productConfigs = { - // 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': { - specFile: path.join(API_DOCS_ROOT, 'influxdb/cloud/v2/ref.yml'), - pagesDir: path.join(DOCS_ROOT, 'content/influxdb/cloud'), - description: 'InfluxDB Cloud (v2 API)', - menuKey: 'influxdb_cloud', - skipParentMenu: true, - useTagBasedGeneration: true, - }, - 'oss-v2': { - specFile: path.join(API_DOCS_ROOT, 'influxdb/v2/v2/ref.yml'), - 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/v3/ref.yml'), - 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/v3/ref.yml'), - 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/v2/ref.yml'), - 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': { - specFile: path.join(API_DOCS_ROOT, 'influxdb3/cloud-serverless/v2/ref.yml'), - 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/v2/ref.yml'), - 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/v1/ref.yml'), - 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/v1/ref.yml'), - pagesDir: path.join(DOCS_ROOT, 'content/enterprise_influxdb/v1'), - description: 'InfluxDB Enterprise v1', - menuKey: 'enterprise_influxdb_v1', - skipParentMenu: true, - useTagBasedGeneration: true, - }, + // 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': { + specFile: path.join(API_DOCS_ROOT, 'influxdb/cloud/v2/ref.yml'), + pagesDir: path.join(DOCS_ROOT, 'content/influxdb/cloud'), + description: 'InfluxDB Cloud (v2 API)', + menuKey: 'influxdb_cloud', + skipParentMenu: true, + useTagBasedGeneration: true, + }, + 'oss-v2': { + specFile: path.join(API_DOCS_ROOT, 'influxdb/v2/v2/ref.yml'), + 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/v3/ref.yml'), + 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/v3/ref.yml'), + 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/v2/ref.yml'), + 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': { + specFile: path.join(API_DOCS_ROOT, 'influxdb3/cloud-serverless/v2/ref.yml'), + 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/v2/ref.yml'), + 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/v1/ref.yml'), + 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/v1/ref.yml'), + pagesDir: path.join(DOCS_ROOT, 'content/enterprise_influxdb/v1'), + description: 'InfluxDB Enterprise v1', + menuKey: 'enterprise_influxdb_v1', + skipParentMenu: true, + useTagBasedGeneration: true, + }, }; exports.productConfigs = productConfigs; /** Fields that can contain markdown with links */ @@ -871,14 +813,12 @@ exports.LINK_PATTERN = LINK_PATTERN; * 'api-docs/enterprise_influxdb/v1/v1/ref.yml' → '/enterprise_influxdb/v1' */ function deriveProductPath(specPath) { - // Match: api-docs/(enterprise_influxdb|influxdb3|influxdb)/(product-or-version)/... - const match = specPath.match( - /api-docs\/(enterprise_influxdb|influxdb3?)\/([\w-]+)\// - ); - if (!match) { - throw new Error(`Cannot derive product path from: ${specPath}`); - } - return `/${match[1]}/${match[2]}`; + // Match: api-docs/(enterprise_influxdb|influxdb3|influxdb)/(product-or-version)/... + const match = specPath.match(/api-docs\/(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. @@ -889,32 +829,34 @@ function deriveProductPath(specPath) { * @returns Spec with transformed links (new object, original unchanged) */ function transformDocLinks(spec, productPath) { - function transformValue(value) { - if (typeof value === 'string') { - return value.replace(LINK_PATTERN, `${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; } - if (Array.isArray(value)) { - return value.map(transformValue); + 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; } - 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); + return transformObject(spec); } /** * Resolve a URL path to a content file path. @@ -923,16 +865,16 @@ function transformDocLinks(spec, productPath) { * '/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; // Return expected path for error message + 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 } /** * Validate that transformed links point to existing content. @@ -942,37 +884,35 @@ function resolveContentPath(urlPath, contentDir) { * @returns Array of error messages for broken links */ 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; - // Only validate internal links (start with /) - if (linkUrl.startsWith('/') && !linkUrl.startsWith('//')) { - const contentPath = resolveContentPath(linkUrl, contentDir); - if (!fs.existsSync(contentPath)) { - errors.push( - `Broken link at ${jsonPath}: [${linkText}](${linkUrl})` - ); - } + 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; + // Only validate internal links (start with /) + if (linkUrl.startsWith('/') && !linkUrl.startsWith('//')) { + const contentPath = resolveContentPath(linkUrl, contentDir); + if (!fs.existsSync(contentPath)) { + errors.push(`Broken link at ${jsonPath}: [${linkText}](${linkUrl})`); + } + } + } + // Reset regex lastIndex for next string + 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}`); + } } - } - // Reset regex lastIndex for next string - 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; + extractLinks(spec, 'spec'); + return errors; } /** * Convert display name to filename-safe slug @@ -981,10 +921,10 @@ function validateDocLinks(spec, contentDir) { * @returns Filename-safe slug (e.g., "management-api") */ function slugifyDisplayName(displayName) { - return displayName - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-|-$/g, ''); + return displayName + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, ''); } /** * Process a single spec file: transform links, write to static folder @@ -996,58 +936,45 @@ function slugifyDisplayName(displayName) { * @returns Object with paths to generated files, or null if processing failed */ function processSpecFile(specConfig, staticPath, staticDirName, productKey) { - const yaml = require('js-yaml'); - if (!fs.existsSync(specConfig.path)) { - console.warn(`⚠️ Spec file not found: ${specConfig.path}`); - return null; - } - // Generate filename from display name or use default - const specSlug = specConfig.displayName - ? slugifyDisplayName(specConfig.displayName) - : path.parse(specConfig.path).name; - const staticSpecPath = path.join( - staticPath, - `${staticDirName}-${specSlug}.yml` - ); - const staticJsonSpecPath = path.join( - staticPath, - `${staticDirName}-${specSlug}.json` - ); - const articlesPath = path.join( - DOCS_ROOT, - `data/article_data/influxdb/${productKey}/${specSlug}` - ); - try { - const specContent = fs.readFileSync(specConfig.path, 'utf8'); - const specObject = yaml.load(specContent); - // 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}` - ); - // 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}`)); - } + const yaml = require('js-yaml'); + if (!fs.existsSync(specConfig.path)) { + console.warn(`⚠️ Spec file not found: ${specConfig.path}`); + return null; + } + // Generate filename from display name or use default + const specSlug = specConfig.displayName + ? slugifyDisplayName(specConfig.displayName) + : path.parse(specConfig.path).name; + const staticSpecPath = path.join(staticPath, `${staticDirName}-${specSlug}.yml`); + const staticJsonSpecPath = path.join(staticPath, `${staticDirName}-${specSlug}.json`); + const articlesPath = path.join(DOCS_ROOT, `data/article_data/influxdb/${productKey}/${specSlug}`); + try { + const specContent = fs.readFileSync(specConfig.path, 'utf8'); + const specObject = yaml.load(specContent); + // 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}`); + // 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}`)); + } + } + // 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; } - // 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; - } } /** * Process a single product: fetch spec, generate data, and create pages @@ -1060,202 +987,164 @@ function processSpecFile(specConfig, staticPath, staticDirName, productKey) { * @param config - Product configuration */ function processProduct(productKey, config) { - console.log('\n' + '='.repeat(80)); - console.log(`Processing ${config.description || productKey}`); - console.log('='.repeat(80)); - 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}` - ); - // Ensure static directory exists - if (!fs.existsSync(staticPath)) { - fs.mkdirSync(staticPath, { recursive: true }); - } - try { - // Step 1: Execute the getswagger.sh script to fetch/bundle the spec - if (skipFetch) { - console.log(`⏭️ Skipping getswagger.sh (--skip-fetch flag set)`); - } else { - const getswaggerScript = path.join(API_DOCS_ROOT, 'getswagger.sh'); - if (fs.existsSync(getswaggerScript)) { - execCommand( - `cd ${API_DOCS_ROOT} && ./getswagger.sh ${productKey} -B`, - `Fetching OpenAPI spec for ${productKey}` - ); - } else { - console.log(`⚠️ getswagger.sh not found, skipping fetch step`); - } + console.log('\n' + '='.repeat(80)); + console.log(`Processing ${config.description || productKey}`); + console.log('='.repeat(80)); + 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}`); + // Ensure static directory exists + if (!fs.existsSync(staticPath)) { + fs.mkdirSync(staticPath, { recursive: true }); } - // Determine spec files to process - const specFiles = 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 = []; - 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 + try { + // Step 1: Execute the getswagger.sh script to fetch/bundle the spec + if (skipFetch) { + console.log(`⏭️ Skipping getswagger.sh (--skip-fetch flag set)`); + } + else { + const getswaggerScript = path.join(API_DOCS_ROOT, 'getswagger.sh'); + if (fs.existsSync(getswaggerScript)) { + execCommand(`cd ${API_DOCS_ROOT} && ./getswagger.sh ${productKey} -B`, `Fetching OpenAPI spec for ${productKey}`); + } + else { + console.log(`⚠️ getswagger.sh not found, skipping fetch step`); + } + } + // Determine spec files to process + const specFiles = 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 = []; + 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 + if (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, key) => { + allPathSpecFiles.set(key, value); + }); + } + } + } + // Step 5: 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}`); + } + } + // Step 6: Generate Hugo content pages from (merged) article data if (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, key) => { - allPathSpecFiles.set(key, value); - }); + generateTagPagesFromArticleData({ + articlesPath: mergedArticlesPath, + contentPath: config.pagesDir, + menuKey: config.menuKey, + menuParent: 'InfluxDB HTTP API', + skipParentMenu: config.skipParentMenu, + pathSpecFiles: allPathSpecFiles, + }); } - } - } - // Step 5: 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 }); + else { + generatePagesFromArticleData({ + articlesPath: mergedArticlesPath, + contentPath: config.pagesDir, + menuKey: config.menuKey, + menuParent: 'InfluxDB HTTP API', + skipParentMenu: config.skipParentMenu, + }); } - fs.copyFileSync(sourceArticles, targetArticles); - fs.copyFileSync( - sourceArticles.replace('.yml', '.json'), - targetArticles.replace('.yml', '.json') - ); - console.log(`✓ Copied article data to ${mergedArticlesPath}`); - } + console.log(`\n✅ Successfully processed ${config.description || productKey}\n`); } - // Step 6: 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, - }); + catch (error) { + console.error(`\n❌ Error processing ${productKey}:`, error); + process.exit(1); } - console.log( - `\n✅ Successfully processed ${config.description || productKey}\n` - ); - } catch (error) { - console.error(`\n❌ Error processing ${productKey}:`, error); - process.exit(1); - } } /** * Main execution function */ function main() { - // Filter out CLI flags from arguments - const args = process.argv.slice(2).filter((arg) => !arg.startsWith('--')); - // Determine which products to process - let productsToProcess; - 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) { - console.error( - `\n❌ Invalid product identifier(s): ${invalidProducts.join(', ')}` - ); - console.error('\nValid products:'); - Object.keys(productConfigs).forEach((key) => { - console.error(` - ${key}: ${productConfigs[key].description}`); + // Filter out CLI flags from arguments + const args = process.argv.slice(2).filter((arg) => !arg.startsWith('--')); + // Determine which products to process + let productsToProcess; + 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) { + console.error(`\n❌ Invalid product identifier(s): ${invalidProducts.join(', ')}`); + console.error('\nValid products:'); + Object.keys(productConfigs).forEach((key) => { + console.error(` - ${key}: ${productConfigs[key].description}`); + }); + process.exit(1); + } + // Process each product + productsToProcess.forEach((productKey) => { + const config = productConfigs[productKey]; + processProduct(productKey, config); }); - process.exit(1); - } - // Process each product - productsToProcess.forEach((productKey) => { - const config = productConfigs[productKey]; - processProduct(productKey, config); - }); - console.log('\n' + '='.repeat(80)); - console.log('✅ All products processed successfully!'); - console.log('='.repeat(80) + '\n'); + 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(); + main(); } -//# sourceMappingURL=generate-openapi-articles.js.map +//# 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 index c58dae954..e9a18ddca 100644 --- a/api-docs/scripts/dist/openapi-paths-to-hugo-data/index.js +++ b/api-docs/scripts/dist/openapi-paths-to-hugo-data/index.js @@ -1,4 +1,4 @@ -'use strict'; +"use strict"; /** * OpenAPI to Hugo Data Converter * @@ -7,58 +7,47 @@ * * @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 (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) - for (var k in mod) - if (k !== 'default' && Object.prototype.hasOwnProperty.call(mod, k)) - __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; - }; -Object.defineProperty(exports, '__esModule', { value: true }); +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')); +const yaml = __importStar(require("js-yaml")); +const fs = __importStar(require("fs")); +const path = __importStar(require("path")); /** * Read a YAML file and parse it * @@ -67,8 +56,8 @@ const path = __importStar(require('path')); * @returns Parsed YAML content */ function readFile(filepath, encoding = 'utf8') { - const content = fs.readFileSync(filepath, encoding); - return yaml.load(content); + const content = fs.readFileSync(filepath, encoding); + return yaml.load(content); } /** * Write data to a YAML file @@ -77,7 +66,7 @@ function readFile(filepath, encoding = 'utf8') { * @param outputTo - Output file path */ function writeDataFile(data, outputTo) { - fs.writeFileSync(outputTo, yaml.dump(data)); + fs.writeFileSync(outputTo, yaml.dump(data)); } /** * Write data to a JSON file @@ -86,22 +75,22 @@ function writeDataFile(data, outputTo) { * @param outputTo - Output file path */ function writeJsonFile(data, outputTo) { - fs.writeFileSync(outputTo, JSON.stringify(data, null, 2)); + 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); - }, + /** + * 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 @@ -110,35 +99,35 @@ const openapiUtils = { * @returns URL-friendly slug (e.g., "write-data", "processing-engine") */ function slugifyTag(tagName) { - return tagName - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-|-$/g, ''); + 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', + // 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 @@ -147,20 +136,20 @@ const TAG_MENU_GROUPS = { * @returns Menu group name or 'Other' if not mapped */ function getMenuGroupForTag(tagName) { - return TAG_MENU_GROUPS[tagName] || 'Other'; + return TAG_MENU_GROUPS[tagName] || 'Other'; } /** * HTTP methods to check for operations */ const HTTP_METHODS = [ - 'get', - 'post', - 'put', - 'patch', - 'delete', - 'options', - 'head', - 'trace', + 'get', + 'post', + 'put', + 'patch', + 'delete', + 'options', + 'head', + 'trace', ]; /** * Extract all operations from an OpenAPI document grouped by tag @@ -169,40 +158,45 @@ const HTTP_METHODS = [ * @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, - }; - } - // Add operation to each of its tags - (operation.tags || []).forEach((tag) => { - if (!tagOperations.has(tag)) { - tagOperations.set(tag, []); - } - tagOperations.get(tag).push(opMeta); + 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']; + } + // 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; + return tagOperations; } /** * Write OpenAPI specs grouped by tag to separate files @@ -213,85 +207,83 @@ function extractOperationsByTag(openapi) { * @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 RapiDoc from rendering the operation multiple times - // (once per tag) when an operation belongs to multiple tags - const filteredOperation = { ...operation, tags: [tagName] }; - filteredPathItem[method] = filteredOperation; - hasOperations = true; + 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 RapiDoc from rendering the operation 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']); } - }); - // Include path-level parameters if we have operations - if (hasOperations) { - if (pathItem.parameters) { - filteredPathItem.parameters = pathItem.parameters; + // 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); + } } - 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 @@ -300,12 +292,12 @@ function writeTagOpenapis(openapi, prefix, outPath) { * @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 + 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) @@ -318,65 +310,61 @@ function pathToFileSlug(apiPath) { * @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 in RapiDoc - // RapiDoc renders operations once per tag, so multiple tags cause duplicates - 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 RapiDoc displays auth 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'] - ); + const pathSpecFiles = new Map(); + if (!fs.existsSync(outPath)) { + fs.mkdirSync(outPath, { recursive: true }); } - // 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; + 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 in RapiDoc + // RapiDoc renders operations once per tag, so multiple tags cause duplicates + 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 RapiDoc displays auth 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 @@ -387,80 +375,79 @@ function writePathSpecificSpecs(openapi, outPath) { * @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)); + 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); } - }); }); - // 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 @@ -469,119 +456,107 @@ function writePathOpenapis(openapi, prefix, outPath) { * @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) - ) - ); + 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 ''; } - // Extract OpenAPI tags from operation - if (operation.tags && Array.isArray(operation.tags)) { - operation.tags.forEach((tag) => { - if (!apiTags.includes(tag)) { - apiTags.push(tag); + // 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; + // 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 @@ -592,50 +567,49 @@ function createArticleDataForPathGroup(openapi) { * @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 }); + /** + * 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); } - 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 @@ -652,37 +626,34 @@ function writeOpenapiArticleData(sourcePath, targetPath, opts) { * @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. RapiDoc fragment links (#section/..., #operation/..., #tag/...) -> replace with just the text - // 2. Relative links with fragments (/path/#anchor) -> keep link but remove fragment - sanitized = sanitized.replace( - /\[([^\]]+)\]\(([^)]+)\)/g, - (match, text, url) => { - // Case 1: RapiDoc fragment links (starts with #section/, #operation/, #tag/) - 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; + if (!description) { + return ''; } - ); - // Clean up extra whitespace left by directive removals - sanitized = sanitized.replace(/\n\n\n+/g, '\n\n').trim(); - return sanitized; + let sanitized = description; + // Remove ReDoc injection directives (e.g., ) + sanitized = sanitized.replace(//g, ''); + // Handle markdown links: + // 1. RapiDoc fragment links (#section/..., #operation/..., #tag/...) -> replace with just the text + // 2. Relative links with fragments (/path/#anchor) -> keep link but remove fragment + sanitized = sanitized.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => { + // Case 1: RapiDoc fragment links (starts with #section/, #operation/, #tag/) + 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 @@ -693,59 +664,70 @@ function sanitizeDescription(description) { * @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; - } - // Aggregate unique externalDocs URLs from operations into article-level related - // This populates Hugo frontmatter `related` field for "Related content" links - const relatedUrls = new Set(); - // First check tag-level externalDocs - if (tagMeta?.externalDocs?.url) { - relatedUrls.add(tagMeta.externalDocs.url); - } - // Then aggregate from operations - operations.forEach((op) => { - if (op.externalDocs?.url) { - relatedUrls.add(op.externalDocs.url); + 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); } - }); - if (relatedUrls.size > 0) { - article.fields.related = Array.from(relatedUrls); - } - return article; + // 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; + } + // Aggregate unique related URLs from multiple sources into article-level related + // This populates Hugo frontmatter `related` field for "Related content" links + const relatedUrls = new Set(); + // First check tag-level x-influxdatadocs-related + if (tagMeta?.['x-influxdatadocs-related']) { + tagMeta['x-influxdatadocs-related'].forEach((url) => relatedUrls.add(url)); + } + // Then check tag-level externalDocs (legacy single link) + if (tagMeta?.externalDocs?.url) { + relatedUrls.add(tagMeta.externalDocs.url); + } + // Then aggregate from operations + operations.forEach((op) => { + // Check operation-level x-influxdatadocs-related (via opMeta.related) + if (op.related) { + op.related.forEach((url) => relatedUrls.add(url)); + } + // Check operation-level externalDocs (legacy single link) + if (op.externalDocs?.url) { + relatedUrls.add(op.externalDocs.url); + } + }); + if (relatedUrls.size > 0) { + article.fields.related = Array.from(relatedUrls); + } + return article; } /** * Write tag-based OpenAPI article metadata to Hugo data files @@ -757,85 +739,82 @@ function createArticleDataForTag(openapi, operations, tagMeta) { * @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, - }; - } - operations.push(opMeta); - } - }); + 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']; + } + operations.push(opMeta); + } + }); + }); + const article = createArticleDataForTag(tagOpenapi, operations, tagMeta); + article.fields.source = filePath; + article.fields.staticFilePath = filePath.replace(/^static\//, '/'); + return article; }); - 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 }); + 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); } - 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 @@ -849,28 +828,24 @@ function writeOpenapiTagArticleData(sourcePath, targetPath, openapi, opts) { * @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'); + 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 @@ -884,17 +859,15 @@ function generateHugoDataByTag(options) { * @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'); + 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 @@ -906,14 +879,14 @@ function generateHugoData(options) { * @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); + const openapi = readFile(specFile, 'utf8'); + return writePathSpecificSpecs(openapi, outPath); } // CommonJS export for backward compatibility module.exports = { - generateHugoData, - generateHugoDataByTag, - generatePathSpecificSpecs, - writePathSpecificSpecs, + generateHugoData, + generateHugoDataByTag, + generatePathSpecificSpecs, + writePathSpecificSpecs, }; -//# sourceMappingURL=index.js.map +//# sourceMappingURL=index.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 95abf9c59..ceb6d8ee7 100644 --- a/api-docs/scripts/generate-openapi-articles.ts +++ b/api-docs/scripts/generate-openapi-articles.ts @@ -441,6 +441,7 @@ function generateTagPagesFromArticleData( staticFilePath?: string; operations?: OperationMeta[]; related?: string[]; + weight?: number; }; }>; }; @@ -571,13 +572,16 @@ All {{% product-name %}} API endpoints, sorted by path. 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: 100, + weight, // Tag-based fields tag: article.fields.tag, isConceptual, @@ -734,6 +738,7 @@ function generatePathPages(options: GeneratePathPagesOptions): void { staticFilePath?: string; operations?: OperationMeta[]; related?: string[]; + weight?: number; }; }>; }; diff --git a/api-docs/scripts/openapi-paths-to-hugo-data/index.ts b/api-docs/scripts/openapi-paths-to-hugo-data/index.ts index a562cc3a2..ed67780bf 100644 --- a/api-docs/scripts/openapi-paths-to-hugo-data/index.ts +++ b/api-docs/scripts/openapi-paths-to-hugo-data/index.ts @@ -41,6 +41,8 @@ interface Operation { externalDocs?: ExternalDocs; /** Compatibility version for migration context (v1 or v2) */ 'x-compatibility-version'?: string; + /** Related documentation links (replaces inline "Related guides" sections) */ + 'x-influxdatadocs-related'?: string[]; [key: string]: unknown; } @@ -216,6 +218,8 @@ interface Tag { externalDocs?: ExternalDocs; /** Indicates this is a conceptual/supplementary tag (no operations) */ 'x-traitTag'?: boolean; + /** Related documentation links (replaces inline "Related guides" sections) */ + 'x-influxdatadocs-related'?: string[]; [key: string]: unknown; } @@ -235,6 +239,8 @@ interface OperationMeta { description: string; url: string; }; + /** Related documentation links */ + related?: string[]; } /** @@ -277,6 +283,8 @@ interface Article { menuGroup?: string; /** Operations metadata for TOC generation */ operations?: OperationMeta[]; + /** Page weight for ordering in navigation */ + weight?: number; }; } @@ -457,6 +465,14 @@ function extractOperationsByTag( }; } + // Extract x-influxdatadocs-related if present + if ( + operation['x-influxdatadocs-related'] && + Array.isArray(operation['x-influxdatadocs-related']) + ) { + opMeta.related = operation['x-influxdatadocs-related']; + } + // Add operation to each of its tags (operation.tags || []).forEach((tag) => { if (!tagOperations.has(tag)) { @@ -1089,17 +1105,34 @@ function createArticleDataForTag( article.fields.showSecuritySchemes = true; } - // Aggregate unique externalDocs URLs from operations into article-level related + // Set custom weight for Quick start to appear first in nav + if (tagName === 'Quick start') { + article.fields.weight = 1; + } + + // Aggregate unique related URLs from multiple sources into article-level related // This populates Hugo frontmatter `related` field for "Related content" links const relatedUrls = new Set(); - // First check tag-level externalDocs + // First check tag-level x-influxdatadocs-related + if (tagMeta?.['x-influxdatadocs-related']) { + (tagMeta['x-influxdatadocs-related'] as string[]).forEach((url) => + relatedUrls.add(url) + ); + } + + // Then check tag-level externalDocs (legacy single link) if (tagMeta?.externalDocs?.url) { relatedUrls.add(tagMeta.externalDocs.url); } // Then aggregate from operations operations.forEach((op) => { + // Check operation-level x-influxdatadocs-related (via opMeta.related) + if (op.related) { + op.related.forEach((url) => relatedUrls.add(url)); + } + // Check operation-level externalDocs (legacy single link) if (op.externalDocs?.url) { relatedUrls.add(op.externalDocs.url); } @@ -1185,6 +1218,14 @@ function writeOpenapiTagArticleData( }; } + // Extract x-influxdatadocs-related if present + if ( + operation['x-influxdatadocs-related'] && + Array.isArray(operation['x-influxdatadocs-related']) + ) { + opMeta.related = operation['x-influxdatadocs-related']; + } + operations.push(opMeta); } }); diff --git a/content/influxdb3/cloud-serverless/api/quick-start/_index.md b/content/influxdb3/cloud-serverless/api/quick-start/_index.md index 284fe4442..56dc231f7 100644 --- a/content/influxdb3/cloud-serverless/api/quick-start/_index.md +++ b/content/influxdb3/cloud-serverless/api/quick-start/_index.md @@ -35,7 +35,7 @@ type: api layout: single staticFilePath: >- /openapi/influxdb-cloud-serverless/ref/tags/influxdb-cloud-serverless-ref-quick-start.yaml -weight: 100 +weight: 1 tag: Quick start isConceptual: true menuGroup: Concepts