jamdesk 1.1.211 → 1.1.213
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/__tests__/unit/deploy.test.js +13 -0
- package/dist/__tests__/unit/deploy.test.js.map +1 -1
- package/dist/__tests__/unit/docs-config.test.js +26 -0
- package/dist/__tests__/unit/docs-config.test.js.map +1 -1
- package/dist/__tests__/unit/dusk-embargo.test.d.ts +2 -0
- package/dist/__tests__/unit/dusk-embargo.test.d.ts.map +1 -0
- package/dist/__tests__/unit/dusk-embargo.test.js +25 -0
- package/dist/__tests__/unit/dusk-embargo.test.js.map +1 -0
- package/dist/__tests__/unit/migrate-resolve-theme.test.js +11 -0
- package/dist/__tests__/unit/migrate-resolve-theme.test.js.map +1 -1
- package/dist/commands/deploy.js +4 -4
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/migrate/index.d.ts.map +1 -1
- package/dist/commands/migrate/index.js +7 -3
- package/dist/commands/migrate/index.js.map +1 -1
- package/dist/commands/migrate/types.d.ts +4 -1
- package/dist/commands/migrate/types.d.ts.map +1 -1
- package/dist/commands/migrate/types.js +13 -1
- package/dist/commands/migrate/types.js.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/docs-config.d.ts +1 -1
- package/dist/lib/docs-config.d.ts.map +1 -1
- package/dist/lib/docs-config.js +1 -1
- package/dist/lib/docs-config.js.map +1 -1
- package/dist/lib/dusk-embargo.d.ts +15 -0
- package/dist/lib/dusk-embargo.d.ts.map +1 -0
- package/dist/lib/dusk-embargo.js +15 -0
- package/dist/lib/dusk-embargo.js.map +1 -0
- package/dist/lib/normalize-config.js +1 -1
- package/dist/lib/normalize-config.js.map +1 -1
- package/package.json +1 -1
- package/vendored/app/(unlock)/jd/unlock/page.tsx +2 -2
- package/vendored/components/layout/LayoutWrapper.tsx +7 -3
- package/vendored/components/navigation/DefaultLogo.tsx +25 -18
- package/vendored/components/navigation/Header.tsx +78 -29
- package/vendored/components/navigation/Sidebar.tsx +5 -3
- package/vendored/components/navigation/SocialFooter.tsx +5 -6
- package/vendored/components/navigation/ThemePreviewChips.tsx +3 -2
- package/vendored/lib/branding-url.ts +9 -0
- package/vendored/lib/build/error-parser.ts +1 -1
- package/vendored/lib/docs-types.ts +5 -1
- package/vendored/lib/dusk-embargo.ts +16 -0
- package/vendored/lib/extract-highlights.ts +1 -1
- package/vendored/lib/layout-helpers.tsx +19 -2
- package/vendored/lib/normalize-config.ts +1 -1
- package/vendored/lib/r2-cleanup.ts +14 -10
- package/vendored/lib/static-artifacts.ts +84 -30
- package/vendored/lib/static-file-route.ts +8 -7
- package/vendored/lib/theme-preview.ts +16 -8
- package/vendored/lib/validate-config.ts +4 -4
- package/vendored/schema/docs-schema.json +124 -0
- package/vendored/themes/dusk/variables.css +426 -0
- package/vendored/themes/index.ts +28 -0
- package/vendored/themes/jam/variables.css +6 -5
- package/vendored/themes/nebula/variables.css +1 -1
- package/vendored/themes/pulsar/variables.css +4 -3
- package/vendored/themes/types.ts +8 -0
- package/vendored/workspace-package-lock.json +9 -9
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
import type { CSSProperties } from 'react';
|
|
4
4
|
import type { DocsConfig, SocialPlatform, FooterLinkColumn } from '@/lib/docs-types';
|
|
5
5
|
import { getIconClass } from '@/lib/icon-utils';
|
|
6
|
-
import { getBrandingUrl } from '@/lib/branding-url';
|
|
7
|
-
|
|
8
|
-
const showBranding = process.env.NEXT_PUBLIC_SHOW_BRANDING !== 'false';
|
|
6
|
+
import { getBrandingUrl, isBrandingVisible } from '@/lib/branding-url';
|
|
9
7
|
|
|
10
8
|
// Wordmark renders as a CSS mask so its fill inherits `currentColor`, letting
|
|
11
9
|
// the link's text color (and hover state) drive the SVG color in one place.
|
|
@@ -149,9 +147,8 @@ function SocialIcons({ socials }: { socials: Partial<Record<SocialPlatform, stri
|
|
|
149
147
|
}
|
|
150
148
|
|
|
151
149
|
/** "Powered by Jamdesk" attribution link, shared by the full footer and the
|
|
152
|
-
* embed footer.
|
|
150
|
+
* embed footer. Callers decide whether to render it (isBrandingVisible). */
|
|
153
151
|
function BrandingLink({ projectSlug }: { projectSlug?: string }) {
|
|
154
|
-
if (!showBranding) return null;
|
|
155
152
|
return (
|
|
156
153
|
<a
|
|
157
154
|
href={getBrandingUrl(projectSlug)}
|
|
@@ -171,6 +168,8 @@ function BrandingLink({ projectSlug }: { projectSlug?: string }) {
|
|
|
171
168
|
}
|
|
172
169
|
|
|
173
170
|
export function SocialFooter({ config, hidden, projectSlug, embed }: SocialFooterProps) {
|
|
171
|
+
const showBranding = isBrandingVisible(config);
|
|
172
|
+
|
|
174
173
|
// Embed render (widget modal): keep ONLY the "Powered by Jamdesk" attribution.
|
|
175
174
|
// The link columns + social icons read as out of place inside an embedded
|
|
176
175
|
// changelog; the attribution stays even when the normal footer is hidden,
|
|
@@ -202,7 +201,7 @@ export function SocialFooter({ config, hidden, projectSlug, embed }: SocialFoote
|
|
|
202
201
|
{hasLinks && <LinkColumns columns={links} />}
|
|
203
202
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
204
203
|
{hasSocials && <SocialIcons socials={socials} />}
|
|
205
|
-
<BrandingLink projectSlug={projectSlug} />
|
|
204
|
+
{showBranding && <BrandingLink projectSlug={projectSlug} />}
|
|
206
205
|
</div>
|
|
207
206
|
</footer>
|
|
208
207
|
);
|
|
@@ -4,7 +4,8 @@ import { useId, useState } from 'react';
|
|
|
4
4
|
import { useLinkPrefix } from '@/lib/link-prefix-context';
|
|
5
5
|
import { useThemePreview } from '@/lib/theme-preview-context';
|
|
6
6
|
import { applyThemePreview, type ThemePreviewSurface } from '@/lib/theme-preview-client';
|
|
7
|
-
import {
|
|
7
|
+
import { getPreviewThemes } from '@/lib/theme-preview';
|
|
8
|
+
import type { ThemeName } from '@/themes';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Jamdesk-only affordance: re-skins jamdesk.com/docs with any built-in theme
|
|
@@ -52,7 +53,7 @@ export function ThemePreviewChips() {
|
|
|
52
53
|
applyThemePreview(name, { linkPrefix, surface });
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
const themes =
|
|
56
|
+
const themes = getPreviewThemes();
|
|
56
57
|
const previewing = activeTheme !== defaultTheme;
|
|
57
58
|
const activeName = themes.find((t) => t.name === activeTheme)?.displayName ?? activeTheme;
|
|
58
59
|
|
|
@@ -7,3 +7,12 @@ export function getBrandingUrl(projectSlug?: string | null): string {
|
|
|
7
7
|
);
|
|
8
8
|
return `https://www.jamdesk.com?utm_campaign=poweredBy&utm_medium=referral&utm_source=${slug}`;
|
|
9
9
|
}
|
|
10
|
+
|
|
11
|
+
/** Whether to show "Powered by Jamdesk". The build writes `_showBranding: false`
|
|
12
|
+
* into the site config only when the owner hid it, so a missing setting (an
|
|
13
|
+
* older build, local preview) or a missing config keeps the badge. */
|
|
14
|
+
export function isBrandingVisible(
|
|
15
|
+
config: { _showBranding?: boolean } | null | undefined,
|
|
16
|
+
): boolean {
|
|
17
|
+
return config?._showBranding !== false;
|
|
18
|
+
}
|
|
@@ -262,7 +262,7 @@ export function parseErrorDetails(
|
|
|
262
262
|
const requiredFields =
|
|
263
263
|
'Required fields:\n' +
|
|
264
264
|
'• "name": Your site name\n' +
|
|
265
|
-
'• "theme": One of "jam", "nebula", "pulsar", or "
|
|
265
|
+
'• "theme": One of "jam", "nebula", "pulsar", "halo", or "dusk"\n' +
|
|
266
266
|
'• "colors": { "primary": "#hexcolor" }\n' +
|
|
267
267
|
'• "navigation": one of "tabs", "groups", "pages", "dropdowns", ' +
|
|
268
268
|
'"versions", "languages", or "products" ' +
|
|
@@ -23,7 +23,7 @@ export const ASSET_PREFIX = '/_jd';
|
|
|
23
23
|
/**
|
|
24
24
|
* Available themes
|
|
25
25
|
*/
|
|
26
|
-
export type ThemeName = 'jam' | 'nebula' | 'pulsar' | 'halo';
|
|
26
|
+
export type ThemeName = 'jam' | 'nebula' | 'pulsar' | 'halo' | 'dusk';
|
|
27
27
|
|
|
28
28
|
// =============================================================================
|
|
29
29
|
// ICON TYPES
|
|
@@ -973,6 +973,10 @@ export interface DocsConfig {
|
|
|
973
973
|
_hasCustomCss?: boolean;
|
|
974
974
|
_hasCustomJs?: boolean;
|
|
975
975
|
|
|
976
|
+
// Set by the build to `false` only when the owner hid "Powered by Jamdesk"
|
|
977
|
+
// (paid plans). Absent means shown. Read by isBrandingVisible.
|
|
978
|
+
_showBranding?: boolean;
|
|
979
|
+
|
|
976
980
|
// Runtime: Set by ISR middleware based on project config
|
|
977
981
|
// When true, all navigation links should be prefixed with /docs
|
|
978
982
|
hostAtDocs?: boolean;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dusk launch embargo — flip to `true` on October 1, 2026.
|
|
3
|
+
*
|
|
4
|
+
* One of four twins (build-service, CLI, dashboard, marketing); separate apps
|
|
5
|
+
* with separate deploys, so they cannot share a module. Flip all four in one
|
|
6
|
+
* commit. See docs/plans/2026-09-23-dusk-october-1-release-plan.md.
|
|
7
|
+
*
|
|
8
|
+
* Here it gates the jamdesk-docs theme picker, which lists every registry
|
|
9
|
+
* theme and is live on jamdesk.com/docs — without it the next ISR deploy
|
|
10
|
+
* would announce Dusk. The registry, schema and validation are NOT gated: a
|
|
11
|
+
* docs.json that names "dusk" must build (the demo site soaks on it).
|
|
12
|
+
*
|
|
13
|
+
* The `: boolean` annotation is LOAD-BEARING: without it TS narrows to the
|
|
14
|
+
* literal `false` and stops type-checking the launch branch.
|
|
15
|
+
*/
|
|
16
|
+
export const DUSK_PUBLIC: boolean = false;
|
|
@@ -11,7 +11,7 @@ import type { DocsConfig } from './docs-types.js';
|
|
|
11
11
|
*/
|
|
12
12
|
export interface ExtractedHighlights {
|
|
13
13
|
siteName: string;
|
|
14
|
-
theme: 'jam' | 'nebula' | 'pulsar' | 'halo';
|
|
14
|
+
theme: 'jam' | 'nebula' | 'pulsar' | 'halo' | 'dusk';
|
|
15
15
|
primaryColor?: string;
|
|
16
16
|
seoIndexable: boolean;
|
|
17
17
|
analyticsIntegrations: string[];
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// language, project slug) are resolved upstream from middleware headers
|
|
3
3
|
// or — in non-ISR dev/tests — from URL params; this module owns the
|
|
4
4
|
// rendering once those have resolved.
|
|
5
|
-
import { Inter, JetBrains_Mono, Figtree } from 'next/font/google';
|
|
5
|
+
import { Inter, JetBrains_Mono, Figtree, IBM_Plex_Serif } from 'next/font/google';
|
|
6
6
|
import { preinit } from 'react-dom';
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
@@ -107,6 +107,18 @@ export const figtree = Figtree({
|
|
|
107
107
|
preload: false,
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
+
// Dusk's H1 face. Exposed as a variable only (--font-serif) — Dusk keeps Inter
|
|
111
|
+
// on the body. Not a variable font, so the weight ladder is explicit.
|
|
112
|
+
// preload: false for the same reason as figtree: the theme is only known at
|
|
113
|
+
// runtime from docs.json, so preload would be all-pages-or-none.
|
|
114
|
+
export const ibmPlexSerif = IBM_Plex_Serif({
|
|
115
|
+
subsets: ['latin'],
|
|
116
|
+
display: 'swap',
|
|
117
|
+
weight: ['400', '600'],
|
|
118
|
+
variable: '--font-serif',
|
|
119
|
+
preload: false,
|
|
120
|
+
});
|
|
121
|
+
|
|
110
122
|
export function getLocalFileContent(filename: string): string | null {
|
|
111
123
|
try {
|
|
112
124
|
const filePath = path.join(process.cwd(), 'public', filename);
|
|
@@ -315,6 +327,7 @@ export function getFontClassName(
|
|
|
315
327
|
themeName: ThemeName | undefined,
|
|
316
328
|
customFonts?: FontConfig,
|
|
317
329
|
): string {
|
|
330
|
+
const theme = getTheme(themeName);
|
|
318
331
|
const primaryFont = getPrimaryFontFamily(customFonts);
|
|
319
332
|
|
|
320
333
|
if (primaryFont) {
|
|
@@ -333,16 +346,20 @@ export function getFontClassName(
|
|
|
333
346
|
}
|
|
334
347
|
|
|
335
348
|
classes.push(jetbrainsMono.variable);
|
|
349
|
+
// Dusk's serif H1 survives a custom body font — it is theme identity.
|
|
350
|
+
if (theme.name === 'dusk') classes.push(ibmPlexSerif.variable);
|
|
336
351
|
return classes.join(' ');
|
|
337
352
|
}
|
|
338
353
|
|
|
339
|
-
const theme = getTheme(themeName);
|
|
340
354
|
if (theme.name === 'nebula') {
|
|
341
355
|
return `${jetbrainsMono.variable} font-mono`;
|
|
342
356
|
}
|
|
343
357
|
if (theme.name === 'halo') {
|
|
344
358
|
return `${figtree.variable} ${jetbrainsMono.variable} ${figtree.className}`;
|
|
345
359
|
}
|
|
360
|
+
if (theme.name === 'dusk') {
|
|
361
|
+
return `${inter.variable} ${ibmPlexSerif.variable} ${jetbrainsMono.variable} ${inter.className}`;
|
|
362
|
+
}
|
|
346
363
|
return `${inter.variable} ${jetbrainsMono.variable} ${inter.className}`;
|
|
347
364
|
}
|
|
348
365
|
|
|
@@ -96,7 +96,7 @@ export function normalizeConfig(config: DocsConfigInput): NormalizeResult {
|
|
|
96
96
|
// 3. Warn about layout
|
|
97
97
|
if (layout) {
|
|
98
98
|
warnings.push(
|
|
99
|
-
'layout field is ignored. Jamdesk determines layout from theme (pulsar=sidebar, jam/nebula/halo=header).'
|
|
99
|
+
'layout field is ignored. Jamdesk determines layout from theme (pulsar=sidebar, jam/nebula/halo/dusk=header).'
|
|
100
100
|
);
|
|
101
101
|
}
|
|
102
102
|
|
|
@@ -409,9 +409,13 @@ export async function pruneRemovedContent(
|
|
|
409
409
|
|
|
410
410
|
/**
|
|
411
411
|
* Given top-level CommonPrefixes from a delimiter='/' listing of `{slug}/`,
|
|
412
|
-
* return per-locale llms.txt keys whose locale is a KNOWN
|
|
413
|
-
* is no longer
|
|
414
|
-
*
|
|
412
|
+
* return per-locale llms.txt / llms-full.txt keys whose locale is a KNOWN
|
|
413
|
+
* language code but is no longer active — left behind when a project removes
|
|
414
|
+
* a language. One set covers both files: the default locale has only an
|
|
415
|
+
* llms-full.txt, and treating its dir as active keeps the sweep from deleting
|
|
416
|
+
* a `<default>/llms.txt` that never existed on every build. The cost is a
|
|
417
|
+
* cosmetic orphan if the default language ever changes.
|
|
418
|
+
* Only ever targets our own `<locale>/llms*.txt` artifact keys, so content
|
|
415
419
|
* dirs (content/, snippets/, openapi/, assets/) can never be touched.
|
|
416
420
|
*/
|
|
417
421
|
export function staleLocaleLlmsKeys(
|
|
@@ -430,14 +434,14 @@ export function staleLocaleLlmsKeys(
|
|
|
430
434
|
// casing differs is either a renamed locale or a leftover from a prior
|
|
431
435
|
// casing change — both legitimately stale.
|
|
432
436
|
if (activeLocales.has(dir)) continue;
|
|
433
|
-
keys.push(`${projectSlug}/${dir}/llms.txt`);
|
|
437
|
+
keys.push(`${projectSlug}/${dir}/llms.txt`, `${projectSlug}/${dir}/llms-full.txt`);
|
|
434
438
|
}
|
|
435
439
|
return keys;
|
|
436
440
|
}
|
|
437
441
|
|
|
438
442
|
/**
|
|
439
|
-
* List `{slug}/`'s top-level dirs and delete `<locale>/llms.txt`
|
|
440
|
-
* known-language dir no longer
|
|
443
|
+
* List `{slug}/`'s top-level dirs and delete `<locale>/llms.txt` and
|
|
444
|
+
* `<locale>/llms-full.txt` for any known-language dir no longer active. Paginates the listing like
|
|
441
445
|
* deleteAllProjectR2Objects — a delimiter listing of a huge project could
|
|
442
446
|
* truncate, and an abandoned page would silently orphan its stale locales.
|
|
443
447
|
* Non-fatal by contract: callers treat a throw as a warning — a stale
|
|
@@ -474,15 +478,15 @@ export async function sweepStaleLocaleLlmsTxt(
|
|
|
474
478
|
} while (continuationToken);
|
|
475
479
|
|
|
476
480
|
const staleKeys = staleLocaleLlmsKeys(prefixes, projectSlug, activeLocales);
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
481
|
+
await Promise.all(
|
|
482
|
+
staleKeys.map((key) => client.send(new DeleteObjectCommand({ Bucket: bucketName, Key: key }))),
|
|
483
|
+
);
|
|
480
484
|
return staleKeys;
|
|
481
485
|
}
|
|
482
486
|
|
|
483
487
|
/**
|
|
484
488
|
* Delete a project's AI-context files: the root `llms.txt` + `llms-full.txt` and
|
|
485
|
-
* every per-locale `<locale>/llms.txt`. Called on every build where `seo.ai.llmsTxt`
|
|
489
|
+
* every per-locale `<locale>/llms.txt` + `<locale>/llms-full.txt`. Called on every build where `seo.ai.llmsTxt`
|
|
486
490
|
* is false so a previously published llms.txt stops serving after a customer opts
|
|
487
491
|
* out — the build gate stops REGENERATING these files but otherwise leaves the
|
|
488
492
|
* already-uploaded copies in place. The deletes are idempotent (DeleteObject is a
|
|
@@ -303,19 +303,26 @@ export interface LlmsTxtFilesOptions extends Omit<LlmsTxtOptions, 'sections' | '
|
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
/**
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
* same semantics as the search index), NOT from nav position. Cross-links
|
|
310
|
-
* between locale files render under the trailing "## Optional" section.
|
|
306
|
+
* Pages grouped by locale for the per-locale AI-context files (llms.txt,
|
|
307
|
+
* llms-full.txt). Shared so the two can never disagree on which page belongs
|
|
308
|
+
* to which language. Null when the project declares no visible languages.
|
|
311
309
|
*/
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
310
|
+
interface LocaleBuckets<P> {
|
|
311
|
+
defaultCode: string;
|
|
312
|
+
/** Declared, non-hidden codes that have pages; the default always included. */
|
|
313
|
+
activeCodes: string[];
|
|
314
|
+
pagesFor: (code: string) => P[];
|
|
315
|
+
}
|
|
318
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Bucket pages by locale. Membership comes from the page path prefix
|
|
319
|
+
* (resolveLocaleFromPath, whitelisted against declared codes — same semantics
|
|
320
|
+
* as the search index), NOT from nav position.
|
|
321
|
+
*/
|
|
322
|
+
function bucketPagesByLocale<P extends { path: string }>(
|
|
323
|
+
pages: P[],
|
|
324
|
+
languages: LanguageConfig[] | undefined,
|
|
325
|
+
): LocaleBuckets<P> | null {
|
|
319
326
|
const declared = (languages ?? []).filter(
|
|
320
327
|
(l): l is LanguageConfig & { language: string } =>
|
|
321
328
|
typeof l.language === 'string' && !l.hidden,
|
|
@@ -329,14 +336,11 @@ export function generateLlmsTxtFiles(
|
|
|
329
336
|
(code, i, all) =>
|
|
330
337
|
all.findIndex((c) => c.toLowerCase() === code.toLowerCase()) === i,
|
|
331
338
|
);
|
|
339
|
+
if (codes.length === 0) return null;
|
|
332
340
|
const defaultCode = resolveLanguageWithFallback(null, languages);
|
|
341
|
+
const loweredCodes = buildLoweredLocaleSet(codes);
|
|
333
342
|
|
|
334
|
-
|
|
335
|
-
const root = generateLlmsTxt({ ...rest, baseUrl, hostAtDocs, docsPrefix, pages, sections });
|
|
336
|
-
return { root, byLocale: {} };
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const byLocalePages = new Map<string, typeof pages>();
|
|
343
|
+
const byLocalePages = new Map<string, P[]>();
|
|
340
344
|
for (const page of pages) {
|
|
341
345
|
// resolveLocaleFromPath returns '' (NOT the default code) for unprefixed
|
|
342
346
|
// paths and for prefixes outside the whitelist — both belong to the root
|
|
@@ -346,21 +350,43 @@ export function generateLlmsTxtFiles(
|
|
|
346
350
|
// CANONICAL casing (e.g. 'fr-CA' for a declared 'fr-ca'), so bucketing by
|
|
347
351
|
// its raw return and reading back by the declared code would silently
|
|
348
352
|
// drop those pages from every llms.txt file.
|
|
349
|
-
const locale = (
|
|
353
|
+
const locale = (resolveLocaleWithLoweredSet(page.path, loweredCodes) || defaultCode).toLowerCase();
|
|
350
354
|
if (!byLocalePages.has(locale)) byLocalePages.set(locale, []);
|
|
351
355
|
byLocalePages.get(locale)!.push(page);
|
|
352
356
|
}
|
|
357
|
+
const pagesFor = (code: string): P[] => byLocalePages.get(code.toLowerCase()) ?? [];
|
|
358
|
+
|
|
359
|
+
// Declared-but-untranslated languages produce empty files nobody should be
|
|
360
|
+
// sent to — generate and cross-link only locales that actually have pages
|
|
361
|
+
// (the default locale always exists as the root file).
|
|
362
|
+
const activeCodes = codes.filter((c) => c === defaultCode || pagesFor(c).length > 0);
|
|
363
|
+
return { defaultCode, activeCodes, pagesFor };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Render the root llms.txt (default locale) plus one file per declared,
|
|
368
|
+
* non-hidden, non-default language (see bucketPagesByLocale for membership).
|
|
369
|
+
* Cross-links between locale files render under the trailing "## Optional"
|
|
370
|
+
* section.
|
|
371
|
+
*/
|
|
372
|
+
export function generateLlmsTxtFiles(
|
|
373
|
+
options: LlmsTxtFilesOptions,
|
|
374
|
+
): { root: string; byLocale: Record<string, string> } {
|
|
375
|
+
const { navigation, languages, baseUrl, hostAtDocs = false, docsPrefix, pages, ...rest } = options;
|
|
376
|
+
const urlPrefix = docsPrefix ?? (hostAtDocs ? '/docs' : '');
|
|
377
|
+
const sections = extractNavigationSections(navigation);
|
|
378
|
+
|
|
379
|
+
const buckets = bucketPagesByLocale(pages, languages);
|
|
380
|
+
if (!buckets) {
|
|
381
|
+
const root = generateLlmsTxt({ ...rest, baseUrl, hostAtDocs, docsPrefix, pages, sections });
|
|
382
|
+
return { root, byLocale: {} };
|
|
383
|
+
}
|
|
384
|
+
const { defaultCode, activeCodes, pagesFor } = buckets;
|
|
353
385
|
|
|
354
386
|
const llmsUrl = (code: string): string =>
|
|
355
387
|
code === defaultCode
|
|
356
388
|
? `${baseUrl}${urlPrefix}/llms.txt`
|
|
357
389
|
: `${baseUrl}${urlPrefix}/${code}/llms.txt`;
|
|
358
|
-
// Declared-but-untranslated languages produce empty files nobody should be
|
|
359
|
-
// sent to — generate and cross-link only locales that actually have pages
|
|
360
|
-
// (the default locale always exists as the root file).
|
|
361
|
-
const activeCodes = codes.filter(
|
|
362
|
-
(c) => c === defaultCode || (byLocalePages.get(c.toLowerCase())?.length ?? 0) > 0,
|
|
363
|
-
);
|
|
364
390
|
const activeCrossLinks = (selfCode: string) =>
|
|
365
391
|
activeCodes
|
|
366
392
|
.filter((c) => c !== selfCode)
|
|
@@ -368,7 +394,7 @@ export function generateLlmsTxtFiles(
|
|
|
368
394
|
|
|
369
395
|
const root = generateLlmsTxt({
|
|
370
396
|
...rest, baseUrl, hostAtDocs, docsPrefix, sections,
|
|
371
|
-
pages:
|
|
397
|
+
pages: pagesFor(defaultCode),
|
|
372
398
|
otherLanguages: activeCrossLinks(defaultCode),
|
|
373
399
|
});
|
|
374
400
|
|
|
@@ -377,7 +403,7 @@ export function generateLlmsTxtFiles(
|
|
|
377
403
|
if (code === defaultCode) continue;
|
|
378
404
|
byLocale[code] = generateLlmsTxt({
|
|
379
405
|
...rest, baseUrl, hostAtDocs, docsPrefix, sections,
|
|
380
|
-
pages:
|
|
406
|
+
pages: pagesFor(code),
|
|
381
407
|
otherLanguages: activeCrossLinks(code),
|
|
382
408
|
});
|
|
383
409
|
}
|
|
@@ -723,6 +749,30 @@ export function generateLlmsFullTxt(options: LlmsFullTxtOptions): string {
|
|
|
723
749
|
return parts.join('').trim();
|
|
724
750
|
}
|
|
725
751
|
|
|
752
|
+
/**
|
|
753
|
+
* Render the root llms-full.txt plus one file per active locale.
|
|
754
|
+
*
|
|
755
|
+
* Unlike llms.txt, the root stays EVERY language: existing consumers of the
|
|
756
|
+
* combined file keep receiving what they always have. Because of that, the
|
|
757
|
+
* default locale gets its own file too (e.g. /en/llms-full.txt) — otherwise
|
|
758
|
+
* there would be no way to fetch the default language alone.
|
|
759
|
+
*/
|
|
760
|
+
export function generateLlmsFullTxtFiles(
|
|
761
|
+
options: LlmsFullTxtOptions & { languages?: LanguageConfig[] },
|
|
762
|
+
): { root: string; byLocale: Record<string, string> } {
|
|
763
|
+
const { languages, ...rest } = options;
|
|
764
|
+
const root = generateLlmsFullTxt(rest);
|
|
765
|
+
const buckets = bucketPagesByLocale(rest.pages, languages);
|
|
766
|
+
// One active locale means the root already IS that language — a per-locale
|
|
767
|
+
// copy would only duplicate a multi-MB file.
|
|
768
|
+
if (!buckets || buckets.activeCodes.length < 2) return { root, byLocale: {} };
|
|
769
|
+
const byLocale: Record<string, string> = {};
|
|
770
|
+
for (const code of buckets.activeCodes) {
|
|
771
|
+
byLocale[code] = generateLlmsFullTxt({ ...rest, pages: buckets.pagesFor(code) });
|
|
772
|
+
}
|
|
773
|
+
return { root, byLocale };
|
|
774
|
+
}
|
|
775
|
+
|
|
726
776
|
/**
|
|
727
777
|
* Options for generating all artifacts.
|
|
728
778
|
*/
|
|
@@ -775,6 +825,8 @@ export interface GeneratedArtifacts {
|
|
|
775
825
|
/** Non-default-locale llms.txt files, keyed by language code. */
|
|
776
826
|
llmsTxtByLocale: Record<string, string>;
|
|
777
827
|
llmsFullTxt: string;
|
|
828
|
+
/** Per-locale llms-full.txt files, keyed by language code (default locale included). */
|
|
829
|
+
llmsFullTxtByLocale: Record<string, string>;
|
|
778
830
|
robotsTxt: string;
|
|
779
831
|
rssFeed: string | null;
|
|
780
832
|
/** changelog.json for the embeddable widget — built from the same updates as rssFeed. */
|
|
@@ -856,9 +908,9 @@ export function generateAllArtifacts(options: GenerateAllOptions): GeneratedArti
|
|
|
856
908
|
name, description, baseUrl, pages, hostAtDocs, docsPrefix, noindex, visibility,
|
|
857
909
|
navigation, languages,
|
|
858
910
|
});
|
|
859
|
-
const llmsFullTxt = llmsFullPages
|
|
860
|
-
?
|
|
861
|
-
: '';
|
|
911
|
+
const { root: llmsFullTxt, byLocale: llmsFullTxtByLocale } = llmsFullPages
|
|
912
|
+
? generateLlmsFullTxtFiles({ name, pages: llmsFullPages, noindex, visibility, languages })
|
|
913
|
+
: { root: '', byLocale: {} };
|
|
862
914
|
const robotsTxt = generateRobotsTxt({ baseUrl, hostAtDocs, docsPrefix, noindex });
|
|
863
915
|
|
|
864
916
|
// Extract <Update> entries once — they power BOTH the RSS feed and
|
|
@@ -877,7 +929,9 @@ export function generateAllArtifacts(options: GenerateAllOptions): GeneratedArti
|
|
|
877
929
|
|
|
878
930
|
const changelog = generateChangelog(updates);
|
|
879
931
|
|
|
880
|
-
return {
|
|
932
|
+
return {
|
|
933
|
+
sitemap, llmsTxt, llmsTxtByLocale, llmsFullTxt, llmsFullTxtByLocale, robotsTxt, rssFeed, changelog,
|
|
934
|
+
};
|
|
881
935
|
}
|
|
882
936
|
|
|
883
937
|
// =============================================================================
|
|
@@ -25,7 +25,7 @@ export const STATIC_FILE_NAMES = [
|
|
|
25
25
|
] as const;
|
|
26
26
|
|
|
27
27
|
/** All CDN paths for static file routes — used by revalidation to purge CDN cache. */
|
|
28
|
-
// Per-locale llms.txt paths (/{locale}/llms.txt) are intentionally absent:
|
|
28
|
+
// Per-locale llms.txt / llms-full.txt paths (/{locale}/llms.txt) are intentionally absent:
|
|
29
29
|
// locales are per-project, unenumerable here. They self-heal via s-maxage=3600,
|
|
30
30
|
// identical to llms.txt's behavior between explicit purges.
|
|
31
31
|
export const STATIC_REVALIDATION_PATHS = STATIC_FILE_NAMES.flatMap(
|
|
@@ -165,7 +165,8 @@ export function createCorsStaticFileHandler(
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
/**
|
|
168
|
-
* GET handler for per-locale llms.txt
|
|
168
|
+
* GET handler for per-locale llms.txt / llms-full.txt (`/{locale}/llms.txt`,
|
|
169
|
+
* `/docs/{locale}/llms-full.txt`, ...).
|
|
169
170
|
*
|
|
170
171
|
* Locale is validated (case-insensitively) against the supported language-code
|
|
171
172
|
* list BEFORE any R2 I/O so arbitrary path segments (`/foo/llms.txt`) fast-404.
|
|
@@ -173,7 +174,7 @@ export function createCorsStaticFileHandler(
|
|
|
173
174
|
* `x-jd-noindex` — AI agents fetch them directly and don't honor robots
|
|
174
175
|
* semantics.
|
|
175
176
|
*/
|
|
176
|
-
export function createLocaleLlmsTxtHandler(): (
|
|
177
|
+
export function createLocaleLlmsTxtHandler(file: 'llms.txt' | 'llms-full.txt' = 'llms.txt'): (
|
|
177
178
|
request: NextRequest,
|
|
178
179
|
ctx: { params: Promise<{ locale: string }> },
|
|
179
180
|
) => Promise<NextResponse> {
|
|
@@ -185,7 +186,7 @@ export function createLocaleLlmsTxtHandler(): (
|
|
|
185
186
|
// Fetch with the segment verbatim: R2 keys are written with the DECLARED
|
|
186
187
|
// code, and every advertised URL is generated from that same code, so
|
|
187
188
|
// segment casing and key casing always agree for real links.
|
|
188
|
-
const filename = `${locale}
|
|
189
|
+
const filename = `${locale}/${file}`;
|
|
189
190
|
|
|
190
191
|
if (!isIsrMode()) {
|
|
191
192
|
const localPath = path.join(process.cwd(), 'public', filename);
|
|
@@ -199,7 +200,7 @@ export function createLocaleLlmsTxtHandler(): (
|
|
|
199
200
|
|
|
200
201
|
const projectSlug = request.headers.get('x-project-slug');
|
|
201
202
|
if (!projectSlug) {
|
|
202
|
-
log('warn',
|
|
203
|
+
log('warn', `Locale ${file} request missing project slug`);
|
|
203
204
|
return new NextResponse('Project not found', { status: 404 });
|
|
204
205
|
}
|
|
205
206
|
|
|
@@ -215,8 +216,8 @@ export function createLocaleLlmsTxtHandler(): (
|
|
|
215
216
|
},
|
|
216
217
|
});
|
|
217
218
|
} catch (error) {
|
|
218
|
-
log('error',
|
|
219
|
-
return new NextResponse(
|
|
219
|
+
log('error', `Error serving locale ${file}`, { projectSlug, locale, error: String(error) });
|
|
220
|
+
return new NextResponse(`Error serving locale ${file}`, { status: 500 });
|
|
220
221
|
}
|
|
221
222
|
};
|
|
222
223
|
}
|
|
@@ -56,7 +56,8 @@
|
|
|
56
56
|
* keep serving one visitor's previewed HTML to everyone else regardless of
|
|
57
57
|
* a `Vary` header.
|
|
58
58
|
*/
|
|
59
|
-
import { getAllThemes, type ThemeName } from '@/themes';
|
|
59
|
+
import { getAllThemes, type ThemeConfig, type ThemeName } from '@/themes';
|
|
60
|
+
import { DUSK_PUBLIC } from '@/lib/dusk-embargo';
|
|
60
61
|
|
|
61
62
|
/** Cookie the picker writes; read server-side in app/layout.tsx. */
|
|
62
63
|
export const THEME_PREVIEW_COOKIE = 'jd_theme';
|
|
@@ -69,14 +70,21 @@ export const THEME_PREVIEW_SLUG = 'jamdesk-docs';
|
|
|
69
70
|
// setting, and should not outlive the visit. The clear path still sends
|
|
70
71
|
// `Max-Age=0` — see theme-preview-client.ts's applyThemePreview.
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
//
|
|
73
|
+
/** Themes the picker offers. Dusk joins on launch day (lib/dusk-embargo.ts). */
|
|
74
|
+
export function getPreviewThemes(): ThemeConfig[] {
|
|
75
|
+
return getAllThemes().filter((theme) => DUSK_PUBLIC || theme.name !== 'dusk');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Built from the themes the picker actually offers, rather than using
|
|
79
|
+
// `isValidTheme`, which is a plain `name in themes` check and therefore also
|
|
80
|
+
// returns true for inherited Object keys ('constructor', 'toString'). Those
|
|
81
|
+
// would reach getTheme(), which falls back to jam silently for unknown names
|
|
82
|
+
// but returns Object's own constructor for 'constructor' — yielding
|
|
83
|
+
// data-theme="constructor" and no theme CSS. A Set of the offered themes' own
|
|
84
|
+
// names is immune, picks up new themes for free, and keeps an embargoed theme
|
|
85
|
+
// out of both the picker and the cookie reader below.
|
|
78
86
|
const VALID_THEME_NAMES: ReadonlySet<string> = new Set(
|
|
79
|
-
|
|
87
|
+
getPreviewThemes().map((theme) => theme.name),
|
|
80
88
|
);
|
|
81
89
|
|
|
82
90
|
/**
|
|
@@ -27,7 +27,7 @@ import { validateJwtAuth } from './jwt-auth-config.js';
|
|
|
27
27
|
export interface DocsConfig {
|
|
28
28
|
name: string;
|
|
29
29
|
projectId?: string;
|
|
30
|
-
theme?: 'jam' | 'nebula' | 'pulsar' | 'halo';
|
|
30
|
+
theme?: 'jam' | 'nebula' | 'pulsar' | 'halo' | 'dusk';
|
|
31
31
|
navigation: NavigationConfig;
|
|
32
32
|
integrations?: IntegrationsConfig;
|
|
33
33
|
[key: string]: unknown;
|
|
@@ -170,7 +170,7 @@ export function parseConfig(content: string): DocsConfig {
|
|
|
170
170
|
/**
|
|
171
171
|
* Filter out noise from anyOf validation errors.
|
|
172
172
|
*
|
|
173
|
-
* The master schema uses a
|
|
173
|
+
* The master schema uses a 5-branch anyOf (one per theme) and Ajv emits errors
|
|
174
174
|
* from every branch it tried, producing messages like `must be "nebula"` when
|
|
175
175
|
* the user typed `theme: "jam"`. This helper:
|
|
176
176
|
*
|
|
@@ -568,7 +568,7 @@ export async function validateConfig(
|
|
|
568
568
|
}
|
|
569
569
|
|
|
570
570
|
// Validate theme (case-insensitive — "MINT" still falls through to the Mintlify branch below).
|
|
571
|
-
const validThemes = ['jam', 'nebula', 'pulsar', 'halo'];
|
|
571
|
+
const validThemes = ['jam', 'nebula', 'pulsar', 'halo', 'dusk'];
|
|
572
572
|
if (typeof config.theme === 'string') {
|
|
573
573
|
const themeLower = config.theme.toLowerCase();
|
|
574
574
|
if (!validThemes.includes(themeLower)) {
|
|
@@ -582,7 +582,7 @@ export async function validateConfig(
|
|
|
582
582
|
};
|
|
583
583
|
}
|
|
584
584
|
// Normalize so downstream Ajv schema (lowercase enum) accepts it.
|
|
585
|
-
config.theme = themeLower as 'jam' | 'nebula' | 'pulsar' | 'halo';
|
|
585
|
+
config.theme = themeLower as 'jam' | 'nebula' | 'pulsar' | 'halo' | 'dusk';
|
|
586
586
|
}
|
|
587
587
|
|
|
588
588
|
if (legacyAnchors) {
|