jamdesk 1.1.215 → 1.1.217
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/migrate-resolve-theme.test.js +4 -6
- package/dist/__tests__/unit/migrate-resolve-theme.test.js.map +1 -1
- package/dist/commands/migrate/index.d.ts.map +1 -1
- package/dist/commands/migrate/index.js +1 -4
- package/dist/commands/migrate/index.js.map +1 -1
- package/dist/commands/migrate/types.d.ts +1 -1
- package/dist/commands/migrate/types.d.ts.map +1 -1
- package/dist/commands/migrate/types.js +2 -3
- package/dist/commands/migrate/types.js.map +1 -1
- package/package.json +1 -1
- package/vendored/app/api/og/route.tsx +60 -55
- package/vendored/lib/docs-types.ts +17 -0
- package/vendored/lib/og-card.ts +119 -0
- package/vendored/lib/seo.ts +197 -60
- package/vendored/lib/theme-preview.ts +3 -5
- package/vendored/schema/docs-schema.json +40 -0
- package/vendored/themes/index.ts +5 -0
- package/vendored/themes/types.ts +7 -0
- package/vendored/workspace-package-lock.json +61 -61
- package/dist/__tests__/unit/dusk-embargo.test.d.ts +0 -2
- package/dist/__tests__/unit/dusk-embargo.test.d.ts.map +0 -1
- package/dist/__tests__/unit/dusk-embargo.test.js +0 -26
- package/dist/__tests__/unit/dusk-embargo.test.js.map +0 -1
- package/dist/lib/dusk-embargo.d.ts +0 -15
- package/dist/lib/dusk-embargo.d.ts.map +0 -1
- package/dist/lib/dusk-embargo.js +0 -15
- package/dist/lib/dusk-embargo.js.map +0 -1
- package/vendored/lib/dusk-embargo.ts +0 -16
package/vendored/lib/seo.ts
CHANGED
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Metadata } from 'next';
|
|
12
12
|
import { stripMarkdownForMeta } from './openapi/operation-description';
|
|
13
|
-
import type { DocsConfig, Logo, LogoConfig, Favicon, LanguageConfig, LanguageCode } from './docs-types';
|
|
14
|
-
import { normalizeLogo } from './docs-types';
|
|
13
|
+
import type { DocsConfig, Logo, LogoConfig, Favicon, LanguageConfig, LanguageCode, SocialCardConfig, ColorsConfig } from './docs-types';
|
|
14
|
+
import { normalizeLogo, transformConfigImagePath } from './docs-types';
|
|
15
|
+
import { normalizeHex } from './og-card';
|
|
16
|
+
import { getTheme } from '../themes';
|
|
15
17
|
import { findHreflangAliasCollisions, transformLanguagePath, toHreflang, resolveLocaleFromPath } from './language-utils';
|
|
16
18
|
import { fallbackSeo } from './locale-fallback';
|
|
17
19
|
import { logger } from '../shared/logger';
|
|
@@ -59,11 +61,39 @@ export const FALLBACK_METADATA: Metadata = {
|
|
|
59
61
|
description: 'Documentation',
|
|
60
62
|
};
|
|
61
63
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a Logo or Favicon config to a path string. Light prefers the light
|
|
66
|
+
* variant, dark prefers the dark one; each falls back to the other so a
|
|
67
|
+
* single-variant config still yields an image.
|
|
68
|
+
*/
|
|
69
|
+
function resolveImagePath(image?: Logo | Favicon, variant: 'light' | 'dark' = 'light'): string {
|
|
64
70
|
if (!image) return '';
|
|
65
71
|
if (typeof image === 'string') return image;
|
|
66
|
-
|
|
72
|
+
const { light, dark } = image as LogoConfig;
|
|
73
|
+
return (variant === 'dark' ? dark || light : light || dark) || '';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Make a config-supplied image reference fetchable by the OG route, which loads
|
|
78
|
+
* it server-side. Local paths go through the /_jd asset prefix (where the
|
|
79
|
+
* pipeline serves them); an existing /_jd path is not doubled. Returns '' for
|
|
80
|
+
* data: URIs: the route drops them and base64 would bloat the og:image URL.
|
|
81
|
+
*/
|
|
82
|
+
export function toAbsoluteAssetUrl(pathOrUrl: string, rootUrl: string): string {
|
|
83
|
+
if (pathOrUrl.startsWith('data:')) return '';
|
|
84
|
+
if (pathOrUrl.startsWith('http://') || pathOrUrl.startsWith('https://')) return pathOrUrl;
|
|
85
|
+
// transformConfigImagePath passes '//host' through untouched, which would
|
|
86
|
+
// then be glued onto rootUrl as a path.
|
|
87
|
+
if (pathOrUrl.startsWith('//')) return `https:${pathOrUrl}`;
|
|
88
|
+
return `${rootUrl}${transformConfigImagePath(pathOrUrl) ?? ''}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* True for a WebP or AVIF image path/URL. Satori in the OG route cannot decode
|
|
93
|
+
* either and answers with an empty PNG instead of an error.
|
|
94
|
+
*/
|
|
95
|
+
function isUndecodableImage(pathOrUrl: string): boolean {
|
|
96
|
+
return /\.(?:webp|avif)$/i.test(pathOrUrl.split(/[?#]/)[0]);
|
|
67
97
|
}
|
|
68
98
|
|
|
69
99
|
/**
|
|
@@ -102,6 +132,9 @@ function buildOgImageUrl(
|
|
|
102
132
|
logo?: Logo;
|
|
103
133
|
favicon?: Favicon;
|
|
104
134
|
bg?: string;
|
|
135
|
+
card?: SocialCardConfig;
|
|
136
|
+
colors?: ColorsConfig;
|
|
137
|
+
theme?: string;
|
|
105
138
|
}
|
|
106
139
|
): string {
|
|
107
140
|
const params = new URLSearchParams();
|
|
@@ -122,32 +155,73 @@ function buildOgImageUrl(
|
|
|
122
155
|
const { prefix: resolvedPrefix, rootUrl } = resolveRootUrl(baseUrl, docsPrefix);
|
|
123
156
|
const isHostAtDocs = resolvedPrefix !== '';
|
|
124
157
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
158
|
+
const card = options?.card;
|
|
159
|
+
if (!card) {
|
|
160
|
+
// Tenants without seo.socialCard keep today's URLs byte for byte, including
|
|
161
|
+
// the non-prefixed asset paths: cached cards and shared links must not change.
|
|
162
|
+
// Handle logo - prefer light variant for OG images (cream background)
|
|
163
|
+
// Satori (@vercel/og) can't render webp, so fall back to favicon if logo is webp
|
|
164
|
+
if (options?.logo || options?.favicon) {
|
|
165
|
+
let logoPath = resolveImagePath(options?.logo);
|
|
166
|
+
// Fall back to favicon when logo is webp (Satori can't decode webp)
|
|
167
|
+
if ((!logoPath || logoPath.endsWith('.webp')) && options?.favicon) {
|
|
168
|
+
logoPath = resolveImagePath(options.favicon);
|
|
169
|
+
}
|
|
170
|
+
// Make logo URL absolute (use root URL since assets are at domain root)
|
|
171
|
+
if (logoPath && !logoPath.startsWith('http')) {
|
|
172
|
+
logoPath = `${rootUrl}${logoPath.startsWith('/') ? '' : '/'}${logoPath}`;
|
|
173
|
+
}
|
|
174
|
+
if (logoPath) {
|
|
175
|
+
params.set('logo', logoPath);
|
|
176
|
+
}
|
|
132
177
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
178
|
+
|
|
179
|
+
// Custom background image: composite the card text over it instead of using it
|
|
180
|
+
// as a bare static image. The OG route fetches it server-side, so it must be
|
|
181
|
+
// absolute — assets live at the domain root, so resolve against rootUrl.
|
|
182
|
+
if (options?.bg) {
|
|
183
|
+
let bgPath = options.bg;
|
|
184
|
+
if (!bgPath.startsWith('http')) {
|
|
185
|
+
bgPath = `${rootUrl}${bgPath.startsWith('/') ? '' : '/'}${bgPath}`;
|
|
186
|
+
}
|
|
187
|
+
params.set('bg', bgPath);
|
|
136
188
|
}
|
|
137
|
-
|
|
138
|
-
|
|
189
|
+
} else {
|
|
190
|
+
const variant = card.theme === 'dark' ? 'dark' : 'light';
|
|
191
|
+
|
|
192
|
+
// Resolved before any param is set so the param order stays as it was.
|
|
193
|
+
let logoUrl = '';
|
|
194
|
+
let logoIsFavicon = false;
|
|
195
|
+
if (options?.logo || options?.favicon) {
|
|
196
|
+
let logoPath = resolveImagePath(options?.logo, variant);
|
|
197
|
+
if ((!logoPath || isUndecodableImage(logoPath)) && options?.favicon) {
|
|
198
|
+
logoPath = resolveImagePath(options.favicon, variant);
|
|
199
|
+
logoIsFavicon = true;
|
|
200
|
+
}
|
|
201
|
+
// A webp/avif logo makes the route return a 0-byte PNG that the CDN then
|
|
202
|
+
// caches, so a card with no logo is the better outcome.
|
|
203
|
+
if (logoPath && !isUndecodableImage(logoPath)) logoUrl = toAbsoluteAssetUrl(logoPath, rootUrl);
|
|
139
204
|
}
|
|
140
|
-
}
|
|
141
205
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
206
|
+
if (variant === 'dark') params.set('theme', 'dark');
|
|
207
|
+
if (card.accentBar === false) params.set('hideBar', '1');
|
|
208
|
+
// Hiding the name is only safe when the tenant's own logo is drawn: with the
|
|
209
|
+
// favicon (or nothing) in its place the card would carry no brand at all.
|
|
210
|
+
if (card.siteName === false && logoUrl && !logoIsFavicon) params.set('hideName', '1');
|
|
211
|
+
|
|
212
|
+
if (card.brandColor === true) {
|
|
213
|
+
const colors = options?.colors;
|
|
214
|
+
const configured = variant === 'dark' ? colors?.light ?? colors?.primary : colors?.primary;
|
|
215
|
+
const accent = normalizeHex(configured ?? getTheme(options?.theme).defaultAccent);
|
|
216
|
+
if (accent) params.set('accent', accent.slice(1));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (logoUrl) params.set('logo', logoUrl);
|
|
220
|
+
|
|
221
|
+
if (options?.bg && !isUndecodableImage(options.bg)) {
|
|
222
|
+
const bgUrl = toAbsoluteAssetUrl(options.bg, rootUrl);
|
|
223
|
+
if (bgUrl) params.set('bg', bgUrl);
|
|
149
224
|
}
|
|
150
|
-
params.set('bg', bgPath);
|
|
151
225
|
}
|
|
152
226
|
|
|
153
227
|
// hostAtDocs: use /_jd/og (proxied to ISR app's /api/og) for same-domain OG images
|
|
@@ -157,37 +231,79 @@ function buildOgImageUrl(
|
|
|
157
231
|
}
|
|
158
232
|
|
|
159
233
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
234
|
+
* Resolve the social image a page advertises, plus whether it is the generated
|
|
235
|
+
* card. Mirrors the global+page metatag merge that buildSeoMetadata performs, so
|
|
236
|
+
* callers outside the metadata path (the JSON-LD layer) get the exact image the
|
|
237
|
+
* og:image points at.
|
|
238
|
+
*
|
|
239
|
+
* Default: always the generated /api/og card, with a custom og:image composited
|
|
240
|
+
* in as its background. With `seo.socialCard.customImage: 'replace'` and an
|
|
241
|
+
* og:image set, that image is served untouched instead (`generated: false`).
|
|
242
|
+
*
|
|
243
|
+
* Declared dimensions come from the SAME layer that supplied the winning
|
|
244
|
+
* og:image: a page image must not inherit the site default's width/height, which
|
|
245
|
+
* describe a different file.
|
|
163
246
|
*
|
|
164
247
|
* @param docsPrefix - This tenant's external docs URL prefix ('' | '/docs' |
|
|
165
248
|
* a custom subpath like '/help'). Falls back to the legacy baseUrl-suffix
|
|
166
249
|
* sniff when omitted — see buildOgImageUrl.
|
|
167
250
|
*/
|
|
168
|
-
export function
|
|
251
|
+
export function resolvePageSocialImage(
|
|
169
252
|
config: DocsConfig,
|
|
170
253
|
frontmatter: PageFrontmatter,
|
|
171
254
|
baseUrl: string,
|
|
172
255
|
docsPrefix?: string,
|
|
173
|
-
): string {
|
|
256
|
+
): { url: string; generated: boolean; width?: number; height?: number } {
|
|
174
257
|
const globalMeta = config.seo?.metatags || {};
|
|
175
258
|
const pageMeta = normalizePageMetatags(frontmatter as Record<string, unknown>);
|
|
176
259
|
const metatags: Record<string, string> = { ...globalMeta, ...pageMeta };
|
|
177
260
|
const customImage = metatags['og:image']?.trim();
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
261
|
+
|
|
262
|
+
if (config.seo?.socialCard?.customImage === 'replace' && customImage) {
|
|
263
|
+
const { rootUrl } = resolveRootUrl(baseUrl, docsPrefix);
|
|
264
|
+
const url = toAbsoluteAssetUrl(customImage, rootUrl);
|
|
265
|
+
// A data: URI resolves to '' (it would bloat the tag and crawlers drop it),
|
|
266
|
+
// so fall through to the generated card rather than emit an empty og:image.
|
|
267
|
+
if (url) {
|
|
268
|
+
const dims = pageMeta['og:image']?.trim() ? pageMeta : globalMeta;
|
|
269
|
+
return {
|
|
270
|
+
url,
|
|
271
|
+
generated: false,
|
|
272
|
+
width: positiveIntMeta(dims, 'og:image:width'),
|
|
273
|
+
height: positiveIntMeta(dims, 'og:image:height'),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
generated: true,
|
|
280
|
+
url: buildOgImageUrl(
|
|
281
|
+
baseUrl,
|
|
282
|
+
metatags['og:title'] || frontmatter.title || 'Documentation',
|
|
283
|
+
config.name,
|
|
284
|
+
docsPrefix,
|
|
285
|
+
{
|
|
286
|
+
description: metaDescriptionText(metatags['og:description'] || frontmatter.description),
|
|
287
|
+
section: frontmatter.section,
|
|
288
|
+
logo: config.logo,
|
|
289
|
+
favicon: config.favicon,
|
|
290
|
+
bg: customImage || undefined,
|
|
291
|
+
card: config.seo?.socialCard,
|
|
292
|
+
colors: config.colors,
|
|
293
|
+
theme: config.theme,
|
|
294
|
+
},
|
|
295
|
+
),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** The social image URL only; see resolvePageSocialImage. */
|
|
300
|
+
export function buildPageOgImageUrl(
|
|
301
|
+
config: DocsConfig,
|
|
302
|
+
frontmatter: PageFrontmatter,
|
|
303
|
+
baseUrl: string,
|
|
304
|
+
docsPrefix?: string,
|
|
305
|
+
): string {
|
|
306
|
+
return resolvePageSocialImage(config, frontmatter, baseUrl, docsPrefix).url;
|
|
191
307
|
}
|
|
192
308
|
|
|
193
309
|
/**
|
|
@@ -297,9 +413,21 @@ function numericMeta(metatags: Record<string, string>, key: string): number | un
|
|
|
297
413
|
return Number.isNaN(n) ? undefined : n;
|
|
298
414
|
}
|
|
299
415
|
|
|
416
|
+
// numericMeta lets 0, negatives, fractions and whitespace-only strings
|
|
417
|
+
// (Number(' ') === 0) through; a crawler can't use any of those as a pixel size.
|
|
418
|
+
function positiveIntMeta(metatags: Record<string, string>, key: string): number | undefined {
|
|
419
|
+
const n = numericMeta(metatags, key);
|
|
420
|
+
return n !== undefined && Number.isInteger(n) && n > 0 ? n : undefined;
|
|
421
|
+
}
|
|
422
|
+
|
|
300
423
|
type FallbackOg = {
|
|
301
424
|
title?: string; description?: string; url: string; siteName: string; ogImageUrl: string;
|
|
302
425
|
locale?: string;
|
|
426
|
+
/** False when ogImageUrl is the author's own image (customImage: 'replace'). */
|
|
427
|
+
ogImageGenerated?: boolean;
|
|
428
|
+
/** Declared size of an author-supplied image; ignored for the generated card. */
|
|
429
|
+
ogImageWidth?: number;
|
|
430
|
+
ogImageHeight?: number;
|
|
303
431
|
};
|
|
304
432
|
|
|
305
433
|
/**
|
|
@@ -344,16 +472,21 @@ export function buildOpenGraphMetadata(
|
|
|
344
472
|
const ttl = numericMeta(metatags, 'og:ttl');
|
|
345
473
|
if (ttl !== undefined) og.ttl = ttl;
|
|
346
474
|
|
|
347
|
-
// Image:
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
// user
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
475
|
+
// Image: the generated card (1200x630 PNG) unless the caller resolved the
|
|
476
|
+
// author's own image (customImage: 'replace'). In the default mode a custom
|
|
477
|
+
// og:image is composited as the card BACKGROUND upstream (via /api/og's `bg`
|
|
478
|
+
// param), so the emitted image is never the raw user file. For a raw image we
|
|
479
|
+
// can't know the format or size, so emit only the dimensions the author
|
|
480
|
+
// declared and no `type`. og:image:alt describes either.
|
|
481
|
+
const image: Record<string, unknown> = { url: fb.ogImageUrl };
|
|
482
|
+
if (fb.ogImageGenerated === false) {
|
|
483
|
+
if (fb.ogImageWidth !== undefined) image.width = fb.ogImageWidth;
|
|
484
|
+
if (fb.ogImageHeight !== undefined) image.height = fb.ogImageHeight;
|
|
485
|
+
} else {
|
|
486
|
+
image.width = 1200;
|
|
487
|
+
image.height = 630;
|
|
488
|
+
image.type = 'image/png';
|
|
489
|
+
}
|
|
357
490
|
if (metatags['og:image:alt']) image.alt = metatags['og:image:alt'];
|
|
358
491
|
og.images = [image];
|
|
359
492
|
|
|
@@ -827,17 +960,21 @@ export function buildSeoMetadata(
|
|
|
827
960
|
(k) => k.startsWith('og:') || k.startsWith('article:') || k.startsWith('book:') || k.startsWith('profile:'),
|
|
828
961
|
);
|
|
829
962
|
if (hasOgFamily || frontmatter.title) {
|
|
830
|
-
//
|
|
831
|
-
//
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
|
|
963
|
+
// By default a custom og:image becomes the card BACKGROUND (text renders on
|
|
964
|
+
// top) and the emitted image is the generated card. With
|
|
965
|
+
// seo.socialCard.customImage: 'replace' the author's image is emitted as-is.
|
|
966
|
+
// The generated card's text follows og:title/og:description overrides so the
|
|
967
|
+
// image matches the link-card text built in buildOpenGraphMetadata.
|
|
968
|
+
const socialImage = resolvePageSocialImage(config, frontmatter, baseUrl, docsPrefix);
|
|
835
969
|
metadata.openGraph = buildOpenGraphMetadata(metatags, {
|
|
836
970
|
title: frontmatter.title,
|
|
837
971
|
description: metaDescriptionText(frontmatter.description),
|
|
838
972
|
url: pageUrl,
|
|
839
973
|
siteName: config.name,
|
|
840
|
-
ogImageUrl,
|
|
974
|
+
ogImageUrl: socialImage.url,
|
|
975
|
+
ogImageGenerated: socialImage.generated,
|
|
976
|
+
ogImageWidth: socialImage.width,
|
|
977
|
+
ogImageHeight: socialImage.height,
|
|
841
978
|
// og:locale describes the language of the CONTENT, so on the fallback it
|
|
842
979
|
// must follow the English body, not the locale URL. (og:url deliberately
|
|
843
980
|
// still points at the locale URL — that is the page's real address, and
|
|
@@ -57,7 +57,6 @@
|
|
|
57
57
|
* a `Vary` header.
|
|
58
58
|
*/
|
|
59
59
|
import { getAllThemes, type ThemeConfig, type ThemeName } from '@/themes';
|
|
60
|
-
import { DUSK_PUBLIC } from '@/lib/dusk-embargo';
|
|
61
60
|
|
|
62
61
|
/** Cookie the picker writes; read server-side in app/layout.tsx. */
|
|
63
62
|
export const THEME_PREVIEW_COOKIE = 'jd_theme';
|
|
@@ -70,9 +69,9 @@ export const THEME_PREVIEW_SLUG = 'jamdesk-docs';
|
|
|
70
69
|
// setting, and should not outlive the visit. The clear path still sends
|
|
71
70
|
// `Max-Age=0` — see theme-preview-client.ts's applyThemePreview.
|
|
72
71
|
|
|
73
|
-
/** Themes the picker offers.
|
|
72
|
+
/** Themes the picker offers. */
|
|
74
73
|
export function getPreviewThemes(): ThemeConfig[] {
|
|
75
|
-
return getAllThemes()
|
|
74
|
+
return getAllThemes();
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
// Built from the themes the picker actually offers, rather than using
|
|
@@ -81,8 +80,7 @@ export function getPreviewThemes(): ThemeConfig[] {
|
|
|
81
80
|
// would reach getTheme(), which falls back to jam silently for unknown names
|
|
82
81
|
// but returns Object's own constructor for 'constructor' — yielding
|
|
83
82
|
// data-theme="constructor" and no theme CSS. A Set of the offered themes' own
|
|
84
|
-
// names is immune
|
|
85
|
-
// out of both the picker and the cookie reader below.
|
|
83
|
+
// names is immune and picks up new themes for free.
|
|
86
84
|
const VALID_THEME_NAMES: ReadonlySet<string> = new Set(
|
|
87
85
|
getPreviewThemes().map((theme) => theme.name),
|
|
88
86
|
);
|
|
@@ -609,6 +609,46 @@
|
|
|
609
609
|
"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)."
|
|
610
610
|
}
|
|
611
611
|
}
|
|
612
|
+
},
|
|
613
|
+
"socialCard": {
|
|
614
|
+
"type": "object",
|
|
615
|
+
"additionalProperties": false,
|
|
616
|
+
"properties": {
|
|
617
|
+
"theme": {
|
|
618
|
+
"type": "string",
|
|
619
|
+
"enum": [
|
|
620
|
+
"light",
|
|
621
|
+
"dark"
|
|
622
|
+
],
|
|
623
|
+
"default": "light",
|
|
624
|
+
"description": "Text and background style of the card. `dark` uses light text on a dark background and your `logo.dark` image. Defaults to `light`."
|
|
625
|
+
},
|
|
626
|
+
"brandColor": {
|
|
627
|
+
"type": "boolean",
|
|
628
|
+
"default": false,
|
|
629
|
+
"description": "Use your `colors.primary` for the card's accent bar and section badge (`colors.light` on a dark card). Defaults to `false`, which keeps the default red."
|
|
630
|
+
},
|
|
631
|
+
"accentBar": {
|
|
632
|
+
"type": "boolean",
|
|
633
|
+
"default": true,
|
|
634
|
+
"description": "Show the colored bar along the bottom of the card. Defaults to `true`."
|
|
635
|
+
},
|
|
636
|
+
"siteName": {
|
|
637
|
+
"type": "boolean",
|
|
638
|
+
"default": true,
|
|
639
|
+
"description": "Show the site name under the logo. Set `false` when your logo already contains the name. Defaults to `true`."
|
|
640
|
+
},
|
|
641
|
+
"customImage": {
|
|
642
|
+
"type": "string",
|
|
643
|
+
"enum": [
|
|
644
|
+
"background",
|
|
645
|
+
"replace"
|
|
646
|
+
],
|
|
647
|
+
"default": "background",
|
|
648
|
+
"description": "How a custom `og:image` is used. `background` draws the page title on top of it. `replace` uses the image as-is, with no text. Defaults to `background`."
|
|
649
|
+
}
|
|
650
|
+
},
|
|
651
|
+
"description": "Customize the generated social sharing card (og:image) for every page."
|
|
612
652
|
}
|
|
613
653
|
},
|
|
614
654
|
"additionalProperties": false,
|
package/vendored/themes/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ const jamTheme: ThemeConfig = {
|
|
|
31
31
|
},
|
|
32
32
|
},
|
|
33
33
|
cssPath: 'jam/variables.css',
|
|
34
|
+
defaultAccent: '#3b82f6',
|
|
34
35
|
};
|
|
35
36
|
|
|
36
37
|
const nebulaTheme: ThemeConfig = {
|
|
@@ -52,6 +53,7 @@ const nebulaTheme: ThemeConfig = {
|
|
|
52
53
|
},
|
|
53
54
|
},
|
|
54
55
|
cssPath: 'nebula/variables.css',
|
|
56
|
+
defaultAccent: '#f97316',
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
const pulsarTheme: ThemeConfig = {
|
|
@@ -73,6 +75,7 @@ const pulsarTheme: ThemeConfig = {
|
|
|
73
75
|
},
|
|
74
76
|
},
|
|
75
77
|
cssPath: 'pulsar/variables.css',
|
|
78
|
+
defaultAccent: '#3b82f6',
|
|
76
79
|
};
|
|
77
80
|
|
|
78
81
|
const haloTheme: ThemeConfig = {
|
|
@@ -98,6 +101,7 @@ const haloTheme: ThemeConfig = {
|
|
|
98
101
|
},
|
|
99
102
|
},
|
|
100
103
|
cssPath: 'halo/variables.css',
|
|
104
|
+
defaultAccent: '#C0873F',
|
|
101
105
|
};
|
|
102
106
|
|
|
103
107
|
const duskTheme: ThemeConfig = {
|
|
@@ -125,6 +129,7 @@ const duskTheme: ThemeConfig = {
|
|
|
125
129
|
},
|
|
126
130
|
},
|
|
127
131
|
cssPath: 'dusk/variables.css',
|
|
132
|
+
defaultAccent: '#2158A1',
|
|
128
133
|
};
|
|
129
134
|
|
|
130
135
|
// Theme registry
|
package/vendored/themes/types.ts
CHANGED
|
@@ -78,6 +78,13 @@ export interface ThemeConfig {
|
|
|
78
78
|
|
|
79
79
|
/** CSS file path relative to themes directory */
|
|
80
80
|
cssPath: string;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The theme's light-mode `--color-accent`. The OG social card uses it as the
|
|
84
|
+
* accent when a tenant opts into brandColor but sets no colors.primary.
|
|
85
|
+
* Pinned to variables.css by __tests__/theme-default-accent.test.ts.
|
|
86
|
+
*/
|
|
87
|
+
defaultAccent: string;
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
/**
|