jamdesk 1.1.135 → 1.1.136

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jamdesk",
3
- "version": "1.1.135",
3
+ "version": "1.1.136",
4
4
  "description": "CLI for Jamdesk — build, preview, and deploy documentation sites from MDX. Dev server with hot reload, 50+ components, OpenAPI support, AI search, and Mintlify migration",
5
5
  "keywords": [
6
6
  "jamdesk",
@@ -17,6 +17,7 @@ import { siteHasOpenApiSpecs } from '@/lib/api-specs-bundle';
17
17
  import { getContextualOptions } from '@/lib/contextual-defaults';
18
18
  import { isIsrMode, getBaseUrlFromConfig } from '@/lib/page-isr-helpers';
19
19
  import { apiSpecsMarkdownFooter } from '@/lib/api-specs-markdown-hint';
20
+ import { injectAgentDirective } from '@/lib/agent-directive';
20
21
  import { logger } from '@/shared/logger';
21
22
  import type { DocsConfig } from '@/lib/docs-types';
22
23
 
@@ -60,10 +61,31 @@ export async function GET(
60
61
  }
61
62
 
62
63
  const filtered = filterVisibility(raw, 'agents');
63
- // Arg order is load-bearing: `filtered` is the agent-visible body the footer
64
- // is appended to; `raw` is the unfiltered source the helper reads frontmatter
65
- // from (visibility filtering never touches the leading YAML block).
66
- const body = await withApiSpecsHint(filtered, raw, project);
64
+
65
+ // One config fetch feeds both the directive (hostAtDocs link prefix) and the
66
+ // api-specs hint. A failed fetch must never 500 an export: directive falls
67
+ // back to the subdomain-rooted /llms.txt and the hint keeps its own defaults.
68
+ let config: DocsConfig | null = null;
69
+ try {
70
+ config = await fetchDocsConfig(project);
71
+ } catch (err) {
72
+ logger.warn('[markdown-export] config fetch failed', {
73
+ project,
74
+ error: err instanceof Error ? err.message : String(err),
75
+ });
76
+ }
77
+
78
+ // hostAtDocs rides the middleware-stamped header — the stored docs.json
79
+ // never carries it (runtime-only field, see DocsConfig), so reading it off
80
+ // `config` alone always resolved false in prod. The config check stays as a
81
+ // fallback for callers that bypassed the middleware rewrite.
82
+ const hostAtDocs =
83
+ request.headers.get('x-host-at-docs') === 'true' || !!config?.hostAtDocs;
84
+ const withDirective = injectAgentDirective(filtered, hostAtDocs);
85
+ // Arg order is load-bearing: `withDirective` is the agent-visible body the
86
+ // footer is appended to; `raw` is the unfiltered source the helper reads
87
+ // frontmatter from (visibility filtering never touches the leading YAML block).
88
+ const body = await withApiSpecsHint(withDirective, raw, project, config);
67
89
 
68
90
  // Two paths reach this handler via proxy rewrite:
69
91
  // (a) `.md` URL: unique CDN cache key, safe to share.
@@ -102,6 +124,7 @@ async function withApiSpecsHint(
102
124
  markdown: string,
103
125
  raw: string,
104
126
  project: string,
127
+ config: DocsConfig | null,
105
128
  ): Promise<string> {
106
129
  try {
107
130
  if (!isIsrMode()) return markdown;
@@ -111,20 +134,10 @@ async function withApiSpecsHint(
111
134
  const pageHasApi = !!data.api;
112
135
  if (!(pageHasOpenApi || pageHasApi)) return markdown;
113
136
 
114
- // Config drives the site-wide spec check, the menu-enabled check, and the
115
- // hostAtDocs link prefix. A failure must not 500 the export and must not
116
- // block a page-level `openapi:` page (which qualifies on its own). Treat a
117
- // failed/missing config as "no site specs" but keep the default-enabled menu.
118
- let config: DocsConfig | null = null;
119
- try {
120
- config = await fetchDocsConfig(project);
121
- } catch (err) {
122
- logger.warn('[api-specs-hint] config fetch failed', {
123
- project,
124
- error: err instanceof Error ? err.message : String(err),
125
- });
126
- }
127
-
137
+ // Config (fetched once in GET) drives the site-wide spec check, the
138
+ // menu-enabled check, and the hostAtDocs link prefix. A failed/missing
139
+ // config must not block a page-level `openapi:` page (which qualifies on
140
+ // its own): treat it as "no site specs" but keep the default-enabled menu.
128
141
  const zipUrl = `${getBaseUrlFromConfig(project, !!config?.hostAtDocs)}/api-specs.zip`;
129
142
 
130
143
  const footer = apiSpecsMarkdownFooter({
@@ -0,0 +1,101 @@
1
+ // builder/build-service/lib/agent-directive.tsx
2
+ import React from 'react';
3
+
4
+ /**
5
+ * Agent-facing llms.txt directive (agentdocsspec.com "llms-txt-directive-html"
6
+ * and "llms-txt-directive-md" checks).
7
+ *
8
+ * Most AI agents fetch HTML by default and have no signal that a markdown
9
+ * path exists. The HTML variant is visually hidden but present in the DOM
10
+ * near the top of <body>, so HTML→markdown conversion surfaces it first.
11
+ * aria-hidden keeps it out of the accessibility tree — screen readers must
12
+ * not announce it on every page.
13
+ */
14
+
15
+ /** Site-relative llms.txt path. Relative URLs resolve against the fetch origin. */
16
+ export function llmsTxtPath(hostAtDocs: boolean): string {
17
+ return hostAtDocs ? '/docs/llms.txt' : '/llms.txt';
18
+ }
19
+
20
+ /** Plain-text directive for the hidden HTML element — points agents at llms.txt and the .md exports. */
21
+ export function agentDirectiveText(hostAtDocs: boolean): string {
22
+ return (
23
+ `For AI agents: the documentation index for this site is at ${llmsTxtPath(hostAtDocs)}. ` +
24
+ 'For a markdown version of any page, append .md to its URL or request it ' +
25
+ 'with Accept: text/markdown.'
26
+ );
27
+ }
28
+
29
+ /** Single-line blockquote — markdown pages put this directly after frontmatter. */
30
+ export function agentDirectiveMarkdown(hostAtDocs: boolean): string {
31
+ return (
32
+ `> **For AI agents:** the complete documentation index is at ` +
33
+ `[llms.txt](${llmsTxtPath(hostAtDocs)}). Append \`.md\` to any page URL ` +
34
+ 'for its markdown version.'
35
+ );
36
+ }
37
+
38
+ /**
39
+ * Frontmatter must stay first — tools parse the leading YAML block. Anchored
40
+ * match only (a `---` thematic break mid-document is not frontmatter). The
41
+ * closing fence may sit at EOF with no trailing newline — R2 user content
42
+ * doesn't guarantee one (cf. frontmatter-utils.ts).
43
+ */
44
+ const FRONTMATTER_RE = /^\uFEFF?---\r?\n[\s\S]*?\r?\n---(\r?\n|$)/;
45
+
46
+ export function injectAgentDirective(markdown: string, hostAtDocs: boolean): string {
47
+ if (agentDirectiveDisabled()) return markdown;
48
+ const directive = agentDirectiveMarkdown(hostAtDocs);
49
+ const m = markdown.match(FRONTMATTER_RE);
50
+ if (m) {
51
+ const head = m[0];
52
+ const rest = markdown.slice(head.length).replace(/^\r?\n/, '');
53
+ return `${head}\n${directive}\n\n${rest}`;
54
+ }
55
+ return `${directive}\n\n${markdown}`;
56
+ }
57
+
58
+ /** Inline (not Tailwind) so it works on every theme and in vendored CLI dev. */
59
+ const SR_ONLY: React.CSSProperties = {
60
+ position: 'absolute',
61
+ width: 1,
62
+ height: 1,
63
+ padding: 0,
64
+ margin: -1,
65
+ overflow: 'hidden',
66
+ clip: 'rect(0, 0, 0, 0)',
67
+ whiteSpace: 'nowrap',
68
+ border: 0,
69
+ };
70
+
71
+ /**
72
+ * Env kill switch — disable the directive fleet-wide without a code revert
73
+ * (precedent: EMBED_WIDGET_ENABLED, JD_PRUNE_CONTENT_DISABLED). Vercel env
74
+ * change + redeploy beats reverting three surfaces.
75
+ */
76
+ export function agentDirectiveDisabled(): boolean {
77
+ // Lenient env-flag semantics, matching isCacheDisabled() (cached-redis.ts)
78
+ // and envFlag() (r2-cleanup.ts): Vercel env values can pick up trailing
79
+ // newlines, and operators set "true" as often as "1". Implemented locally —
80
+ // importing envFlag from r2-cleanup would pull @aws-sdk into a
81
+ // layout-rendered file.
82
+ const v = process.env.JD_AGENT_DIRECTIVE_DISABLED?.trim().toLowerCase();
83
+ if (!v) return false;
84
+ return v !== '0' && v !== 'false';
85
+ }
86
+
87
+ export function AgentDirective({ hostAtDocs }: { hostAtDocs: boolean }) {
88
+ if (agentDirectiveDisabled()) return null;
89
+ return (
90
+ // data-nosnippet keeps Google from surfacing the directive as a search
91
+ // snippet on description-less pages. The real <a> is what afdocs'
92
+ // LINK_PATTERN prefers and what DOM-walking agents can follow;
93
+ // tabIndex={-1} because an aria-hidden subtree must not be tabbable.
94
+ <div data-jd-agent-directive aria-hidden="true" data-nosnippet="" style={SR_ONLY}>
95
+ {agentDirectiveText(hostAtDocs)}{' '}
96
+ <a href={llmsTxtPath(hostAtDocs)} tabIndex={-1}>
97
+ llms.txt
98
+ </a>
99
+ </div>
100
+ );
101
+ }
@@ -24,6 +24,7 @@ const brandIcons = new Set([
24
24
  'flickr',
25
25
  'goodreads',
26
26
  'guilded',
27
+ 'hacker-news',
27
28
  'instagram',
28
29
  'kakao-talk',
29
30
  'line',
@@ -335,7 +335,9 @@ export function transformLanguagePath(
335
335
  }
336
336
 
337
337
  /**
338
- * Check if a string is a valid language code
338
+ * Case-SENSITIVE membership test: only canonical-cased codes ('fr-CA', not
339
+ * 'fr-ca') return true. For URL segments or declared docs.json codes — which
340
+ * may carry any casing — use isKnownLanguageCode instead.
339
341
  */
340
342
  export function isValidLanguageCode(code: string): code is LanguageCode {
341
343
  return code in LANGUAGE_DISPLAY_NAMES;
@@ -358,6 +360,31 @@ const LANGUAGE_CODE_BY_LOWER: Map<string, LanguageCode> = (() => {
358
360
  return m;
359
361
  })();
360
362
 
363
+ /**
364
+ * Case-insensitive membership test against the supported language-code list.
365
+ * Use this (not the case-sensitive isValidLanguageCode) for input that may
366
+ * carry declared or URL-sourced casing (e.g. 'fr-ca' declared in docs.json,
367
+ * '/fr-ca/' URL segment).
368
+ */
369
+ export function isKnownLanguageCode(code: string): boolean {
370
+ return LANGUAGE_CODE_BY_LOWER.has(code.toLowerCase());
371
+ }
372
+
373
+ /**
374
+ * Display name for a declared language code, tolerant of casing drift.
375
+ * Direct `LANGUAGE_DISPLAY_NAMES` hit first, then the lowered canonical map
376
+ * above (declared `'pt-br'` → canonical `'pt-BR'` → 'Português (BR)' —
377
+ * display-name keys are canonical-cased, so a strict keyed lookup would
378
+ * silently fall back to the bare code for lowercase region variants), else
379
+ * the bare code. Used by llms.txt cross-language links.
380
+ */
381
+ export function languageDisplayName(code: string): string {
382
+ const direct = (LANGUAGE_DISPLAY_NAMES as Record<string, string>)[code];
383
+ if (direct) return direct;
384
+ const canonical = LANGUAGE_CODE_BY_LOWER.get(code.toLowerCase());
385
+ return canonical ? LANGUAGE_DISPLAY_NAMES[canonical] : code;
386
+ }
387
+
361
388
  /**
362
389
  * Extract language code from a pathname
363
390
  *
@@ -25,6 +25,7 @@ import type { BackgroundConfig, DocsConfig, FontConfig, LanguageCode } from '@/l
25
25
  import { LinkPrefixProvider } from '@/lib/link-prefix-context';
26
26
  import { ProjectSlugProvider } from '@/lib/project-slug-context';
27
27
  import { getAnalyticsScript } from '@/lib/analytics-script';
28
+ import { AgentDirective } from './agent-directive';
28
29
  import { fetchCustomCss, fetchCustomJs } from '@/lib/r2-content';
29
30
  import { toHreflang } from '@/lib/language-utils';
30
31
 
@@ -655,6 +656,7 @@ export async function DocsChrome({
655
656
  )}
656
657
  </head>
657
658
  <body className={fontClassName} data-theme={themeName || 'jam'} data-decoration={decoration || undefined} suppressHydrationWarning>
659
+ {!embed && <AgentDirective hostAtDocs={!!config.hostAtDocs} />}
658
660
  {config.integrations?.gtm?.tagId && (
659
661
  <ConditionalGTM gtmId={config.integrations.gtm.tagId} />
660
662
  )}
@@ -12,6 +12,7 @@ import {
12
12
  ListObjectsV2Command,
13
13
  } from '@aws-sdk/client-s3';
14
14
  import { getR2S3Client, getR2Config } from './r2.js';
15
+ import { isKnownLanguageCode } from './language-utils.js';
15
16
  import { logger } from '../shared/logger.js';
16
17
 
17
18
  // AWS / R2 cap DeleteObjects to 1000 keys per request. Match ListObjectsV2's
@@ -315,6 +316,79 @@ export async function pruneRemovedContent(
315
316
  return { deleted, failed, skipped: false };
316
317
  }
317
318
 
319
+ /**
320
+ * Given top-level CommonPrefixes from a delimiter='/' listing of `{slug}/`,
321
+ * return per-locale llms.txt keys whose locale is a KNOWN language code but
322
+ * is no longer declared — left behind when a project removes a language.
323
+ * Only ever targets our own `<locale>/llms.txt` artifact keys, so content
324
+ * dirs (content/, snippets/, openapi/, assets/) can never be touched.
325
+ */
326
+ export function staleLocaleLlmsKeys(
327
+ commonPrefixes: string[],
328
+ projectSlug: string,
329
+ activeLocales: Set<string>,
330
+ ): string[] {
331
+ const keys: string[] = [];
332
+ for (const prefix of commonPrefixes) {
333
+ if (!prefix.startsWith(`${projectSlug}/`)) continue;
334
+ const dir = prefix.slice(projectSlug.length + 1).replace(/\/$/, '');
335
+ if (!dir || dir.includes('/')) continue;
336
+ if (!isKnownLanguageCode(dir)) continue;
337
+ // Case-sensitive has() is correct here: activeLocales keys use declared
338
+ // casing, which matches the R2 paths this build just uploaded. A dir whose
339
+ // casing differs is either a renamed locale or a leftover from a prior
340
+ // casing change — both legitimately stale.
341
+ if (activeLocales.has(dir)) continue;
342
+ keys.push(`${projectSlug}/${dir}/llms.txt`);
343
+ }
344
+ return keys;
345
+ }
346
+
347
+ /**
348
+ * List `{slug}/`'s top-level dirs and delete `<locale>/llms.txt` for any
349
+ * known-language dir no longer declared. Paginates the listing like
350
+ * deleteAllProjectR2Objects — a delimiter listing of a huge project could
351
+ * truncate, and an abandoned page would silently orphan its stale locales.
352
+ * Non-fatal by contract: callers treat a throw as a warning — a stale
353
+ * locale index is a cosmetic orphan.
354
+ */
355
+ export async function sweepStaleLocaleLlmsTxt(
356
+ projectSlug: string,
357
+ activeLocales: Set<string>,
358
+ ): Promise<string[]> {
359
+ const client = getR2S3Client();
360
+ const { bucketName } = getR2Config();
361
+ const prefixes: string[] = [];
362
+ let continuationToken: string | undefined;
363
+
364
+ do {
365
+ const listed: {
366
+ CommonPrefixes?: Array<{ Prefix?: string }>;
367
+ IsTruncated?: boolean;
368
+ NextContinuationToken?: string;
369
+ } = await client.send(
370
+ new ListObjectsV2Command({
371
+ Bucket: bucketName,
372
+ Prefix: `${projectSlug}/`,
373
+ Delimiter: '/',
374
+ ContinuationToken: continuationToken,
375
+ }),
376
+ );
377
+ for (const p of listed.CommonPrefixes ?? []) {
378
+ if (typeof p.Prefix === 'string') prefixes.push(p.Prefix);
379
+ }
380
+ continuationToken = listed.IsTruncated ?
381
+ listed.NextContinuationToken :
382
+ undefined;
383
+ } while (continuationToken);
384
+
385
+ const staleKeys = staleLocaleLlmsKeys(prefixes, projectSlug, activeLocales);
386
+ for (const key of staleKeys) {
387
+ await client.send(new DeleteObjectCommand({ Bucket: bucketName, Key: key }));
388
+ }
389
+ return staleKeys;
390
+ }
391
+
318
392
  /**
319
393
  * Delete EVERY R2 object under the `{slug}/` prefix.
320
394
  *
@@ -710,13 +710,15 @@ export function buildSeoMetadata(
710
710
  if (hasOgFamily || frontmatter.title) {
711
711
  // A custom og:image becomes the card BACKGROUND (text renders on top) rather
712
712
  // than a bare static image, so the emitted OG image is always the generated card.
713
+ // The card text follows og:title/og:description overrides so the image matches
714
+ // the link-card text built in buildOpenGraphMetadata.
713
715
  const customImage = metatags['og:image']?.trim();
714
716
  const ogImageUrl = buildOgImageUrl(
715
717
  baseUrl,
716
- frontmatter.title || 'Documentation',
718
+ metatags['og:title'] || frontmatter.title || 'Documentation',
717
719
  config.name,
718
720
  {
719
- description: frontmatter.description,
721
+ description: metatags['og:description'] || frontmatter.description,
720
722
  section: frontmatter.section,
721
723
  logo: config.logo,
722
724
  favicon: config.favicon,
@@ -10,6 +10,8 @@ import { RECURSE_KEYS } from './enhance-navigation.js';
10
10
  import { filterVisibility } from './visibility-filter.js';
11
11
  import {
12
12
  buildLoweredLocaleSet,
13
+ languageDisplayName,
14
+ resolveLanguageWithFallback,
13
15
  resolveLocaleFromPath,
14
16
  resolveLocaleWithLoweredSet,
15
17
  } from './language-utils.js';
@@ -170,6 +172,22 @@ export interface LlmsTxtOptions {
170
172
  * handling across all artifacts.
171
173
  */
172
174
  visibility?: VisibilityInputs;
175
+ /** Page path → section title (from extractNavigationSections). Nav order matters. */
176
+ sections?: Map<string, string>;
177
+ /** Cross-links to sibling-locale llms.txt files; rendered as the final H2 section. */
178
+ otherLanguages?: Array<{ label: string; url: string }>;
179
+ }
180
+
181
+ /**
182
+ * llmstxt.org reserves the bare "## Optional" heading as a skip-signal —
183
+ * agents wanting shorter context may drop that whole section. A customer
184
+ * tab/group literally named "Optional" must not inherit that semantics (its
185
+ * pages would be silently skipped) nor collide with our cross-language
186
+ * section, so nav sections render a disambiguated heading; only the language
187
+ * cross-link section emits the reserved bare name.
188
+ */
189
+ function renderSectionName(section: string): string {
190
+ return section === 'Optional' ? 'Optional (docs)' : section;
173
191
  }
174
192
 
175
193
  /**
@@ -182,7 +200,10 @@ export interface LlmsTxtOptions {
182
200
  * @returns Plain text string
183
201
  */
184
202
  export function generateLlmsTxt(options: LlmsTxtOptions): string {
185
- const { name, description, baseUrl, pages, hostAtDocs = false, visibility } = options;
203
+ const {
204
+ name, description, baseUrl, pages, hostAtDocs = false, visibility,
205
+ sections, otherLanguages,
206
+ } = options;
186
207
 
187
208
  // NOTE: noindex is intentionally NOT respected here. AI agents fetch llms.txt
188
209
  // directly and do not honor robots directives, so this file is generated
@@ -192,20 +213,158 @@ export function generateLlmsTxt(options: LlmsTxtOptions): string {
192
213
  const urlPrefix = hostAtDocs ? '/docs' : '';
193
214
 
194
215
  let output = `# ${name}\n\n`;
216
+ // Always emit the blockquote — llms-txt-valid requires H1 + blockquote +
217
+ // sections, and most projects never set a docs.json description.
218
+ output += `> ${description || `Documentation for ${name}.`}\n\n`;
219
+
220
+ const included = pages.filter((page) => isPageMetadataIncluded(page, visibility));
221
+
222
+ // Bucket pages by section. Section order = nav order (Map insertion order);
223
+ // pages keep nav order within a section via their index in the sections Map.
224
+ // Unsectioned pages land in a trailing "Documentation" bucket.
225
+ const FALLBACK_SECTION = 'Documentation';
226
+ const sectionOrder: string[] = sections ? [...new Set(sections.values())] : [];
227
+ const navIndex = new Map<string, number>(
228
+ sections ? [...sections.keys()].map((p, i) => [p, i] as const) : [],
229
+ );
230
+ const buckets = new Map<string, typeof included>();
231
+ for (const page of included) {
232
+ const section = sections?.get(page.path) ?? FALLBACK_SECTION;
233
+ if (!buckets.has(section)) buckets.set(section, []);
234
+ buckets.get(section)!.push(page);
235
+ }
236
+ const orderedSections = [
237
+ ...sectionOrder.filter((s) => buckets.has(s)),
238
+ ...(buckets.has(FALLBACK_SECTION) && !sectionOrder.includes(FALLBACK_SECTION)
239
+ ? [FALLBACK_SECTION]
240
+ : []),
241
+ ];
195
242
 
196
- if (description) {
197
- output += `> ${description}\n\n`;
243
+ for (const section of orderedSections) {
244
+ output += `## ${renderSectionName(section)}\n\n`;
245
+ const bucket = buckets.get(section)!;
246
+ // `?? 0` (not Infinity): named buckets always carry nav indexes, so the
247
+ // fallback only fires in the Documentation bucket where EVERY page lacks
248
+ // one — all-zero comparisons let the stable sort keep page-array order.
249
+ // (`Infinity - Infinity` = NaN, a spec-undefined comparator result.)
250
+ bucket.sort((a, b) => (navIndex.get(a.path) ?? 0) - (navIndex.get(b.path) ?? 0));
251
+ for (const page of bucket) {
252
+ const url = `${baseUrl}${urlPrefix}/${page.path}.md`;
253
+ // Frontmatter strings can carry raw newlines — collapse them so a
254
+ // crafted title/description can't forge fake "## " section headings or
255
+ // split the entry across lines (llms.txt is line-oriented).
256
+ const safeTitle = page.title.replace(/[\r\n]+/g, ' ');
257
+ const desc = page.description ? `: ${page.description.replace(/[\r\n]+/g, ' ')}` : '';
258
+ output += `- [${safeTitle}](${url})${desc}\n`;
259
+ }
260
+ output += '\n';
261
+ }
262
+
263
+ if (otherLanguages && otherLanguages.length > 0) {
264
+ // llmstxt.org reserves "## Optional" as the skip-signal section — agents
265
+ // wanting shorter context skip it, agents needing another language still
266
+ // see the links. Descriptive labels keep it human-readable.
267
+ output += `## Optional\n\n`;
268
+ for (const lang of otherLanguages) {
269
+ output += `- [${lang.label}](${lang.url}): ${lang.label} documentation index\n`;
270
+ }
271
+ output += '\n';
198
272
  }
199
273
 
274
+ return output.trim();
275
+ }
276
+
277
+ /**
278
+ * Options for generating the per-locale llms.txt file set.
279
+ */
280
+ export interface LlmsTxtFilesOptions extends Omit<LlmsTxtOptions, 'sections' | 'otherLanguages'> {
281
+ /** docs.json navigation — source of the H2 section mapping. */
282
+ navigation?: NavigationConfig;
283
+ /** Declared languages (navigation.languages) — drives the locale split. */
284
+ languages?: LanguageConfig[];
285
+ }
286
+
287
+ /**
288
+ * Render the root llms.txt (default locale) plus one file per declared,
289
+ * non-hidden, non-default language. Locale membership comes from the page
290
+ * path prefix (resolveLocaleFromPath, whitelisted against declared codes —
291
+ * same semantics as the search index), NOT from nav position. Cross-links
292
+ * between locale files render under the trailing "## Optional" section.
293
+ */
294
+ export function generateLlmsTxtFiles(
295
+ options: LlmsTxtFilesOptions,
296
+ ): { root: string; byLocale: Record<string, string> } {
297
+ const { navigation, languages, baseUrl, hostAtDocs = false, pages, ...rest } = options;
298
+ const urlPrefix = hostAtDocs ? '/docs' : '';
299
+ const sections = extractNavigationSections(navigation);
300
+
301
+ const declared = (languages ?? []).filter(
302
+ (l): l is LanguageConfig & { language: string } =>
303
+ typeof l.language === 'string' && !l.hidden,
304
+ );
305
+ // Dedupe case-insensitively, first declaration wins: a language declared
306
+ // twice in docs.json would otherwise render its cross-link twice in every
307
+ // "## Optional" section.
308
+ const codes = declared
309
+ .map((l) => l.language)
310
+ .filter(
311
+ (code, i, all) =>
312
+ all.findIndex((c) => c.toLowerCase() === code.toLowerCase()) === i,
313
+ );
314
+ const defaultCode = resolveLanguageWithFallback(null, languages);
315
+
316
+ if (codes.length === 0) {
317
+ const root = generateLlmsTxt({ ...rest, baseUrl, hostAtDocs, pages, sections });
318
+ return { root, byLocale: {} };
319
+ }
320
+
321
+ const byLocalePages = new Map<string, typeof pages>();
200
322
  for (const page of pages) {
201
- if (!isPageMetadataIncluded(page, visibility)) continue;
323
+ // resolveLocaleFromPath returns '' (NOT the default code) for unprefixed
324
+ // paths and for prefixes outside the whitelist — both belong to the root
325
+ // (default-locale) file. `|| defaultCode` is load-bearing: without it,
326
+ // every standard site's root llms.txt would render empty.
327
+ // `.toLowerCase()` is equally load-bearing: the resolver returns the
328
+ // CANONICAL casing (e.g. 'fr-CA' for a declared 'fr-ca'), so bucketing by
329
+ // its raw return and reading back by the declared code would silently
330
+ // drop those pages from every llms.txt file.
331
+ const locale = (resolveLocaleFromPath(page.path, codes) || defaultCode).toLowerCase();
332
+ if (!byLocalePages.has(locale)) byLocalePages.set(locale, []);
333
+ byLocalePages.get(locale)!.push(page);
334
+ }
335
+
336
+ const llmsUrl = (code: string): string =>
337
+ code === defaultCode
338
+ ? `${baseUrl}${urlPrefix}/llms.txt`
339
+ : `${baseUrl}${urlPrefix}/${code}/llms.txt`;
340
+ // Declared-but-untranslated languages produce empty files nobody should be
341
+ // sent to — generate and cross-link only locales that actually have pages
342
+ // (the default locale always exists as the root file).
343
+ const activeCodes = codes.filter(
344
+ (c) => c === defaultCode || (byLocalePages.get(c.toLowerCase())?.length ?? 0) > 0,
345
+ );
346
+ const activeCrossLinks = (selfCode: string) =>
347
+ activeCodes
348
+ .filter((c) => c !== selfCode)
349
+ .map((c) => ({ label: languageDisplayName(c), url: llmsUrl(c) }));
350
+
351
+ const root = generateLlmsTxt({
352
+ ...rest, baseUrl, hostAtDocs, sections,
353
+ pages: byLocalePages.get(defaultCode.toLowerCase()) ?? [],
354
+ otherLanguages: activeCrossLinks(defaultCode),
355
+ });
202
356
 
203
- const url = `${baseUrl}${urlPrefix}/${page.path}.md`;
204
- const desc = page.description ? `: ${page.description}` : '';
205
- output += `- [${page.title}](${url})${desc}\n`;
357
+ const byLocale: Record<string, string> = {};
358
+ for (const code of activeCodes) {
359
+ if (code === defaultCode) continue;
360
+ byLocale[code] = generateLlmsTxt({
361
+ ...rest, baseUrl, hostAtDocs, sections,
362
+ pages: byLocalePages.get(code.toLowerCase()) ?? [],
363
+ otherLanguages: activeCrossLinks(code),
364
+ });
206
365
  }
207
366
 
208
- return output.trim();
367
+ return { root, byLocale };
209
368
  }
210
369
 
211
370
  /**
@@ -332,6 +491,64 @@ export function extractNavigationPaths(
332
491
  return paths;
333
492
  }
334
493
 
494
+ /** Container keys whose string value names a section, checked in order. */
495
+ const SECTION_NAME_KEYS = ['tab', 'group', 'dropdown', 'product', 'anchor'] as const;
496
+
497
+ /**
498
+ * Map every nav page path to its OUTERMOST named container (tab > top-level
499
+ * group > dropdown/product/legacy-anchor). Languages and versions are
500
+ * transparent: locale is derived from the page path, not the section, so
501
+ * "Guides" in the fr subtree and the en subtree share a section name.
502
+ * First mapping wins (a page referenced twice keeps its first home).
503
+ * Insertion order of values follows nav order — generateLlmsTxt relies on it.
504
+ *
505
+ * Traverses the same tree shapes as extractNavigationPaths above — keep the
506
+ * two walkers' pages-handling in sync.
507
+ */
508
+ export function extractNavigationSections(
509
+ navigation: NavigationConfig | undefined,
510
+ ): Map<string, string> {
511
+ const sections = new Map<string, string>();
512
+ if (!navigation) return sections;
513
+
514
+ function walk(node: Record<string, unknown>, inherited: string | null): void {
515
+ let section = inherited;
516
+ if (!section) {
517
+ for (const key of SECTION_NAME_KEYS) {
518
+ if (typeof node[key] === 'string') {
519
+ section = (node[key] as string).replace(/\s+/g, ' ').trim();
520
+ break;
521
+ }
522
+ }
523
+ }
524
+
525
+ if (Array.isArray(node.pages)) {
526
+ for (const item of node.pages) {
527
+ if (typeof item === 'string') {
528
+ if (section && !sections.has(item)) sections.set(item, section);
529
+ } else if (typeof item === 'object' && item !== null) {
530
+ const obj = item as Record<string, unknown>;
531
+ if (typeof obj.page === 'string' && section && !sections.has(obj.page)) {
532
+ sections.set(obj.page, section);
533
+ }
534
+ if ('group' in obj) walk(obj, section);
535
+ }
536
+ }
537
+ }
538
+
539
+ for (const key of RECURSE_KEYS) {
540
+ if (Array.isArray(node[key])) {
541
+ for (const child of node[key] as Record<string, unknown>[]) {
542
+ walk(child, section);
543
+ }
544
+ }
545
+ }
546
+ }
547
+
548
+ walk(navigation as Record<string, unknown>, null);
549
+ return sections;
550
+ }
551
+
335
552
  /**
336
553
  * Page info with content for llms-full.txt generation.
337
554
  */
@@ -447,6 +664,8 @@ export interface GenerateAllOptions {
447
664
  llmsFullPages?: LlmsFullPageInfo[];
448
665
  /** Language configurations (forwarded to sitemap for hreflang siblings) */
449
666
  languages?: LanguageConfig[];
667
+ /** docs.json navigation tree (enhanced) — powers llms.txt H2 sections. */
668
+ navigation?: NavigationConfig;
450
669
  /**
451
670
  * Visibility inputs from lib/visibility.ts. When provided, all artifact
452
671
  * generators use computePageVisibility for consistent hidden/orphan/searchable
@@ -463,6 +682,8 @@ export interface GenerateAllOptions {
463
682
  export interface GeneratedArtifacts {
464
683
  sitemap: string;
465
684
  llmsTxt: string;
685
+ /** Non-default-locale llms.txt files, keyed by language code. */
686
+ llmsTxtByLocale: Record<string, string>;
466
687
  llmsFullTxt: string;
467
688
  robotsTxt: string;
468
689
  rssFeed: string | null;
@@ -484,12 +705,13 @@ export interface GeneratedArtifacts {
484
705
  export function generateAllArtifacts(options: GenerateAllOptions): GeneratedArtifacts {
485
706
  const {
486
707
  baseUrl, name, description, pages, hostAtDocs, noindex, rssPages, llmsFullPages, languages,
487
- visibility,
708
+ navigation, visibility,
488
709
  } = options;
489
710
 
490
711
  const sitemap = generateSitemap({ baseUrl, pages, hostAtDocs, noindex, languages, visibility });
491
- const llmsTxt = generateLlmsTxt({
712
+ const { root: llmsTxt, byLocale: llmsTxtByLocale } = generateLlmsTxtFiles({
492
713
  name, description, baseUrl, pages, hostAtDocs, noindex, visibility,
714
+ navigation, languages,
493
715
  });
494
716
  const llmsFullTxt = llmsFullPages
495
717
  ? generateLlmsFullTxt({ name, pages: llmsFullPages, noindex, visibility })
@@ -510,7 +732,7 @@ export function generateAllArtifacts(options: GenerateAllOptions): GeneratedArti
510
732
 
511
733
  const changelog = generateChangelog(updates);
512
734
 
513
- return { sitemap, llmsTxt, llmsFullTxt, robotsTxt, rssFeed, changelog };
735
+ return { sitemap, llmsTxt, llmsTxtByLocale, llmsFullTxt, robotsTxt, rssFeed, changelog };
514
736
  }
515
737
 
516
738
  // =============================================================================
@@ -11,6 +11,7 @@ import path from 'path';
11
11
 
12
12
  import { NextRequest, NextResponse } from 'next/server';
13
13
 
14
+ import { isKnownLanguageCode } from '@/lib/language-utils';
14
15
  import { log } from '@/lib/logger';
15
16
  import { isIsrMode } from '@/lib/page-isr-helpers';
16
17
  import { fetchStaticFile } from '@/lib/r2-content';
@@ -22,6 +23,9 @@ export const STATIC_FILE_NAMES = [
22
23
  ] as const;
23
24
 
24
25
  /** All CDN paths for static file routes — used by revalidation to purge CDN cache. */
26
+ // Per-locale llms.txt paths (/{locale}/llms.txt) are intentionally absent:
27
+ // locales are per-project, unenumerable here. They self-heal via s-maxage=3600,
28
+ // identical to llms.txt's behavior between explicit purges.
25
29
  export const STATIC_REVALIDATION_PATHS = STATIC_FILE_NAMES.flatMap(
26
30
  f => [`/${f}`, `/docs/${f}`]
27
31
  );
@@ -129,6 +133,63 @@ export function createCorsStaticFileHandler(
129
133
  };
130
134
  }
131
135
 
136
+ /**
137
+ * GET handler for per-locale llms.txt (`/{locale}/llms.txt`, `/docs/{locale}/llms.txt`).
138
+ *
139
+ * Locale is validated (case-insensitively) against the supported language-code
140
+ * list BEFORE any R2 I/O so arbitrary path segments (`/foo/llms.txt`) fast-404.
141
+ * Like llms.txt, locale files are intentionally NOT suppressed on
142
+ * `x-jd-noindex` — AI agents fetch them directly and don't honor robots
143
+ * semantics.
144
+ */
145
+ export function createLocaleLlmsTxtHandler(): (
146
+ request: NextRequest,
147
+ ctx: { params: Promise<{ locale: string }> },
148
+ ) => Promise<NextResponse> {
149
+ return async function GET(request, ctx): Promise<NextResponse> {
150
+ const { locale } = await ctx.params;
151
+ if (!isKnownLanguageCode(locale)) {
152
+ return new NextResponse('Not found', { status: 404 });
153
+ }
154
+ // Fetch with the segment verbatim: R2 keys are written with the DECLARED
155
+ // code, and every advertised URL is generated from that same code, so
156
+ // segment casing and key casing always agree for real links.
157
+ const filename = `${locale}/llms.txt`;
158
+
159
+ if (!isIsrMode()) {
160
+ const localPath = path.join(process.cwd(), 'public', filename);
161
+ if (fs.existsSync(localPath)) {
162
+ return new NextResponse(fs.readFileSync(localPath), {
163
+ headers: { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-cache' },
164
+ });
165
+ }
166
+ return new NextResponse('Not found', { status: 404 });
167
+ }
168
+
169
+ const projectSlug = request.headers.get('x-project-slug');
170
+ if (!projectSlug) {
171
+ log('warn', 'Locale llms.txt request missing project slug');
172
+ return new NextResponse('Project not found', { status: 404 });
173
+ }
174
+
175
+ try {
176
+ const content = await fetchStaticFile(projectSlug, filename);
177
+ if (content === null) {
178
+ return new NextResponse('Not found', { status: 404 });
179
+ }
180
+ return new NextResponse(content, {
181
+ headers: {
182
+ 'Content-Type': 'text/plain; charset=utf-8',
183
+ 'Cache-Control': 'public, max-age=3600, s-maxage=3600',
184
+ },
185
+ });
186
+ } catch (error) {
187
+ log('error', 'Error serving locale llms.txt', { projectSlug, locale, error: String(error) });
188
+ return new NextResponse('Error serving locale llms.txt', { status: 500 });
189
+ }
190
+ };
191
+ }
192
+
132
193
  /**
133
194
  * Create a GET handler that serves a static file from R2.
134
195
  *
@@ -764,6 +764,12 @@ body[data-theme="pulsar"] header [data-theme-toggle] {
764
764
  body[data-theme="pulsar"] #content-scroll-container {
765
765
  display: flex;
766
766
  justify-content: center;
767
+ /* Without this, the default stretch pins the article's height to the
768
+ * visible container height; long content overflows the article box, the
769
+ * article's bottom padding lands mid-page, and the scroll area ends flush
770
+ * against the footer's last pixel ("Powered by Jamdesk" touching the
771
+ * viewport bottom). flex-start restores the article's natural height. */
772
+ align-items: flex-start;
767
773
  /* Hide scrollbar but keep scrollable */
768
774
  scrollbar-width: none; /* Firefox */
769
775
  -ms-overflow-style: none; /* IE/Edge */
@@ -912,15 +912,15 @@
912
912
  }
913
913
  },
914
914
  "node_modules/@next/env": {
915
- "version": "16.2.7",
916
- "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.7.tgz",
917
- "integrity": "sha512-tMJizPlj6ZYpBMMdK8S0LJufrP4QTdR6pcv9KQ/bVETPAmg0j1mlHE9G2c38UyGHxoBapgwuj7XjbGJ2RcDFOg==",
915
+ "version": "16.2.9",
916
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.9.tgz",
917
+ "integrity": "sha512-ki5VxxXfzD/9TDe13wyeTKIjQTAwBVpnr8KhRDUr8ltMUq1/NBpWNT5tiPoxiGl+PHM4X2ahSOiPk6iAimIzPg==",
918
918
  "license": "MIT"
919
919
  },
920
920
  "node_modules/@next/mdx": {
921
- "version": "16.2.7",
922
- "resolved": "https://registry.npmjs.org/@next/mdx/-/mdx-16.2.7.tgz",
923
- "integrity": "sha512-4RmM0KISxvfHr37/cn9TAGD2oy1nvTQ+ycgknz2xpd8IrY980N7XDU3CXhfKOXPhIVgbshxFF9HQEQC32ZVa9A==",
921
+ "version": "16.2.9",
922
+ "resolved": "https://registry.npmjs.org/@next/mdx/-/mdx-16.2.9.tgz",
923
+ "integrity": "sha512-SdweShKGCuN639JjyFSMQ8uldo+I+254+HucpjwdbFfaWHqUNN6dnQ1Of6laahnFyo48CcfDXEc2OBCS/Wfngw==",
924
924
  "license": "MIT",
925
925
  "dependencies": {
926
926
  "source-map": "^0.7.0"
@@ -939,9 +939,9 @@
939
939
  }
940
940
  },
941
941
  "node_modules/@next/swc-darwin-arm64": {
942
- "version": "16.2.7",
943
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.7.tgz",
944
- "integrity": "sha512-vm1EDI/pVaBNNiychmxk3fft+OhQPVD9cIM/tReLZIQ3TfQ4kqI9DwKk00dzuS1ulC7icbrzCFrmRRlk9PfNdw==",
942
+ "version": "16.2.9",
943
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.9.tgz",
944
+ "integrity": "sha512-HkfxNYUCmcct0Xsqib5KxqMSHV4AHJq857BNRchyBDs4YS19aHzVfn1kDuBYKqLLQBjXgnkIsjV2Kd4d2wzYhw==",
945
945
  "cpu": [
946
946
  "arm64"
947
947
  ],
@@ -955,9 +955,9 @@
955
955
  }
956
956
  },
957
957
  "node_modules/@next/swc-darwin-x64": {
958
- "version": "16.2.7",
959
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.7.tgz",
960
- "integrity": "sha512-O3IRSv1ZBL1zs0WrIgefTEcTKFVn+ryxBNe54erJ6KsD+2f/Mmt7g2jOYh8PSBdUwPtKQJuCsTMlZ7tIu2AcsQ==",
958
+ "version": "16.2.9",
959
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.9.tgz",
960
+ "integrity": "sha512-7IAtK4MeybpqRV9GRABWEhJ62mOS+rzWOzOTFie4cSEtm12xsoOMJRcECoZx3FHPzFAqN/IJtHqWAFOLfl152w==",
961
961
  "cpu": [
962
962
  "x64"
963
963
  ],
@@ -971,9 +971,9 @@
971
971
  }
972
972
  },
973
973
  "node_modules/@next/swc-linux-arm64-gnu": {
974
- "version": "16.2.7",
975
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.7.tgz",
976
- "integrity": "sha512-Re6PZtjBDd0aMU+VcZcC/PrIvj4WhrjDYtMhhCVQamWN4L90EVP0pcEOBQD25prSlw7OzNw5QpHLWMilRLsRNw==",
974
+ "version": "16.2.9",
975
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.9.tgz",
976
+ "integrity": "sha512-hBD75iWpUtkL9SmQmcRhmLomn9jgkPzCEkbOcLgHymPEKzv+6ONy13RRiIEz/iEObjkS2Jlb5gYS2XGoS3X4rw==",
977
977
  "cpu": [
978
978
  "arm64"
979
979
  ],
@@ -990,9 +990,9 @@
990
990
  }
991
991
  },
992
992
  "node_modules/@next/swc-linux-arm64-musl": {
993
- "version": "16.2.7",
994
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.7.tgz",
995
- "integrity": "sha512-qyogG9QtBzWxgJfeGBvOEHI3851gTfCF3wLZ5RDLTBJGAmE9p1qDwKCOdrBrvBzRvYDT+gUDp72pzlSEfAXgNA==",
993
+ "version": "16.2.9",
994
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.9.tgz",
995
+ "integrity": "sha512-qZTI3pf9SGc/obr8NkQAekBxmp1QK+kVm+VAf3BALLfFAj+1kUhkTxmrWpVos9R/UYIA8AWX2p6cGI5WdwzVUA==",
996
996
  "cpu": [
997
997
  "arm64"
998
998
  ],
@@ -1009,9 +1009,9 @@
1009
1009
  }
1010
1010
  },
1011
1011
  "node_modules/@next/swc-linux-x64-gnu": {
1012
- "version": "16.2.7",
1013
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.7.tgz",
1014
- "integrity": "sha512-Vhe4ZDuBpmMogrGi5D4R2Kq4JAQlj6+wvgaFYy31zfES0zPmt6TLA+cuYpM/OLrPZjo2MYQTHVqNUSCR6+fDZQ==",
1012
+ "version": "16.2.9",
1013
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.9.tgz",
1014
+ "integrity": "sha512-xm0HfRNX+UkH4R3c18ynswjj5o5uEj/7iI9p9omdtTSIsRCzQqkGMA+10nzJ4EHnYC3as65IMhbbl5fWRUWHYg==",
1015
1015
  "cpu": [
1016
1016
  "x64"
1017
1017
  ],
@@ -1028,9 +1028,9 @@
1028
1028
  }
1029
1029
  },
1030
1030
  "node_modules/@next/swc-linux-x64-musl": {
1031
- "version": "16.2.7",
1032
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.7.tgz",
1033
- "integrity": "sha512-srvian89JahFLw1YLBEuhvPJ0DO5lpUeJQMXy4xYo7g628ZlNgXdNkqoxSAv9OYrBfByh6vxISMwW/mRbzCY+g==",
1031
+ "version": "16.2.9",
1032
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.9.tgz",
1033
+ "integrity": "sha512-QumimHkGEG6vM3PfEDWKyKen03NcqLOkeKB1EfcPe7VxzmEiCa4jNnMyBn/US5zcd/VE1CI+O8Ovb3lfjVHfGw==",
1034
1034
  "cpu": [
1035
1035
  "x64"
1036
1036
  ],
@@ -1047,9 +1047,9 @@
1047
1047
  }
1048
1048
  },
1049
1049
  "node_modules/@next/swc-win32-arm64-msvc": {
1050
- "version": "16.2.7",
1051
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.7.tgz",
1052
- "integrity": "sha512-GX3wvLpULFuRFJzwHaKfm7QZJ18F4ZSuxlPJ96BoBglCzBmdSjyeBKF+ZhWhvL/ckxNfLnNa7bsObO2ipYpszw==",
1050
+ "version": "16.2.9",
1051
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.9.tgz",
1052
+ "integrity": "sha512-hzQpKZvw8rAwI6A2uQh6SacCSvNAXaIkPNsWwzqqfRiIMiXMfH936skDhz1OO6KpvdKkJrgHHtqQOq5PIXOvdQ==",
1053
1053
  "cpu": [
1054
1054
  "arm64"
1055
1055
  ],
@@ -1063,9 +1063,9 @@
1063
1063
  }
1064
1064
  },
1065
1065
  "node_modules/@next/swc-win32-x64-msvc": {
1066
- "version": "16.2.7",
1067
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.7.tgz",
1068
- "integrity": "sha512-J4WlM72NMk076Qsg0jTdK3SNXatlSdnjW7L7oNGLst1tAGjHrJh/FYi+pw9wyIjEtGRKDNzD0zuiY16oWYWVaw==",
1066
+ "version": "16.2.9",
1067
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.9.tgz",
1068
+ "integrity": "sha512-qr2VL3Ce5QrwgO2yh1ujSBawrimjVKX8FGF/cOynmdYKJY0BdHpGVNIRK1tqONB10Vkm25Ub1BD2bkjWs4+96w==",
1069
1069
  "cpu": [
1070
1070
  "x64"
1071
1071
  ],
@@ -1079,9 +1079,9 @@
1079
1079
  }
1080
1080
  },
1081
1081
  "node_modules/@next/third-parties": {
1082
- "version": "16.2.7",
1083
- "resolved": "https://registry.npmjs.org/@next/third-parties/-/third-parties-16.2.7.tgz",
1084
- "integrity": "sha512-mSvF8eg2egIqP0GaPYn0sDvsP45CodwVsavl7RkLsOVQL/CDy7wUg0WUV5uEtlWoWOq0pEYRRsbld7NQIh+Eqg==",
1082
+ "version": "16.2.9",
1083
+ "resolved": "https://registry.npmjs.org/@next/third-parties/-/third-parties-16.2.9.tgz",
1084
+ "integrity": "sha512-JZPGQRN8eQs2WMfBXOf6Zjrma+JIN0KyA24MvmIa3bUI4BwTaJ1UlxmYVc5ri6l30LQ8fPv6Kmx20447hCltrg==",
1085
1085
  "license": "MIT",
1086
1086
  "dependencies": {
1087
1087
  "third-party-capital": "1.0.20"
@@ -2145,9 +2145,9 @@
2145
2145
  }
2146
2146
  },
2147
2147
  "node_modules/baseline-browser-mapping": {
2148
- "version": "2.10.34",
2149
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.34.tgz",
2150
- "integrity": "sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw==",
2148
+ "version": "2.10.35",
2149
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.35.tgz",
2150
+ "integrity": "sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg==",
2151
2151
  "license": "Apache-2.0",
2152
2152
  "bin": {
2153
2153
  "baseline-browser-mapping": "dist/cli.cjs"
@@ -2924,9 +2924,9 @@
2924
2924
  }
2925
2925
  },
2926
2926
  "node_modules/dompurify": {
2927
- "version": "3.4.8",
2928
- "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz",
2929
- "integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==",
2927
+ "version": "3.4.9",
2928
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.9.tgz",
2929
+ "integrity": "sha512-4dPSRMRDqHvs0V4YDFCsaIZo4if5u0xM+llyxiM2fwuZFdKArUBAF3VtI2+n8NKg9P870WMdYk0UhqQNoWXbfQ==",
2930
2930
  "license": "(MPL-2.0 OR Apache-2.0)",
2931
2931
  "optionalDependencies": {
2932
2932
  "@types/trusted-types": "^2.0.7"
@@ -2939,9 +2939,9 @@
2939
2939
  "license": "MIT"
2940
2940
  },
2941
2941
  "node_modules/electron-to-chromium": {
2942
- "version": "1.5.368",
2943
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz",
2944
- "integrity": "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==",
2942
+ "version": "1.5.371",
2943
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.371.tgz",
2944
+ "integrity": "sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w==",
2945
2945
  "license": "ISC"
2946
2946
  },
2947
2947
  "node_modules/enhanced-resolve": {
@@ -5306,12 +5306,12 @@
5306
5306
  }
5307
5307
  },
5308
5308
  "node_modules/next": {
5309
- "version": "16.2.7",
5310
- "resolved": "https://registry.npmjs.org/next/-/next-16.2.7.tgz",
5311
- "integrity": "sha512-eMJxgjRzBaj3olkP4cBamHDXL79A8FC6u1GcsO1D1Tsx8bw/LLXUJCaoajVxtnhD3A1IJqIT8IcRJjgBIPJq4w==",
5309
+ "version": "16.2.9",
5310
+ "resolved": "https://registry.npmjs.org/next/-/next-16.2.9.tgz",
5311
+ "integrity": "sha512-MEOJiq/UvuezAdqVSceHbqDgZt1kDw2tpGVOlsdIoJsQdbN2JY2hpVG4xnXGkbdJUOEWhnRfiu/O4Hpc9Juwww==",
5312
5312
  "license": "MIT",
5313
5313
  "dependencies": {
5314
- "@next/env": "16.2.7",
5314
+ "@next/env": "16.2.9",
5315
5315
  "@swc/helpers": "0.5.15",
5316
5316
  "baseline-browser-mapping": "^2.9.19",
5317
5317
  "caniuse-lite": "^1.0.30001579",
@@ -5325,14 +5325,14 @@
5325
5325
  "node": ">=20.9.0"
5326
5326
  },
5327
5327
  "optionalDependencies": {
5328
- "@next/swc-darwin-arm64": "16.2.7",
5329
- "@next/swc-darwin-x64": "16.2.7",
5330
- "@next/swc-linux-arm64-gnu": "16.2.7",
5331
- "@next/swc-linux-arm64-musl": "16.2.7",
5332
- "@next/swc-linux-x64-gnu": "16.2.7",
5333
- "@next/swc-linux-x64-musl": "16.2.7",
5334
- "@next/swc-win32-arm64-msvc": "16.2.7",
5335
- "@next/swc-win32-x64-msvc": "16.2.7",
5328
+ "@next/swc-darwin-arm64": "16.2.9",
5329
+ "@next/swc-darwin-x64": "16.2.9",
5330
+ "@next/swc-linux-arm64-gnu": "16.2.9",
5331
+ "@next/swc-linux-arm64-musl": "16.2.9",
5332
+ "@next/swc-linux-x64-gnu": "16.2.9",
5333
+ "@next/swc-linux-x64-musl": "16.2.9",
5334
+ "@next/swc-win32-arm64-msvc": "16.2.9",
5335
+ "@next/swc-win32-x64-msvc": "16.2.9",
5336
5336
  "sharp": "^0.34.5"
5337
5337
  },
5338
5338
  "peerDependencies": {
@@ -6079,9 +6079,9 @@
6079
6079
  }
6080
6080
  },
6081
6081
  "node_modules/semver": {
6082
- "version": "7.8.3",
6083
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.3.tgz",
6084
- "integrity": "sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg==",
6082
+ "version": "7.8.4",
6083
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
6084
+ "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
6085
6085
  "license": "ISC",
6086
6086
  "optional": true,
6087
6087
  "bin": {
@@ -6323,9 +6323,9 @@
6323
6323
  }
6324
6324
  },
6325
6325
  "node_modules/ts-dedent": {
6326
- "version": "2.2.0",
6327
- "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz",
6328
- "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
6326
+ "version": "2.3.0",
6327
+ "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz",
6328
+ "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==",
6329
6329
  "license": "MIT",
6330
6330
  "engines": {
6331
6331
  "node": ">=6.10"