jamdesk 1.1.218 → 1.1.220

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.
Files changed (43) hide show
  1. package/dist/__tests__/unit/docs-config.test.js +20 -0
  2. package/dist/__tests__/unit/docs-config.test.js.map +1 -1
  3. package/dist/__tests__/unit/logo-svg-text-wiring.test.d.ts +2 -0
  4. package/dist/__tests__/unit/logo-svg-text-wiring.test.d.ts.map +1 -0
  5. package/dist/__tests__/unit/logo-svg-text-wiring.test.js +14 -0
  6. package/dist/__tests__/unit/logo-svg-text-wiring.test.js.map +1 -0
  7. package/dist/__tests__/unit/logo-svg-text.test.d.ts +2 -0
  8. package/dist/__tests__/unit/logo-svg-text.test.d.ts.map +1 -0
  9. package/dist/__tests__/unit/logo-svg-text.test.js +64 -0
  10. package/dist/__tests__/unit/logo-svg-text.test.js.map +1 -0
  11. package/dist/__tests__/unit/svg-text-sync.test.d.ts +2 -0
  12. package/dist/__tests__/unit/svg-text-sync.test.d.ts.map +1 -0
  13. package/dist/__tests__/unit/svg-text-sync.test.js +16 -0
  14. package/dist/__tests__/unit/svg-text-sync.test.js.map +1 -0
  15. package/dist/commands/dev.d.ts.map +1 -1
  16. package/dist/commands/dev.js +5 -1
  17. package/dist/commands/dev.js.map +1 -1
  18. package/dist/commands/validate.d.ts.map +1 -1
  19. package/dist/commands/validate.js +5 -1
  20. package/dist/commands/validate.js.map +1 -1
  21. package/dist/lib/docs-config.d.ts.map +1 -1
  22. package/dist/lib/docs-config.js +3 -0
  23. package/dist/lib/docs-config.js.map +1 -1
  24. package/dist/lib/logo-svg-text.d.ts +8 -0
  25. package/dist/lib/logo-svg-text.d.ts.map +1 -0
  26. package/dist/lib/logo-svg-text.js +53 -0
  27. package/dist/lib/logo-svg-text.js.map +1 -0
  28. package/dist/lib/svg-text.d.ts +40 -0
  29. package/dist/lib/svg-text.d.ts.map +1 -0
  30. package/dist/lib/svg-text.js +189 -0
  31. package/dist/lib/svg-text.js.map +1 -0
  32. package/package.json +1 -1
  33. package/vendored/app/api/og/route.tsx +280 -167
  34. package/vendored/app/layout.tsx +2 -2
  35. package/vendored/lib/docs-types.ts +2 -0
  36. package/vendored/lib/logo-svg-text-warning.ts +55 -0
  37. package/vendored/lib/og-card.ts +42 -6
  38. package/vendored/lib/seo.ts +16 -0
  39. package/vendored/lib/svg-text.ts +185 -0
  40. package/vendored/lib/validate-config.ts +2 -0
  41. package/vendored/schema/docs-schema.json +6 -0
  42. package/vendored/shared/status-reporter.ts +5 -0
  43. package/vendored/workspace-package-lock.json +121 -121
@@ -0,0 +1,185 @@
1
+ // No imports on purpose: this file is copied verbatim into the CLI.
2
+
3
+ export const SVG_TEXT_MAX_BYTES = 1_048_576;
4
+ export const SVG_TEXT_MESSAGE =
5
+ "This SVG logo has text in it that link previews can't show. Convert the text to outlines (paths) in your design tool and export it again.";
6
+ export const SVG_TEXT_LINK = 'https://jamdesk.com/docs/content/seo#customize-the-social-card';
7
+
8
+ // Elements whose contents are never painted, so a <text> inside them is not visible text.
9
+ const NON_RENDERED = new Set(['title', 'desc', 'metadata', 'script', 'style']);
10
+
11
+ /** Character data of a <text> body with nested tags (tspan etc.) dropped. */
12
+ function visibleData(body: string): boolean {
13
+ let i = 0;
14
+ while (i < body.length) {
15
+ const lt = body.indexOf('<', i);
16
+ const end = lt === -1 ? body.length : lt;
17
+ for (let k = i; k < end; k++) {
18
+ const c = body.charCodeAt(k);
19
+ // Anything that is not ASCII whitespace counts, entities such as &#x44; included.
20
+ if (c !== 32 && c !== 9 && c !== 10 && c !== 13 && c !== 12) return true;
21
+ }
22
+ if (lt === -1) return false;
23
+ const gt = body.indexOf('>', lt);
24
+ if (gt === -1) return false;
25
+ i = gt + 1;
26
+ }
27
+ return false;
28
+ }
29
+
30
+ /**
31
+ * Index of the close tag for rawName at or after `from`, or -1. A match only
32
+ * counts when the name ends there, so </textPath is not the close of <text.
33
+ * The search only moves forward, so it stays linear.
34
+ */
35
+ function findClose(lc: string, rawName: string, from: number): number {
36
+ const needle = '</' + rawName;
37
+ let at = lc.indexOf(needle, from);
38
+ while (at !== -1) {
39
+ const c = lc.charCodeAt(at + needle.length);
40
+ if (c === 62 || c === 32 || c === 9 || c === 10 || c === 13 || c === 12) return at;
41
+ at = lc.indexOf(needle, at + 1);
42
+ }
43
+ return -1;
44
+ }
45
+
46
+ // display:none on an element or its style hides the whole subtree. Design tools
47
+ // leave the original text in a hidden layer after outlining a copy of it.
48
+ // visibility:hidden is not here on purpose: a child can set it back to visible.
49
+ const DISPLAY_NONE_ATTR = /(?:^|\s)display\s*=\s*(["'])\s*none\s*\1/;
50
+ const STYLE_ATTR = /(?:^|\s)style\s*=\s*(?:"([^"]*)"|'([^']*)')/;
51
+ const DISPLAY_NONE_CSS = /(?:^|[;\s])display\s*:\s*none(?![a-z0-9_-])/;
52
+
53
+ function isDisplayNone(attrs: string): boolean {
54
+ if (DISPLAY_NONE_ATTR.test(attrs)) return true;
55
+ const style = STYLE_ATTR.exec(attrs);
56
+ return !!style && DISPLAY_NONE_CSS.test(style[1] ?? style[2]);
57
+ }
58
+
59
+ /**
60
+ * True when the SVG has a <text> element with visible characters.
61
+ * @vercel/og has no fonts for SVGs embedded as images, so such text draws blank
62
+ * (reproduced in prod 2026-10-05). Single forward scan with indexOf: every
63
+ * search moves past what it consumed, and anything unterminated ends the scan.
64
+ * A display:none element is tracked by name and depth inside the same scan, so
65
+ * skipping it never re-reads input.
66
+ */
67
+ export function svgHasText(svg: string): boolean {
68
+ const lc = svg.toLowerCase();
69
+ // Raw name of the display:none element being skipped, and how many opens of
70
+ // that name are still unclosed. 0 means not inside a hidden element.
71
+ let hiddenName = '';
72
+ let hiddenDepth = 0;
73
+ let i = 0;
74
+ while (i < lc.length) {
75
+ const lt = lc.indexOf('<', i);
76
+ if (lt === -1) return false;
77
+
78
+ if (lc.startsWith('<!--', lt)) {
79
+ const end = lc.indexOf('-->', lt + 4);
80
+ if (end === -1) return false;
81
+ i = end + 3;
82
+ continue;
83
+ }
84
+ if (lc.startsWith('<![cdata[', lt)) {
85
+ const end = lc.indexOf(']]>', lt + 9);
86
+ if (end === -1) return false;
87
+ i = end + 3;
88
+ continue;
89
+ }
90
+ if (lc.startsWith('<?', lt)) {
91
+ const end = lc.indexOf('?>', lt + 2);
92
+ if (end === -1) return false;
93
+ i = end + 2;
94
+ continue;
95
+ }
96
+
97
+ const isClose = lc.charCodeAt(lt + 1) === 47;
98
+ const nameStart = isClose ? lt + 2 : lt + 1;
99
+ let n = nameStart;
100
+ while (n < lc.length) {
101
+ const c = lc.charCodeAt(n);
102
+ if (c === 32 || c === 9 || c === 10 || c === 13 || c === 12 || c === 47 || c === 62 || c === 60) break;
103
+ n++;
104
+ }
105
+ const rawName = lc.slice(nameStart, n);
106
+ // Every tag is read up to its '>' and the scan resumes after it, so no
107
+ // character is looked at twice. With no '>' left, no later tag can be whole.
108
+ const gt = lc.indexOf('>', n);
109
+ if (gt === -1) return false;
110
+ const selfClosing = lc.charCodeAt(gt - 1) === 47;
111
+
112
+ if (isClose) {
113
+ if (hiddenDepth > 0 && rawName === hiddenName) hiddenDepth--;
114
+ i = gt + 1;
115
+ continue;
116
+ }
117
+
118
+ const local = rawName.slice(rawName.indexOf(':') + 1);
119
+ const isText = local === 'text';
120
+ if (isText || NON_RENDERED.has(local)) {
121
+ if (selfClosing) {
122
+ i = gt + 1;
123
+ continue;
124
+ }
125
+ // Jump over the body even when hidden: script or title content could
126
+ // otherwise look like tags and throw off the hidden depth count.
127
+ const close = findClose(lc, rawName, gt + 1);
128
+ if (close === -1) return false;
129
+ if (
130
+ isText &&
131
+ hiddenDepth === 0 &&
132
+ !isDisplayNone(lc.slice(n, gt)) &&
133
+ visibleData(lc.slice(gt + 1, close))
134
+ ) {
135
+ return true;
136
+ }
137
+ i = close;
138
+ continue;
139
+ }
140
+
141
+ if (!selfClosing) {
142
+ if (hiddenDepth > 0) {
143
+ if (rawName === hiddenName) hiddenDepth++;
144
+ } else if (isDisplayNone(lc.slice(n, gt))) {
145
+ hiddenName = rawName;
146
+ hiddenDepth = 1;
147
+ }
148
+ }
149
+ i = gt + 1;
150
+ }
151
+ return false;
152
+ }
153
+
154
+ type ImageValue = string | { light?: string; dark?: string } | undefined;
155
+
156
+ /**
157
+ * The SVG logo the generated social card draws, or null when it draws none.
158
+ * SVG logos are never rewritten, and neither fallback in seo.ts (the favicon
159
+ * swap at :176 and :212) applies to .svg, so a returned ref is the file drawn.
160
+ * Mirrors resolveImagePath and the card variant choice in seo.ts.
161
+ */
162
+ export function cardSvgLogoRef(config: {
163
+ logo?: ImageValue;
164
+ seo?: { metatags?: Record<string, string>; socialCard?: { theme?: string; customImage?: string } };
165
+ }): string | null {
166
+ const card = config.seo?.socialCard;
167
+ // 'replace' with a usable global og:image serves that image instead of the card.
168
+ if (card?.customImage === 'replace') {
169
+ const og = config.seo?.metatags?.['og:image'];
170
+ const trimmed = typeof og === 'string' ? og.trim() : '';
171
+ if (trimmed && !trimmed.startsWith('data:')) return null;
172
+ }
173
+
174
+ const logo = config.logo;
175
+ if (!logo) return null;
176
+ let ref = '';
177
+ if (typeof logo === 'string') {
178
+ ref = logo;
179
+ } else {
180
+ const { light, dark } = logo;
181
+ ref = (card?.theme === 'dark' ? dark || light : light || dark) || '';
182
+ }
183
+ if (!ref) return null;
184
+ return /\.svg$/i.test(ref.split(/[?#]/)[0]) ? ref : null;
185
+ }
@@ -347,6 +347,8 @@ export function formatValidationErrors(errors: ErrorObject[] | null | undefined,
347
347
  case 'minLength':
348
348
  return `Value at ${path} must not be empty`;
349
349
  case 'pattern':
350
+ // A bare "Invalid format" gives no hint that %s is the missing piece.
351
+ if (err.instancePath === '/seo/titleTemplate') return `Invalid value at ${path}: must contain %s where the page title goes`;
350
352
  return `Invalid format at ${path}`;
351
353
  case 'anyOf':
352
354
  return `Invalid value at ${path}: does not match any allowed format`;
@@ -610,6 +610,12 @@
610
610
  }
611
611
  }
612
612
  },
613
+ "titleTemplate": {
614
+ "type": "string",
615
+ "pattern": "%s",
616
+ "maxLength": 200,
617
+ "description": "Format of the browser tab and search result title on every page. Each `%s` is replaced with the page title, for example `%s | Acme`. Use `%s` alone to leave out the site name. Does not change og:title. Defaults to `%s — <name>`."
618
+ },
613
619
  "socialCard": {
614
620
  "type": "object",
615
621
  "additionalProperties": false,
@@ -58,6 +58,10 @@ export const BUILD_WARNING_TYPES = [
58
58
  // reusing invalid_openapi_spec: the key may be `api.asyncapi`, which has no
59
59
  // OpenAPI spec to have failed to parse.
60
60
  'inert_config_key',
61
+ // New (2026-10-05): the SVG logo the OG social card draws has visible <text>,
62
+ // which the card renderer paints blank. Suggestion tier: the site still
63
+ // builds and the page logo is fine, only the link preview loses the text.
64
+ 'logo_svg_text',
61
65
  ] as const;
62
66
 
63
67
  export type BuildWarningType = (typeof BUILD_WARNING_TYPES)[number];
@@ -104,6 +108,7 @@ export const SUGGESTION_TYPES: ReadonlySet<BuildWarningType> = new Set([
104
108
  'page_missing_description',
105
109
  'page_short_description',
106
110
  'logo_oversized',
111
+ 'logo_svg_text',
107
112
  ]);
108
113
 
109
114
  /**