idcmd 0.0.1 → 0.0.2
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/LICENSE +21 -0
- package/README.md +96 -2
- package/package.json +52 -6
- package/public/_idcmd/live-reload.js +18 -0
- package/public/_idcmd/llm-menu.js +153 -0
- package/public/_idcmd/nav-prefetch.js +30 -0
- package/public/_idcmd/right-rail-scrollspy.js +262 -0
- package/public/anthropic-black.svg +16 -0
- package/public/anthropic-white.svg +16 -0
- package/public/favicon.svg +13 -0
- package/public/live-reload.js +18 -0
- package/public/llm-menu.js +153 -0
- package/public/openai-black.svg +15 -0
- package/public/openai-white.svg +15 -0
- package/public/right-rail-scrollspy.js +262 -0
- package/src/build.ts +230 -0
- package/src/cli/args.ts +101 -0
- package/src/cli/commands/build.ts +43 -0
- package/src/cli/commands/deploy.ts +82 -0
- package/src/cli/commands/dev.ts +79 -0
- package/src/cli/commands/init.ts +211 -0
- package/src/cli/commands/preview.ts +57 -0
- package/src/cli/fs.ts +47 -0
- package/src/cli/main.ts +120 -0
- package/src/cli/normalize.ts +26 -0
- package/src/cli/path.ts +30 -0
- package/src/cli/prompt.ts +74 -0
- package/src/cli/run.ts +17 -0
- package/src/cli/version.ts +12 -0
- package/src/cli.ts +6 -0
- package/src/client/index.ts +7 -0
- package/src/content/components/expand.ts +351 -0
- package/src/content/components/install-tabs.ts +120 -0
- package/src/content/components/registry.ts +12 -0
- package/src/content/components/types.ts +21 -0
- package/src/content/frontmatter.ts +89 -0
- package/src/content/icons.ts +78 -0
- package/src/content/llms.ts +94 -0
- package/src/content/meta.ts +92 -0
- package/src/content/navigation.ts +156 -0
- package/src/content/paths.ts +34 -0
- package/src/content/store.ts +10 -0
- package/src/project/paths.ts +86 -0
- package/src/render/layout-loader.ts +46 -0
- package/src/render/layout.tsx +340 -0
- package/src/render/markdown.ts +14 -0
- package/src/render/page-renderer.ts +321 -0
- package/src/render/right-rail.tsx +250 -0
- package/src/render/toc.ts +66 -0
- package/src/search/api.ts +76 -0
- package/src/search/contract.ts +44 -0
- package/src/search/index.ts +265 -0
- package/src/search/page.tsx +96 -0
- package/src/search/server-page.ts +99 -0
- package/src/seo/files.ts +124 -0
- package/src/seo/server.ts +103 -0
- package/src/server/headers.ts +10 -0
- package/src/server/live-reload.ts +121 -0
- package/src/server/static.ts +59 -0
- package/src/server/user-routes.ts +209 -0
- package/src/server.ts +234 -0
- package/src/site/config.ts +244 -0
- package/src/site/url-policy.ts +60 -0
- package/src/site/urls.ts +46 -0
- package/templates/default/README.md +26 -0
- package/templates/default/package.json +29 -0
- package/templates/default/site/client/layout.tsx +2 -0
- package/templates/default/site/client/right-rail.tsx +1 -0
- package/templates/default/site/client/search-page.tsx +1 -0
- package/templates/default/site/content/404.md +8 -0
- package/templates/default/site/content/about.md +10 -0
- package/templates/default/site/content/index.md +10 -0
- package/templates/default/site/icons/file.svg +1 -0
- package/templates/default/site/icons/home.svg +1 -0
- package/templates/default/site/icons/info.svg +1 -0
- package/templates/default/site/public/_idcmd/live-reload.js +18 -0
- package/templates/default/site/public/_idcmd/llm-menu.js +153 -0
- package/templates/default/site/public/_idcmd/nav-prefetch.js +30 -0
- package/templates/default/site/public/_idcmd/right-rail-scrollspy.js +262 -0
- package/templates/default/site/public/anthropic-white.svg +16 -0
- package/templates/default/site/public/favicon.svg +13 -0
- package/templates/default/site/public/openai-white.svg +15 -0
- package/templates/default/site/server/routes/api/hello.ts +2 -0
- package/templates/default/site/server/server.ts +4 -0
- package/templates/default/site/site.jsonc +21 -0
- package/templates/default/site/styles/tailwind.css +452 -0
- package/templates/default/tsconfig.json +23 -0
- package/templates/default/vercel.json +7 -0
- package/index.js +0 -2
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { getMarkdownFilePath } from "./paths";
|
|
2
|
+
|
|
3
|
+
export const getMarkdownFile = async (slug: string): Promise<string | null> => {
|
|
4
|
+
const filePath = await getMarkdownFilePath(slug);
|
|
5
|
+
const file = Bun.file(filePath);
|
|
6
|
+
if (await file.exists()) {
|
|
7
|
+
return file.text();
|
|
8
|
+
}
|
|
9
|
+
return null;
|
|
10
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const DEFAULT_SITE_DIR = "site";
|
|
2
|
+
const DEFAULT_DIST_DIR = "dist";
|
|
3
|
+
const ASSET_PREFIX = "/_idcmd" as const;
|
|
4
|
+
|
|
5
|
+
export interface ProjectPaths {
|
|
6
|
+
assetPrefix: typeof ASSET_PREFIX;
|
|
7
|
+
contentDir: string;
|
|
8
|
+
distDir: string;
|
|
9
|
+
iconsDir: string;
|
|
10
|
+
publicDir: string;
|
|
11
|
+
routesDir: string;
|
|
12
|
+
siteConfigPath: string;
|
|
13
|
+
siteDir: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ResolveProjectPathsOptions {
|
|
17
|
+
cwd?: string;
|
|
18
|
+
distDir?: string;
|
|
19
|
+
siteDir?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const trimTrailingSlash = (value: string): string =>
|
|
23
|
+
value.endsWith("/") ? value.replaceAll(/\/+$/g, "") : value;
|
|
24
|
+
|
|
25
|
+
const joinPath = (...parts: string[]): string => {
|
|
26
|
+
const filtered = parts.filter((p) => p.length > 0);
|
|
27
|
+
if (filtered.length === 0) {
|
|
28
|
+
return ".";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const normalized = filtered.map((p, idx) => {
|
|
32
|
+
const trimmed =
|
|
33
|
+
idx === 0 ? trimTrailingSlash(p) : p.replaceAll(/^\/+/g, "");
|
|
34
|
+
return trimTrailingSlash(trimmed);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return normalized.join("/");
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const hasNewLayout = (cwd: string, siteDirName: string): Promise<boolean> =>
|
|
41
|
+
Bun.file(joinPath(cwd, siteDirName, "site.jsonc")).exists();
|
|
42
|
+
|
|
43
|
+
const buildPaths = (
|
|
44
|
+
cwd: string,
|
|
45
|
+
rootDir: string,
|
|
46
|
+
distDirName: string
|
|
47
|
+
): ProjectPaths => {
|
|
48
|
+
const distDir = joinPath(cwd, distDirName);
|
|
49
|
+
const contentDir = joinPath(rootDir, "content");
|
|
50
|
+
const publicDir = joinPath(rootDir, "public");
|
|
51
|
+
const iconsDir = joinPath(rootDir, "icons");
|
|
52
|
+
const routesDir = joinPath(rootDir, "server", "routes");
|
|
53
|
+
const siteConfigPath = joinPath(rootDir, "site.jsonc");
|
|
54
|
+
|
|
55
|
+
const siteDir = rootDir === cwd ? null : rootDir;
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
assetPrefix: ASSET_PREFIX,
|
|
59
|
+
contentDir,
|
|
60
|
+
distDir,
|
|
61
|
+
iconsDir,
|
|
62
|
+
publicDir,
|
|
63
|
+
routesDir,
|
|
64
|
+
siteConfigPath,
|
|
65
|
+
siteDir,
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const resolveProjectPaths = async (
|
|
70
|
+
options: ResolveProjectPathsOptions = {}
|
|
71
|
+
): Promise<ProjectPaths> => {
|
|
72
|
+
const cwd = trimTrailingSlash(options.cwd ?? process.cwd());
|
|
73
|
+
const distDirName = options.distDir ?? DEFAULT_DIST_DIR;
|
|
74
|
+
const siteDirName = options.siteDir ?? DEFAULT_SITE_DIR;
|
|
75
|
+
|
|
76
|
+
const siteRoot = joinPath(cwd, siteDirName);
|
|
77
|
+
const rootDir = (await hasNewLayout(cwd, siteDirName)) ? siteRoot : cwd;
|
|
78
|
+
return buildPaths(cwd, rootDir, distDirName);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
let cached: Promise<ProjectPaths> | null = null;
|
|
82
|
+
|
|
83
|
+
export const getProjectPaths = (): Promise<ProjectPaths> => {
|
|
84
|
+
cached ??= resolveProjectPaths();
|
|
85
|
+
return cached;
|
|
86
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { LayoutProps } from "./layout";
|
|
2
|
+
|
|
3
|
+
import { renderLayout as defaultRenderLayout } from "./layout";
|
|
4
|
+
|
|
5
|
+
export type RenderLayout = (props: LayoutProps) => string;
|
|
6
|
+
|
|
7
|
+
const USER_LAYOUT_PATH = "site/client/layout.tsx";
|
|
8
|
+
|
|
9
|
+
const loadUserLayout = async (
|
|
10
|
+
filePath: string
|
|
11
|
+
): Promise<RenderLayout | null> => {
|
|
12
|
+
try {
|
|
13
|
+
const url = Bun.pathToFileURL(filePath);
|
|
14
|
+
const mod = (await import(url.toString())) as unknown as {
|
|
15
|
+
renderLayout?: RenderLayout;
|
|
16
|
+
};
|
|
17
|
+
return mod.renderLayout ?? null;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
+
console.warn(
|
|
21
|
+
`[layout] Failed to load user layout from ${filePath}: ${message}`
|
|
22
|
+
);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const tryLoadUserRenderLayout = async (): Promise<RenderLayout | null> => {
|
|
28
|
+
const exists = await Bun.file(USER_LAYOUT_PATH).exists();
|
|
29
|
+
if (!exists) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return loadUserLayout(USER_LAYOUT_PATH);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
let cached: RenderLayout | null = null;
|
|
36
|
+
let attempted = false;
|
|
37
|
+
|
|
38
|
+
export const getRenderLayout = async (): Promise<RenderLayout> => {
|
|
39
|
+
if (attempted) {
|
|
40
|
+
return cached ?? defaultRenderLayout;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
attempted = true;
|
|
44
|
+
cached = await tryLoadUserRenderLayout();
|
|
45
|
+
return cached ?? defaultRenderLayout;
|
|
46
|
+
};
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/* eslint-disable react/no-danger */
|
|
2
|
+
import type { JSX } from "preact";
|
|
3
|
+
|
|
4
|
+
import { render } from "preact-render-to-string";
|
|
5
|
+
|
|
6
|
+
import type { NavGroup, NavItem } from "@/content/navigation";
|
|
7
|
+
import type { ResolvedRightRailConfig } from "@/site/config";
|
|
8
|
+
|
|
9
|
+
import type { TocItem } from "./toc";
|
|
10
|
+
|
|
11
|
+
import { RightRail } from "./right-rail";
|
|
12
|
+
|
|
13
|
+
export interface LayoutProps {
|
|
14
|
+
title: string;
|
|
15
|
+
siteName: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
canonicalUrl?: string;
|
|
18
|
+
content: string;
|
|
19
|
+
cssPath?: string;
|
|
20
|
+
inlineCss?: string;
|
|
21
|
+
currentPath: string;
|
|
22
|
+
navigation: NavGroup[];
|
|
23
|
+
scriptPaths?: string[];
|
|
24
|
+
searchQuery?: string;
|
|
25
|
+
showRightRail?: boolean;
|
|
26
|
+
rightRail: ResolvedRightRailConfig;
|
|
27
|
+
tocItems: TocItem[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const Icon = ({ svg }: { svg: string }): JSX.Element => (
|
|
31
|
+
<span
|
|
32
|
+
class="inline-flex w-[18px] h-[18px]"
|
|
33
|
+
dangerouslySetInnerHTML={{ __html: svg }}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const isActiveLink = (item: NavItem, currentPath: string): boolean =>
|
|
38
|
+
currentPath === item.href ||
|
|
39
|
+
(item.href !== "/" && currentPath.startsWith(item.href));
|
|
40
|
+
|
|
41
|
+
const NavLink = ({
|
|
42
|
+
item,
|
|
43
|
+
currentPath,
|
|
44
|
+
}: {
|
|
45
|
+
item: NavItem;
|
|
46
|
+
currentPath: string;
|
|
47
|
+
}): JSX.Element => {
|
|
48
|
+
const activeClass = isActiveLink(item, currentPath)
|
|
49
|
+
? "border-l-2 border-sidebar-primary font-medium text-sidebar-foreground"
|
|
50
|
+
: "border-l-2 border-transparent";
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<a
|
|
54
|
+
href={item.href}
|
|
55
|
+
data-prefetch="hover"
|
|
56
|
+
class={`flex items-center gap-3 px-3 py-1.5 text-sm hover:text-sidebar-foreground transition-colors ${activeClass}`}
|
|
57
|
+
>
|
|
58
|
+
<Icon svg={item.iconSvg} />
|
|
59
|
+
<span>{item.title}</span>
|
|
60
|
+
</a>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const NavGroupComponent = ({
|
|
65
|
+
group,
|
|
66
|
+
currentPath,
|
|
67
|
+
}: {
|
|
68
|
+
group: NavGroup;
|
|
69
|
+
currentPath: string;
|
|
70
|
+
}): JSX.Element => (
|
|
71
|
+
<div class="py-2">
|
|
72
|
+
<div class="px-3 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
73
|
+
{group.label}
|
|
74
|
+
</div>
|
|
75
|
+
<nav class="space-y-1">
|
|
76
|
+
{group.items.map((item) => (
|
|
77
|
+
<NavLink key={item.href} item={item} currentPath={currentPath} />
|
|
78
|
+
))}
|
|
79
|
+
</nav>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const Sidebar = ({
|
|
84
|
+
siteName,
|
|
85
|
+
navigation,
|
|
86
|
+
currentPath,
|
|
87
|
+
}: {
|
|
88
|
+
siteName: string;
|
|
89
|
+
navigation: NavGroup[];
|
|
90
|
+
currentPath: string;
|
|
91
|
+
}): JSX.Element => (
|
|
92
|
+
<aside class="sidebar">
|
|
93
|
+
<div class="sidebar-header">
|
|
94
|
+
<a
|
|
95
|
+
href="/"
|
|
96
|
+
class="text-sm font-medium tracking-tight"
|
|
97
|
+
data-prefetch="hover"
|
|
98
|
+
>
|
|
99
|
+
<span class="text-muted-foreground">~/</span>
|
|
100
|
+
{siteName}
|
|
101
|
+
</a>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="sidebar-content">
|
|
104
|
+
{navigation.map((group) => (
|
|
105
|
+
<NavGroupComponent
|
|
106
|
+
key={group.id}
|
|
107
|
+
group={group}
|
|
108
|
+
currentPath={currentPath}
|
|
109
|
+
/>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
</aside>
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const SearchForm = ({ query }: { query?: string }): JSX.Element => (
|
|
116
|
+
<form
|
|
117
|
+
method="get"
|
|
118
|
+
action="/search/"
|
|
119
|
+
class="flex w-full items-center"
|
|
120
|
+
role="search"
|
|
121
|
+
noValidate
|
|
122
|
+
>
|
|
123
|
+
<label htmlFor="site-search" class="sr-only">
|
|
124
|
+
Search pages
|
|
125
|
+
</label>
|
|
126
|
+
<input
|
|
127
|
+
id="site-search"
|
|
128
|
+
name="q"
|
|
129
|
+
type="search"
|
|
130
|
+
autoComplete="off"
|
|
131
|
+
spellcheck={false}
|
|
132
|
+
placeholder="Search..."
|
|
133
|
+
defaultValue={query ?? ""}
|
|
134
|
+
class="w-full border-b border-input bg-transparent px-1 py-1.5 text-sm placeholder:text-muted-foreground focus:border-foreground focus:outline-none transition-colors"
|
|
135
|
+
/>
|
|
136
|
+
</form>
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const TopNavbar = ({
|
|
140
|
+
query,
|
|
141
|
+
siteName,
|
|
142
|
+
}: {
|
|
143
|
+
query?: string;
|
|
144
|
+
siteName: string;
|
|
145
|
+
}): JSX.Element => (
|
|
146
|
+
<header class="sticky top-0 z-30 border-b border-border bg-background/80 backdrop-blur-sm">
|
|
147
|
+
<div class="mx-auto max-w-6xl px-8 py-3">
|
|
148
|
+
<div class="flex items-center gap-4">
|
|
149
|
+
<a
|
|
150
|
+
href="/"
|
|
151
|
+
class="text-sm font-medium tracking-tight font-mono md:hidden"
|
|
152
|
+
data-prefetch="hover"
|
|
153
|
+
>
|
|
154
|
+
<span class="text-muted-foreground">~/</span>
|
|
155
|
+
{siteName}
|
|
156
|
+
</a>
|
|
157
|
+
<div class="not-prose w-full max-w-xs ml-auto">
|
|
158
|
+
<SearchForm query={query} />
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</header>
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
interface DocumentHeadProps {
|
|
166
|
+
canonicalUrl?: string;
|
|
167
|
+
description?: string;
|
|
168
|
+
inlineCss?: string;
|
|
169
|
+
resolvedCssPath?: string;
|
|
170
|
+
title: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const DocumentHead = ({
|
|
174
|
+
canonicalUrl,
|
|
175
|
+
description,
|
|
176
|
+
inlineCss,
|
|
177
|
+
resolvedCssPath,
|
|
178
|
+
title,
|
|
179
|
+
}: DocumentHeadProps): JSX.Element => (
|
|
180
|
+
<head>
|
|
181
|
+
<meta charset="utf-8" />
|
|
182
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
183
|
+
<title>{title}</title>
|
|
184
|
+
{description ? <meta name="description" content={description} /> : null}
|
|
185
|
+
{canonicalUrl ? <link rel="canonical" href={canonicalUrl} /> : null}
|
|
186
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
187
|
+
<link
|
|
188
|
+
rel="preconnect"
|
|
189
|
+
href="https://fonts.gstatic.com"
|
|
190
|
+
crossOrigin="anonymous"
|
|
191
|
+
/>
|
|
192
|
+
<link
|
|
193
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap"
|
|
194
|
+
rel="stylesheet"
|
|
195
|
+
/>
|
|
196
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
|
197
|
+
{inlineCss ? <style>{inlineCss}</style> : null}
|
|
198
|
+
{resolvedCssPath ? <link rel="stylesheet" href={resolvedCssPath} /> : null}
|
|
199
|
+
</head>
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const buildHtmlClass = (smoothScroll: boolean): string =>
|
|
203
|
+
smoothScroll ? "dark smooth-scroll" : "dark";
|
|
204
|
+
|
|
205
|
+
interface ScrollSpyDataset {
|
|
206
|
+
scrollspy?: string;
|
|
207
|
+
scrollspyCenter?: string;
|
|
208
|
+
scrollspyUpdateHash?: string;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const buildScrollSpyDataset = (
|
|
212
|
+
isScrollSpyEnabled: boolean,
|
|
213
|
+
rightRail: ResolvedRightRailConfig
|
|
214
|
+
): ScrollSpyDataset =>
|
|
215
|
+
isScrollSpyEnabled
|
|
216
|
+
? {
|
|
217
|
+
scrollspy: "1",
|
|
218
|
+
scrollspyCenter: rightRail.scrollSpy.centerActiveItem ? "1" : undefined,
|
|
219
|
+
scrollspyUpdateHash: rightRail.scrollSpy.updateHash,
|
|
220
|
+
}
|
|
221
|
+
: {};
|
|
222
|
+
|
|
223
|
+
interface DocumentBodyProps {
|
|
224
|
+
canonicalUrl?: string;
|
|
225
|
+
content: string;
|
|
226
|
+
currentPath: string;
|
|
227
|
+
navigation: NavGroup[];
|
|
228
|
+
rightRail: ResolvedRightRailConfig;
|
|
229
|
+
scriptPaths: string[];
|
|
230
|
+
scrollSpyDataset: ScrollSpyDataset;
|
|
231
|
+
searchQuery?: string;
|
|
232
|
+
shouldShowRightRail: boolean;
|
|
233
|
+
siteName: string;
|
|
234
|
+
tocItems: TocItem[];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const DocumentBody = ({
|
|
238
|
+
canonicalUrl,
|
|
239
|
+
content,
|
|
240
|
+
currentPath,
|
|
241
|
+
navigation,
|
|
242
|
+
rightRail,
|
|
243
|
+
scriptPaths,
|
|
244
|
+
scrollSpyDataset,
|
|
245
|
+
searchQuery,
|
|
246
|
+
shouldShowRightRail,
|
|
247
|
+
siteName,
|
|
248
|
+
tocItems,
|
|
249
|
+
}: DocumentBodyProps): JSX.Element => (
|
|
250
|
+
<body
|
|
251
|
+
class="bg-background text-foreground font-sans"
|
|
252
|
+
data-scrollspy={scrollSpyDataset.scrollspy}
|
|
253
|
+
data-scrollspy-center={scrollSpyDataset.scrollspyCenter}
|
|
254
|
+
data-scrollspy-update-hash={scrollSpyDataset.scrollspyUpdateHash}
|
|
255
|
+
>
|
|
256
|
+
<Sidebar
|
|
257
|
+
siteName={siteName}
|
|
258
|
+
navigation={navigation}
|
|
259
|
+
currentPath={currentPath}
|
|
260
|
+
/>
|
|
261
|
+
<div class="main-wrapper">
|
|
262
|
+
<TopNavbar query={searchQuery} siteName={siteName} />
|
|
263
|
+
<main class="main-content">
|
|
264
|
+
<div class="mx-auto flex w-full max-w-6xl items-start gap-10">
|
|
265
|
+
<article
|
|
266
|
+
class={`prose min-w-0 flex-1${currentPath === "/" ? " prose-home" : ""}`}
|
|
267
|
+
dangerouslySetInnerHTML={{ __html: content }}
|
|
268
|
+
/>
|
|
269
|
+
{shouldShowRightRail ? (
|
|
270
|
+
<RightRail
|
|
271
|
+
canonicalUrl={canonicalUrl}
|
|
272
|
+
currentPath={currentPath}
|
|
273
|
+
tocItems={tocItems}
|
|
274
|
+
rightRailConfig={rightRail}
|
|
275
|
+
/>
|
|
276
|
+
) : null}
|
|
277
|
+
</div>
|
|
278
|
+
</main>
|
|
279
|
+
<footer class="site-footer">
|
|
280
|
+
Built with Preact SSR + Tailwind | Zero JavaScript on
|
|
281
|
+
content pages
|
|
282
|
+
</footer>
|
|
283
|
+
</div>
|
|
284
|
+
{scriptPaths.map((scriptPath) => (
|
|
285
|
+
<script key={scriptPath} defer src={scriptPath} />
|
|
286
|
+
))}
|
|
287
|
+
</body>
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const Layout = ({
|
|
291
|
+
title,
|
|
292
|
+
siteName,
|
|
293
|
+
description,
|
|
294
|
+
canonicalUrl,
|
|
295
|
+
content,
|
|
296
|
+
cssPath,
|
|
297
|
+
inlineCss,
|
|
298
|
+
currentPath,
|
|
299
|
+
navigation,
|
|
300
|
+
scriptPaths = [],
|
|
301
|
+
searchQuery,
|
|
302
|
+
showRightRail = true,
|
|
303
|
+
rightRail,
|
|
304
|
+
tocItems,
|
|
305
|
+
}: LayoutProps): JSX.Element => {
|
|
306
|
+
const resolvedCssPath = inlineCss ? undefined : (cssPath ?? "/styles.css");
|
|
307
|
+
const shouldShowRightRail = showRightRail && rightRail.enabled;
|
|
308
|
+
const isScrollSpyEnabled =
|
|
309
|
+
shouldShowRightRail && rightRail.scrollSpy.enabled && tocItems.length > 0;
|
|
310
|
+
const htmlClass = buildHtmlClass(rightRail.smoothScroll);
|
|
311
|
+
const scrollSpyDataset = buildScrollSpyDataset(isScrollSpyEnabled, rightRail);
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<html lang="en" class={htmlClass}>
|
|
315
|
+
<DocumentHead
|
|
316
|
+
canonicalUrl={canonicalUrl}
|
|
317
|
+
description={description}
|
|
318
|
+
inlineCss={inlineCss}
|
|
319
|
+
resolvedCssPath={resolvedCssPath}
|
|
320
|
+
title={title}
|
|
321
|
+
/>
|
|
322
|
+
<DocumentBody
|
|
323
|
+
canonicalUrl={canonicalUrl}
|
|
324
|
+
content={content}
|
|
325
|
+
currentPath={currentPath}
|
|
326
|
+
navigation={navigation}
|
|
327
|
+
rightRail={rightRail}
|
|
328
|
+
scriptPaths={scriptPaths}
|
|
329
|
+
scrollSpyDataset={scrollSpyDataset}
|
|
330
|
+
searchQuery={searchQuery}
|
|
331
|
+
shouldShowRightRail={shouldShowRightRail}
|
|
332
|
+
siteName={siteName}
|
|
333
|
+
tocItems={tocItems}
|
|
334
|
+
/>
|
|
335
|
+
</html>
|
|
336
|
+
);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
export const renderLayout = (props: LayoutProps): string =>
|
|
340
|
+
`<!DOCTYPE html>${render(<Layout {...props} />)}`;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const MARKDOWN_OPTIONS = {
|
|
2
|
+
autolinks: true,
|
|
3
|
+
hardSoftBreaks: true,
|
|
4
|
+
headings: true,
|
|
5
|
+
latexMath: true,
|
|
6
|
+
strikethrough: true,
|
|
7
|
+
tables: true,
|
|
8
|
+
tasklists: true,
|
|
9
|
+
underline: true,
|
|
10
|
+
wikiLinks: true,
|
|
11
|
+
} satisfies Bun.markdown.Options;
|
|
12
|
+
|
|
13
|
+
export const renderMarkdownToHtml = (markdown: string): string =>
|
|
14
|
+
Bun.markdown.html(markdown, MARKDOWN_OPTIONS);
|