jamdesk 1.1.147 → 1.1.149
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/seo-ai-schema.test.d.ts +2 -0
- package/dist/__tests__/unit/seo-ai-schema.test.d.ts.map +1 -0
- package/dist/__tests__/unit/seo-ai-schema.test.js +40 -0
- package/dist/__tests__/unit/seo-ai-schema.test.js.map +1 -0
- package/package.json +1 -1
- package/vendored/components/ConsentGate.tsx +87 -0
- package/vendored/components/mdx/Accordion.tsx +6 -1
- package/vendored/components/mdx/MDXComponents.tsx +14 -1
- package/vendored/components/mdx/MermaidInner.tsx +2 -8
- package/vendored/lib/consent-gating.ts +119 -0
- package/vendored/lib/docs-types.ts +6 -1
- package/vendored/lib/extract-faq.ts +54 -0
- package/vendored/lib/json-ld.ts +16 -5
- package/vendored/lib/layout-helpers.tsx +57 -21
- package/vendored/lib/llms-flag.ts +5 -0
- package/vendored/lib/logo-warning.ts +35 -0
- package/vendored/lib/noindex-warning.ts +14 -0
- package/vendored/lib/r2-cleanup.ts +26 -0
- package/vendored/lib/render-doc-page.tsx +3 -0
- package/vendored/lib/seo.ts +20 -6
- package/vendored/lib/validate-config.ts +3 -0
- package/vendored/lib/validate-page-frontmatter.ts +30 -0
- package/vendored/schema/docs-schema.json +31 -2
- package/vendored/shared/status-reporter.ts +27 -1
- package/vendored/workspace-package-lock.json +90 -6
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo-ai-schema.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/unit/seo-ai-schema.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const Ajv = require('ajv').default || require('ajv');
|
|
8
|
+
// Load the schema via fs (not `import ... from '.json'`): the CLI is module:NodeNext,
|
|
9
|
+
// where a JSON import needs an import attribute. fs.readFileSync mirrors how the real
|
|
10
|
+
// validate command (src/lib/docs-config.ts) and other CLI tests load bundled JSON.
|
|
11
|
+
const schemaDir = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const schema = JSON.parse(fs.readFileSync(path.join(schemaDir, '../../../vendored/schema/docs-schema.json'), 'utf-8'));
|
|
13
|
+
// Minimal config that satisfies anyOf[0] (jam theme) with no extra properties.
|
|
14
|
+
// Required: theme (const "jam"), name, navigation (must match one of the
|
|
15
|
+
// navigationSchema anyOf variants; anyOf[6] = { pages: [] } is the simplest).
|
|
16
|
+
const BASE = {
|
|
17
|
+
theme: 'jam',
|
|
18
|
+
name: 'Test',
|
|
19
|
+
navigation: { pages: [] },
|
|
20
|
+
};
|
|
21
|
+
describe('docs.json schema: seo.ai.llmsTxt', () => {
|
|
22
|
+
const compile = () => new Ajv({ allErrors: true, strict: false, logger: false }).compile(schema);
|
|
23
|
+
it('baseline config validates (control)', () => {
|
|
24
|
+
const validate = compile();
|
|
25
|
+
validate(BASE);
|
|
26
|
+
expect(validate.errors ?? []).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
it('accepts seo.ai.llmsTxt: false', () => {
|
|
29
|
+
const validate = compile();
|
|
30
|
+
const ok = validate({ ...BASE, seo: { ...BASE.seo, ai: { llmsTxt: false } } });
|
|
31
|
+
expect(validate.errors ?? []).toEqual([]);
|
|
32
|
+
expect(ok).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
it('rejects an unknown key under seo.ai (additionalProperties:false)', () => {
|
|
35
|
+
const validate = compile();
|
|
36
|
+
validate({ ...BASE, seo: { ...BASE.seo, ai: { bogus: true } } });
|
|
37
|
+
expect((validate.errors ?? []).length).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
//# sourceMappingURL=seo-ai-schema.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo-ai-schema.test.js","sourceRoot":"","sources":["../../../src/__tests__/unit/seo-ai-schema.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC/C,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,OAAO,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC;AACrD,sFAAsF;AACtF,sFAAsF;AACtF,mFAAmF;AACnF,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC/D,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CACvB,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,2CAA2C,CAAC,EAAE,OAAO,CAAC,CAC5F,CAAC;AAEF,+EAA+E;AAC/E,yEAAyE;AACzE,8EAA8E;AAC9E,MAAM,IAAI,GAAG;IACT,KAAK,EAAE,KAAK;IACZ,IAAI,EAAE,MAAM;IACZ,UAAU,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;CAC5B,CAAC;AAEF,QAAQ,CAAC,kCAAkC,EAAE,GAAG,EAAE;IAC9C,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,IAAI,GAAG,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,MAAgB,CAAC,CAAC;IAE3G,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC3C,MAAM,QAAQ,GAAG,OAAO,EAAE,CAAC;QAC3B,QAAQ,CAAC,IAAI,CAAC,CAAC;QACf,MAAM,CAAC,QAAQ,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACrC,MAAM,QAAQ,GAAG,OAAO,EAAE,CAAC;QAC3B,MAAM,EAAE,GAAG,QAAQ,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,EAAE,GAAI,IAAY,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;QACxF,MAAM,CAAC,QAAQ,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC1C,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QACxE,MAAM,QAAQ,GAAG,OAAO,EAAE,CAAC;QAC3B,QAAQ,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,EAAE,GAAI,IAAY,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;QAC1E,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jamdesk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.149",
|
|
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",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// builder/build-service/components/ConsentGate.tsx
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import { useEffect, useRef } from 'react';
|
|
5
|
+
import type { GatedScript } from '../lib/consent-gating';
|
|
6
|
+
|
|
7
|
+
const POLL_MS = 1000;
|
|
8
|
+
|
|
9
|
+
function hasConsent(key: string, value: string): boolean {
|
|
10
|
+
try {
|
|
11
|
+
return window.localStorage.getItem(key) === value;
|
|
12
|
+
} catch {
|
|
13
|
+
// Storage unavailable (private mode / blocked iframe) → treat as no consent
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function injectScripts(scripts: GatedScript[]): void {
|
|
19
|
+
for (const s of scripts) {
|
|
20
|
+
const el = document.createElement('script');
|
|
21
|
+
el.setAttribute('data-jd-consent-script', s.id);
|
|
22
|
+
if (s.kind === 'src' && s.src) {
|
|
23
|
+
el.src = s.src;
|
|
24
|
+
for (const [name, val] of Object.entries(s.attrs ?? {})) el.setAttribute(name, val);
|
|
25
|
+
} else if (s.kind === 'inline' && s.code) {
|
|
26
|
+
el.textContent = s.code;
|
|
27
|
+
} else {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
(s.appendTo === 'head' ? document.head : document.body).appendChild(el);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Consent gate for analytics/tracking scripts (integrations.cookies).
|
|
36
|
+
* Renders nothing. Injects the given scripts once localStorage[consentKey]
|
|
37
|
+
* equals consentValue — immediately on mount if consent pre-exists, else via
|
|
38
|
+
* a storage listener (cross-tab) + poll (same-tab CMP writes don't fire
|
|
39
|
+
* storage events). Scripts are injected at most once per page load; consent
|
|
40
|
+
* revocation takes effect on the next page load.
|
|
41
|
+
*/
|
|
42
|
+
export function ConsentGate({
|
|
43
|
+
consentKey,
|
|
44
|
+
consentValue,
|
|
45
|
+
scripts,
|
|
46
|
+
}: {
|
|
47
|
+
consentKey: string;
|
|
48
|
+
consentValue: string;
|
|
49
|
+
scripts: GatedScript[];
|
|
50
|
+
}) {
|
|
51
|
+
// scripts is a fresh array identity each render (built server-side, serialized
|
|
52
|
+
// across the RSC boundary). Hold it in a ref so the effect doesn't list it as
|
|
53
|
+
// a dependency — otherwise a parent re-render before consent arrives would
|
|
54
|
+
// tear down and restart the polling window. Content is stable per page.
|
|
55
|
+
const scriptsRef = useRef(scripts);
|
|
56
|
+
scriptsRef.current = scripts;
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if ((window as any).__jdConsentInjected) return;
|
|
60
|
+
let intervalId: ReturnType<typeof setInterval> | undefined;
|
|
61
|
+
|
|
62
|
+
const stop = () => {
|
|
63
|
+
if (intervalId !== undefined) clearInterval(intervalId);
|
|
64
|
+
window.removeEventListener('storage', check);
|
|
65
|
+
};
|
|
66
|
+
const check = () => {
|
|
67
|
+
if ((window as any).__jdConsentInjected) { stop(); return; }
|
|
68
|
+
if (!hasConsent(consentKey, consentValue)) return;
|
|
69
|
+
(window as any).__jdConsentInjected = true;
|
|
70
|
+
stop();
|
|
71
|
+
injectScripts(scriptsRef.current);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
check();
|
|
75
|
+
if (!(window as any).__jdConsentInjected) {
|
|
76
|
+
// Diagnostics: wrong key/value otherwise fails silently forever.
|
|
77
|
+
console.debug(
|
|
78
|
+
`[Jamdesk] Consent gating active — analytics withheld until localStorage["${consentKey}"] equals the configured value.`,
|
|
79
|
+
);
|
|
80
|
+
window.addEventListener('storage', check);
|
|
81
|
+
intervalId = setInterval(check, POLL_MS);
|
|
82
|
+
}
|
|
83
|
+
return stop;
|
|
84
|
+
}, [consentKey, consentValue]);
|
|
85
|
+
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
@@ -35,6 +35,10 @@ export const Accordion = memo(function Accordion({
|
|
|
35
35
|
|
|
36
36
|
return (
|
|
37
37
|
<div className="border border-[var(--color-border)] rounded-lg mb-2 last:mb-0 not-prose overflow-hidden">
|
|
38
|
+
{/* WAI-ARIA accordion pattern: the heading WRAPS the toggle button. A heading
|
|
39
|
+
nested inside the button would be pruned from the a11y tree (button name is
|
|
40
|
+
computed from contents) and flagged by axe — so it goes on the outside. */}
|
|
41
|
+
<div role="heading" aria-level={3}>
|
|
38
42
|
<button
|
|
39
43
|
type="button"
|
|
40
44
|
onClick={() => setIsOpen(!isOpen)}
|
|
@@ -66,7 +70,8 @@ export const Accordion = memo(function Accordion({
|
|
|
66
70
|
aria-hidden="true"
|
|
67
71
|
/>
|
|
68
72
|
</button>
|
|
69
|
-
|
|
73
|
+
</div>
|
|
74
|
+
|
|
70
75
|
{/* Content */}
|
|
71
76
|
<div
|
|
72
77
|
className={`overflow-hidden transition-all duration-200 ${
|
|
@@ -414,7 +414,20 @@ export const MDXComponents = {
|
|
|
414
414
|
// Check for mermaid diagrams - render with Mermaid component
|
|
415
415
|
if (language === 'mermaid') {
|
|
416
416
|
const diagramCode = extractTextFromChildren(children);
|
|
417
|
-
return
|
|
417
|
+
return (
|
|
418
|
+
<>
|
|
419
|
+
<Mermaid>{diagramCode}</Mermaid>
|
|
420
|
+
{/* Crawler / AI fallback: the client-only SVG leaves the server HTML empty.
|
|
421
|
+
Use <noscript>, NOT sr-only: a JS-disabled crawler reads it, while users
|
|
422
|
+
with JS (and screen readers) get the real rendered diagram and never see
|
|
423
|
+
duplicate text. */}
|
|
424
|
+
{diagramCode.trim() && (
|
|
425
|
+
<noscript>
|
|
426
|
+
<pre>{diagramCode}</pre>
|
|
427
|
+
</noscript>
|
|
428
|
+
)}
|
|
429
|
+
</>
|
|
430
|
+
);
|
|
418
431
|
}
|
|
419
432
|
|
|
420
433
|
// Check for HTTP code blocks - render with special styling
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect, useLayoutEffect,
|
|
3
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
4
4
|
import mermaid from 'mermaid';
|
|
5
5
|
import {
|
|
6
6
|
CACHE_KEY_PREFIX,
|
|
@@ -355,11 +355,6 @@ export function MermaidInner({ children, className, minWidth }: MermaidInnerProp
|
|
|
355
355
|
});
|
|
356
356
|
const [error, setError] = useState<string | null>(null);
|
|
357
357
|
|
|
358
|
-
// Reserves space so the skeleton → SVG swap doesn't shift layout below it.
|
|
359
|
-
// Derived from svg (not separate state) so it can't desync. The regex is
|
|
360
|
-
// cheap (small string, no backtracking).
|
|
361
|
-
const minHeightPx = useMemo(() => (svg ? readSvgHeight(svg) : 0), [svg]);
|
|
362
|
-
|
|
363
358
|
useEffect(() => {
|
|
364
359
|
let cancelled = false;
|
|
365
360
|
|
|
@@ -482,8 +477,7 @@ export function MermaidInner({ children, className, minWidth }: MermaidInnerProp
|
|
|
482
477
|
return (
|
|
483
478
|
<div
|
|
484
479
|
ref={containerRef}
|
|
485
|
-
className={`mermaid-container my-6 flex justify-center overflow-x-auto ${className || ''}`}
|
|
486
|
-
style={minHeightPx ? { minHeight: `${minHeightPx}px` } : undefined}
|
|
480
|
+
className={`mermaid-container my-6 flex justify-center items-start overflow-x-auto ${className || ''}`}
|
|
487
481
|
dangerouslySetInnerHTML={{ __html: svg }}
|
|
488
482
|
/>
|
|
489
483
|
);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// builder/build-service/lib/consent-gating.ts
|
|
2
|
+
// Consent gating for analytics/tracking scripts (integrations.cookies) and
|
|
3
|
+
// CMP loader validation (integrations.osano / integrations.termly).
|
|
4
|
+
// Pure module — no DOM, no React — so the server layout and tests share it.
|
|
5
|
+
import type { IntegrationsConfig } from './docs-types';
|
|
6
|
+
|
|
7
|
+
export interface GatedScript {
|
|
8
|
+
/** Stable id — used as DOM marker (data-jd-consent-script) and React key */
|
|
9
|
+
id: string;
|
|
10
|
+
kind: 'inline' | 'src';
|
|
11
|
+
/** Inline JS source (kind: 'inline') */
|
|
12
|
+
code?: string;
|
|
13
|
+
/** External src (kind: 'src') */
|
|
14
|
+
src?: string;
|
|
15
|
+
/** Extra attributes set via setAttribute (kind: 'src'); '' = boolean attr */
|
|
16
|
+
attrs?: Record<string, string>;
|
|
17
|
+
appendTo: 'head' | 'body';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Tracking-ID guards: these values get interpolated into inline JS / URLs, so
|
|
21
|
+
// they must be strictly validated (same posture as the existing Crisp guard).
|
|
22
|
+
// /i is intentional: tolerate lowercase-typed IDs (alphanumerics + hyphen
|
|
23
|
+
// can't break out of the inline JS string regardless of case).
|
|
24
|
+
const GA4_ID_RE = /^G-[A-Z0-9]+$/i;
|
|
25
|
+
const GTM_ID_RE = /^GTM-[A-Z0-9]+$/i;
|
|
26
|
+
const CRISP_ID_RE = /^[a-f0-9-]{36}$/; // mirrors layout-helpers.tsx guard
|
|
27
|
+
|
|
28
|
+
// CMP loader URL pins. Defense-in-depth: schema validates these too, but the
|
|
29
|
+
// renderer must not trust config blobs from R2. Path charset includes '.' so
|
|
30
|
+
// every schema-valid Osano URL also passes here (schema only pins prefix/suffix).
|
|
31
|
+
const OSANO_SRC_RE = /^https:\/\/cmp\.osano\.com\/[A-Za-z0-9._/-]+\/osano\.js$/;
|
|
32
|
+
const TERMLY_SRC_RE =
|
|
33
|
+
/^https:\/\/app\.termly\.io\/resource-blocker\/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(\?autoBlock=(on|off))?$/;
|
|
34
|
+
|
|
35
|
+
export function isConsentGatingEnabled(integrations: IntegrationsConfig | undefined): boolean {
|
|
36
|
+
const c = integrations?.cookies;
|
|
37
|
+
return typeof c?.key === 'string' && c.key.length > 0 &&
|
|
38
|
+
typeof c?.value === 'string' && c.value.length > 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Validated CMP script srcs to render ungated in <head> (Osano first). */
|
|
42
|
+
export function getCmpScriptSrcs(integrations: IntegrationsConfig | undefined): string[] {
|
|
43
|
+
const srcs: string[] = [];
|
|
44
|
+
const osano = integrations?.osano?.scriptSource;
|
|
45
|
+
if (osano && OSANO_SRC_RE.test(osano)) srcs.push(osano);
|
|
46
|
+
const termly = integrations?.termly?.scriptSource;
|
|
47
|
+
if (termly && TERMLY_SRC_RE.test(termly)) srcs.push(termly);
|
|
48
|
+
return srcs;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build serializable descriptors for every analytics/tracking script.
|
|
53
|
+
* Emitted unconditionally — the caller is responsible for the prod gate:
|
|
54
|
+
* layout-helpers renders <ConsentGate> (and thus calls this) only when
|
|
55
|
+
* NODE_ENV === 'production', matching the dev-suppression the direct render
|
|
56
|
+
* paths already apply to Plausible/Crisp/customJs/first-party analytics.
|
|
57
|
+
*/
|
|
58
|
+
export function buildGatedScripts({
|
|
59
|
+
integrations,
|
|
60
|
+
customJs,
|
|
61
|
+
analyticsScript,
|
|
62
|
+
}: {
|
|
63
|
+
integrations: IntegrationsConfig | undefined;
|
|
64
|
+
customJs: string | null;
|
|
65
|
+
analyticsScript: string | null;
|
|
66
|
+
}): GatedScript[] {
|
|
67
|
+
const scripts: GatedScript[] = [];
|
|
68
|
+
const i = integrations ?? {};
|
|
69
|
+
|
|
70
|
+
if (i.plausible?.scriptUrl) {
|
|
71
|
+
// Proxy mode — mirrors PlausibleScript's scriptUrl branch in layout-helpers
|
|
72
|
+
scripts.push({ id: 'plausible-proxy', kind: 'src', src: i.plausible.scriptUrl, attrs: { async: '' }, appendTo: 'head' });
|
|
73
|
+
scripts.push({
|
|
74
|
+
id: 'plausible-proxy-init', kind: 'inline', appendTo: 'head',
|
|
75
|
+
code: 'window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};plausible.init()',
|
|
76
|
+
});
|
|
77
|
+
} else if (i.plausible?.domain) {
|
|
78
|
+
const baseServer = (i.plausible.server || 'https://plausible.io').replace(/\/+$/, '');
|
|
79
|
+
const attrs: Record<string, string> = { defer: '', 'data-domain': i.plausible.domain };
|
|
80
|
+
if (i.plausible.server) attrs['data-api'] = `${baseServer}/api/event`;
|
|
81
|
+
scripts.push({ id: 'plausible', kind: 'src', src: `${baseServer}/js/script.js`, attrs, appendTo: 'head' });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// GTM/GA4 here are raw inline equivalents of the standard snippets. The
|
|
85
|
+
// ungated direct renders use @next/third-parties (ConditionalGTM/ConditionalGA)
|
|
86
|
+
// instead; these descriptors deliberately bypass it because they're injected
|
|
87
|
+
// client-side at consent time, not at SSR. Functionally equivalent for tracking,
|
|
88
|
+
// but they carry NO Next.js CSP nonce and GTM omits the <noscript> iframe
|
|
89
|
+
// fallback (irrelevant — no-JS clients can't trigger the consent gate anyway).
|
|
90
|
+
if (i.gtm?.tagId && GTM_ID_RE.test(i.gtm.tagId)) {
|
|
91
|
+
scripts.push({
|
|
92
|
+
id: 'gtm', kind: 'inline', appendTo: 'head',
|
|
93
|
+
code: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${i.gtm.tagId}');`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (i.ga4?.measurementId && GA4_ID_RE.test(i.ga4.measurementId)) {
|
|
98
|
+
scripts.push({
|
|
99
|
+
id: 'ga4-loader', kind: 'src', appendTo: 'head', attrs: { async: '' },
|
|
100
|
+
src: `https://www.googletagmanager.com/gtag/js?id=${i.ga4.measurementId}`,
|
|
101
|
+
});
|
|
102
|
+
scripts.push({
|
|
103
|
+
id: 'ga4-init', kind: 'inline', appendTo: 'head',
|
|
104
|
+
code: `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','${i.ga4.measurementId}');`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (i.crisp?.websiteId && CRISP_ID_RE.test(i.crisp.websiteId)) {
|
|
109
|
+
scripts.push({
|
|
110
|
+
id: 'crisp', kind: 'inline', appendTo: 'body',
|
|
111
|
+
code: `window.$crisp=[];window.CRISP_WEBSITE_ID="${i.crisp.websiteId}";(function(){var d=document;var s=d.createElement("script");s.src="https://client.crisp.chat/l.js";s.async=1;d.getElementsByTagName("head")[0].appendChild(s)})();`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (customJs) scripts.push({ id: 'custom-js', kind: 'inline', code: customJs, appendTo: 'body' });
|
|
116
|
+
if (analyticsScript) scripts.push({ id: 'jd-analytics', kind: 'inline', code: analyticsScript, appendTo: 'body' });
|
|
117
|
+
|
|
118
|
+
return scripts;
|
|
119
|
+
}
|
|
@@ -644,7 +644,10 @@ export interface NewsletterConfig {
|
|
|
644
644
|
}
|
|
645
645
|
|
|
646
646
|
/**
|
|
647
|
-
* Analytics and integration configurations
|
|
647
|
+
* Analytics and integration configurations.
|
|
648
|
+
* Implemented: ga4, gtm, plausible, crisp, osano, termly, cookies (consent
|
|
649
|
+
* gating), analytics opt-out via AnalyticsConfig. Others remain config-only
|
|
650
|
+
* stubs accepted for Mintlify compatibility.
|
|
648
651
|
*/
|
|
649
652
|
export interface IntegrationsConfig {
|
|
650
653
|
ga4?: { measurementId: string };
|
|
@@ -664,6 +667,7 @@ export interface IntegrationsConfig {
|
|
|
664
667
|
logrocket?: { appId: string };
|
|
665
668
|
mixpanel?: { projectToken: string };
|
|
666
669
|
osano?: { scriptSource: string };
|
|
670
|
+
termly?: { scriptSource: string };
|
|
667
671
|
pirsch?: { id: string };
|
|
668
672
|
posthog?: { apiKey: string; apiHost?: string };
|
|
669
673
|
plausible?: { domain?: string; server?: string; scriptUrl?: string };
|
|
@@ -707,6 +711,7 @@ export interface SeoConfig {
|
|
|
707
711
|
indexing?: 'navigable' | 'all';
|
|
708
712
|
/** Include hidden pages in sitemap (default: false) */
|
|
709
713
|
indexHiddenPages?: boolean;
|
|
714
|
+
ai?: { llmsTxt?: boolean }; // default true; false suppresses llms.txt + llms-full.txt
|
|
710
715
|
}
|
|
711
716
|
|
|
712
717
|
/**
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface FaqPair { question: string; answer: string; }
|
|
2
|
+
|
|
3
|
+
// Nested component subtrees that are NOT part of the FAQ answer prose (CTAs,
|
|
4
|
+
// cards, media, callouts). Drop them WITH their inner text before flattening.
|
|
5
|
+
const NESTED_BLOCK_TAGS = [
|
|
6
|
+
'Card', 'CardGroup', 'Columns', 'Frame', 'Note', 'Warning', 'Tip', 'Info',
|
|
7
|
+
'Steps', 'Step', 'Tabs', 'Tab', 'CodeGroup',
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
function stripMarkdown(s: string): string {
|
|
11
|
+
return s
|
|
12
|
+
.replace(/```[\s\S]*?```/g, ' ') // fenced code
|
|
13
|
+
.replace(/`([^`]+)`/g, '$1') // inline code
|
|
14
|
+
.replace(/!\[[^\]]*\]\([^)]*\)/g, ' ') // images
|
|
15
|
+
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1') // links → text
|
|
16
|
+
.replace(/(\*\*|__)(.*?)\1/g, '$2') // bold
|
|
17
|
+
.replace(/(\*|_)(.*?)\1/g, '$2') // italic
|
|
18
|
+
.replace(/^#{1,6}\s+/gm, '') // ATX headings
|
|
19
|
+
.replace(/^\s*[-*+]\s+/gm, '') // bullet markers
|
|
20
|
+
.replace(/^\s*\d+\.\s+/gm, ''); // ordered markers
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Lightweight extraction of <Accordion title="...">body</Accordion>. Only the
|
|
24
|
+
// QUOTED-title form is matched (the title={expr} form is intentionally skipped —
|
|
25
|
+
// an expression can't be resolved to display text). Answers are reduced to plain
|
|
26
|
+
// text for schema.org: nested component subtrees dropped, markdown stripped.
|
|
27
|
+
//
|
|
28
|
+
// KNOWN LIMITATION (regex, not AST): an <Accordion> nested INSIDE another
|
|
29
|
+
// <Accordion> body truncates the outer answer at the first </Accordion> and
|
|
30
|
+
// merges the inner answer in (the inner question is lost). Harmless for the
|
|
31
|
+
// FAQPage JSON-LD — output stays valid, just an imperfect answer string on the
|
|
32
|
+
// rare nested page. Sibling accordions in an <AccordionGroup> are unaffected.
|
|
33
|
+
// The fix for true nesting is the compiled-MDX AST walk (mdxJsxFlowElement),
|
|
34
|
+
// the documented upgrade path; kept regex-first per the plan's decision.
|
|
35
|
+
export function extractFaqPairs(rawMdx: string): FaqPair[] {
|
|
36
|
+
const pairs: FaqPair[] = [];
|
|
37
|
+
const re = /<Accordion\b[^>]*?\btitle\s*=\s*("|')(.*?)\1[^>]*>([\s\S]*?)<\/Accordion>/g;
|
|
38
|
+
let m: RegExpExecArray | null;
|
|
39
|
+
while ((m = re.exec(rawMdx))) {
|
|
40
|
+
const question = m[2].trim();
|
|
41
|
+
let body = m[3];
|
|
42
|
+
for (const tag of NESTED_BLOCK_TAGS) {
|
|
43
|
+
body = body
|
|
44
|
+
.replace(new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}>`, 'g'), ' ') // paired
|
|
45
|
+
.replace(new RegExp(`<${tag}\\b[^>]*/>`, 'g'), ' '); // self-closing
|
|
46
|
+
}
|
|
47
|
+
const answer = stripMarkdown(body)
|
|
48
|
+
.replace(/<[^>]+>/g, ' ') // any remaining tags
|
|
49
|
+
.replace(/\s+/g, ' ')
|
|
50
|
+
.trim();
|
|
51
|
+
if (question && answer) pairs.push({ question, answer });
|
|
52
|
+
}
|
|
53
|
+
return pairs;
|
|
54
|
+
}
|
package/vendored/lib/json-ld.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { DocsConfig, NavigationPage, GroupConfig } from './docs-types';
|
|
12
12
|
import { normalizeNavPage } from './docs-types';
|
|
13
|
+
import type { FaqPair } from './extract-faq';
|
|
13
14
|
|
|
14
15
|
interface BreadcrumbEntry {
|
|
15
16
|
name: string;
|
|
@@ -21,6 +22,7 @@ export interface JsonLdOptions {
|
|
|
21
22
|
pagePath: string;
|
|
22
23
|
pageTitle: string;
|
|
23
24
|
baseUrl: string;
|
|
25
|
+
faqPairs?: FaqPair[];
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export interface JsonLdGraph {
|
|
@@ -106,7 +108,7 @@ export function findBreadcrumbPath(
|
|
|
106
108
|
// Search all navigation structures (mirrors Breadcrumb.tsx order)
|
|
107
109
|
let trail: BreadcrumbEntry[] | null = null;
|
|
108
110
|
|
|
109
|
-
if (!trail && nav.languages) {
|
|
111
|
+
if (nav && !trail && nav.languages) {
|
|
110
112
|
for (const lang of nav.languages) {
|
|
111
113
|
if (!lang.tabs) continue;
|
|
112
114
|
for (const tab of lang.tabs) {
|
|
@@ -116,19 +118,19 @@ export function findBreadcrumbPath(
|
|
|
116
118
|
if (trail) break;
|
|
117
119
|
}
|
|
118
120
|
}
|
|
119
|
-
if (!trail && nav.anchors) {
|
|
121
|
+
if (nav && !trail && nav.anchors) {
|
|
120
122
|
for (const anchor of nav.anchors) {
|
|
121
123
|
if (anchor.groups) trail = searchGroups(anchor.groups);
|
|
122
124
|
if (trail) break;
|
|
123
125
|
}
|
|
124
126
|
}
|
|
125
|
-
if (!trail && nav.tabs) {
|
|
127
|
+
if (nav && !trail && nav.tabs) {
|
|
126
128
|
for (const tab of nav.tabs) {
|
|
127
129
|
if (tab.groups) trail = searchGroups(tab.groups);
|
|
128
130
|
if (trail) break;
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
|
-
if (!trail && nav.groups) {
|
|
133
|
+
if (nav && !trail && nav.groups) {
|
|
132
134
|
trail = searchGroups(nav.groups);
|
|
133
135
|
}
|
|
134
136
|
|
|
@@ -145,7 +147,7 @@ export function findBreadcrumbPath(
|
|
|
145
147
|
* Returns a schema.org @graph with WebSite + BreadcrumbList.
|
|
146
148
|
*/
|
|
147
149
|
export function buildJsonLd(options: JsonLdOptions): JsonLdGraph {
|
|
148
|
-
const { config, pagePath, pageTitle, baseUrl } = options;
|
|
150
|
+
const { config, pagePath, pageTitle, baseUrl, faqPairs } = options;
|
|
149
151
|
const breadcrumbs = findBreadcrumbPath(config, pagePath, baseUrl, pageTitle);
|
|
150
152
|
|
|
151
153
|
return {
|
|
@@ -166,6 +168,15 @@ export function buildJsonLd(options: JsonLdOptions): JsonLdGraph {
|
|
|
166
168
|
item: item.url,
|
|
167
169
|
})),
|
|
168
170
|
},
|
|
171
|
+
...(faqPairs && faqPairs.length
|
|
172
|
+
? [{
|
|
173
|
+
'@type': 'FAQPage',
|
|
174
|
+
mainEntity: faqPairs.map((p) => ({
|
|
175
|
+
'@type': 'Question', name: p.question,
|
|
176
|
+
acceptedAnswer: { '@type': 'Answer', text: p.answer },
|
|
177
|
+
})),
|
|
178
|
+
}]
|
|
179
|
+
: []),
|
|
169
180
|
],
|
|
170
181
|
};
|
|
171
182
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// or — in non-ISR dev/tests — from URL params; this module owns the
|
|
4
4
|
// rendering once those have resolved.
|
|
5
5
|
import { Inter, JetBrains_Mono } from 'next/font/google';
|
|
6
|
-
import {
|
|
6
|
+
import { preinit } from 'react-dom';
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { ThemeProvider } from '@/components/theme/ThemeProvider';
|
|
@@ -25,6 +25,8 @@ 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 { isConsentGatingEnabled, buildGatedScripts, getCmpScriptSrcs } from '@/lib/consent-gating';
|
|
29
|
+
import { ConsentGate } from '@/components/ConsentGate';
|
|
28
30
|
import { AgentDirective } from './agent-directive';
|
|
29
31
|
import { fetchCustomCss, fetchCustomJs } from '@/lib/r2-content';
|
|
30
32
|
import { toHreflang } from '@/lib/language-utils';
|
|
@@ -429,6 +431,21 @@ export async function DocsChrome({
|
|
|
429
431
|
? getAnalyticsScript(resolvedProjectSlug)
|
|
430
432
|
: null;
|
|
431
433
|
|
|
434
|
+
// integrations.cookies consent gating: when key+value are configured, all
|
|
435
|
+
// analytics/tracking scripts are withheld until the CMP writes
|
|
436
|
+
// localStorage[key] === value (ConsentGate injects them client-side).
|
|
437
|
+
const consentGating = isConsentGatingEnabled(config.integrations);
|
|
438
|
+
// CMP loaders (Osano/Termly) are NEVER gated — they ARE the consent banner.
|
|
439
|
+
const cmpScriptSrcs = process.env.NODE_ENV === 'production'
|
|
440
|
+
? getCmpScriptSrcs(config.integrations)
|
|
441
|
+
: [];
|
|
442
|
+
// Preconnect origins are best-effort: getCmpScriptSrcs only returns
|
|
443
|
+
// regex-validated absolute https URLs (so new URL can't throw today), but
|
|
444
|
+
// guard anyway — a malformed preconnect must never 500 the whole render.
|
|
445
|
+
const cmpPreconnectOrigins = cmpScriptSrcs
|
|
446
|
+
.map((src) => { try { return new URL(src).origin; } catch { return null; } })
|
|
447
|
+
.filter((origin): origin is string => origin !== null);
|
|
448
|
+
|
|
432
449
|
// Global banner: only emit the pre-paint no-flash guard for a dismissible
|
|
433
450
|
// banner (a non-dismissible banner is never hidden, so the guard would risk
|
|
434
451
|
// hiding a mandatory message via a stale dismissal hash). Trimmed content
|
|
@@ -437,26 +454,34 @@ export async function DocsChrome({
|
|
|
437
454
|
const bannerNoFlash = !embed && !!bannerContent && config.banner?.dismissible === true && !!resolvedProjectSlug;
|
|
438
455
|
|
|
439
456
|
// Font Awesome CSS uses preinit (not preload) so React 19 emits the
|
|
440
|
-
// stylesheet link in <head> at SSR time.
|
|
441
|
-
// beforeInteractive Script
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
// are matched to their @font-face declarations as soon as the stylesheet
|
|
446
|
-
// parses, which now starts at HTML-parse time alongside the preloads.
|
|
457
|
+
// stylesheet link in <head> at SSR time. preinit (rather than a runtime
|
|
458
|
+
// beforeInteractive <Script>/<link> injection) is what gets the @font-face
|
|
459
|
+
// rules parsed early: the previous Script that ran document.createElement at
|
|
460
|
+
// parse time delayed @font-face parsing, so the actual woff2 fetch started
|
|
461
|
+
// late. preinit hoists the stylesheet at HTML-parse time instead.
|
|
447
462
|
//
|
|
448
|
-
// The
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
//
|
|
463
|
+
// The individual woff2 weights are deliberately NOT preloaded — they load on
|
|
464
|
+
// demand when the stylesheet's @font-face rules are matched to the icons a
|
|
465
|
+
// page actually uses. The default docs icon weight is solid, so the previous
|
|
466
|
+
// unconditional fa-light-300 / fa-brands-400 preloads were almost never used
|
|
467
|
+
// and Chrome flagged them "preloaded but not used within a few seconds."
|
|
468
|
+
// The <noscript> fallback below uses dangerouslySetInnerHTML because a JSX
|
|
469
|
+
// <link rel=stylesheet> inside noscript was being hoisted into <head> as a
|
|
470
|
+
// real stylesheet by React 19.
|
|
453
471
|
preinit(FA_CSS_HREF, { as: 'style' });
|
|
454
|
-
preload('/_jd/fonts/fontawesome/webfonts/fa-light-300.woff2', { as: 'font', type: 'font/woff2', crossOrigin: 'anonymous' });
|
|
455
|
-
preload('/_jd/fonts/fontawesome/webfonts/fa-brands-400.woff2', { as: 'font', type: 'font/woff2', crossOrigin: 'anonymous' });
|
|
456
472
|
|
|
457
473
|
return (
|
|
458
474
|
<html lang={toHreflang(lang)} dir={dir} suppressHydrationWarning data-scroll-behavior="smooth" data-scroll-locked="true">
|
|
459
475
|
<head>
|
|
476
|
+
{/* CMP scripts must be first + synchronous: auto-blocking CMPs
|
|
477
|
+
(Osano/Termly) need to intercept later scripts. Never gated.
|
|
478
|
+
preconnect first: the sync fetch is render-blocking. */}
|
|
479
|
+
{cmpPreconnectOrigins.map((origin) => (
|
|
480
|
+
<link key={`pc-${origin}`} rel="preconnect" href={origin} />
|
|
481
|
+
))}
|
|
482
|
+
{cmpScriptSrcs.map((src) => (
|
|
483
|
+
<script key={src} src={src} />
|
|
484
|
+
))}
|
|
460
485
|
{/*
|
|
461
486
|
SSR scroll lock — prevents Chrome's same-tab cross-origin "preserve
|
|
462
487
|
scroll" heuristic from scrolling #content-scroll-container on first
|
|
@@ -674,7 +699,7 @@ export async function DocsChrome({
|
|
|
674
699
|
{customCss}
|
|
675
700
|
</style>
|
|
676
701
|
)}
|
|
677
|
-
{process.env.NODE_ENV === 'production' && (config.integrations?.plausible?.domain || config.integrations?.plausible?.scriptUrl) && (
|
|
702
|
+
{!consentGating && process.env.NODE_ENV === 'production' && (config.integrations?.plausible?.domain || config.integrations?.plausible?.scriptUrl) && (
|
|
678
703
|
<PlausibleScript
|
|
679
704
|
domain={config.integrations.plausible.domain}
|
|
680
705
|
server={config.integrations.plausible.server}
|
|
@@ -684,7 +709,7 @@ export async function DocsChrome({
|
|
|
684
709
|
</head>
|
|
685
710
|
<body className={fontClassName} data-theme={themeName || 'jam'} data-decoration={decoration || undefined} suppressHydrationWarning>
|
|
686
711
|
{!embed && <AgentDirective hostAtDocs={!!config.hostAtDocs} />}
|
|
687
|
-
{config.integrations?.gtm?.tagId && (
|
|
712
|
+
{!consentGating && config.integrations?.gtm?.tagId && (
|
|
688
713
|
<ConditionalGTM gtmId={config.integrations.gtm.tagId} />
|
|
689
714
|
)}
|
|
690
715
|
<ThemeProvider
|
|
@@ -712,7 +737,7 @@ export async function DocsChrome({
|
|
|
712
737
|
client" warning in `jamdesk dev`. Same pattern as the
|
|
713
738
|
project-subdomain hydration suppressor in <head>.
|
|
714
739
|
*/}
|
|
715
|
-
{process.env.NODE_ENV === 'production' && config.integrations?.crisp?.websiteId &&
|
|
740
|
+
{!consentGating && process.env.NODE_ENV === 'production' && config.integrations?.crisp?.websiteId &&
|
|
716
741
|
/^[a-f0-9-]{36}$/.test(config.integrations.crisp.websiteId) && (
|
|
717
742
|
<script
|
|
718
743
|
dangerouslySetInnerHTML={{
|
|
@@ -720,15 +745,26 @@ export async function DocsChrome({
|
|
|
720
745
|
}}
|
|
721
746
|
/>
|
|
722
747
|
)}
|
|
723
|
-
{process.env.NODE_ENV === 'production' && customJs && (
|
|
748
|
+
{!consentGating && process.env.NODE_ENV === 'production' && customJs && (
|
|
724
749
|
<script dangerouslySetInnerHTML={{ __html: customJs }} />
|
|
725
750
|
)}
|
|
726
|
-
{process.env.NODE_ENV === 'production' && analyticsScript && (
|
|
751
|
+
{!consentGating && process.env.NODE_ENV === 'production' && analyticsScript && (
|
|
727
752
|
<script dangerouslySetInnerHTML={{ __html: analyticsScript }} />
|
|
728
753
|
)}
|
|
729
|
-
{config.integrations?.ga4?.measurementId && (
|
|
754
|
+
{!consentGating && config.integrations?.ga4?.measurementId && (
|
|
730
755
|
<ConditionalGA gaId={config.integrations.ga4.measurementId} />
|
|
731
756
|
)}
|
|
757
|
+
{consentGating && process.env.NODE_ENV === 'production' && (
|
|
758
|
+
<ConsentGate
|
|
759
|
+
consentKey={config.integrations!.cookies!.key!}
|
|
760
|
+
consentValue={config.integrations!.cookies!.value!}
|
|
761
|
+
scripts={buildGatedScripts({
|
|
762
|
+
integrations: config.integrations,
|
|
763
|
+
customJs,
|
|
764
|
+
analyticsScript,
|
|
765
|
+
})}
|
|
766
|
+
/>
|
|
767
|
+
)}
|
|
732
768
|
<JdReadySentinel />
|
|
733
769
|
</body>
|
|
734
770
|
</html>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { BuildWarning } from '../shared/status-reporter.js'; // vendored copy — NOT '../../shared'
|
|
2
|
+
import type { Logo } from './docs-types.js';
|
|
3
|
+
|
|
4
|
+
const MAX_LOGO_BYTES = 100 * 1024;
|
|
5
|
+
const MAX_REASONABLE_WIDTH = 720; // 2x the LOGO_MAX_WIDTH target
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Collect the distinct image refs from a logo config (string or {light,dark}),
|
|
9
|
+
* in light-then-dark order with empties dropped.
|
|
10
|
+
*
|
|
11
|
+
* Used to SNAPSHOT the originals before the convertToWebp pass can rewrite them:
|
|
12
|
+
* an object {light,dark} logo shares its nested object across the enhancedConfig
|
|
13
|
+
* shallow spread, so a later in-place ".webp" rewrite mutates docsConfig.logo too —
|
|
14
|
+
* destroying the ".png"/".jpg" refs the always-on downscale needs. Reading from a
|
|
15
|
+
* pre-convert snapshot makes the downscale work for object logos, not just strings.
|
|
16
|
+
*/
|
|
17
|
+
export function collectLogoRefs(logo: Logo | undefined): string[] {
|
|
18
|
+
const refs: string[] = [];
|
|
19
|
+
if (typeof logo === 'string') {
|
|
20
|
+
refs.push(logo);
|
|
21
|
+
} else if (logo && typeof logo === 'object') {
|
|
22
|
+
if (logo.light) refs.push(logo.light);
|
|
23
|
+
if (logo.dark) refs.push(logo.dark);
|
|
24
|
+
}
|
|
25
|
+
return [...new Set(refs.filter(Boolean))];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildLogoWarning(logo: { path: string; width: number; height: number; bytes: number }): BuildWarning | null {
|
|
29
|
+
if (logo.bytes <= MAX_LOGO_BYTES && logo.width <= MAX_REASONABLE_WIDTH) return null;
|
|
30
|
+
return {
|
|
31
|
+
type: 'logo_oversized',
|
|
32
|
+
file: logo.path,
|
|
33
|
+
message: `Logo is ${logo.width}×${logo.height} (${Math.round(logo.bytes / 1024)} KB). It's auto-downscaled, but uploading a logo ≤ ${MAX_REASONABLE_WIDTH}px wide / ≤ 100 KB avoids shipping a large source.`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// NOTE: lib/*.ts import the VENDORED shared copy at build-service/shared via
|
|
2
|
+
// '../shared/...js' (see other lib files). '../../shared' would resolve outside the
|
|
3
|
+
// build-service package and break tsc + the prod build.
|
|
4
|
+
import type { BuildWarning } from '../shared/status-reporter.js';
|
|
5
|
+
|
|
6
|
+
export function buildNoindexWarning(noindex: boolean): BuildWarning | null {
|
|
7
|
+
if (!noindex) return null;
|
|
8
|
+
return {
|
|
9
|
+
type: 'site_noindex',
|
|
10
|
+
file: 'docs.json',
|
|
11
|
+
message:
|
|
12
|
+
'This site is set to noindex: robots.txt is "Disallow: /" and the sitemap is empty, so it will not be indexed by any search engines. AI crawlers are still served via llms.txt (set seo.ai.llmsTxt: false to disable). Remove "noindex" from seo.metatags.robots to allow search indexing.',
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -442,6 +442,32 @@ export async function sweepStaleLocaleLlmsTxt(
|
|
|
442
442
|
return staleKeys;
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Delete a project's AI-context files: the root `llms.txt` + `llms-full.txt` and
|
|
447
|
+
* every per-locale `<locale>/llms.txt`. Called on every build where `seo.ai.llmsTxt`
|
|
448
|
+
* is false so a previously published llms.txt stops serving after a customer opts
|
|
449
|
+
* out — the build gate stops REGENERATING these files but otherwise leaves the
|
|
450
|
+
* already-uploaded copies in place. The deletes are idempotent (DeleteObject is a
|
|
451
|
+
* no-op-success on a missing key), so re-running on later disabled-llms builds is
|
|
452
|
+
* harmless and self-healing. Per-locale files are discovered and deleted by
|
|
453
|
+
* sweepStaleLocaleLlmsTxt with an EMPTY active-locale set (every known-language
|
|
454
|
+
* locale then counts as removable). Note the returned array lists the keys
|
|
455
|
+
* TARGETED, not necessarily ones that existed. Non-fatal by contract: callers treat
|
|
456
|
+
* a throw as a warning — a stale llms.txt is a cosmetic orphan, never build-failing.
|
|
457
|
+
*/
|
|
458
|
+
export async function deleteLlmsTxtArtifacts(projectSlug: string): Promise<string[]> {
|
|
459
|
+
const localeKeys = await sweepStaleLocaleLlmsTxt(projectSlug, new Set());
|
|
460
|
+
const client = getR2S3Client();
|
|
461
|
+
const { bucketName } = getR2Config();
|
|
462
|
+
const rootKeys = [`${projectSlug}/llms.txt`, `${projectSlug}/llms-full.txt`];
|
|
463
|
+
// Independent keys — delete concurrently (matches deleteRemovedR2Objects' Promise.all).
|
|
464
|
+
// A rejection propagates so the caller's non-fatal try/catch logs it; see doc above.
|
|
465
|
+
await Promise.all(
|
|
466
|
+
rootKeys.map((key) => client.send(new DeleteObjectCommand({ Bucket: bucketName, Key: key }))),
|
|
467
|
+
);
|
|
468
|
+
return [...localeKeys, ...rootKeys];
|
|
469
|
+
}
|
|
470
|
+
|
|
445
471
|
/**
|
|
446
472
|
* Delete EVERY R2 object under the `{slug}/` prefix.
|
|
447
473
|
*
|
|
@@ -63,6 +63,7 @@ import { getContentDir } from '@/lib/docs';
|
|
|
63
63
|
import type { DocsConfig } from '@/lib/docs-types';
|
|
64
64
|
import { buildSeoMetadata, generateAutoDescription, buildSiteTitle } from '@/lib/seo';
|
|
65
65
|
import { buildJsonLd } from '@/lib/json-ld';
|
|
66
|
+
import { extractFaqPairs } from '@/lib/extract-faq';
|
|
66
67
|
import {
|
|
67
68
|
getContentLoader,
|
|
68
69
|
isIsrMode,
|
|
@@ -337,11 +338,13 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
|
|
|
337
338
|
const rawContent = parsed.content;
|
|
338
339
|
|
|
339
340
|
const baseUrl = resolveBaseUrl(requestHeaders, projectSlug, hostAtDocs);
|
|
341
|
+
const faqPairs = extractFaqPairs(rawContent);
|
|
340
342
|
const jsonLd = buildJsonLd({
|
|
341
343
|
config,
|
|
342
344
|
pagePath,
|
|
343
345
|
pageTitle: data.title || pagePath,
|
|
344
346
|
baseUrl,
|
|
347
|
+
faqPairs,
|
|
345
348
|
});
|
|
346
349
|
const jsonLdScript = renderJsonLdScript(jsonLd);
|
|
347
350
|
|
package/vendored/lib/seo.ts
CHANGED
|
@@ -615,6 +615,18 @@ function derivePageLocale(pagePath: string, languages?: LanguageConfig[]): strin
|
|
|
615
615
|
* the full hreflang cluster, so language discovery is preserved.
|
|
616
616
|
* @returns Partial<Metadata> to spread into generateMetadata return
|
|
617
617
|
*/
|
|
618
|
+
|
|
619
|
+
// Next.js Metadata.robots can be a string OR an object. When we must ALSO emit
|
|
620
|
+
// googlebot (object-only), a raw string robots directive would be lost — convert
|
|
621
|
+
// it to object form first so the general directive survives the merge.
|
|
622
|
+
function parseRobotsString(robots: string): { index: boolean; follow: boolean } {
|
|
623
|
+
const tokens = robots.toLowerCase().split(',').map((t) => t.trim());
|
|
624
|
+
return {
|
|
625
|
+
index: !tokens.includes('noindex'),
|
|
626
|
+
follow: !tokens.includes('nofollow'),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
618
630
|
export function buildSeoMetadata(
|
|
619
631
|
config: DocsConfig,
|
|
620
632
|
frontmatter: PageFrontmatter,
|
|
@@ -653,12 +665,14 @@ export function buildSeoMetadata(
|
|
|
653
665
|
|
|
654
666
|
// 3. Googlebot (separate from robots)
|
|
655
667
|
if (metatags.googlebot) {
|
|
656
|
-
// Merge googlebot with existing robots config
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
668
|
+
// Merge googlebot with existing robots config. When metadata.robots is a string
|
|
669
|
+
// (set above from metatags.robots), coerce it to object form first so the general
|
|
670
|
+
// directive (e.g. noindex) is not silently dropped by the spread.
|
|
671
|
+
const base =
|
|
672
|
+
typeof metadata.robots === 'string'
|
|
673
|
+
? parseRobotsString(metadata.robots)
|
|
674
|
+
: (metadata.robots ?? {});
|
|
675
|
+
metadata.robots = { ...base, googleBot: metatags.googlebot };
|
|
662
676
|
}
|
|
663
677
|
|
|
664
678
|
// 4. Google site verification
|
|
@@ -36,6 +36,9 @@ export interface IntegrationsConfig {
|
|
|
36
36
|
ga4?: { measurementId: string };
|
|
37
37
|
gtm?: { tagId: string };
|
|
38
38
|
plausible?: { domain?: string; server?: string; scriptUrl?: string };
|
|
39
|
+
osano?: { scriptSource: string };
|
|
40
|
+
termly?: { scriptSource: string };
|
|
41
|
+
cookies?: { key?: string; value?: string };
|
|
39
42
|
[key: string]: unknown;
|
|
40
43
|
}
|
|
41
44
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { BuildWarning } from '../shared/status-reporter.js'; // vendored copy — NOT '../../shared'
|
|
2
|
+
|
|
3
|
+
const MIN_DESCRIPTION_CHARS = 110; // matches the Ahrefs "too short" floor used elsewhere
|
|
4
|
+
|
|
5
|
+
interface PageInfo {
|
|
6
|
+
path: string;
|
|
7
|
+
frontmatter: { title?: unknown; description?: unknown; hidden?: unknown };
|
|
8
|
+
content: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function validatePageFrontmatter(pages: PageInfo[]): BuildWarning[] {
|
|
12
|
+
const warnings: BuildWarning[] = [];
|
|
13
|
+
for (const page of pages) {
|
|
14
|
+
const fm = page.frontmatter ?? {};
|
|
15
|
+
// Hidden pages are intentionally unlisted (not indexed, not in nav) — don't nag.
|
|
16
|
+
if (fm.hidden === true) continue;
|
|
17
|
+
const title = typeof fm.title === 'string' ? fm.title.trim() : '';
|
|
18
|
+
const description = typeof fm.description === 'string' ? fm.description.trim() : '';
|
|
19
|
+
|
|
20
|
+
if (!title) {
|
|
21
|
+
warnings.push({ type: 'page_missing_title', file: page.path, message: 'Page has no frontmatter title; navigation and llms.txt fall back to the file path.' });
|
|
22
|
+
}
|
|
23
|
+
if (!description) {
|
|
24
|
+
warnings.push({ type: 'page_missing_description', file: page.path, message: 'Page has no frontmatter description; add one for SEO and llms.txt.' });
|
|
25
|
+
} else if (description.length < MIN_DESCRIPTION_CHARS) {
|
|
26
|
+
warnings.push({ type: 'page_short_description', file: page.path, message: `Description is ${description.length} chars; aim for ${MIN_DESCRIPTION_CHARS}–160.` });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return warnings;
|
|
30
|
+
}
|
|
@@ -598,6 +598,16 @@
|
|
|
598
598
|
"indexHiddenPages": {
|
|
599
599
|
"type": "boolean",
|
|
600
600
|
"description": "Include hidden pages in the sitemap for search engine indexing. Defaults to `false`"
|
|
601
|
+
},
|
|
602
|
+
"ai": {
|
|
603
|
+
"type": "object",
|
|
604
|
+
"additionalProperties": false,
|
|
605
|
+
"properties": {
|
|
606
|
+
"llmsTxt": {
|
|
607
|
+
"type": "boolean",
|
|
608
|
+
"description": "Generate llms.txt and llms-full.txt for AI crawlers. Default true. Set false to stop publishing AI-context files (independent of search noindex)."
|
|
609
|
+
}
|
|
610
|
+
}
|
|
601
611
|
}
|
|
602
612
|
},
|
|
603
613
|
"additionalProperties": false,
|
|
@@ -1258,6 +1268,19 @@
|
|
|
1258
1268
|
],
|
|
1259
1269
|
"additionalProperties": false
|
|
1260
1270
|
},
|
|
1271
|
+
"termly": {
|
|
1272
|
+
"type": "object",
|
|
1273
|
+
"properties": {
|
|
1274
|
+
"scriptSource": {
|
|
1275
|
+
"type": "string",
|
|
1276
|
+
"pattern": "^https://app\\.termly\\.io/resource-blocker/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(\\?autoBlock=(on|off))?$"
|
|
1277
|
+
}
|
|
1278
|
+
},
|
|
1279
|
+
"required": [
|
|
1280
|
+
"scriptSource"
|
|
1281
|
+
],
|
|
1282
|
+
"additionalProperties": false
|
|
1283
|
+
},
|
|
1261
1284
|
"pirsch": {
|
|
1262
1285
|
"type": "object",
|
|
1263
1286
|
"properties": {
|
|
@@ -1345,12 +1368,18 @@
|
|
|
1345
1368
|
"type": "object",
|
|
1346
1369
|
"properties": {
|
|
1347
1370
|
"key": {
|
|
1348
|
-
"type": "string"
|
|
1371
|
+
"type": "string",
|
|
1372
|
+
"minLength": 1
|
|
1349
1373
|
},
|
|
1350
1374
|
"value": {
|
|
1351
|
-
"type": "string"
|
|
1375
|
+
"type": "string",
|
|
1376
|
+
"minLength": 1
|
|
1352
1377
|
}
|
|
1353
1378
|
},
|
|
1379
|
+
"required": [
|
|
1380
|
+
"key",
|
|
1381
|
+
"value"
|
|
1382
|
+
],
|
|
1354
1383
|
"additionalProperties": false
|
|
1355
1384
|
}
|
|
1356
1385
|
},
|
|
@@ -22,7 +22,29 @@ export interface ProgressUpdate {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** Warning types that can occur during builds (non-blocking) */
|
|
25
|
-
export
|
|
25
|
+
export const BUILD_WARNING_TYPES = [
|
|
26
|
+
'broken_link',
|
|
27
|
+
'auto_migrate',
|
|
28
|
+
'missing_asset',
|
|
29
|
+
'missing_image',
|
|
30
|
+
'missing_page',
|
|
31
|
+
'missing_openapi_ref',
|
|
32
|
+
'inline_code_on_api_page',
|
|
33
|
+
'invalid_openapi_spec',
|
|
34
|
+
'missing_snippet',
|
|
35
|
+
'risky_expression',
|
|
36
|
+
'missing_favicon',
|
|
37
|
+
'missing_description',
|
|
38
|
+
'missing_logo',
|
|
39
|
+
// New (2026-06-16 docs-platform-review fixes):
|
|
40
|
+
'site_noindex',
|
|
41
|
+
'page_missing_title',
|
|
42
|
+
'page_missing_description',
|
|
43
|
+
'page_short_description',
|
|
44
|
+
'logo_oversized',
|
|
45
|
+
] as const;
|
|
46
|
+
|
|
47
|
+
export type BuildWarningType = (typeof BUILD_WARNING_TYPES)[number];
|
|
26
48
|
|
|
27
49
|
/** Build warning structure */
|
|
28
50
|
export interface BuildWarning {
|
|
@@ -55,6 +77,10 @@ export const SUGGESTION_TYPES: ReadonlySet<BuildWarningType> = new Set([
|
|
|
55
77
|
'missing_favicon',
|
|
56
78
|
'missing_description',
|
|
57
79
|
'missing_logo',
|
|
80
|
+
'site_noindex',
|
|
81
|
+
'page_missing_description',
|
|
82
|
+
'page_short_description',
|
|
83
|
+
'logo_oversized',
|
|
58
84
|
]);
|
|
59
85
|
|
|
60
86
|
/**
|
|
@@ -373,6 +373,9 @@
|
|
|
373
373
|
"cpu": [
|
|
374
374
|
"arm"
|
|
375
375
|
],
|
|
376
|
+
"libc": [
|
|
377
|
+
"glibc"
|
|
378
|
+
],
|
|
376
379
|
"license": "LGPL-3.0-or-later",
|
|
377
380
|
"optional": true,
|
|
378
381
|
"os": [
|
|
@@ -389,6 +392,9 @@
|
|
|
389
392
|
"cpu": [
|
|
390
393
|
"arm64"
|
|
391
394
|
],
|
|
395
|
+
"libc": [
|
|
396
|
+
"glibc"
|
|
397
|
+
],
|
|
392
398
|
"license": "LGPL-3.0-or-later",
|
|
393
399
|
"optional": true,
|
|
394
400
|
"os": [
|
|
@@ -405,6 +411,9 @@
|
|
|
405
411
|
"cpu": [
|
|
406
412
|
"ppc64"
|
|
407
413
|
],
|
|
414
|
+
"libc": [
|
|
415
|
+
"glibc"
|
|
416
|
+
],
|
|
408
417
|
"license": "LGPL-3.0-or-later",
|
|
409
418
|
"optional": true,
|
|
410
419
|
"os": [
|
|
@@ -421,6 +430,9 @@
|
|
|
421
430
|
"cpu": [
|
|
422
431
|
"riscv64"
|
|
423
432
|
],
|
|
433
|
+
"libc": [
|
|
434
|
+
"glibc"
|
|
435
|
+
],
|
|
424
436
|
"license": "LGPL-3.0-or-later",
|
|
425
437
|
"optional": true,
|
|
426
438
|
"os": [
|
|
@@ -437,6 +449,9 @@
|
|
|
437
449
|
"cpu": [
|
|
438
450
|
"s390x"
|
|
439
451
|
],
|
|
452
|
+
"libc": [
|
|
453
|
+
"glibc"
|
|
454
|
+
],
|
|
440
455
|
"license": "LGPL-3.0-or-later",
|
|
441
456
|
"optional": true,
|
|
442
457
|
"os": [
|
|
@@ -453,6 +468,9 @@
|
|
|
453
468
|
"cpu": [
|
|
454
469
|
"x64"
|
|
455
470
|
],
|
|
471
|
+
"libc": [
|
|
472
|
+
"glibc"
|
|
473
|
+
],
|
|
456
474
|
"license": "LGPL-3.0-or-later",
|
|
457
475
|
"optional": true,
|
|
458
476
|
"os": [
|
|
@@ -469,6 +487,9 @@
|
|
|
469
487
|
"cpu": [
|
|
470
488
|
"arm64"
|
|
471
489
|
],
|
|
490
|
+
"libc": [
|
|
491
|
+
"musl"
|
|
492
|
+
],
|
|
472
493
|
"license": "LGPL-3.0-or-later",
|
|
473
494
|
"optional": true,
|
|
474
495
|
"os": [
|
|
@@ -485,6 +506,9 @@
|
|
|
485
506
|
"cpu": [
|
|
486
507
|
"x64"
|
|
487
508
|
],
|
|
509
|
+
"libc": [
|
|
510
|
+
"musl"
|
|
511
|
+
],
|
|
488
512
|
"license": "LGPL-3.0-or-later",
|
|
489
513
|
"optional": true,
|
|
490
514
|
"os": [
|
|
@@ -501,6 +525,9 @@
|
|
|
501
525
|
"cpu": [
|
|
502
526
|
"arm"
|
|
503
527
|
],
|
|
528
|
+
"libc": [
|
|
529
|
+
"glibc"
|
|
530
|
+
],
|
|
504
531
|
"license": "Apache-2.0",
|
|
505
532
|
"optional": true,
|
|
506
533
|
"os": [
|
|
@@ -523,6 +550,9 @@
|
|
|
523
550
|
"cpu": [
|
|
524
551
|
"arm64"
|
|
525
552
|
],
|
|
553
|
+
"libc": [
|
|
554
|
+
"glibc"
|
|
555
|
+
],
|
|
526
556
|
"license": "Apache-2.0",
|
|
527
557
|
"optional": true,
|
|
528
558
|
"os": [
|
|
@@ -545,6 +575,9 @@
|
|
|
545
575
|
"cpu": [
|
|
546
576
|
"ppc64"
|
|
547
577
|
],
|
|
578
|
+
"libc": [
|
|
579
|
+
"glibc"
|
|
580
|
+
],
|
|
548
581
|
"license": "Apache-2.0",
|
|
549
582
|
"optional": true,
|
|
550
583
|
"os": [
|
|
@@ -567,6 +600,9 @@
|
|
|
567
600
|
"cpu": [
|
|
568
601
|
"riscv64"
|
|
569
602
|
],
|
|
603
|
+
"libc": [
|
|
604
|
+
"glibc"
|
|
605
|
+
],
|
|
570
606
|
"license": "Apache-2.0",
|
|
571
607
|
"optional": true,
|
|
572
608
|
"os": [
|
|
@@ -589,6 +625,9 @@
|
|
|
589
625
|
"cpu": [
|
|
590
626
|
"s390x"
|
|
591
627
|
],
|
|
628
|
+
"libc": [
|
|
629
|
+
"glibc"
|
|
630
|
+
],
|
|
592
631
|
"license": "Apache-2.0",
|
|
593
632
|
"optional": true,
|
|
594
633
|
"os": [
|
|
@@ -611,6 +650,9 @@
|
|
|
611
650
|
"cpu": [
|
|
612
651
|
"x64"
|
|
613
652
|
],
|
|
653
|
+
"libc": [
|
|
654
|
+
"glibc"
|
|
655
|
+
],
|
|
614
656
|
"license": "Apache-2.0",
|
|
615
657
|
"optional": true,
|
|
616
658
|
"os": [
|
|
@@ -633,6 +675,9 @@
|
|
|
633
675
|
"cpu": [
|
|
634
676
|
"arm64"
|
|
635
677
|
],
|
|
678
|
+
"libc": [
|
|
679
|
+
"musl"
|
|
680
|
+
],
|
|
636
681
|
"license": "Apache-2.0",
|
|
637
682
|
"optional": true,
|
|
638
683
|
"os": [
|
|
@@ -655,6 +700,9 @@
|
|
|
655
700
|
"cpu": [
|
|
656
701
|
"x64"
|
|
657
702
|
],
|
|
703
|
+
"libc": [
|
|
704
|
+
"musl"
|
|
705
|
+
],
|
|
658
706
|
"license": "Apache-2.0",
|
|
659
707
|
"optional": true,
|
|
660
708
|
"os": [
|
|
@@ -929,6 +977,9 @@
|
|
|
929
977
|
"cpu": [
|
|
930
978
|
"arm64"
|
|
931
979
|
],
|
|
980
|
+
"libc": [
|
|
981
|
+
"glibc"
|
|
982
|
+
],
|
|
932
983
|
"license": "MIT",
|
|
933
984
|
"optional": true,
|
|
934
985
|
"os": [
|
|
@@ -945,6 +996,9 @@
|
|
|
945
996
|
"cpu": [
|
|
946
997
|
"arm64"
|
|
947
998
|
],
|
|
999
|
+
"libc": [
|
|
1000
|
+
"musl"
|
|
1001
|
+
],
|
|
948
1002
|
"license": "MIT",
|
|
949
1003
|
"optional": true,
|
|
950
1004
|
"os": [
|
|
@@ -961,6 +1015,9 @@
|
|
|
961
1015
|
"cpu": [
|
|
962
1016
|
"x64"
|
|
963
1017
|
],
|
|
1018
|
+
"libc": [
|
|
1019
|
+
"glibc"
|
|
1020
|
+
],
|
|
964
1021
|
"license": "MIT",
|
|
965
1022
|
"optional": true,
|
|
966
1023
|
"os": [
|
|
@@ -977,6 +1034,9 @@
|
|
|
977
1034
|
"cpu": [
|
|
978
1035
|
"x64"
|
|
979
1036
|
],
|
|
1037
|
+
"libc": [
|
|
1038
|
+
"musl"
|
|
1039
|
+
],
|
|
980
1040
|
"license": "MIT",
|
|
981
1041
|
"optional": true,
|
|
982
1042
|
"os": [
|
|
@@ -1316,6 +1376,9 @@
|
|
|
1316
1376
|
"cpu": [
|
|
1317
1377
|
"arm64"
|
|
1318
1378
|
],
|
|
1379
|
+
"libc": [
|
|
1380
|
+
"glibc"
|
|
1381
|
+
],
|
|
1319
1382
|
"license": "MIT",
|
|
1320
1383
|
"optional": true,
|
|
1321
1384
|
"os": [
|
|
@@ -1332,6 +1395,9 @@
|
|
|
1332
1395
|
"cpu": [
|
|
1333
1396
|
"arm64"
|
|
1334
1397
|
],
|
|
1398
|
+
"libc": [
|
|
1399
|
+
"musl"
|
|
1400
|
+
],
|
|
1335
1401
|
"license": "MIT",
|
|
1336
1402
|
"optional": true,
|
|
1337
1403
|
"os": [
|
|
@@ -1348,6 +1414,9 @@
|
|
|
1348
1414
|
"cpu": [
|
|
1349
1415
|
"x64"
|
|
1350
1416
|
],
|
|
1417
|
+
"libc": [
|
|
1418
|
+
"glibc"
|
|
1419
|
+
],
|
|
1351
1420
|
"license": "MIT",
|
|
1352
1421
|
"optional": true,
|
|
1353
1422
|
"os": [
|
|
@@ -1364,6 +1433,9 @@
|
|
|
1364
1433
|
"cpu": [
|
|
1365
1434
|
"x64"
|
|
1366
1435
|
],
|
|
1436
|
+
"libc": [
|
|
1437
|
+
"musl"
|
|
1438
|
+
],
|
|
1367
1439
|
"license": "MIT",
|
|
1368
1440
|
"optional": true,
|
|
1369
1441
|
"os": [
|
|
@@ -2852,9 +2924,9 @@
|
|
|
2852
2924
|
}
|
|
2853
2925
|
},
|
|
2854
2926
|
"node_modules/dompurify": {
|
|
2855
|
-
"version": "3.4.
|
|
2856
|
-
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.
|
|
2857
|
-
"integrity": "sha512-
|
|
2927
|
+
"version": "3.4.11",
|
|
2928
|
+
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz",
|
|
2929
|
+
"integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==",
|
|
2858
2930
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
|
2859
2931
|
"optionalDependencies": {
|
|
2860
2932
|
"@types/trusted-types": "^2.0.7"
|
|
@@ -2867,9 +2939,9 @@
|
|
|
2867
2939
|
"license": "MIT"
|
|
2868
2940
|
},
|
|
2869
2941
|
"node_modules/electron-to-chromium": {
|
|
2870
|
-
"version": "1.5.
|
|
2871
|
-
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.
|
|
2872
|
-
"integrity": "sha512-
|
|
2942
|
+
"version": "1.5.375",
|
|
2943
|
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.375.tgz",
|
|
2944
|
+
"integrity": "sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q==",
|
|
2873
2945
|
"license": "ISC"
|
|
2874
2946
|
},
|
|
2875
2947
|
"node_modules/enhanced-resolve": {
|
|
@@ -3865,6 +3937,9 @@
|
|
|
3865
3937
|
"cpu": [
|
|
3866
3938
|
"arm64"
|
|
3867
3939
|
],
|
|
3940
|
+
"libc": [
|
|
3941
|
+
"glibc"
|
|
3942
|
+
],
|
|
3868
3943
|
"license": "MPL-2.0",
|
|
3869
3944
|
"optional": true,
|
|
3870
3945
|
"os": [
|
|
@@ -3885,6 +3960,9 @@
|
|
|
3885
3960
|
"cpu": [
|
|
3886
3961
|
"arm64"
|
|
3887
3962
|
],
|
|
3963
|
+
"libc": [
|
|
3964
|
+
"musl"
|
|
3965
|
+
],
|
|
3888
3966
|
"license": "MPL-2.0",
|
|
3889
3967
|
"optional": true,
|
|
3890
3968
|
"os": [
|
|
@@ -3905,6 +3983,9 @@
|
|
|
3905
3983
|
"cpu": [
|
|
3906
3984
|
"x64"
|
|
3907
3985
|
],
|
|
3986
|
+
"libc": [
|
|
3987
|
+
"glibc"
|
|
3988
|
+
],
|
|
3908
3989
|
"license": "MPL-2.0",
|
|
3909
3990
|
"optional": true,
|
|
3910
3991
|
"os": [
|
|
@@ -3925,6 +4006,9 @@
|
|
|
3925
4006
|
"cpu": [
|
|
3926
4007
|
"x64"
|
|
3927
4008
|
],
|
|
4009
|
+
"libc": [
|
|
4010
|
+
"musl"
|
|
4011
|
+
],
|
|
3928
4012
|
"license": "MPL-2.0",
|
|
3929
4013
|
"optional": true,
|
|
3930
4014
|
"os": [
|