docula 1.10.1 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/docula.d.ts CHANGED
@@ -129,6 +129,12 @@ type ApiSpecData = {
129
129
  securitySchemes: ApiSecurityScheme[];
130
130
  };
131
131
 
132
+ type DoculaOpenApiSpecEntry = {
133
+ name: string;
134
+ url: string;
135
+ order?: number;
136
+ apiSpec?: ApiSpecData;
137
+ };
132
138
  type DoculaChangelogEntry = {
133
139
  title: string;
134
140
  date: string;
@@ -160,9 +166,9 @@ type DoculaData = {
160
166
  documents?: DoculaDocument[];
161
167
  sidebarItems?: DoculaSection[];
162
168
  announcement?: string;
163
- openApiUrl?: string;
164
169
  hasApi?: boolean;
165
170
  apiSpec?: ApiSpecData;
171
+ openApiSpecs?: DoculaOpenApiSpecEntry[];
166
172
  changelogEntries?: DoculaChangelogEntry[];
167
173
  hasReadme?: boolean;
168
174
  themeMode?: string;
@@ -236,6 +242,11 @@ type DoculaHeaderLink = {
236
242
  url: string;
237
243
  icon?: string;
238
244
  };
245
+ type DoculaOpenApiSpec = {
246
+ name: string;
247
+ url: string;
248
+ order?: number;
249
+ };
239
250
  type DoculaOpenGraph = {
240
251
  title?: string;
241
252
  description?: string;
@@ -297,11 +308,11 @@ declare class DoculaOptions {
297
308
  */
298
309
  sections?: DoculaSection[];
299
310
  /**
300
- * OpenAPI specification URL for API documentation.
301
- * When provided, creates a dedicated /api page
302
- * Supports both external URLs (https://...) and relative paths (/openapi.json)
311
+ * OpenAPI specification for API documentation.
312
+ * Pass a string URL for a single spec, or an array of DoculaOpenApiSpec for multiple specs.
313
+ * When provided, creates a dedicated /api page.
303
314
  */
304
- openApiUrl?: string;
315
+ openApiUrl?: string | DoculaOpenApiSpec[];
305
316
  /**
306
317
  * When true, GitHub releases are converted to changelog entries and merged
307
318
  * with file-based changelog entries. Requires a changelog template to exist.
@@ -442,6 +453,7 @@ declare class DoculaBuilder {
442
453
  buildDocsPages(data: DoculaData): Promise<void>;
443
454
  renderApiContent(data: DoculaData): Promise<string>;
444
455
  buildApiPage(data: DoculaData): Promise<void>;
456
+ buildAllApiPages(data: DoculaData): Promise<void>;
445
457
  buildApiHomePage(data: DoculaData): Promise<void>;
446
458
  getChangelogEntries(changelogPath: string, cachedEntries?: Map<string, DoculaChangelogEntry>, previousHashes?: Record<string, string>, currentHashes?: Record<string, string>): DoculaChangelogEntry[];
447
459
  parseChangelogEntry(filePath: string): DoculaChangelogEntry;
@@ -575,4 +587,4 @@ declare class Docula {
575
587
  serve(options: DoculaOptions): Promise<http.Server>;
576
588
  }
577
589
 
578
- export { type DoculaAIOptions, type DoculaCacheOptions, type DoculaChangelogEntry, DoculaConsole, type DoculaCookieAuth, type DoculaHeaderLink, DoculaOptions, Docula as default };
590
+ export { type DoculaAIOptions, type DoculaCacheOptions, type DoculaChangelogEntry, DoculaConsole, type DoculaCookieAuth, type DoculaHeaderLink, type DoculaOpenApiSpec, DoculaOptions, Docula as default };
package/dist/docula.js CHANGED
@@ -222,7 +222,7 @@ function parseOpenApiSpec(specJson) {
222
222
  if (!operation) {
223
223
  continue;
224
224
  }
225
- const tags = Array.isArray(operation.tags) && operation.tags.length > 0 ? operation.tags : ["Default"];
225
+ const tags = Array.isArray(operation.tags) && operation.tags.length > 0 ? operation.tags : [""];
226
226
  const parameters = extractParameters(operation, pathItem, spec);
227
227
  const requestBody = extractRequestBody(operation, spec);
228
228
  const responses = extractResponses(operation, spec);
@@ -832,7 +832,7 @@ async function buildSiteMapPage(data, options) {
832
832
  url: `${data.siteUrl}${data.baseUrl}/changelog-latest.json`
833
833
  });
834
834
  }
835
- if (data.openApiUrl && data.templates?.api) {
835
+ if (data.hasApi || data.openApiSpecs?.[0] && data.templates?.api) {
836
836
  urls.push({
837
837
  url: `${data.siteUrl}${data.apiUrl}`
838
838
  });
@@ -926,26 +926,16 @@ var writrOptions3 = {
926
926
  throwOnEmptyListeners: false
927
927
  };
928
928
  function resolveOpenApiSpecUrl(data) {
929
- if (!data.openApiUrl) {
929
+ const specUrl = data.openApiSpecs?.[0]?.url;
930
+ if (!specUrl) {
930
931
  return void 0;
931
932
  }
932
- if (isRemoteUrl(data.openApiUrl)) {
933
- return data.openApiUrl;
933
+ if (isRemoteUrl(specUrl)) {
934
+ return specUrl;
934
935
  }
935
- const normalizedPath = data.openApiUrl.startsWith("/") ? data.openApiUrl : `/${data.openApiUrl}`;
936
+ const normalizedPath = specUrl.startsWith("/") ? specUrl : `/${specUrl}`;
936
937
  return buildAbsoluteSiteUrl(data.siteUrl, normalizedPath);
937
938
  }
938
- function resolveLocalOpenApiPath(data) {
939
- if (!data.openApiUrl || isRemoteUrl(data.openApiUrl)) {
940
- return void 0;
941
- }
942
- const openApiPathWithoutQuery = data.openApiUrl.split(/[?#]/)[0];
943
- if (!openApiPathWithoutQuery) {
944
- return void 0;
945
- }
946
- const normalizedPath = openApiPathWithoutQuery.startsWith("/") ? openApiPathWithoutQuery.slice(1) : openApiPathWithoutQuery;
947
- return path3.join(data.sitePath, normalizedPath);
948
- }
949
939
  async function getSafeSiteOverrideFileContent(sitePath, fileName) {
950
940
  const resolvedSitePath = path3.resolve(sitePath);
951
941
  const candidatePath = path3.resolve(sitePath, fileName);
@@ -975,45 +965,152 @@ async function getSafeSiteOverrideFileContent(sitePath, fileName) {
975
965
  return fs3.promises.readFile(realCandidatePath, "utf8");
976
966
  }
977
967
  async function getSafeLocalOpenApiSpec(data) {
978
- const localOpenApiPath = resolveLocalOpenApiPath(data);
979
- if (!localOpenApiPath) {
968
+ const specUrl = data.openApiSpecs?.[0]?.url;
969
+ if (!specUrl) {
970
+ return void 0;
971
+ }
972
+ return getSafeLocalOpenApiSpecForSpec(data, specUrl);
973
+ }
974
+ function resolveLocalOpenApiPathForSpec(data, specUrl) {
975
+ if (isRemoteUrl(specUrl)) {
976
+ return void 0;
977
+ }
978
+ const urlWithoutQuery = specUrl.split(/[?#]/)[0];
979
+ if (!urlWithoutQuery) {
980
+ return void 0;
981
+ }
982
+ const normalizedPath = urlWithoutQuery.startsWith("/") ? urlWithoutQuery.slice(1) : urlWithoutQuery;
983
+ return path3.join(data.sitePath, normalizedPath);
984
+ }
985
+ async function getSafeLocalOpenApiSpecForSpec(data, specUrl) {
986
+ const localPath = resolveLocalOpenApiPathForSpec(data, specUrl);
987
+ if (!localPath) {
980
988
  return void 0;
981
989
  }
982
990
  const resolvedSitePath = path3.resolve(data.sitePath);
983
- const resolvedLocalOpenApiPath = path3.resolve(localOpenApiPath);
984
- if (!isPathWithinBasePath(resolvedLocalOpenApiPath, resolvedSitePath)) {
991
+ const resolvedLocalPath = path3.resolve(localPath);
992
+ if (!isPathWithinBasePath(resolvedLocalPath, resolvedSitePath)) {
985
993
  return void 0;
986
994
  }
987
- let localOpenApiStats;
995
+ let localStats;
988
996
  try {
989
- localOpenApiStats = await fs3.promises.lstat(resolvedLocalOpenApiPath);
997
+ localStats = await fs3.promises.lstat(resolvedLocalPath);
990
998
  } catch {
991
999
  return void 0;
992
1000
  }
993
- if (!localOpenApiStats.isFile() || localOpenApiStats.isSymbolicLink()) {
1001
+ if (!localStats.isFile() || localStats.isSymbolicLink()) {
994
1002
  return void 0;
995
1003
  }
996
1004
  let realSitePath;
997
- let realLocalOpenApiPath;
1005
+ let realLocalPath;
998
1006
  try {
999
1007
  realSitePath = await fs3.promises.realpath(resolvedSitePath);
1000
- realLocalOpenApiPath = await fs3.promises.realpath(resolvedLocalOpenApiPath);
1008
+ realLocalPath = await fs3.promises.realpath(resolvedLocalPath);
1001
1009
  } catch {
1002
1010
  return void 0;
1003
1011
  }
1004
- if (!isPathWithinBasePath(realLocalOpenApiPath, realSitePath)) {
1012
+ if (!isPathWithinBasePath(realLocalPath, realSitePath)) {
1005
1013
  return void 0;
1006
1014
  }
1007
- const localOpenApiContent = (await fs3.promises.readFile(realLocalOpenApiPath, "utf8")).trim();
1008
- return {
1009
- sourcePath: realLocalOpenApiPath,
1010
- content: localOpenApiContent
1011
- };
1015
+ const content = (await fs3.promises.readFile(realLocalPath, "utf8")).trim();
1016
+ return { sourcePath: realLocalPath, content };
1017
+ }
1018
+ function resolveSpecUrl(data, specUrl) {
1019
+ if (isRemoteUrl(specUrl)) {
1020
+ return specUrl;
1021
+ }
1022
+ const normalizedPath = specUrl.startsWith("/") ? specUrl : `/${specUrl}`;
1023
+ return buildAbsoluteSiteUrl(data.siteUrl, normalizedPath);
1024
+ }
1025
+ async function parseAndRenderSpec(data, specUrl) {
1026
+ let apiSpec;
1027
+ const localSpec = await getSafeLocalOpenApiSpecForSpec(data, specUrl);
1028
+ if (localSpec) {
1029
+ apiSpec = parseOpenApiSpec(localSpec.content);
1030
+ } else if (isRemoteUrl(specUrl)) {
1031
+ try {
1032
+ const response = await fetch(specUrl);
1033
+ const specContent = await response.text();
1034
+ apiSpec = parseOpenApiSpec(specContent);
1035
+ } catch {
1036
+ }
1037
+ }
1038
+ if (apiSpec) {
1039
+ apiSpec.info.description = new Writr3(
1040
+ apiSpec.info.description,
1041
+ writrOptions3
1042
+ ).renderSync();
1043
+ for (const group of apiSpec.groups) {
1044
+ group.description = new Writr3(
1045
+ group.description,
1046
+ writrOptions3
1047
+ ).renderSync();
1048
+ for (const op of group.operations) {
1049
+ op.description = new Writr3(op.description, writrOptions3).renderSync();
1050
+ }
1051
+ }
1052
+ }
1053
+ return apiSpec;
1054
+ }
1055
+ async function copySpecSourceFile(data, specUrl, outputDir) {
1056
+ const safeSpec = await getSafeLocalOpenApiSpecForSpec(data, specUrl);
1057
+ if (safeSpec) {
1058
+ await fs3.promises.mkdir(outputDir, { recursive: true });
1059
+ await fs3.promises.copyFile(
1060
+ safeSpec.sourcePath,
1061
+ `${outputDir}/swagger.json`
1062
+ );
1063
+ }
1064
+ }
1065
+ async function renderCombinedApiContent(ecto, data) {
1066
+ if (!data.templates?.api) {
1067
+ throw new Error("No API template found");
1068
+ }
1069
+ const specs = data.openApiSpecs ?? [];
1070
+ const apiSpecs = [];
1071
+ for (const spec of specs) {
1072
+ const urlDir = spec.url.replace(/[?#].*$/, "").replace(/\/[^/]+$/, "");
1073
+ const specOutputDir = `${data.output}/${urlDir}`;
1074
+ await copySpecSourceFile(data, spec.url, specOutputDir);
1075
+ const apiSpec = await parseAndRenderSpec(data, spec.url);
1076
+ apiSpecs.push({
1077
+ specName: spec.name,
1078
+ apiSpec,
1079
+ specUrl: resolveSpecUrl(data, spec.url)
1080
+ });
1081
+ }
1082
+ const apiTemplate = `${data.templatePath}/${data.templates.api}`;
1083
+ return ecto.renderFromFile(
1084
+ apiTemplate,
1085
+ {
1086
+ ...data,
1087
+ apiSpecs,
1088
+ // Backward compat: set single apiSpec for single-spec sites
1089
+ apiSpec: apiSpecs[0]?.apiSpec,
1090
+ specUrl: apiSpecs[0]?.specUrl,
1091
+ ...resolveOpenGraphData(data, `/${data.apiPath}/`),
1092
+ jsonLd: resolveJsonLd("api", data, `/${data.apiPath}/`)
1093
+ },
1094
+ data.templatePath
1095
+ );
1096
+ }
1097
+ async function buildAllApiPages(ecto, data) {
1098
+ if (!data.openApiSpecs || data.openApiSpecs.length === 0 || !data.templates?.api) {
1099
+ return;
1100
+ }
1101
+ const outputDir = `${data.output}/${data.apiPath}`;
1102
+ await fs3.promises.mkdir(outputDir, { recursive: true });
1103
+ const content = await renderCombinedApiContent(ecto, data);
1104
+ await fs3.promises.writeFile(`${outputDir}/index.html`, content, "utf8");
1012
1105
  }
1013
1106
  async function renderApiContent(ecto, data) {
1014
- if (!data.openApiUrl || !data.templates?.api) {
1107
+ const firstSpecUrl = data.openApiSpecs?.[0]?.url;
1108
+ if (!firstSpecUrl || !data.templates?.api) {
1015
1109
  throw new Error("No API template or openApiUrl found");
1016
1110
  }
1111
+ if (data.openApiSpecs && data.openApiSpecs.length > 0) {
1112
+ return renderCombinedApiContent(ecto, data);
1113
+ }
1017
1114
  const swaggerSource = `${data.sitePath}/api/swagger.json`;
1018
1115
  const apiOutputPath = `${data.output}/${data.apiPath}`;
1019
1116
  await fs3.promises.mkdir(apiOutputPath, { recursive: true });
@@ -1024,9 +1121,9 @@ async function renderApiContent(ecto, data) {
1024
1121
  const localSpec = await getSafeLocalOpenApiSpec(data);
1025
1122
  if (localSpec) {
1026
1123
  apiSpec = parseOpenApiSpec(localSpec.content);
1027
- } else if (data.openApiUrl && isRemoteUrl(data.openApiUrl)) {
1124
+ } else if (firstSpecUrl && isRemoteUrl(firstSpecUrl)) {
1028
1125
  try {
1029
- const response = await fetch(data.openApiUrl);
1126
+ const response = await fetch(firstSpecUrl);
1030
1127
  const specContent = await response.text();
1031
1128
  apiSpec = parseOpenApiSpec(specContent);
1032
1129
  } catch {
@@ -1052,8 +1149,9 @@ async function renderApiContent(ecto, data) {
1052
1149
  apiTemplate,
1053
1150
  {
1054
1151
  ...data,
1055
- specUrl: data.openApiUrl,
1152
+ specUrl: firstSpecUrl,
1056
1153
  apiSpec,
1154
+ apiSpecs: [{ specName: "API Reference", apiSpec, specUrl: firstSpecUrl }],
1057
1155
  ...resolveOpenGraphData(data, `/${data.apiPath}/`),
1058
1156
  jsonLd: resolveJsonLd("api", data, `/${data.apiPath}/`)
1059
1157
  },
@@ -1061,12 +1159,13 @@ async function renderApiContent(ecto, data) {
1061
1159
  );
1062
1160
  }
1063
1161
  async function buildApiPage(ecto, data) {
1064
- if (!data.openApiUrl || !data.templates?.api) {
1162
+ if (!data.openApiSpecs?.[0]?.url || !data.templates?.api) {
1065
1163
  return;
1066
1164
  }
1067
- const apiPath = `${data.output}/${data.apiPath}/index.html`;
1165
+ const apiDir = `${data.output}/${data.apiPath}`;
1166
+ await fs3.promises.mkdir(apiDir, { recursive: true });
1068
1167
  const apiContent = await renderApiContent(ecto, data);
1069
- await fs3.promises.writeFile(apiPath, apiContent, "utf8");
1168
+ await fs3.promises.writeFile(`${apiDir}/index.html`, apiContent, "utf8");
1070
1169
  }
1071
1170
  async function buildApiHomePage(ecto, data) {
1072
1171
  const indexPath = `${data.output}/index.html`;
@@ -2282,7 +2381,13 @@ function generateLlmsIndexContent(data) {
2282
2381
  }
2283
2382
  lines.push("");
2284
2383
  lines.push("## API Reference");
2285
- if (data.hasApi) {
2384
+ if (data.openApiSpecs && data.openApiSpecs.length > 1) {
2385
+ for (const spec of data.openApiSpecs) {
2386
+ lines.push(
2387
+ `- [${spec.name}](${buildAbsoluteSiteUrl(data.siteUrl, data.apiUrl)})`
2388
+ );
2389
+ }
2390
+ } else if (data.hasApi) {
2286
2391
  lines.push(
2287
2392
  `- [API Documentation](${buildAbsoluteSiteUrl(data.siteUrl, data.apiUrl)})`
2288
2393
  );
@@ -2344,8 +2449,22 @@ async function generateLlmsFullContent(data) {
2344
2449
  }
2345
2450
  lines.push("");
2346
2451
  lines.push("## API Reference");
2347
- if (data.hasApi) {
2348
- lines.push(`URL: ${buildAbsoluteSiteUrl(data.siteUrl, data.apiUrl)}`);
2452
+ lines.push(`URL: ${buildAbsoluteSiteUrl(data.siteUrl, data.apiUrl)}`);
2453
+ if (data.openApiSpecs && data.openApiSpecs.length > 1) {
2454
+ for (const spec of data.openApiSpecs) {
2455
+ lines.push("");
2456
+ lines.push(`### ${spec.name}`);
2457
+ lines.push("");
2458
+ const localSpec = await getSafeLocalOpenApiSpecForSpec(data, spec.url);
2459
+ if (localSpec) {
2460
+ lines.push(`OpenAPI Spec Source: ${toPosixPath(localSpec.sourcePath)}`);
2461
+ lines.push("");
2462
+ lines.push(localSpec.content || "_No content_");
2463
+ } else {
2464
+ lines.push(`OpenAPI Spec URL: ${resolveSpecUrl(data, spec.url)}`);
2465
+ }
2466
+ }
2467
+ } else if (data.hasApi) {
2349
2468
  lines.push("");
2350
2469
  const localOpenApiSpec = await getSafeLocalOpenApiSpec(data);
2351
2470
  if (localOpenApiSpec) {
@@ -2933,9 +3052,9 @@ var DoculaOptions = class {
2933
3052
  */
2934
3053
  sections;
2935
3054
  /**
2936
- * OpenAPI specification URL for API documentation.
2937
- * When provided, creates a dedicated /api page
2938
- * Supports both external URLs (https://...) and relative paths (/openapi.json)
3055
+ * OpenAPI specification for API documentation.
3056
+ * Pass a string URL for a single spec, or an array of DoculaOpenApiSpec for multiple specs.
3057
+ * When provided, creates a dedicated /api page.
2939
3058
  */
2940
3059
  openApiUrl;
2941
3060
  /**
@@ -3104,7 +3223,20 @@ var DoculaOptions = class {
3104
3223
  this.port = options.port;
3105
3224
  }
3106
3225
  if (options.openApiUrl) {
3107
- this.openApiUrl = options.openApiUrl;
3226
+ if (typeof options.openApiUrl === "string") {
3227
+ this.openApiUrl = options.openApiUrl;
3228
+ } else if (Array.isArray(options.openApiUrl)) {
3229
+ const validSpecs = options.openApiUrl.filter(
3230
+ (spec) => typeof spec === "object" && spec !== null && typeof spec.name === "string" && typeof spec.url === "string"
3231
+ );
3232
+ if (validSpecs.length > 0) {
3233
+ this.openApiUrl = validSpecs.map((spec) => ({
3234
+ name: spec.name,
3235
+ url: spec.url,
3236
+ ...typeof spec.order === "number" && { order: spec.order }
3237
+ }));
3238
+ }
3239
+ }
3108
3240
  }
3109
3241
  if (options.enableReleaseChangelog !== void 0 && typeof options.enableReleaseChangelog === "boolean") {
3110
3242
  this.enableReleaseChangelog = options.enableReleaseChangelog;
@@ -3285,7 +3417,6 @@ var DoculaBuilder = class {
3285
3417
  output: this.options.output,
3286
3418
  githubPath: this.options.githubPath,
3287
3419
  sections: this.options.sections,
3288
- openApiUrl: this.options.openApiUrl,
3289
3420
  hasReadme: fs11.existsSync(`${this.options.sitePath}/README.md`),
3290
3421
  themeMode: this.options.themeMode,
3291
3422
  cookieAuth: this.options.cookieAuth,
@@ -3309,12 +3440,63 @@ var DoculaBuilder = class {
3309
3440
  if (doculaData.hasReadme) {
3310
3441
  currentAssetHashes["README.md"] = hashFile(this._hash, readmePath);
3311
3442
  }
3312
- if (!doculaData.openApiUrl && fs11.existsSync(`${doculaData.sitePath}/api/swagger.json`)) {
3313
- doculaData.openApiUrl = buildUrlPath(
3314
- this.options.baseUrl,
3315
- this.options.apiPath,
3316
- "swagger.json"
3317
- );
3443
+ if (Array.isArray(this.options.openApiUrl)) {
3444
+ doculaData.openApiSpecs = this.options.openApiUrl.map((spec) => ({
3445
+ name: spec.name,
3446
+ url: isRemoteUrl(spec.url) ? spec.url : buildUrlPath(this.options.apiPath, spec.url),
3447
+ ...typeof spec.order === "number" && { order: spec.order }
3448
+ }));
3449
+ } else if (typeof this.options.openApiUrl === "string") {
3450
+ doculaData.openApiSpecs = [
3451
+ { name: "API Reference", url: this.options.openApiUrl }
3452
+ ];
3453
+ } else {
3454
+ const detectedSpecs = [];
3455
+ if (fs11.existsSync(`${doculaData.sitePath}/api/swagger.json`)) {
3456
+ const rootUrl = buildUrlPath(
3457
+ this.options.baseUrl,
3458
+ this.options.apiPath,
3459
+ "swagger.json"
3460
+ );
3461
+ detectedSpecs.push({
3462
+ name: "API Reference",
3463
+ url: rootUrl
3464
+ });
3465
+ }
3466
+ const apiDir = `${doculaData.sitePath}/api`;
3467
+ if (fs11.existsSync(apiDir)) {
3468
+ try {
3469
+ const entries = await fs11.promises.readdir(apiDir, {
3470
+ withFileTypes: true
3471
+ });
3472
+ for (const entry of entries) {
3473
+ if (entry.isDirectory() && fs11.existsSync(`${apiDir}/${entry.name}/swagger.json`)) {
3474
+ const specUrl = buildUrlPath(
3475
+ this.options.baseUrl,
3476
+ this.options.apiPath,
3477
+ entry.name,
3478
+ "swagger.json"
3479
+ );
3480
+ const displayName = entry.name.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
3481
+ detectedSpecs.push({
3482
+ name: displayName,
3483
+ url: specUrl
3484
+ });
3485
+ }
3486
+ }
3487
+ } catch {
3488
+ }
3489
+ }
3490
+ if (detectedSpecs.length > 0) {
3491
+ doculaData.openApiSpecs = detectedSpecs;
3492
+ }
3493
+ }
3494
+ if (doculaData.openApiSpecs && doculaData.openApiSpecs.length > 1) {
3495
+ doculaData.openApiSpecs.sort((a, b) => {
3496
+ const aOrder = a.order ?? Number.MAX_SAFE_INTEGER;
3497
+ const bOrder = b.order ?? Number.MAX_SAFE_INTEGER;
3498
+ return aOrder - bOrder;
3499
+ });
3318
3500
  }
3319
3501
  if (this.options.githubPath) {
3320
3502
  doculaData.github = await this.getGithubData(this.options.githubPath);
@@ -3404,7 +3586,7 @@ var DoculaBuilder = class {
3404
3586
  doculaData.hasChangelog
3405
3587
  );
3406
3588
  doculaData.hasApi = Boolean(
3407
- doculaData.openApiUrl && doculaData.templates?.api
3589
+ doculaData.openApiSpecs && doculaData.openApiSpecs.length > 0 && doculaData.templates?.api
3408
3590
  );
3409
3591
  doculaData.lastModified = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
3410
3592
  this._console.step("Building pages...");
@@ -3445,7 +3627,7 @@ var DoculaBuilder = class {
3445
3627
  }
3446
3628
  if (doculaData.hasApi) {
3447
3629
  this._console.step("Building API page...");
3448
- await this.buildApiPage(doculaData);
3630
+ await this.buildAllApiPages(doculaData);
3449
3631
  this._console.fileBuilt(`${this.options.apiPath}/index.html`);
3450
3632
  }
3451
3633
  if (doculaData.hasChangelog) {
@@ -3547,6 +3729,27 @@ var DoculaBuilder = class {
3547
3729
  swaggerPath
3548
3730
  );
3549
3731
  }
3732
+ const apiDirPath = `${siteRelativePath}/api`;
3733
+ if (fs11.existsSync(apiDirPath)) {
3734
+ try {
3735
+ const apiEntries = await fs11.promises.readdir(apiDirPath, {
3736
+ withFileTypes: true
3737
+ });
3738
+ for (const entry of apiEntries) {
3739
+ if (entry.isDirectory()) {
3740
+ const subSwaggerPath = `${apiDirPath}/${entry.name}/swagger.json`;
3741
+ if (fs11.existsSync(subSwaggerPath)) {
3742
+ const hashKey = `api/${entry.name}/swagger.json`;
3743
+ currentAssetHashes[hashKey] = hashFile(
3744
+ this._hash,
3745
+ subSwaggerPath
3746
+ );
3747
+ }
3748
+ }
3749
+ }
3750
+ } catch {
3751
+ }
3752
+ }
3550
3753
  copyPublicFolder(
3551
3754
  this._console,
3552
3755
  this._hash,
@@ -3631,19 +3834,6 @@ ${readmeContent}`;
3631
3834
  }
3632
3835
  await fs11.promises.mkdir(this._options.sitePath, { recursive: true });
3633
3836
  await fs11.promises.writeFile(siteReadmePath, readmeContent, "utf8");
3634
- const availableAssets = listContentAssets(this._options, cwdDir);
3635
- for (const assetRelPath of availableAssets) {
3636
- if (readmeContent.includes(assetRelPath)) {
3637
- const source = path12.join(cwdDir, assetRelPath);
3638
- const stat = await fs11.promises.lstat(source);
3639
- if (stat.isSymbolicLink()) {
3640
- continue;
3641
- }
3642
- const target = path12.join(this._options.sitePath, assetRelPath);
3643
- await fs11.promises.mkdir(path12.dirname(target), { recursive: true });
3644
- await fs11.promises.copyFile(source, target);
3645
- }
3646
- }
3647
3837
  }
3648
3838
  async getGithubData(githubPath) {
3649
3839
  const paths = githubPath.split("/");
@@ -3849,6 +4039,9 @@ ${readmeContent}`;
3849
4039
  async buildApiPage(data) {
3850
4040
  return buildApiPage(this._ecto, data);
3851
4041
  }
4042
+ async buildAllApiPages(data) {
4043
+ return buildAllApiPages(this._ecto, data);
4044
+ }
3852
4045
  async buildApiHomePage(data) {
3853
4046
  return buildApiHomePage(this._ecto, data);
3854
4047
  }
@@ -4380,6 +4573,7 @@ export {
4380
4573
  /* v8 ignore start -- @preserve */
4381
4574
  /* v8 ignore next -- @preserve */
4382
4575
  /* v8 ignore next 3 -- @preserve */
4576
+ /* v8 ignore next 6 -- @preserve */
4383
4577
  /* v8 ignore next 9 -- @preserve */
4384
4578
  /* v8 ignore next 4 -- @preserve */
4385
4579
  /* v8 ignore next 5 -- @preserve */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docula",
3
- "version": "1.10.1",
3
+ "version": "1.11.1",
4
4
  "description": "Beautiful Website for Your Projects",
5
5
  "type": "module",
6
6
  "main": "./dist/docula.js",
@@ -87,7 +87,8 @@
87
87
  "generate-init-file": "tsx scripts/generate-init-file.ts",
88
88
  "website:build": "node bin/docula.js build -s ./site -o ./site/dist",
89
89
  "website:serve": "pnpm build && node bin/docula.js dev -s ./site -p 3333",
90
- "website:mega": "node bin/docula.js serve -s ./test/fixtures/mega-page-site --watch --clean",
90
+ "website:mapi": "pnpm build && node bin/docula.js dev -s ./test/fixtures/multi-api-site -p 3333",
91
+ "website:mega": "node bin/docula.js dev -s ./test/fixtures/mega-page-site",
91
92
  "website:mega:custom": "node bin/docula.js serve -s ./test/fixtures/mega-custom-template --watch",
92
93
  "website:singlepage": "node bin/docula.js dev -s ./test/fixtures/single-page-site --clean",
93
94
  "website:nohome": "node bin/docula.js serve -s ./test/fixtures/mega-page-site-no-home-page --watch --clean",
@@ -29,7 +29,7 @@
29
29
  </style>
30
30
  {{/if}}
31
31
 
32
- {{#if apiSpec}}
32
+ {{#if apiSpecs}}
33
33
  <div class="api-mobile-toggle">
34
34
  <button id="api-sidebar-toggle">
35
35
  <span>API Navigation</span>
@@ -41,12 +41,17 @@
41
41
  <aside class="api-sidebar" id="api-sidebar">
42
42
  <input type="text" class="api-search" id="api-search" placeholder="Search endpoints..." />
43
43
 
44
- {{#each apiSpec.groups}}
44
+ {{#each apiSpecs}}
45
+ {{#if this.apiSpec}}
46
+ <div class="api-sidebar__spec-heading">{{this.specName}}</div>
47
+ {{#each this.apiSpec.groups}}
45
48
  <div class="api-sidebar__group" data-group="{{this.id}}">
49
+ {{#if this.name}}
46
50
  <button class="api-sidebar__group-toggle">
47
51
  <span>{{this.name}}</span>
48
52
  <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
49
53
  </button>
54
+ {{/if}}
50
55
  <div class="api-sidebar__group-items">
51
56
  {{#each this.operations}}
52
57
  <a href="#{{this.id}}" class="api-sidebar__item" data-method="{{this.method}}" data-path="{{this.path}}">
@@ -57,21 +62,25 @@
57
62
  </div>
58
63
  </div>
59
64
  {{/each}}
65
+ {{/if}}
66
+ {{/each}}
60
67
  </aside>
61
68
 
62
69
  <main class="api-content">
70
+ {{#each apiSpecs}}
71
+ {{#if this.apiSpec}}
63
72
  <section class="api-info">
64
- <h1 class="api-info__title">{{apiSpec.info.title}}</h1>
65
- {{#if apiSpec.info.version}}
66
- <span class="api-info__version">v{{apiSpec.info.version}}</span>
73
+ <h1 class="api-info__title">{{this.apiSpec.info.title}}</h1>
74
+ {{#if this.apiSpec.info.version}}
75
+ <span class="api-info__version">v{{this.apiSpec.info.version}}</span>
67
76
  {{/if}}
68
- {{#if apiSpec.info.description}}
69
- <div class="api-info__description">{{{apiSpec.info.description}}}</div>
77
+ {{#if this.apiSpec.info.description}}
78
+ <div class="api-info__description">{{{this.apiSpec.info.description}}}</div>
70
79
  {{/if}}
71
- {{#if apiSpec.servers}}
80
+ {{#if this.apiSpec.servers}}
72
81
  <div class="api-info__servers">
73
82
  <div class="api-info__server-label">Server</div>
74
- {{#each apiSpec.servers}}
83
+ {{#each this.apiSpec.servers}}
75
84
  <code class="api-info__server-url">{{this.url}}</code>
76
85
  {{/each}}
77
86
  </div>
@@ -79,24 +88,26 @@
79
88
  <div class="api-auth">
80
89
  <div class="api-auth__label">Authorization</div>
81
90
  <div class="api-auth__controls">
82
- <select class="api-auth__type" id="api-auth-type">
91
+ <select class="api-auth__type" id="api-auth-type-{{@index}}">
83
92
  <option value="none">None</option>
84
93
  <option value="apikey">API Key (x-api-key)</option>
85
94
  <option value="bearer">Bearer Token</option>
86
95
  </select>
87
- <input type="password" class="api-auth__value api-auth__value--hidden" id="api-auth-value" placeholder="Enter value..." />
96
+ <input type="password" class="api-auth__value api-auth__value--hidden" id="api-auth-value-{{@index}}" placeholder="Enter value..." />
88
97
  </div>
89
98
  </div>
90
99
  </section>
91
100
 
92
- {{#each apiSpec.groups}}
101
+ {{#each this.apiSpec.groups}}
93
102
  <div class="api-group" id="group-{{this.id}}">
103
+ {{#if this.name}}
94
104
  <div class="api-group__header">
95
105
  <h2 class="api-group__title">{{this.name}}</h2>
96
106
  {{#if this.description}}
97
107
  <div class="api-group__description">{{{this.description}}}</div>
98
108
  {{/if}}
99
109
  </div>
110
+ {{/if}}
100
111
 
101
112
  {{#each this.operations}}
102
113
  <div class="api-operation api-operation--collapsed" id="{{this.id}}">
@@ -202,6 +213,8 @@
202
213
  {{/each}}
203
214
  </div>
204
215
  {{/each}}
216
+ {{/if}}
217
+ {{/each}}
205
218
  </main>
206
219
  </div>
207
220
  {{/if}}
@@ -1,5 +1,19 @@
1
1
  /* API Reference - Classic Template */
2
2
 
3
+ .api-sidebar__spec-heading {
4
+ font-size: 13px;
5
+ font-weight: 700;
6
+ color: var(--text-primary, #333);
7
+ padding: 10px 10px 4px;
8
+ margin-top: 8px;
9
+ border-top: 1px solid var(--border-color, #e0e0e0);
10
+ }
11
+
12
+ .api-sidebar__spec-heading:first-child {
13
+ margin-top: 0;
14
+ border-top: none;
15
+ }
16
+
3
17
  .api-reference {
4
18
  display: flex;
5
19
  max-width: 90%;
@@ -1 +1 @@
1
- <a href="/api" class="home-docs-button">API Reference</a>
1
+ <a href="{{apiUrl}}" class="home-docs-button">API Reference</a>
@@ -2,7 +2,7 @@
2
2
  <div class="header-content">
3
3
  <nav class="nav">
4
4
  <div class="header-menu hide-d">
5
- <a class="header-logo" href="/">
5
+ <a class="header-logo" href="{{#if homeUrl}}{{homeUrl}}{{else}}/{{/if}}">
6
6
  <img src="/logo.svg" alt="logo" />
7
7
  </a>
8
8
  {{#if headerLinks}}
@@ -1,6 +1,6 @@
1
1
  <aside class="sidebar hidden" id="sidebar">
2
2
  <div class="sidebar-logo">
3
- <a class="header-link" href="/">
3
+ <a class="header-link" href="{{#if homeUrl}}{{homeUrl}}{{else}}/{{/if}}">
4
4
  <img src="/logo.svg" alt="logo"/>
5
5
  </a>
6
6
  </div>
@@ -12,7 +12,7 @@
12
12
  <body>
13
13
  {{> header-bar }}
14
14
 
15
- {{#if apiSpec}}
15
+ {{#if apiSpecs}}
16
16
  <div class="api-mobile-toggle">
17
17
  <button id="api-sidebar-toggle">
18
18
  <span>API Navigation</span>
@@ -24,12 +24,17 @@
24
24
  <aside class="api-sidebar" id="api-sidebar">
25
25
  <input type="text" class="api-search" id="api-search" placeholder="Search endpoints..." />
26
26
 
27
- {{#each apiSpec.groups}}
27
+ {{#each apiSpecs}}
28
+ {{#if this.apiSpec}}
29
+ <div class="api-sidebar__spec-heading">{{this.specName}}</div>
30
+ {{#each this.apiSpec.groups}}
28
31
  <div class="api-sidebar__group" data-group="{{this.id}}">
32
+ {{#if this.name}}
29
33
  <button class="api-sidebar__group-toggle">
30
34
  <span>{{this.name}}</span>
31
35
  <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
32
36
  </button>
37
+ {{/if}}
33
38
  <div class="api-sidebar__group-items">
34
39
  {{#each this.operations}}
35
40
  <a href="#{{this.id}}" class="api-sidebar__item" data-method="{{this.method}}" data-path="{{this.path}}">
@@ -40,87 +45,56 @@
40
45
  </div>
41
46
  </div>
42
47
  {{/each}}
48
+ {{/if}}
49
+ {{/each}}
43
50
  </aside>
44
51
 
45
52
  <main class="api-content">
53
+ {{#each apiSpecs}}
54
+ {{#if this.apiSpec}}
46
55
  <section class="api-info">
47
- <h1 class="api-info__title">{{apiSpec.info.title}}</h1>
48
- {{#if apiSpec.info.version}}
49
- <span class="api-info__version">v{{apiSpec.info.version}}</span>
56
+ <h1 class="api-info__title">{{this.apiSpec.info.title}}</h1>
57
+ {{#if this.apiSpec.info.version}}
58
+ <span class="api-info__version">v{{this.apiSpec.info.version}}</span>
50
59
  {{/if}}
51
- {{#if apiSpec.info.description}}
52
- <div class="api-info__description">{{{apiSpec.info.description}}}</div>
60
+ {{#if this.apiSpec.info.description}}
61
+ <div class="api-info__description">{{{this.apiSpec.info.description}}}</div>
53
62
  {{/if}}
54
- {{#if apiSpec.servers}}
63
+ {{#if this.apiSpec.servers}}
55
64
  <div class="api-info__servers">
56
65
  <div class="api-info__server-label">Server</div>
57
- {{#each apiSpec.servers}}
66
+ {{#each this.apiSpec.servers}}
58
67
  <code class="api-info__server-url">{{this.url}}</code>
59
68
  {{/each}}
60
69
  </div>
61
70
  {{/if}}
62
- {{#if apiSpec.securitySchemes.length}}
71
+ {{#if this.apiSpec.securitySchemes.length}}
63
72
  <div class="api-auth">
64
73
  <div class="api-auth__label">Authorization</div>
65
74
  <div class="api-auth__controls">
66
- <select class="api-auth__type" id="api-auth-type">
75
+ <select class="api-auth__type" id="api-auth-type-{{@index}}">
67
76
  <option value="none">None</option>
68
- {{#each apiSpec.securitySchemes}}
77
+ {{#each this.apiSpec.securitySchemes}}
69
78
  <option value="{{this.key}}" data-scheme-type="{{this.type}}" data-scheme-scheme="{{this.scheme}}" data-scheme-name="{{this.name}}" data-scheme-in="{{this.in}}">{{this.description}}</option>
70
79
  {{/each}}
71
80
  </select>
72
- <input type="password" class="api-auth__value api-auth__value--hidden" id="api-auth-value" placeholder="Enter value..." />
73
- <span class="api-auth__cookie-status api-auth__cookie-status--hidden" id="api-auth-cookie-status"></span>
74
- <script>
75
- (function() {
76
- var authTypeSelect = document.getElementById('api-auth-type');
77
- if (!authTypeSelect) return;
78
-
79
- // Restore selection from localStorage
80
- var savedAuthType = localStorage.getItem('docula-api-auth-type');
81
- if (savedAuthType) {
82
- for (var i = 0; i < authTypeSelect.options.length; i++) {
83
- if (authTypeSelect.options[i].value === savedAuthType) {
84
- authTypeSelect.selectedIndex = i;
85
- break;
86
- }
87
- }
88
- }
89
-
90
- // Update UI based on selection
91
- var selectedOption = authTypeSelect.options[authTypeSelect.selectedIndex];
92
- var authValueInput = document.getElementById('api-auth-value');
93
- var cookieStatusBadge = document.getElementById('api-auth-cookie-status');
94
-
95
- if (selectedOption.value === 'none') return;
96
-
97
- var isCookieAuth = selectedOption.getAttribute('data-scheme-type') === 'apiKey'
98
- && selectedOption.getAttribute('data-scheme-in') === 'cookie';
99
-
100
- if (isCookieAuth) {
101
- if (cookieStatusBadge) {
102
- var auth = window.__doculaAuth || { loggedIn: false };
103
- cookieStatusBadge.textContent = auth.loggedIn ? 'Logged in' : 'Not logged in — use Login button above';
104
- cookieStatusBadge.className = 'api-auth__cookie-status' + (auth.loggedIn ? ' api-auth__cookie-status--ok' : ' api-auth__cookie-status--warn');
105
- }
106
- } else {
107
- if (authValueInput) authValueInput.classList.remove('api-auth__value--hidden');
108
- }
109
- })();
110
- </script>
81
+ <input type="password" class="api-auth__value api-auth__value--hidden" id="api-auth-value-{{@index}}" placeholder="Enter value..." />
82
+ <span class="api-auth__cookie-status api-auth__cookie-status--hidden" id="api-auth-cookie-status-{{@index}}"></span>
111
83
  </div>
112
84
  </div>
113
85
  {{/if}}
114
86
  </section>
115
87
 
116
- {{#each apiSpec.groups}}
88
+ {{#each this.apiSpec.groups}}
117
89
  <div class="api-group" id="group-{{this.id}}">
90
+ {{#if this.name}}
118
91
  <div class="api-group__header">
119
92
  <h2 class="api-group__title">{{this.name}}</h2>
120
93
  {{#if this.description}}
121
94
  <div class="api-group__description">{{{this.description}}}</div>
122
95
  {{/if}}
123
96
  </div>
97
+ {{/if}}
124
98
 
125
99
  {{#each this.operations}}
126
100
  <div class="api-operation api-operation--collapsed" id="{{this.id}}">
@@ -244,6 +218,8 @@
244
218
  {{/each}}
245
219
  </div>
246
220
  {{/each}}
221
+ {{/if}}
222
+ {{/each}}
247
223
  </main>
248
224
  </div>
249
225
  {{/if}}
@@ -50,6 +50,20 @@
50
50
  color: var(--muted-fg);
51
51
  }
52
52
 
53
+ .api-sidebar__spec-heading {
54
+ font-size: 13px;
55
+ font-weight: 700;
56
+ color: var(--fg);
57
+ padding: 10px 10px 4px;
58
+ margin-top: 8px;
59
+ border-top: 1px solid var(--border);
60
+ }
61
+
62
+ .api-sidebar__spec-heading:first-child {
63
+ margin-top: 0;
64
+ border-top: none;
65
+ }
66
+
53
67
  .api-sidebar__group {
54
68
  margin-bottom: 4px;
55
69
  }
@@ -4,24 +4,18 @@
4
4
  <button class="mobile-menu-toggle" id="mobile-menu-toggle" aria-label="Toggle navigation menu">
5
5
  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
6
6
  </button>
7
- <a href="{{baseUrl}}/" class="logo-link">
7
+ <a href="{{#if homeUrl}}{{homeUrl}}{{else}}{{baseUrl}}/{{/if}}" class="logo-link">
8
8
  <img alt="{{siteTitle}}" class="logo__img" src="{{baseUrl}}/logo.svg">
9
9
  <span class="logo__text">{{siteTitle}}</span>
10
10
  </a>
11
11
  <nav class="header-bottom__nav">
12
- {{#if homeUrl}}
13
- <a class="header-bottom__item header-bottom__item--home" href="{{homeUrl}}">
14
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>
15
- <span>Back to Home</span>
16
- </a>
17
- {{/if}}
18
12
  {{#if hasDocuments}}
19
13
  <a class="header-bottom__item" href="{{docsUrl}}/" id="nav-docs">
20
14
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
21
15
  <span>Documentation</span>
22
16
  </a>
23
17
  {{/if}}
24
- {{#if openApiUrl}}
18
+ {{#if hasApi}}
25
19
  <a class="header-bottom__item" href="{{apiUrl}}" id="nav-api">
26
20
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/></svg>
27
21
  <span>API Reference</span>
@@ -74,19 +68,13 @@
74
68
  </header>
75
69
  <aside class="mobile-sidebar" id="mobile-sidebar">
76
70
  <nav class="mobile-nav">
77
- {{#if homeUrl}}
78
- <a class="mobile-nav__item" href="{{homeUrl}}">
79
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>
80
- <span>Back to Home</span>
81
- </a>
82
- {{/if}}
83
71
  {{#if hasDocuments}}
84
72
  <a class="mobile-nav__item" href="{{docsUrl}}/">
85
73
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
86
74
  <span>Documentation</span>
87
75
  </a>
88
76
  {{/if}}
89
- {{#if openApiUrl}}
77
+ {{#if hasApi}}
90
78
  <a class="mobile-nav__item" href="{{apiUrl}}">
91
79
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/></svg>
92
80
  <span>API Reference</span>
@@ -99,15 +99,18 @@ document.addEventListener('DOMContentLoaded', function() {
99
99
  });
100
100
  }
101
101
 
102
- // Collapse all sidebar groups by default
102
+ // Collapse sidebar groups that have a toggle button (named groups only)
103
103
  document.querySelectorAll('.api-sidebar__group').forEach(function(group) {
104
- group.classList.add('api-sidebar__group--collapsed');
104
+ if (group.querySelector('.api-sidebar__group-toggle')) {
105
+ group.classList.add('api-sidebar__group--collapsed');
106
+ }
105
107
  });
106
108
 
107
109
  // Auth type selector: show/hide value input based on OpenAPI securitySchemes
108
- var authTypeSelect = document.getElementById('api-auth-type');
109
- var authValueInput = document.getElementById('api-auth-value');
110
- var cookieStatusEl = document.getElementById('api-auth-cookie-status');
110
+ // IDs are indexed for multi-spec (e.g. api-auth-type-0), so find by class
111
+ var authTypeSelect = document.querySelector('.api-auth__type');
112
+ var authValueInput = document.querySelector('.api-auth__value');
113
+ var cookieStatusEl = document.querySelector('.api-auth__cookie-status');
111
114
  if (authTypeSelect && authValueInput) {
112
115
  function getSelectedSchemeData() {
113
116
  var option = authTypeSelect.options[authTypeSelect.selectedIndex];
@@ -271,8 +274,8 @@ document.addEventListener('DOMContentLoaded', function() {
271
274
  });
272
275
 
273
276
  // Inject global auth header from selected security scheme
274
- var authType = document.getElementById('api-auth-type');
275
- var authValue = document.getElementById('api-auth-value');
277
+ var authType = document.querySelector('.api-auth__type');
278
+ var authValue = document.querySelector('.api-auth__value');
276
279
  var useCookieAuth = false;
277
280
  if (authType && authType.value !== 'none') {
278
281
  var authOption = authType.options[authType.selectedIndex];