@clidey/dory 0.46.0 → 0.48.0
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 +103 -45
- package/bin/dist/dory.js.map +1 -1
- package/index.html +11 -15
- package/package.json +35 -29
- package/scripts/render-routes.mjs +55 -15
- package/src/components/routes.tsx +38 -93
- package/src/components/search.tsx +1 -0
- package/src/components/store.ts +64 -88
- package/src/config/mdx.js +7 -3
- package/src/config/mdx.ts +6 -2
- package/src/entry-server.tsx +189 -66
- package/src/main.tsx +27 -5
- package/src/mdx/api-playground.tsx +2 -0
- package/src/mdx/code.tsx +8 -5
- package/src/mdx/fence.tsx +10 -2
- package/src/mdx/icon.tsx +21 -14
- package/src/mdx/latex.tsx +5 -0
- package/src/mdx/mdx.tsx +19 -11
- package/src/mdx/mermaid.tsx +10 -1
- package/src/mdx/processor.test.ts +47 -0
- package/src/mdx/step.tsx +3 -3
- package/src/mdx/websocket-playground.tsx +2 -0
- package/src/plugins/analytics-injector.ts +7 -5
- package/src/plugins/docs-assets.ts +5 -4
- package/src/plugins/frontmatter-dev-server.ts +23 -137
- package/src/plugins/frontmatter-generator.ts +64 -111
- package/src/plugins/html-files-middleware.test.ts +21 -0
- package/src/plugins/html-files-middleware.ts +27 -5
- package/src/plugins/html-metadata-injector.ts +94 -53
- package/src/plugins/llm-txt-dev-server.ts +25 -178
- package/src/plugins/llm-txt-generator.ts +94 -133
- package/src/plugins/prerender.ts +156 -50
- package/src/plugins/robots-generator.ts +15 -16
- package/src/plugins/sanitize.test.ts +104 -0
- package/src/plugins/sanitize.ts +91 -60
- package/src/plugins/shared.ts +139 -0
- package/src/plugins/sitemap-generator.ts +58 -44
- package/src/theme.scss +138 -15
- package/src/types/config.ts +42 -2
- package/src/types/globals.d.ts +7 -0
- package/src/ui/header.tsx +12 -4
- package/src/ui/layout.tsx +72 -52
- package/src/ui/table-of-content.tsx +19 -2
- package/src/utils/colors.ts +38 -18
- package/src/utils/fonts.ts +55 -12
- package/src/utils/theme.ts +29 -0
- package/vite.config.embed-loader.ts +3 -11
- package/vite.config.embed-widget.ts +3 -10
- package/vite.config.ssr.ts +2 -1
- package/vite.config.ts +13 -8
- package/scripts/generate-frontmatter-json.js +0 -154
- package/scripts/generate-llm-txt.js +0 -115
package/src/theme.scss
CHANGED
|
@@ -1,39 +1,162 @@
|
|
|
1
1
|
:root {
|
|
2
|
-
font-synthesis: none;
|
|
3
2
|
text-rendering: optimizeLegibility;
|
|
4
3
|
-webkit-font-smoothing: antialiased;
|
|
5
4
|
-moz-osx-font-smoothing: grayscale;
|
|
6
5
|
|
|
7
6
|
// Brand colors (dynamically set from dory.json via loadColors())
|
|
8
|
-
// These variables are set at runtime:
|
|
9
|
-
// --brand-primary: primary color
|
|
10
|
-
// --brand-light: light mode accent
|
|
11
|
-
// --brand-dark: dark mode accent
|
|
12
|
-
// --brand-foreground: legacy/fallback (set to primary)
|
|
13
|
-
|
|
14
|
-
// Default fallback colors (will be overridden by loadColors())
|
|
15
7
|
--brand-foreground: #0ea5e9;
|
|
16
8
|
--brand-primary: #0ea5e9;
|
|
17
9
|
--brand-light: #38bdf8;
|
|
18
10
|
--brand-dark: #0284c7;
|
|
19
11
|
|
|
12
|
+
// Font families
|
|
13
|
+
--font-body: inherit;
|
|
14
|
+
--font-heading: inherit;
|
|
15
|
+
--font-mono: inherit;
|
|
16
|
+
|
|
20
17
|
// ShadCN/clidey/ux compatible primary color
|
|
21
|
-
// Using light color for light mode
|
|
22
18
|
--primary: var(--brand-light);
|
|
23
19
|
--primary-rgb: var(--brand-light-rgb);
|
|
24
|
-
|
|
20
|
+
|
|
25
21
|
--ring: rgba(0, 0, 0, 0.1);
|
|
26
22
|
}
|
|
27
23
|
|
|
28
|
-
// Dark mode overrides
|
|
29
24
|
:root.dark {
|
|
30
|
-
// Use dark color for dark mode
|
|
31
25
|
--primary: var(--brand-dark);
|
|
32
26
|
--primary-rgb: var(--brand-dark-rgb);
|
|
33
|
-
|
|
34
27
|
--ring: rgba(255, 255, 255, 0.1);
|
|
35
28
|
}
|
|
36
|
-
|
|
29
|
+
|
|
30
|
+
// Apply background from CSS variables
|
|
31
|
+
html, body, #app {
|
|
32
|
+
background-color: var(--background);
|
|
33
|
+
color: var(--foreground);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// === THEMED MODE: override Tailwind utility classes ===
|
|
37
|
+
[data-dory-themed="true"] {
|
|
38
|
+
// Navigation sidebar text
|
|
39
|
+
.dory-navigation {
|
|
40
|
+
a {
|
|
41
|
+
color: var(--muted-foreground) !important;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
a:hover {
|
|
45
|
+
color: var(--foreground) !important;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
a.font-semibold {
|
|
49
|
+
color: var(--brand-foreground) !important;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
h2, h3 {
|
|
53
|
+
color: var(--foreground) !important;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
ul[role="list"] {
|
|
57
|
+
border-color: var(--border) !important;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Override hardcoded Tailwind text colors
|
|
62
|
+
.text-slate-500,
|
|
63
|
+
.text-slate-400,
|
|
64
|
+
.text-slate-600,
|
|
65
|
+
.text-zinc-500,
|
|
66
|
+
.text-zinc-400,
|
|
67
|
+
.text-gray-500,
|
|
68
|
+
.text-gray-400 {
|
|
69
|
+
color: var(--muted-foreground) !important;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.text-slate-900,
|
|
73
|
+
.text-zinc-900,
|
|
74
|
+
.text-gray-900 {
|
|
75
|
+
color: var(--foreground) !important;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Override hardcoded Tailwind border colors
|
|
79
|
+
.border-slate-100,
|
|
80
|
+
.border-slate-200,
|
|
81
|
+
.border-slate-800,
|
|
82
|
+
.border-zinc-200,
|
|
83
|
+
.border-zinc-800 {
|
|
84
|
+
border-color: var(--border) !important;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Header
|
|
88
|
+
.dory-header {
|
|
89
|
+
border-color: var(--border) !important;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Accordion & divider borders — subtle low opacity
|
|
93
|
+
[data-slot="accordion-item"],
|
|
94
|
+
hr,
|
|
95
|
+
.border-t,
|
|
96
|
+
.border-b {
|
|
97
|
+
border-color: color-mix(in oklch, var(--foreground) 8%, transparent) !important;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// === FONT OVERRIDES ===
|
|
102
|
+
[data-dory-themed="true"] {
|
|
103
|
+
// Page title — large display serif
|
|
104
|
+
.dory-page-title {
|
|
105
|
+
font-family: var(--font-heading, var(--font-body, inherit));
|
|
106
|
+
font-weight: 380;
|
|
107
|
+
font-variation-settings: "opsz" 144, "SOFT" 30;
|
|
108
|
+
letter-spacing: -0.02em;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Subtitle / eyebrow — mono, muted amber
|
|
112
|
+
.dory-subtitle {
|
|
113
|
+
font-family: var(--font-mono, monospace);
|
|
114
|
+
color: var(--brand-foreground);
|
|
115
|
+
opacity: 0.8;
|
|
116
|
+
letter-spacing: 0.15em;
|
|
117
|
+
font-size: 11px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Article headings — serif with proper optical sizing
|
|
121
|
+
article h2 {
|
|
122
|
+
font-family: var(--font-heading, var(--font-body, inherit));
|
|
123
|
+
font-weight: 400;
|
|
124
|
+
font-variation-settings: "opsz" 48, "SOFT" 50;
|
|
125
|
+
letter-spacing: -0.015em;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
article h3,
|
|
129
|
+
article h4 {
|
|
130
|
+
font-family: var(--font-body, inherit);
|
|
131
|
+
font-weight: 600;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
code, pre {
|
|
135
|
+
font-family: var(--font-mono, monospace);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// === CODE BLOCK STYLING ===
|
|
140
|
+
// Make code blocks translucent to match app feel
|
|
141
|
+
[data-dory-themed="true"] {
|
|
142
|
+
[data-slot="card"],
|
|
143
|
+
.bg-card {
|
|
144
|
+
background-color: transparent !important;
|
|
145
|
+
border-color: var(--border) !important;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
pre {
|
|
149
|
+
background-color: transparent !important;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Inline code
|
|
153
|
+
.dory-mdx-code {
|
|
154
|
+
background-color: transparent !important;
|
|
155
|
+
border: 1px solid var(--border);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// === DEFAULT STYLES ===
|
|
37
160
|
article h2:first-of-type {
|
|
38
161
|
display: none;
|
|
39
162
|
}
|
|
@@ -69,4 +192,4 @@ article img {
|
|
|
69
192
|
outline: none !important;
|
|
70
193
|
border: 0px;
|
|
71
194
|
box-shadow: none !important;
|
|
72
|
-
}
|
|
195
|
+
}
|
package/src/types/config.ts
CHANGED
|
@@ -6,12 +6,29 @@ export interface DoryColors {
|
|
|
6
6
|
primary: string;
|
|
7
7
|
light: string;
|
|
8
8
|
dark: string;
|
|
9
|
+
background?: string;
|
|
10
|
+
surface?: string;
|
|
11
|
+
surfaceElevated?: string;
|
|
12
|
+
foreground?: string;
|
|
13
|
+
mutedForeground?: string;
|
|
14
|
+
border?: string;
|
|
15
|
+
accent?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DoryFontDefinition {
|
|
19
|
+
family: string;
|
|
20
|
+
source?: string;
|
|
21
|
+
format?: string;
|
|
22
|
+
weight?: string;
|
|
9
23
|
}
|
|
10
24
|
|
|
11
25
|
export interface DoryFonts {
|
|
12
26
|
family: string;
|
|
13
|
-
source
|
|
14
|
-
format
|
|
27
|
+
source?: string;
|
|
28
|
+
format?: string;
|
|
29
|
+
stylesheet?: string;
|
|
30
|
+
heading?: DoryFontDefinition;
|
|
31
|
+
mono?: DoryFontDefinition;
|
|
15
32
|
}
|
|
16
33
|
|
|
17
34
|
export interface DoryLogo {
|
|
@@ -85,6 +102,27 @@ export interface DoryAnalytics {
|
|
|
85
102
|
};
|
|
86
103
|
}
|
|
87
104
|
|
|
105
|
+
export interface DorySeo {
|
|
106
|
+
/** Applied to page titles, e.g. "%s — WhoDB"; %s is the page title. Default: no template. */
|
|
107
|
+
titleTemplate?: string;
|
|
108
|
+
keywords?: string[];
|
|
109
|
+
author?: string;
|
|
110
|
+
/** Absolute URL or path resolved against config.url. */
|
|
111
|
+
ogImage?: string;
|
|
112
|
+
twitterCard?: 'summary' | 'summary_large_image';
|
|
113
|
+
/** e.g. "en_US" */
|
|
114
|
+
locale?: string;
|
|
115
|
+
/** Extra lines appended to robots.txt. */
|
|
116
|
+
robotsTxt?: string[];
|
|
117
|
+
/** Site-wide meta robots noindex (for staging environments). */
|
|
118
|
+
noindex?: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface DoryTheme {
|
|
122
|
+
mode?: 'light' | 'dark' | 'system';
|
|
123
|
+
radius?: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
88
126
|
export interface DoryConfig {
|
|
89
127
|
name: string;
|
|
90
128
|
title?: string;
|
|
@@ -95,8 +133,10 @@ export interface DoryConfig {
|
|
|
95
133
|
favicon: string;
|
|
96
134
|
colors?: DoryColors;
|
|
97
135
|
fonts?: DoryFonts;
|
|
136
|
+
theme?: DoryTheme;
|
|
98
137
|
navigation: DoryNavigation;
|
|
99
138
|
logo: DoryLogo;
|
|
100
139
|
embed?: DoryEmbedConfig;
|
|
101
140
|
analytics?: DoryAnalytics;
|
|
141
|
+
seo?: DorySeo;
|
|
102
142
|
}
|
package/src/ui/header.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import { ModeToggle } from '@clidey/ux';
|
|
|
11
11
|
import { useIsEmbedded } from '../components/hooks';
|
|
12
12
|
|
|
13
13
|
const config = docsConfig as DoryConfig;
|
|
14
|
+
const isThemeForced = config.theme?.mode === 'dark' || config.theme?.mode === 'light';
|
|
14
15
|
|
|
15
16
|
function TopLevelNavItem({ href, children, isActive }: { href: string; children: string; isActive?: boolean }) {
|
|
16
17
|
return (
|
|
@@ -30,6 +31,13 @@ function TopLevelNavItem({ href, children, isActive }: { href: string; children:
|
|
|
30
31
|
);
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Site header with logo, top-level navigation, search, and theme toggle.
|
|
36
|
+
*
|
|
37
|
+
* SYNC NOTE: src/entry-server.tsx renders a static SSR copy of this header's
|
|
38
|
+
* structure (the <header> block inside render()). If you change the markup
|
|
39
|
+
* structure here, update entry-server.tsx to match or hydration will mismatch.
|
|
40
|
+
*/
|
|
33
41
|
export function Header({ className }: { className?: string }) {
|
|
34
42
|
const [isScrolled, setIsScrolled] = useState(false);
|
|
35
43
|
const pathname = usePathname();
|
|
@@ -53,13 +61,13 @@ export function Header({ className }: { className?: string }) {
|
|
|
53
61
|
return (
|
|
54
62
|
<header
|
|
55
63
|
className={classNames(
|
|
56
|
-
'sticky top-0 z-50 flex flex-none flex-wrap items-center justify-between
|
|
64
|
+
'dory-header sticky top-0 z-50 flex flex-none flex-wrap items-center justify-between px-4 py-5 sm:px-6 lg:px-8 border-b',
|
|
57
65
|
className,
|
|
58
66
|
{
|
|
59
|
-
'
|
|
60
|
-
'dark:bg-transparent': !isScrolled,
|
|
67
|
+
'backdrop-blur-sm': isScrolled,
|
|
61
68
|
},
|
|
62
69
|
)}
|
|
70
|
+
style={{ backgroundColor: 'var(--background)', borderColor: 'var(--border)' }}
|
|
63
71
|
>
|
|
64
72
|
<div className="relative flex justify-between gap-6 sm:gap-8 md:grow items-center w-full px-4">
|
|
65
73
|
<div className="flex items-center grow gap-2">
|
|
@@ -90,7 +98,7 @@ export function Header({ className }: { className?: string }) {
|
|
|
90
98
|
<MobileSearch />
|
|
91
99
|
<Search />
|
|
92
100
|
{
|
|
93
|
-
!isEmbedded &&
|
|
101
|
+
!isEmbedded && !isThemeForced &&
|
|
94
102
|
<ModeToggle data-testid="mode-toggle" />
|
|
95
103
|
}
|
|
96
104
|
<MobileNavigation />
|
package/src/ui/layout.tsx
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ComponentChildren } from 'preact';
|
|
2
2
|
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks';
|
|
3
3
|
import { Navigation } from '../components/navigation';
|
|
4
|
-
import { ALL_NAVIGATION, ALL_OPENAPI, completeFrontMatter, loadMDXFrontMatterForPath, loadAllMDXFrontMatter,
|
|
4
|
+
import { ALL_NAVIGATION, ALL_OPENAPI, completeFrontMatter, loadMDXFrontMatterForPath, loadAllMDXFrontMatter, preloadFrontmatter, ALL_ASYNCAPI, isFrontmatterReady } from '../components/store';
|
|
5
|
+
import docsConfig from '../../docs/dory.json' with { type: 'json' };
|
|
6
|
+
import type { DoryConfig } from '../types/config';
|
|
5
7
|
import { usePathname } from '../components/hooks';
|
|
6
8
|
import { OpenAPI } from '../mdx/open-api';
|
|
7
9
|
import { AsyncAPI } from '../mdx/async-api';
|
|
@@ -16,6 +18,8 @@ import { useIsEmbedded } from '../components/hooks';
|
|
|
16
18
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
17
19
|
import { cn } from '@clidey/ux';
|
|
18
20
|
|
|
21
|
+
const config = docsConfig as DoryConfig;
|
|
22
|
+
|
|
19
23
|
interface LayoutProps {
|
|
20
24
|
children: ComponentChildren;
|
|
21
25
|
}
|
|
@@ -24,9 +28,19 @@ const prompt = (pathname: string) => `Please read and analyze the content from t
|
|
|
24
28
|
`Then, help me understand and answer any questions I have about it. ` +
|
|
25
29
|
`Please provide clear, detailed explanations and examples where relevant.`;
|
|
26
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Main documentation layout: header, sidebar navigation, article content,
|
|
33
|
+
* table of contents, and prev/next links.
|
|
34
|
+
*
|
|
35
|
+
* SYNC NOTE: src/entry-server.tsx renders a static SSR copy of this layout's
|
|
36
|
+
* shell (the #container / sidebar nav / main / article wrappers in render()).
|
|
37
|
+
* If you change that markup structure here, update entry-server.tsx to match
|
|
38
|
+
* or hydration will mismatch.
|
|
39
|
+
*/
|
|
27
40
|
export default function Layout({ children }: LayoutProps) {
|
|
28
|
-
const [loading, setLoading] = useState(
|
|
29
|
-
const
|
|
41
|
+
const [loading, setLoading] = useState(!isFrontmatterReady());
|
|
42
|
+
const rawPathname = usePathname();
|
|
43
|
+
const pathname = rawPathname === '/' ? ALL_NAVIGATION[0]?.groups[0]?.pages[0]?.href ?? '/' : rawPathname;
|
|
30
44
|
const { showNotification } = useNotification();
|
|
31
45
|
const isEmbedded = useIsEmbedded();
|
|
32
46
|
|
|
@@ -34,12 +48,17 @@ export default function Layout({ children }: LayoutProps) {
|
|
|
34
48
|
window.open(`${window.location.origin}${pathname}.mdx`, '_blank');
|
|
35
49
|
}, [pathname]);
|
|
36
50
|
|
|
37
|
-
const handleCopyMDX = useCallback(() => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
const handleCopyMDX = useCallback(async () => {
|
|
52
|
+
try {
|
|
53
|
+
const response = await fetch(`${pathname}.mdx`);
|
|
54
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
55
|
+
await navigator.clipboard.writeText(await response.text());
|
|
56
|
+
showNotification('Copied to clipboard');
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.warn(`Failed to fetch ${pathname}.mdx:`, error);
|
|
59
|
+
showNotification('Could not copy MDX', 'The raw MDX file is not available.');
|
|
60
|
+
}
|
|
61
|
+
}, [pathname, showNotification]);
|
|
43
62
|
const handleOpenChatGPT = useCallback(() => {
|
|
44
63
|
const url = `https://chat.openai.com/?q=${encodeURIComponent(prompt(pathname))}`;
|
|
45
64
|
window.open(url, '_blank');
|
|
@@ -69,9 +88,9 @@ export default function Layout({ children }: LayoutProps) {
|
|
|
69
88
|
], [handleOpenMDX, handleCopyMDX, handleOpenChatGPT, handleOpenAnthropic]);
|
|
70
89
|
|
|
71
90
|
useEffect(() => {
|
|
72
|
-
// Try to preload frontmatter first (optimized approach)
|
|
91
|
+
// Try to preload frontmatter first (optimized approach).
|
|
92
|
+
// If inline data was available from SSR, preloadFrontmatter() resolves immediately.
|
|
73
93
|
preloadFrontmatter().then(() => {
|
|
74
|
-
// If preloading succeeds, we're done
|
|
75
94
|
setLoading(false);
|
|
76
95
|
}).catch(() => {
|
|
77
96
|
// Fallback to the original approach
|
|
@@ -87,6 +106,7 @@ export default function Layout({ children }: LayoutProps) {
|
|
|
87
106
|
});
|
|
88
107
|
}, [pathname]);
|
|
89
108
|
|
|
109
|
+
|
|
90
110
|
const { title: group, group: title, page } = useMemo(() => {
|
|
91
111
|
const group = ALL_NAVIGATION.flatMap((tab) =>
|
|
92
112
|
tab.groups.find((group) =>
|
|
@@ -101,7 +121,9 @@ export default function Layout({ children }: LayoutProps) {
|
|
|
101
121
|
// Update document title and meta tags when page changes
|
|
102
122
|
useEffect(() => {
|
|
103
123
|
if (page?.title) {
|
|
104
|
-
|
|
124
|
+
// Mirror the build-time SSR title template (config.seo.titleTemplate, e.g. "%s — Site")
|
|
125
|
+
const titleTemplate = (config as { seo?: { titleTemplate?: string } }).seo?.titleTemplate;
|
|
126
|
+
document.title = titleTemplate ? titleTemplate.replace('%s', page.title) : page.title;
|
|
105
127
|
|
|
106
128
|
// Update Open Graph title
|
|
107
129
|
const ogTitle = document.querySelector('meta[property="og:title"]');
|
|
@@ -151,44 +173,20 @@ export default function Layout({ children }: LayoutProps) {
|
|
|
151
173
|
}
|
|
152
174
|
canonical.setAttribute('href', `${window.location.origin}${window.location.pathname}`);
|
|
153
175
|
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
'@context': 'https://schema.org',
|
|
157
|
-
'@graph': [
|
|
158
|
-
{
|
|
159
|
-
'@type': 'Article',
|
|
160
|
-
'headline': page?.title || document.title,
|
|
161
|
-
'description': page?.description || '',
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
'@type': 'BreadcrumbList',
|
|
165
|
-
'itemListElement': [
|
|
166
|
-
{
|
|
167
|
-
'@type': 'ListItem',
|
|
168
|
-
'position': 1,
|
|
169
|
-
'name': 'Home',
|
|
170
|
-
'item': window.location.origin
|
|
171
|
-
},
|
|
172
|
-
...(page?.title ? [{
|
|
173
|
-
'@type': 'ListItem',
|
|
174
|
-
'position': 2,
|
|
175
|
-
'name': page.title,
|
|
176
|
-
'item': window.location.href
|
|
177
|
-
}] : [])
|
|
178
|
-
]
|
|
179
|
-
}
|
|
180
|
-
]
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
let ldScript = document.querySelector('script[type="application/ld+json"]');
|
|
184
|
-
if (!ldScript) {
|
|
185
|
-
ldScript = document.createElement('script');
|
|
186
|
-
ldScript.setAttribute('type', 'application/ld+json');
|
|
187
|
-
document.head.appendChild(ldScript);
|
|
188
|
-
}
|
|
189
|
-
ldScript.textContent = JSON.stringify(structuredData);
|
|
176
|
+
// JSON-LD structured data is emitted at build time (prerender) — crawlers consume
|
|
177
|
+
// that version, so it is intentionally not rewritten on client-side navigation.
|
|
190
178
|
}, [page]);
|
|
191
179
|
|
|
180
|
+
const isLandingPage = useMemo(() => {
|
|
181
|
+
const firstTab = ALL_NAVIGATION[0];
|
|
182
|
+
if (!firstTab) return false;
|
|
183
|
+
return (
|
|
184
|
+
firstTab.groups.length === 1 &&
|
|
185
|
+
firstTab.groups[0].pages.length === 1 &&
|
|
186
|
+
pathname === firstTab.groups[0].pages[0].href
|
|
187
|
+
);
|
|
188
|
+
}, [pathname]);
|
|
189
|
+
|
|
192
190
|
const tab = useMemo(() => {
|
|
193
191
|
return completeFrontMatter.find((page) => page.path === pathname);
|
|
194
192
|
}, [pathname, loading]);
|
|
@@ -225,6 +223,28 @@ export default function Layout({ children }: LayoutProps) {
|
|
|
225
223
|
|
|
226
224
|
if (loading) return <div className="h-[25vh] flex grow"><Loading /></div>;
|
|
227
225
|
|
|
226
|
+
if (isLandingPage) {
|
|
227
|
+
return (
|
|
228
|
+
<div className="flex w-full flex-col">
|
|
229
|
+
<Header />
|
|
230
|
+
<main className="w-full">
|
|
231
|
+
<ErrorBoundary fallback={(error) => (
|
|
232
|
+
<div className="flex flex-col items-center justify-center py-16 px-6 text-center">
|
|
233
|
+
<p className="text-lg font-semibold text-red-700 dark:text-red-300 mb-2">
|
|
234
|
+
This page failed to render
|
|
235
|
+
</p>
|
|
236
|
+
<pre className="text-sm text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-md p-3 max-w-md overflow-auto">
|
|
237
|
+
{error.message}
|
|
238
|
+
</pre>
|
|
239
|
+
</div>
|
|
240
|
+
)}>
|
|
241
|
+
{children}
|
|
242
|
+
</ErrorBoundary>
|
|
243
|
+
</main>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
228
248
|
return (
|
|
229
249
|
<div className="flex w-full flex-col">
|
|
230
250
|
<Header />
|
|
@@ -248,14 +268,14 @@ export default function Layout({ children }: LayoutProps) {
|
|
|
248
268
|
<div className="flex justify-between items-start">
|
|
249
269
|
<header className="mb-8">
|
|
250
270
|
{title && (
|
|
251
|
-
<
|
|
271
|
+
<p className="dory-subtitle text-sm font-medium tracking-widest uppercase mb-2">
|
|
252
272
|
{title}
|
|
253
|
-
</
|
|
273
|
+
</p>
|
|
254
274
|
)}
|
|
255
275
|
{group && (
|
|
256
|
-
<
|
|
276
|
+
<h1 className="dory-page-title text-4xl tracking-tight">
|
|
257
277
|
{group}
|
|
258
|
-
</
|
|
278
|
+
</h1>
|
|
259
279
|
)}
|
|
260
280
|
</header>
|
|
261
281
|
{
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from 'react'
|
|
2
2
|
import classNames from 'classnames'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Slugifies heading text matching rehype-slug (github-slugger) behavior:
|
|
6
|
+
* lowercase, strip punctuation, spaces to dashes. Used only as a fallback
|
|
7
|
+
* for headings that did not get an id at build time, so client-assigned
|
|
8
|
+
* anchors agree with SSR-assigned ones.
|
|
9
|
+
*/
|
|
10
|
+
function slugify(text: string): string {
|
|
11
|
+
return text
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.trim()
|
|
14
|
+
.replace(/[^\p{L}\p{N}\s_-]/gu, '')
|
|
15
|
+
.replace(/\s+/g, '-')
|
|
16
|
+
}
|
|
17
|
+
|
|
4
18
|
export type Section = {
|
|
5
19
|
id: string
|
|
6
20
|
title: string
|
|
@@ -33,8 +47,11 @@ export function TableOfContents() {
|
|
|
33
47
|
const style = window.getComputedStyle(heading)
|
|
34
48
|
if (style.display === 'none' || style.visibility === 'hidden') return
|
|
35
49
|
|
|
36
|
-
|
|
37
|
-
heading.id
|
|
50
|
+
// Prefer the build-time id from rehype-slug; only assign when missing
|
|
51
|
+
const id = heading.id || slugify(heading.textContent || '')
|
|
52
|
+
if (!heading.id && id) {
|
|
53
|
+
heading.id = id
|
|
54
|
+
}
|
|
38
55
|
|
|
39
56
|
if (heading.tagName === 'H2') {
|
|
40
57
|
currentSection = {
|
package/src/utils/colors.ts
CHANGED
|
@@ -3,52 +3,72 @@ import type { DoryConfig } from '../types/config';
|
|
|
3
3
|
|
|
4
4
|
const config = docsConfig as DoryConfig;
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* Converts hex color to RGB values for CSS variable usage
|
|
8
|
-
*/
|
|
9
6
|
function hexToRgb(hex: string): string {
|
|
10
|
-
// Remove # if present
|
|
11
7
|
const cleanHex = hex.replace('#', '');
|
|
12
|
-
|
|
13
|
-
// Parse hex values
|
|
14
8
|
const r = parseInt(cleanHex.substring(0, 2), 16);
|
|
15
9
|
const g = parseInt(cleanHex.substring(2, 4), 16);
|
|
16
10
|
const b = parseInt(cleanHex.substring(4, 6), 16);
|
|
17
|
-
|
|
18
11
|
return `${r} ${g} ${b}`;
|
|
19
12
|
}
|
|
20
13
|
|
|
21
|
-
/**
|
|
22
|
-
* Load and apply brand colors from dory.json configuration
|
|
23
|
-
* Sets CSS variables for use throughout the application
|
|
24
|
-
*/
|
|
25
14
|
export function loadColors() {
|
|
26
15
|
const { colors } = config;
|
|
27
16
|
if (!colors) return;
|
|
28
17
|
|
|
29
|
-
// Set CSS variables on the root element for both light and dark modes
|
|
30
18
|
const root = document.documentElement;
|
|
31
19
|
|
|
32
|
-
//
|
|
20
|
+
// Brand accent colors
|
|
33
21
|
if (colors.primary) {
|
|
34
22
|
root.style.setProperty('--brand-primary', colors.primary);
|
|
35
23
|
root.style.setProperty('--brand-primary-rgb', hexToRgb(colors.primary));
|
|
24
|
+
root.style.setProperty('--brand-foreground', colors.primary);
|
|
36
25
|
}
|
|
37
26
|
|
|
38
|
-
// Set light mode brand color
|
|
39
27
|
if (colors.light) {
|
|
40
28
|
root.style.setProperty('--brand-light', colors.light);
|
|
41
29
|
root.style.setProperty('--brand-light-rgb', hexToRgb(colors.light));
|
|
42
30
|
}
|
|
43
31
|
|
|
44
|
-
// Set dark mode brand color
|
|
45
32
|
if (colors.dark) {
|
|
46
33
|
root.style.setProperty('--brand-dark', colors.dark);
|
|
47
34
|
root.style.setProperty('--brand-dark-rgb', hexToRgb(colors.dark));
|
|
48
35
|
}
|
|
49
36
|
|
|
50
|
-
//
|
|
51
|
-
if (colors.
|
|
52
|
-
root.style.setProperty('--
|
|
37
|
+
// Override shadcn/ux theme variables
|
|
38
|
+
if (colors.background) {
|
|
39
|
+
root.style.setProperty('--background', colors.background);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (colors.foreground) {
|
|
43
|
+
root.style.setProperty('--foreground', colors.foreground);
|
|
44
|
+
root.style.setProperty('--card-foreground', colors.foreground);
|
|
45
|
+
root.style.setProperty('--popover-foreground', colors.foreground);
|
|
46
|
+
root.style.setProperty('--secondary-foreground', colors.foreground);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (colors.surface) {
|
|
50
|
+
root.style.setProperty('--card', colors.surface);
|
|
51
|
+
root.style.setProperty('--popover', colors.surface);
|
|
52
|
+
root.style.setProperty('--sidebar', colors.surface);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (colors.surfaceElevated) {
|
|
56
|
+
root.style.setProperty('--muted', colors.surfaceElevated);
|
|
57
|
+
root.style.setProperty('--secondary', colors.surfaceElevated);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (colors.mutedForeground) {
|
|
61
|
+
root.style.setProperty('--muted-foreground', colors.mutedForeground);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (colors.border) {
|
|
65
|
+
root.style.setProperty('--border', colors.border);
|
|
66
|
+
root.style.setProperty('--input', colors.border);
|
|
67
|
+
root.style.setProperty('--sidebar-border', colors.border);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (colors.accent) {
|
|
71
|
+
root.style.setProperty('--accent', colors.accent);
|
|
72
|
+
root.style.setProperty('--ring', colors.accent);
|
|
53
73
|
}
|
|
54
74
|
}
|