@clidey/dory 1.0.3 → 1.0.6
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/bin/dist/dory.js +36 -2
- package/bin/dist/dory.js.map +1 -1
- package/index.html +0 -1
- package/package.json +27 -31
- package/scripts/check-trigger-children.mjs +60 -0
- package/scripts/trigger-children-lib.mjs +149 -0
- package/scripts/trigger-children.json +47 -0
- package/src/components/hooks.tsx +2 -1
- package/src/components/store.ts +5 -4
- package/src/config/mdx.js +8 -3
- package/src/config/mdx.ts +8 -3
- package/src/embed/widget-iframe.ts +10 -3
- package/src/entry-server.tsx +11 -7
- package/src/main.tsx +5 -1
- package/src/mdx/accordion.tsx +11 -8
- package/src/mdx/api.tsx +2 -352
- package/src/mdx/async-api.tsx +124 -295
- package/src/mdx/callout.tsx +22 -24
- package/src/mdx/card.tsx +20 -22
- package/src/mdx/chip.tsx +14 -0
- package/src/mdx/code.tsx +133 -110
- package/src/mdx/expandable.tsx +8 -8
- package/src/mdx/fence-meta.test.ts +39 -0
- package/src/mdx/fence-meta.ts +61 -0
- package/src/mdx/fence.tsx +155 -44
- package/src/mdx/icon-grid.tsx +29 -0
- package/src/mdx/icon.tsx +2 -2
- package/src/mdx/latex.tsx +14 -7
- package/src/mdx/list.tsx +3 -3
- package/src/mdx/mdx.tsx +7 -1
- package/src/mdx/mermaid.tsx +81 -11
- package/src/mdx/open-api.tsx +396 -239
- package/src/mdx/source.tsx +148 -40
- package/src/mdx/specimen.tsx +69 -0
- package/src/mdx/step.tsx +22 -30
- package/src/mdx/table.tsx +4 -9
- package/src/mdx/tabs.tsx +39 -0
- package/src/mdx/tag.tsx +13 -21
- package/src/mdx/text.tsx +17 -9
- package/src/mdx/type-scale.tsx +28 -0
- package/src/mdx/websocket-playground.tsx +221 -704
- package/src/plugins/code-meta.ts +23 -0
- package/src/plugins/frontmatter-dev-server.ts +6 -2
- package/src/plugins/html-files-middleware.ts +9 -2
- package/src/plugins/html-metadata-injector.ts +4 -3
- package/src/plugins/llm-txt-dev-server.ts +5 -2
- package/src/plugins/llm-txt-generator.ts +2 -1
- package/src/plugins/prerender.ts +7 -4
- package/src/plugins/relative-assets.test.ts +26 -0
- package/src/plugins/relative-assets.ts +80 -0
- package/src/plugins/robots-generator.ts +6 -4
- package/src/plugins/sanitize.test.ts +5 -0
- package/src/plugins/sanitize.ts +2 -0
- package/src/plugins/shared.ts +17 -0
- package/src/plugins/sitemap-generator.ts +2 -1
- package/src/theme.scss +282 -3
- package/src/types/config.ts +3 -0
- package/src/ui/header.tsx +4 -3
- package/src/ui/layout.tsx +22 -4
- package/src/ui/page-meta.tsx +37 -0
- package/src/ui/prev-next-link.tsx +4 -3
- package/src/ui/table-of-content.tsx +11 -3
- package/src/utils/base-path.test.ts +46 -0
- package/src/utils/base-path.ts +41 -0
- package/vite.config.ssr.ts +3 -0
- package/vite.config.ts +3 -0
package/src/theme.scss
CHANGED
|
@@ -157,12 +157,13 @@ html, body, #app {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
// === DEFAULT STYLES ===
|
|
160
|
-
article
|
|
160
|
+
article hr:first-of-type {
|
|
161
161
|
display: none;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
// A rule inside a specimen is the example itself, not a page divider.
|
|
165
|
+
article .dory-specimen hr:first-of-type {
|
|
166
|
+
display: block;
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
article img {
|
|
@@ -193,3 +194,281 @@ article img {
|
|
|
193
194
|
border: 0px;
|
|
194
195
|
box-shadow: none !important;
|
|
195
196
|
}
|
|
197
|
+
|
|
198
|
+
// === EDITORIAL STYLE (Dory v2) ===
|
|
199
|
+
:root {
|
|
200
|
+
--dory-code-string: #b65401;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
:root.dark {
|
|
204
|
+
--dory-code-string: #f4a261;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.font-heading {
|
|
208
|
+
font-family: var(--font-heading, var(--font-body, inherit));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.dory-page-title {
|
|
212
|
+
font-family: var(--font-heading, var(--font-body, inherit));
|
|
213
|
+
font-size: 1.625rem;
|
|
214
|
+
font-weight: 700;
|
|
215
|
+
line-height: 1.15;
|
|
216
|
+
letter-spacing: -0.025em;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.dory-subtitle {
|
|
220
|
+
font-family: var(--font-mono, monospace);
|
|
221
|
+
font-size: 11px;
|
|
222
|
+
letter-spacing: 0.18em;
|
|
223
|
+
color: var(--muted-foreground);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
article {
|
|
227
|
+
counter-reset: dory-section;
|
|
228
|
+
font-size: 0.875rem;
|
|
229
|
+
line-height: 1.6;
|
|
230
|
+
|
|
231
|
+
.dory-h1 {
|
|
232
|
+
font-size: 1.625rem;
|
|
233
|
+
font-weight: 700;
|
|
234
|
+
line-height: 1.15;
|
|
235
|
+
letter-spacing: -0.025em;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.dory-h2 {
|
|
239
|
+
counter-increment: dory-section;
|
|
240
|
+
display: flex;
|
|
241
|
+
align-items: center;
|
|
242
|
+
gap: 0.625rem;
|
|
243
|
+
font-size: 1.3125rem;
|
|
244
|
+
font-weight: 700;
|
|
245
|
+
line-height: 1.2;
|
|
246
|
+
letter-spacing: -0.02em;
|
|
247
|
+
|
|
248
|
+
&::before {
|
|
249
|
+
content: counter(dory-section, decimal-leading-zero);
|
|
250
|
+
flex-shrink: 0;
|
|
251
|
+
padding: 0.0625rem 0.375rem;
|
|
252
|
+
border: 1px solid var(--border);
|
|
253
|
+
border-radius: 0.3125rem;
|
|
254
|
+
background: var(--muted);
|
|
255
|
+
font-family: var(--font-mono, monospace);
|
|
256
|
+
font-size: 0.6875rem;
|
|
257
|
+
font-weight: 400;
|
|
258
|
+
letter-spacing: 0;
|
|
259
|
+
color: var(--muted-foreground);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
+ p {
|
|
263
|
+
color: var(--muted-foreground);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.dory-h3 {
|
|
268
|
+
font-size: 1.0625rem;
|
|
269
|
+
font-weight: 700;
|
|
270
|
+
letter-spacing: -0.01em;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.dory-h4 {
|
|
274
|
+
font-size: 0.9375rem;
|
|
275
|
+
font-weight: 600;
|
|
276
|
+
letter-spacing: -0.005em;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Unordered: small brand-colored squares. Ordered: mono two-digit counters.
|
|
280
|
+
.dory-ul > li::before {
|
|
281
|
+
content: "";
|
|
282
|
+
position: absolute;
|
|
283
|
+
left: 0.5rem;
|
|
284
|
+
top: 0.7em;
|
|
285
|
+
width: 0.375rem;
|
|
286
|
+
height: 0.375rem;
|
|
287
|
+
border-radius: 0.125rem;
|
|
288
|
+
background: var(--primary);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.dory-ul .dory-ul > li {
|
|
292
|
+
color: var(--muted-foreground);
|
|
293
|
+
|
|
294
|
+
&::before {
|
|
295
|
+
background: var(--muted-foreground);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.dory-ol {
|
|
300
|
+
counter-reset: dory-ol;
|
|
301
|
+
|
|
302
|
+
> li {
|
|
303
|
+
counter-increment: dory-ol;
|
|
304
|
+
|
|
305
|
+
&::before {
|
|
306
|
+
content: counter(dory-ol, decimal-leading-zero);
|
|
307
|
+
position: absolute;
|
|
308
|
+
left: 0;
|
|
309
|
+
top: 0.15em;
|
|
310
|
+
font-family: var(--font-mono, monospace);
|
|
311
|
+
font-size: 0.6875rem;
|
|
312
|
+
color: var(--primary);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Task lists (GFM) render a checkbox as the first child, so drop the bullet.
|
|
318
|
+
li:has(> input[type="checkbox"]) {
|
|
319
|
+
padding-left: 0;
|
|
320
|
+
|
|
321
|
+
&::before {
|
|
322
|
+
display: none;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
input[type="checkbox"] {
|
|
327
|
+
width: 0.9375rem;
|
|
328
|
+
height: 0.9375rem;
|
|
329
|
+
margin-right: 0.5rem;
|
|
330
|
+
vertical-align: -0.2em;
|
|
331
|
+
accent-color: var(--primary);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
blockquote {
|
|
335
|
+
margin: 1.5rem 0;
|
|
336
|
+
padding-left: 1rem;
|
|
337
|
+
border-left: 2px solid var(--primary);
|
|
338
|
+
font-size: 0.9375rem;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
kbd {
|
|
342
|
+
padding: 0.0625rem 0.375rem;
|
|
343
|
+
border: 1px solid var(--border);
|
|
344
|
+
border-bottom-width: 2px;
|
|
345
|
+
border-radius: 0.375rem;
|
|
346
|
+
background: var(--card);
|
|
347
|
+
font-family: var(--font-mono, monospace);
|
|
348
|
+
font-size: 0.8125em;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
mark {
|
|
352
|
+
padding: 0.0625rem 0.3rem;
|
|
353
|
+
border-radius: 0.25rem;
|
|
354
|
+
background: var(--highlight);
|
|
355
|
+
color: var(--highlight-foreground);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// === CODE LINES ===
|
|
360
|
+
// Lines bleed to the frame edges (CodeBody padding is 1rem left / 3rem right)
|
|
361
|
+
// so highlighted and diff rows read as full-width bands.
|
|
362
|
+
.dory-code {
|
|
363
|
+
.dory-line {
|
|
364
|
+
display: block;
|
|
365
|
+
margin: 0 -3rem 0 -1rem;
|
|
366
|
+
padding: 0 3rem 0 calc(1rem - 2px);
|
|
367
|
+
border-left: 2px solid transparent;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.dory-line-highlight {
|
|
371
|
+
background: var(--accent);
|
|
372
|
+
border-left-color: var(--primary);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.dory-line-add {
|
|
376
|
+
background: var(--success-muted);
|
|
377
|
+
border-left-color: #2f7d4a;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.dory-line-del {
|
|
381
|
+
background: color-mix(in oklch, var(--destructive) 10%, transparent);
|
|
382
|
+
border-left-color: var(--destructive);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.dory-line-number {
|
|
386
|
+
display: inline-block;
|
|
387
|
+
width: 1.25rem;
|
|
388
|
+
margin-right: 1rem;
|
|
389
|
+
color: var(--muted-foreground);
|
|
390
|
+
user-select: none;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.dory-line-marker {
|
|
394
|
+
display: inline-block;
|
|
395
|
+
width: 2rem;
|
|
396
|
+
user-select: none;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.dory-line-add .dory-line-marker {
|
|
400
|
+
color: #2f7d4a;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.dory-line-del .dory-line-marker {
|
|
404
|
+
color: var(--destructive);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
:root.dark .dory-code .dory-line-add {
|
|
409
|
+
border-left-color: #6fbf87;
|
|
410
|
+
|
|
411
|
+
.dory-line-marker {
|
|
412
|
+
color: #6fbf87;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// === PDF DETAILS ===
|
|
417
|
+
.dory-lead {
|
|
418
|
+
margin-top: 0.75rem;
|
|
419
|
+
font-size: 0.9375rem;
|
|
420
|
+
line-height: 1.6;
|
|
421
|
+
color: var(--foreground);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
article .dory-h2 .dory-anchor {
|
|
425
|
+
margin-left: auto;
|
|
426
|
+
font-family: var(--font-mono, monospace);
|
|
427
|
+
font-size: 0.8125rem;
|
|
428
|
+
font-weight: 400;
|
|
429
|
+
letter-spacing: 0;
|
|
430
|
+
color: var(--muted-foreground);
|
|
431
|
+
text-decoration: none;
|
|
432
|
+
|
|
433
|
+
&:hover {
|
|
434
|
+
color: var(--primary);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Optional numbering for CardGroup and AccordionGroup (numbered prop)
|
|
439
|
+
.dory-cards-numbered {
|
|
440
|
+
counter-reset: dory-card;
|
|
441
|
+
|
|
442
|
+
.dory-card {
|
|
443
|
+
counter-increment: dory-card;
|
|
444
|
+
|
|
445
|
+
&::before {
|
|
446
|
+
content: counter(dory-card, decimal-leading-zero);
|
|
447
|
+
display: block;
|
|
448
|
+
margin-bottom: 1.25rem;
|
|
449
|
+
font-family: var(--font-mono, monospace);
|
|
450
|
+
font-size: 0.75rem;
|
|
451
|
+
color: var(--primary);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.dory-accordion-numbered {
|
|
457
|
+
counter-reset: dory-accordion;
|
|
458
|
+
|
|
459
|
+
[data-slot="accordion-item"] {
|
|
460
|
+
counter-increment: dory-accordion;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
[data-slot="accordion-trigger"] > div::before {
|
|
464
|
+
content: counter(dory-accordion, decimal-leading-zero);
|
|
465
|
+
margin-right: 0.25rem;
|
|
466
|
+
font-family: var(--font-mono, monospace);
|
|
467
|
+
font-size: 0.6875rem;
|
|
468
|
+
color: var(--primary);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.dory-latex-inline .katex {
|
|
473
|
+
font-size: 1.05em;
|
|
474
|
+
}
|
package/src/types/config.ts
CHANGED
|
@@ -128,7 +128,10 @@ export interface DoryConfig {
|
|
|
128
128
|
title?: string;
|
|
129
129
|
description?: string;
|
|
130
130
|
image?: string;
|
|
131
|
+
/** Origin the site is published at, e.g. "https://example.com" (no base path). */
|
|
131
132
|
url?: string;
|
|
133
|
+
/** Path prefix the site is served under, e.g. "/docs". Default: served at the root. */
|
|
134
|
+
basePath?: string;
|
|
132
135
|
twitter?: string;
|
|
133
136
|
favicon: string;
|
|
134
137
|
colors?: DoryColors;
|
package/src/ui/header.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import { ALL_NAVIGATION } from '../components/store';
|
|
|
9
9
|
import { MobileNavigation } from './mobile-navigation';
|
|
10
10
|
import { ModeToggle } from '@clidey/ux';
|
|
11
11
|
import { useIsEmbedded } from '../components/hooks';
|
|
12
|
+
import { assetUrl } from '../utils/base-path';
|
|
12
13
|
|
|
13
14
|
const config = docsConfig as DoryConfig;
|
|
14
15
|
const isThemeForced = config.theme?.mode === 'dark' || config.theme?.mode === 'light';
|
|
@@ -44,7 +45,7 @@ export function Header({ className }: { className?: string }) {
|
|
|
44
45
|
const isEmbedded = useIsEmbedded();
|
|
45
46
|
|
|
46
47
|
const { light, dark } = useMemo(() => {
|
|
47
|
-
return { light: config.logo.light, dark: config.logo.dark };
|
|
48
|
+
return { light: assetUrl(config.logo.light), dark: assetUrl(config.logo.dark) };
|
|
48
49
|
}, []);
|
|
49
50
|
|
|
50
51
|
useEffect(() => {
|
|
@@ -71,8 +72,8 @@ export function Header({ className }: { className?: string }) {
|
|
|
71
72
|
>
|
|
72
73
|
<div className="relative flex justify-between gap-6 sm:gap-8 md:grow items-center w-full px-4">
|
|
73
74
|
<div className="flex items-center grow gap-2">
|
|
74
|
-
<img src={
|
|
75
|
-
<img src={
|
|
75
|
+
<img src={light} alt="logo" className="w-8 hidden dark:block" />
|
|
76
|
+
<img src={dark} alt="logo" className="w-8 block dark:hidden" />
|
|
76
77
|
<h1 className="text-base lg:text-2xl font-bold">{config.name}</h1>
|
|
77
78
|
</div>
|
|
78
79
|
<nav className="hidden md:block">
|
package/src/ui/layout.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ComponentChildren } from 'preact';
|
|
2
|
-
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
|
3
3
|
import { Navigation } from '../components/navigation';
|
|
4
4
|
import { ALL_NAVIGATION, ALL_OPENAPI, completeFrontMatter, loadMDXFrontMatterForPath, loadAllMDXFrontMatter, preloadFrontmatter, ALL_ASYNCAPI, isFrontmatterReady } from '../components/store';
|
|
5
5
|
import docsConfig from '../../docs/dory.json' with { type: 'json' };
|
|
@@ -10,6 +10,7 @@ import { AsyncAPI } from '../mdx/async-api';
|
|
|
10
10
|
import { Header } from './header';
|
|
11
11
|
import { PrevNextLinks } from './prev-next-link';
|
|
12
12
|
import { TableOfContents } from './table-of-content';
|
|
13
|
+
import { PageMeta } from './page-meta';
|
|
13
14
|
import { Loading } from '../components/loading';
|
|
14
15
|
import Dropdown from '../components/dropdown';
|
|
15
16
|
import { useNotification } from '../components/notification';
|
|
@@ -17,6 +18,7 @@ import { SparkleIcon } from 'lucide-react';
|
|
|
17
18
|
import { useIsEmbedded } from '../components/hooks';
|
|
18
19
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
19
20
|
import { cn } from '@clidey/ux';
|
|
21
|
+
import { withBase } from '../utils/base-path';
|
|
20
22
|
|
|
21
23
|
const config = docsConfig as DoryConfig;
|
|
22
24
|
|
|
@@ -24,7 +26,7 @@ interface LayoutProps {
|
|
|
24
26
|
children: ComponentChildren;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
const prompt = (pathname: string) => `Please read and analyze the content from this documentation page: ${window.location.origin}${pathname}.mdx\n\n` +
|
|
29
|
+
const prompt = (pathname: string) => `Please read and analyze the content from this documentation page: ${window.location.origin}${withBase(pathname)}.mdx\n\n` +
|
|
28
30
|
`Then, help me understand and answer any questions I have about it. ` +
|
|
29
31
|
`Please provide clear, detailed explanations and examples where relevant.`;
|
|
30
32
|
|
|
@@ -44,13 +46,27 @@ export default function Layout({ children }: LayoutProps) {
|
|
|
44
46
|
const { showNotification } = useNotification();
|
|
45
47
|
const isEmbedded = useIsEmbedded();
|
|
46
48
|
|
|
49
|
+
// Client-side navigation keeps the previous scroll position, so start each new
|
|
50
|
+
// page at the top. The initial load keeps the browser's own position, and a
|
|
51
|
+
// hash means an in-page anchor is the target.
|
|
52
|
+
const isFirstRender = useRef(true);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (isFirstRender.current) {
|
|
55
|
+
isFirstRender.current = false;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!window.location.hash) {
|
|
59
|
+
window.scrollTo(0, 0);
|
|
60
|
+
}
|
|
61
|
+
}, [pathname]);
|
|
62
|
+
|
|
47
63
|
const handleOpenMDX = useCallback(() => {
|
|
48
|
-
window.open(`${window.location.origin}${pathname}.mdx`, '_blank');
|
|
64
|
+
window.open(`${window.location.origin}${withBase(pathname)}.mdx`, '_blank');
|
|
49
65
|
}, [pathname]);
|
|
50
66
|
|
|
51
67
|
const handleCopyMDX = useCallback(async () => {
|
|
52
68
|
try {
|
|
53
|
-
const response = await fetch(`${pathname}.mdx`);
|
|
69
|
+
const response = await fetch(`${withBase(pathname)}.mdx`);
|
|
54
70
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
55
71
|
await navigator.clipboard.writeText(await response.text());
|
|
56
72
|
showNotification('Copied to clipboard');
|
|
@@ -277,12 +293,14 @@ export default function Layout({ children }: LayoutProps) {
|
|
|
277
293
|
{group}
|
|
278
294
|
</h1>
|
|
279
295
|
)}
|
|
296
|
+
{page?.lead && <p className="dory-lead">{page.lead}</p>}
|
|
280
297
|
</header>
|
|
281
298
|
{
|
|
282
299
|
openAPIJSON == null && asyncAPIJSON == null &&
|
|
283
300
|
<Dropdown className="gap-0" buttonLabel={<SparkleIcon className="w-4 h-4" />} items={dropdownItems} />
|
|
284
301
|
}
|
|
285
302
|
</div>
|
|
303
|
+
<PageMeta meta={page?.meta} />
|
|
286
304
|
<div className="flex-1 min-h-[calc(100vh-2rem)]">
|
|
287
305
|
<ErrorBoundary fallback={(error) => (
|
|
288
306
|
<div className="flex flex-col items-center justify-center py-16 px-6 text-center">
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
type MetaValue = string | number | boolean | Date
|
|
2
|
+
|
|
3
|
+
const isoDate = /^\d{4}-\d{2}-\d{2}(T00:00:00(\.000)?Z)?$/
|
|
4
|
+
|
|
5
|
+
function formatValue(value: MetaValue): string {
|
|
6
|
+
const date = value instanceof Date ? value : typeof value === 'string' && isoDate.test(value) ? new Date(value) : null
|
|
7
|
+
if (date && !Number.isNaN(date.getTime())) {
|
|
8
|
+
return date.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' })
|
|
9
|
+
}
|
|
10
|
+
return String(value)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Label/value strip under the page title, from the frontmatter `meta` map:
|
|
15
|
+
*
|
|
16
|
+
* meta:
|
|
17
|
+
* Edition: Community
|
|
18
|
+
* Updated: 2026-10-12
|
|
19
|
+
*
|
|
20
|
+
* YAML dates render as "12 Oct 2026". Renders nothing when `meta` is absent.
|
|
21
|
+
*/
|
|
22
|
+
export function PageMeta({ meta }: { meta?: Record<string, MetaValue> }) {
|
|
23
|
+
if (!meta || typeof meta !== 'object') return null
|
|
24
|
+
const entries = Object.entries(meta)
|
|
25
|
+
if (entries.length === 0) return null
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<dl className="dory-page-meta mb-8 grid grid-cols-2 gap-x-6 gap-y-3 border-y border-[var(--border)] py-3 sm:grid-cols-3">
|
|
29
|
+
{entries.map(([label, value]) => (
|
|
30
|
+
<div key={label}>
|
|
31
|
+
<dt className="font-mono text-[11px] uppercase tracking-[0.18em] text-muted-foreground">{label}</dt>
|
|
32
|
+
<dd className="mt-0.5 text-sm font-semibold text-foreground">{formatValue(value)}</dd>
|
|
33
|
+
</div>
|
|
34
|
+
))}
|
|
35
|
+
</dl>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ArrowRightIcon } from "@heroicons/react/24/outline";
|
|
2
|
+
import { withBase } from '../utils/base-path';
|
|
2
3
|
import classNames from "classnames";
|
|
3
4
|
import { ALL_NAVIGATION } from "../components/store";
|
|
4
5
|
import { usePathname } from "../components/hooks";
|
|
@@ -15,15 +16,15 @@ function PageLink({
|
|
|
15
16
|
}) {
|
|
16
17
|
return (
|
|
17
18
|
<div {...props}>
|
|
18
|
-
<p className="font-display text-
|
|
19
|
+
<p className="font-display text-xs font-medium text-muted-foreground">
|
|
19
20
|
{dir === 'next' ? 'Next' : 'Previous'}
|
|
20
21
|
</p>
|
|
21
22
|
<div className="mt-1">
|
|
22
|
-
<a href={href} className={classNames('flex items-center gap-x-1 text-
|
|
23
|
+
<a href={withBase(href)} className={classNames('flex items-center gap-x-1 text-sm font-semibold hover:opacity-80', {
|
|
23
24
|
'flex-row-reverse': dir === 'previous',
|
|
24
25
|
},
|
|
25
26
|
)}>
|
|
26
|
-
<p className={classNames("flex items-center gap-x-1 text-
|
|
27
|
+
<p className={classNames("flex items-center gap-x-1 text-sm font-semibold hover:opacity-80", {
|
|
27
28
|
"flex-row-reverse": dir === "previous"
|
|
28
29
|
})}>
|
|
29
30
|
{title}
|
|
@@ -41,6 +41,14 @@ export function TableOfContents() {
|
|
|
41
41
|
const sections: Section[] = []
|
|
42
42
|
let currentSection: Section | null = null
|
|
43
43
|
|
|
44
|
+
// Heading text without the trailing "#" anchor link
|
|
45
|
+
const headingText = (heading: Element) =>
|
|
46
|
+
Array.from(heading.childNodes)
|
|
47
|
+
.filter((node) => !(node instanceof Element && node.classList.contains('dory-anchor')))
|
|
48
|
+
.map((node) => node.textContent ?? '')
|
|
49
|
+
.join('')
|
|
50
|
+
.trim()
|
|
51
|
+
|
|
44
52
|
headings.forEach((heading) => {
|
|
45
53
|
// Check if heading is a valid Element and is visible
|
|
46
54
|
if (!(heading instanceof Element)) return
|
|
@@ -48,7 +56,7 @@ export function TableOfContents() {
|
|
|
48
56
|
if (style.display === 'none' || style.visibility === 'hidden') return
|
|
49
57
|
|
|
50
58
|
// Prefer the build-time id from rehype-slug; only assign when missing
|
|
51
|
-
const id = heading.id || slugify(heading
|
|
59
|
+
const id = heading.id || slugify(headingText(heading))
|
|
52
60
|
if (!heading.id && id) {
|
|
53
61
|
heading.id = id
|
|
54
62
|
}
|
|
@@ -56,14 +64,14 @@ export function TableOfContents() {
|
|
|
56
64
|
if (heading.tagName === 'H2') {
|
|
57
65
|
currentSection = {
|
|
58
66
|
id,
|
|
59
|
-
title: heading
|
|
67
|
+
title: headingText(heading),
|
|
60
68
|
children: []
|
|
61
69
|
}
|
|
62
70
|
sections.push(currentSection)
|
|
63
71
|
} else if (heading.tagName === 'H3' && currentSection) {
|
|
64
72
|
currentSection.children.push({
|
|
65
73
|
id,
|
|
66
|
-
title: heading
|
|
74
|
+
title: headingText(heading)
|
|
67
75
|
})
|
|
68
76
|
}
|
|
69
77
|
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { assetUrl, joinBasePath, normalizeBasePath } from './base-path';
|
|
3
|
+
|
|
4
|
+
describe('normalizeBasePath', () => {
|
|
5
|
+
it('normalizes slashes', () => {
|
|
6
|
+
for (const value of ['docs', '/docs', '/docs/', ' /docs/ ', '//docs//']) {
|
|
7
|
+
expect(normalizeBasePath(value)).toBe('/docs');
|
|
8
|
+
}
|
|
9
|
+
expect(normalizeBasePath('/a/b/')).toBe('/a/b');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('treats root and non-strings as no base', () => {
|
|
13
|
+
for (const value of ['', '/', ' ', undefined, null, 3]) {
|
|
14
|
+
expect(normalizeBasePath(value)).toBe('');
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('joinBasePath', () => {
|
|
20
|
+
it('prefixes root-relative paths', () => {
|
|
21
|
+
expect(joinBasePath('/docs', '/intro')).toBe('/docs/intro');
|
|
22
|
+
expect(joinBasePath('/docs', '/')).toBe('/docs/');
|
|
23
|
+
expect(joinBasePath('/docs', '/frontmatter.json')).toBe('/docs/frontmatter.json');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('is a no-op without a base', () => {
|
|
27
|
+
expect(joinBasePath('', '/intro')).toBe('/intro');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('does not double-prefix or touch non-root URLs', () => {
|
|
31
|
+
expect(joinBasePath('/docs', '/docs/intro')).toBe('/docs/intro');
|
|
32
|
+
expect(joinBasePath('/docs', '/docs')).toBe('/docs');
|
|
33
|
+
expect(joinBasePath('/docs', '/docsy/intro')).toBe('/docs/docsy/intro');
|
|
34
|
+
for (const url of ['https://x.dev/a', '//cdn.dev/a', '#top', 'relative/a', 'mailto:a@b.c']) {
|
|
35
|
+
expect(joinBasePath('/docs', url)).toBe(url);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('assetUrl', () => {
|
|
41
|
+
it('strips config-style prefixes', () => {
|
|
42
|
+
expect(assetUrl('./logo/logo.png')).toBe('/logo/logo.png');
|
|
43
|
+
expect(assetUrl('logo/logo.png')).toBe('/logo/logo.png');
|
|
44
|
+
expect(assetUrl('/logo/logo.png')).toBe('/logo/logo.png');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalizes a configured base path to "" (site served at the origin root) or
|
|
5
|
+
* "/segment[/more]" with a leading slash and no trailing slash. Accepts
|
|
6
|
+
* "docs", "/docs", "/docs/" and "/" interchangeably; non-strings yield "".
|
|
7
|
+
*/
|
|
8
|
+
export function normalizeBasePath(value: unknown): string {
|
|
9
|
+
if (typeof value !== 'string') return '';
|
|
10
|
+
const trimmed = value.trim().replace(/^\/+|\/+$/g, '');
|
|
11
|
+
return trimmed ? `/${trimmed}` : '';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Prefixes a root-relative path ("/x") with `base`. Protocol URLs,
|
|
16
|
+
* protocol-relative URLs, anchors and paths already under `base` are
|
|
17
|
+
* returned unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export function joinBasePath(base: string, path: string): string {
|
|
20
|
+
if (!base || !path.startsWith('/') || path.startsWith('//')) return path;
|
|
21
|
+
if (path === base || path.startsWith(`${base}/`)) return path;
|
|
22
|
+
return `${base}${path}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The base path the site is served under, from Vite's `base` (which the
|
|
27
|
+
* configs derive from `basePath` in dory.json). "" when served at the root.
|
|
28
|
+
*/
|
|
29
|
+
export const BASE_PATH = normalizeBasePath(
|
|
30
|
+
typeof import.meta.env !== 'undefined' ? import.meta.env.BASE_URL : ''
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
/** Prefixes a root-relative path with the configured base path. */
|
|
34
|
+
export function withBase(path: string): string {
|
|
35
|
+
return joinBasePath(BASE_PATH, path);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Resolves a dory.json asset path such as "./logo/logo.png" to a site URL. */
|
|
39
|
+
export function assetUrl(path: string): string {
|
|
40
|
+
return withBase(`/${path.replace(/^\.?\/*/, '')}`);
|
|
41
|
+
}
|
package/vite.config.ssr.ts
CHANGED
|
@@ -3,11 +3,14 @@ import preact from '@preact/preset-vite';
|
|
|
3
3
|
import mdx from '@mdx-js/rollup';
|
|
4
4
|
import { unifiedMdxPreprocessor } from './src/plugins/unified-mdx-plugin';
|
|
5
5
|
import { getMdxConfig } from './src/config/mdx';
|
|
6
|
+
import { getBasePath, readDoryConfig } from './src/plugins/shared';
|
|
6
7
|
|
|
7
8
|
// Minimal Vite config for the SSR build.
|
|
8
9
|
// Only includes plugins needed for MDX compilation — no sitemap, prerender,
|
|
9
10
|
// analytics, or other build-time plugins that target the client output.
|
|
10
11
|
export default defineConfig({
|
|
12
|
+
// Must match vite.config.ts so SSR markup uses the same base-prefixed URLs.
|
|
13
|
+
base: `${getBasePath(readDoryConfig())}/`,
|
|
11
14
|
plugins: [
|
|
12
15
|
unifiedMdxPreprocessor(),
|
|
13
16
|
preact(),
|
package/vite.config.ts
CHANGED
|
@@ -15,8 +15,11 @@ import { sitemapGenerator } from './src/plugins/sitemap-generator';
|
|
|
15
15
|
import { robotsGenerator } from './src/plugins/robots-generator';
|
|
16
16
|
import { prerender } from './src/plugins/prerender';
|
|
17
17
|
import { getMdxConfig } from './src/config/mdx';
|
|
18
|
+
import { getBasePath, readDoryConfig } from './src/plugins/shared';
|
|
18
19
|
|
|
19
20
|
export default defineConfig(({ command }) => ({
|
|
21
|
+
// Serve and build under `basePath` from dory.json (asset URLs, import.meta.env.BASE_URL).
|
|
22
|
+
base: `${getBasePath(readDoryConfig())}/`,
|
|
20
23
|
plugins: [
|
|
21
24
|
// SINGLE SOURCE OF TRUTH: All MDX preprocessing happens in src/mdx/processor.ts
|
|
22
25
|
htmlFilesMiddleware(), // Must be first to intercept HTML file requests
|