stropress 0.0.1
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/index.d.ts +2 -0
- package/dist/index.js +307 -0
- package/dist/theme-default/package.json +20 -0
- package/dist/theme-default/postcss.config.mjs +5 -0
- package/dist/theme-default/src/components/BaseHead.astro +53 -0
- package/dist/theme-default/src/components/DocToc.astro +111 -0
- package/dist/theme-default/src/components/GithubIcon.astro +35 -0
- package/dist/theme-default/src/components/HomePage.astro +198 -0
- package/dist/theme-default/src/components/Icon.astro +78 -0
- package/dist/theme-default/src/components/LocaleSelect.astro +187 -0
- package/dist/theme-default/src/components/NavBar.astro +231 -0
- package/dist/theme-default/src/components/SearchInput.astro +84 -0
- package/dist/theme-default/src/components/SearchModal.astro +209 -0
- package/dist/theme-default/src/components/Sidebar.astro +101 -0
- package/dist/theme-default/src/content/docs/guide/configuration.mdx +195 -0
- package/dist/theme-default/src/content/docs/guide/getting-started.md +98 -0
- package/dist/theme-default/src/content/docs/guide/search.mdx +41 -0
- package/dist/theme-default/src/content/docs/index.css +0 -0
- package/dist/theme-default/src/content/docs/index.md +6 -0
- package/dist/theme-default/src/content/docs/zh/guide/configuration.mdx +149 -0
- package/dist/theme-default/src/content/docs/zh/guide/getting-started.md +31 -0
- package/dist/theme-default/src/content/docs/zh/guide/search.mdx +23 -0
- package/dist/theme-default/src/content/docs/zh/index.astro +75 -0
- package/dist/theme-default/src/content/docs/zh/index.md +6 -0
- package/dist/theme-default/src/content.config.ts +14 -0
- package/dist/theme-default/src/env.d.ts +15 -0
- package/dist/theme-default/src/layouts/DocsLayout.astro +278 -0
- package/dist/theme-default/src/lib/config.ts +195 -0
- package/dist/theme-default/src/lib/custom-home.ts +40 -0
- package/dist/theme-default/src/lib/custom-style.ts +11 -0
- package/dist/theme-default/src/lib/og.ts +275 -0
- package/dist/theme-default/src/pages/[...slug]/index.astro +83 -0
- package/dist/theme-default/src/pages/index.astro +47 -0
- package/dist/theme-default/src/pages/og/[...slug].png.ts +70 -0
- package/dist/theme-default/src/scripts/code-copy.ts +54 -0
- package/dist/theme-default/src/scripts/doc-toc.ts +109 -0
- package/dist/theme-default/src/scripts/search.ts +329 -0
- package/dist/theme-default/src/styles/global.css +70 -0
- package/dist/theme-default/src/styles/markdown.css +141 -0
- package/dist/theme-default/tsconfig.json +10 -0
- package/package.json +32 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
export interface NavItem {
|
|
2
|
+
label: string;
|
|
3
|
+
link: string;
|
|
4
|
+
icon?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface HomeAction {
|
|
8
|
+
text: string;
|
|
9
|
+
link: string;
|
|
10
|
+
theme?: "brand" | "alt";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface HomeFeature {
|
|
14
|
+
title: string;
|
|
15
|
+
details: string;
|
|
16
|
+
icon?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface HomeConfig {
|
|
20
|
+
title?: string;
|
|
21
|
+
tagline?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
actions?: HomeAction[];
|
|
24
|
+
features?: HomeFeature[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SidebarGroup {
|
|
28
|
+
label: string;
|
|
29
|
+
icon?: string;
|
|
30
|
+
items: NavItem[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface LocaleConfig {
|
|
34
|
+
label: string;
|
|
35
|
+
lang?: string;
|
|
36
|
+
site?: {
|
|
37
|
+
url?: string;
|
|
38
|
+
title?: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
};
|
|
41
|
+
home?: HomeConfig;
|
|
42
|
+
nav?: NavItem[];
|
|
43
|
+
socialLinks?: NavItem[];
|
|
44
|
+
navbar?: NavItem[];
|
|
45
|
+
sidebar?: SidebarGroup[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SiteConfig {
|
|
49
|
+
site?: {
|
|
50
|
+
url?: string;
|
|
51
|
+
title?: string;
|
|
52
|
+
description?: string;
|
|
53
|
+
};
|
|
54
|
+
home?: HomeConfig;
|
|
55
|
+
nav?: NavItem[];
|
|
56
|
+
socialLinks?: NavItem[];
|
|
57
|
+
navbar?: NavItem[];
|
|
58
|
+
sidebar?: SidebarGroup[];
|
|
59
|
+
locales?: Record<string, LocaleConfig>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface LocaleLink {
|
|
63
|
+
key: string;
|
|
64
|
+
label: string;
|
|
65
|
+
link: string;
|
|
66
|
+
isActive: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ResolvedSiteConfig {
|
|
70
|
+
localeKey: string;
|
|
71
|
+
localeLabel: string;
|
|
72
|
+
localeLang: string;
|
|
73
|
+
siteTitle: string;
|
|
74
|
+
siteDescription: string;
|
|
75
|
+
homeConfig: HomeConfig;
|
|
76
|
+
nav: NavItem[];
|
|
77
|
+
socialLinks: NavItem[];
|
|
78
|
+
sidebar: SidebarGroup[];
|
|
79
|
+
localeLinks: LocaleLink[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const rawConfig = import.meta.env.STROPRESS_SITE_CONFIG;
|
|
83
|
+
|
|
84
|
+
export const siteConfig: SiteConfig =
|
|
85
|
+
typeof rawConfig === "string" ? JSON.parse(rawConfig) : rawConfig || {};
|
|
86
|
+
export const siteTitle = siteConfig.site?.title || "Stropress Docs";
|
|
87
|
+
export const siteDescription =
|
|
88
|
+
siteConfig.site?.description || "Documentation site";
|
|
89
|
+
export const homeConfig = siteConfig.home || {};
|
|
90
|
+
|
|
91
|
+
const normalizeLocaleKey = (key: string) => {
|
|
92
|
+
const value = key.trim();
|
|
93
|
+
if (!value || value === "/") {
|
|
94
|
+
return "/";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const prefixed = value.startsWith("/") ? value : `/${value}`;
|
|
98
|
+
return prefixed.endsWith("/") ? prefixed : `${prefixed}/`;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const normalizePathname = (pathname: string) => {
|
|
102
|
+
if (!pathname || pathname === "/") {
|
|
103
|
+
return "/";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return pathname.endsWith("/") ? pathname : `${pathname}/`;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const mapLocaleEntries = () => {
|
|
110
|
+
const locales = siteConfig.locales || {};
|
|
111
|
+
const entries = Object.entries(locales).map(
|
|
112
|
+
([key, value]) => [normalizeLocaleKey(key), value] as const,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (!entries.some(([key]) => key === "/")) {
|
|
116
|
+
entries.unshift(["/", { label: "Default" }]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return entries.sort((a, b) => b[0].length - a[0].length);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const stripLocalePrefix = (pathname: string, localeKey: string) => {
|
|
123
|
+
if (localeKey === "/") {
|
|
124
|
+
return pathname;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const normalizedPath = normalizePathname(pathname);
|
|
128
|
+
if (!normalizedPath.startsWith(localeKey)) {
|
|
129
|
+
return pathname;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const stripped = normalizedPath.slice(localeKey.length - 1);
|
|
133
|
+
return stripped || "/";
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const joinLocalePath = (localeKey: string, subPath: string) => {
|
|
137
|
+
const normalizedSubPath = subPath.startsWith("/") ? subPath : `/${subPath}`;
|
|
138
|
+
if (localeKey === "/") {
|
|
139
|
+
return normalizedSubPath;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const suffix = normalizedSubPath === "/" ? "" : normalizedSubPath;
|
|
143
|
+
const joined = `${localeKey.slice(0, -1)}${suffix}`;
|
|
144
|
+
return joined || "/";
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const getResolvedSiteConfig = (pathname: string): ResolvedSiteConfig => {
|
|
148
|
+
const localeEntries = mapLocaleEntries();
|
|
149
|
+
const normalizedPath = normalizePathname(pathname);
|
|
150
|
+
|
|
151
|
+
const currentEntry =
|
|
152
|
+
localeEntries.find(([key]) => {
|
|
153
|
+
if (key === "/") {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return normalizedPath.startsWith(key);
|
|
158
|
+
}) || localeEntries[localeEntries.length - 1];
|
|
159
|
+
|
|
160
|
+
const [localeKey, localeConfig] = currentEntry;
|
|
161
|
+
const localeSubPath = stripLocalePrefix(pathname, localeKey);
|
|
162
|
+
|
|
163
|
+
const resolvedTitle =
|
|
164
|
+
localeConfig.site?.title || siteConfig.site?.title || "Stropress Docs";
|
|
165
|
+
const resolvedDescription =
|
|
166
|
+
localeConfig.site?.description ||
|
|
167
|
+
siteConfig.site?.description ||
|
|
168
|
+
"Documentation site";
|
|
169
|
+
|
|
170
|
+
const localeLinks = localeEntries.map(([key, config]) => ({
|
|
171
|
+
key,
|
|
172
|
+
label: config.label || (key === "/" ? "Default" : key),
|
|
173
|
+
link: joinLocalePath(key, localeSubPath),
|
|
174
|
+
isActive: key === localeKey,
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
localeKey,
|
|
179
|
+
localeLabel:
|
|
180
|
+
localeConfig.label || (localeKey === "/" ? "Default" : localeKey),
|
|
181
|
+
localeLang: localeConfig.lang || "en",
|
|
182
|
+
siteTitle: resolvedTitle,
|
|
183
|
+
siteDescription: resolvedDescription,
|
|
184
|
+
homeConfig: localeConfig.home || siteConfig.home || {},
|
|
185
|
+
nav:
|
|
186
|
+
localeConfig.nav ||
|
|
187
|
+
siteConfig.nav ||
|
|
188
|
+
localeConfig.navbar ||
|
|
189
|
+
siteConfig.navbar ||
|
|
190
|
+
[],
|
|
191
|
+
socialLinks: localeConfig.socialLinks || siteConfig.socialLinks || [],
|
|
192
|
+
sidebar: localeConfig.sidebar || siteConfig.sidebar || [],
|
|
193
|
+
localeLinks,
|
|
194
|
+
};
|
|
195
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface CustomHomeModule {
|
|
2
|
+
default: any;
|
|
3
|
+
title?: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
sidebar?: boolean;
|
|
6
|
+
contentClass?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const customHomeModules = import.meta.glob<CustomHomeModule>(
|
|
10
|
+
"../content/docs/**/index.astro",
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const normalizePathname = (pathname: string) => {
|
|
14
|
+
if (!pathname || pathname === "/") {
|
|
15
|
+
return "/";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return pathname.endsWith("/") ? pathname : `${pathname}/`;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const getCustomHomeModulePath = (pathname: string) => {
|
|
22
|
+
const normalizedPath = normalizePathname(pathname);
|
|
23
|
+
|
|
24
|
+
if (normalizedPath === "/") {
|
|
25
|
+
return "../content/docs/index.astro";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return `../content/docs${normalizedPath.slice(0, -1)}/index.astro`;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const getCustomHomeModule = async (pathname: string) => {
|
|
32
|
+
const modulePath = getCustomHomeModulePath(pathname);
|
|
33
|
+
const loader = customHomeModules[modulePath];
|
|
34
|
+
|
|
35
|
+
if (!loader) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return loader();
|
|
40
|
+
};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import { access, readFile } from "node:fs/promises";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import satori from "satori";
|
|
6
|
+
import { html } from "satori-html";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const { Resvg } =
|
|
11
|
+
require("@resvg/resvg-js") as typeof import("@resvg/resvg-js");
|
|
12
|
+
|
|
13
|
+
interface LoadedFont {
|
|
14
|
+
name: string;
|
|
15
|
+
data: ArrayBuffer;
|
|
16
|
+
weight: 400 | 700;
|
|
17
|
+
style: "normal";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface OgRenderInput {
|
|
21
|
+
title: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
siteTitle?: string;
|
|
24
|
+
routePath?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let cachedOgFonts: LoadedFont[] | null = null;
|
|
28
|
+
|
|
29
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
|
|
31
|
+
export const renderOgPng = async (input: OgRenderInput) => {
|
|
32
|
+
const fonts = await loadOgFonts();
|
|
33
|
+
const svg = await buildOgSvg({
|
|
34
|
+
title: truncateText(input.title || "Untitled", 84),
|
|
35
|
+
description: truncateText(input.description || "", 160),
|
|
36
|
+
siteTitle: truncateText(input.siteTitle || "Stropress Docs", 60),
|
|
37
|
+
routePath: input.routePath || "/preview",
|
|
38
|
+
fonts,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return renderSvgToPng(svg);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const loadOgFonts = async (): Promise<LoadedFont[]> => {
|
|
45
|
+
if (cachedOgFonts) {
|
|
46
|
+
return cachedOgFonts;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const regularPath = await resolveFontsourceFontFile(
|
|
50
|
+
"@fontsource/inter",
|
|
51
|
+
"400",
|
|
52
|
+
);
|
|
53
|
+
const boldPath = await resolveFontsourceFontFile("@fontsource/inter", "700");
|
|
54
|
+
|
|
55
|
+
const regular = await readFile(regularPath);
|
|
56
|
+
const bold = await readFile(boldPath);
|
|
57
|
+
|
|
58
|
+
cachedOgFonts = [
|
|
59
|
+
{
|
|
60
|
+
name: "OG Sans",
|
|
61
|
+
data: regular.buffer.slice(
|
|
62
|
+
regular.byteOffset,
|
|
63
|
+
regular.byteOffset + regular.byteLength,
|
|
64
|
+
),
|
|
65
|
+
weight: 400,
|
|
66
|
+
style: "normal",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "OG Sans",
|
|
70
|
+
data: bold.buffer.slice(
|
|
71
|
+
bold.byteOffset,
|
|
72
|
+
bold.byteOffset + bold.byteLength,
|
|
73
|
+
),
|
|
74
|
+
weight: 700,
|
|
75
|
+
style: "normal",
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
return cachedOgFonts;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const resolveFontsourceFontFile = async (
|
|
83
|
+
packageName: string,
|
|
84
|
+
weight: "400" | "700",
|
|
85
|
+
) => {
|
|
86
|
+
const cssCandidates = [
|
|
87
|
+
path.resolve(process.cwd(), `node_modules/${packageName}/${weight}.css`),
|
|
88
|
+
path.resolve(
|
|
89
|
+
process.cwd(),
|
|
90
|
+
`themes/default/node_modules/${packageName}/${weight}.css`,
|
|
91
|
+
),
|
|
92
|
+
path.resolve(
|
|
93
|
+
process.cwd(),
|
|
94
|
+
`cli/node_modules/${packageName}/${weight}.css`,
|
|
95
|
+
),
|
|
96
|
+
path.resolve(moduleDir, `../node_modules/${packageName}/${weight}.css`),
|
|
97
|
+
path.resolve(moduleDir, `../../node_modules/${packageName}/${weight}.css`),
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const cssPath = await resolvePathCandidate(cssCandidates);
|
|
101
|
+
const cssContent = await readFile(cssPath, "utf8");
|
|
102
|
+
const fontUrl = extractFontUrlFromCss(cssContent);
|
|
103
|
+
|
|
104
|
+
return path.resolve(path.dirname(cssPath), fontUrl);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const resolvePathCandidate = async (candidates: string[]) => {
|
|
108
|
+
for (const candidate of candidates) {
|
|
109
|
+
if (await pathExists(candidate)) {
|
|
110
|
+
return candidate;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
throw new Error(
|
|
115
|
+
"Could not locate Fontsource CSS files for OG generation. Ensure @fontsource/inter is installed.",
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const pathExists = async (filePath: string) => {
|
|
120
|
+
try {
|
|
121
|
+
await access(filePath, fsConstants.R_OK);
|
|
122
|
+
return true;
|
|
123
|
+
} catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const extractFontUrlFromCss = (css: string) => {
|
|
129
|
+
const match =
|
|
130
|
+
css.match(
|
|
131
|
+
/url\(([^)]+)\)\s+format\(['\"]?(woff2?|truetype|opentype)['\"]?\)/i,
|
|
132
|
+
) || css.match(/url\(([^)]+)\)/i);
|
|
133
|
+
|
|
134
|
+
if (!match) {
|
|
135
|
+
throw new Error("Could not parse a font URL from Fontsource CSS.");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const rawUrl = match[1].trim().replace(/^['\"]|['\"]$/g, "");
|
|
139
|
+
if (!rawUrl || rawUrl.startsWith("data:")) {
|
|
140
|
+
throw new Error("Unsupported Fontsource URL format for OG generation.");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return rawUrl;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const buildOgSvg = async (input: {
|
|
147
|
+
title: string;
|
|
148
|
+
description: string;
|
|
149
|
+
siteTitle: string;
|
|
150
|
+
routePath: string;
|
|
151
|
+
fonts: LoadedFont[];
|
|
152
|
+
}) => {
|
|
153
|
+
const routeText = input.routePath === "/" ? "home" : input.routePath;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
return await satori(
|
|
157
|
+
html`<div
|
|
158
|
+
style="
|
|
159
|
+
width: 1200px;
|
|
160
|
+
height: 630px;
|
|
161
|
+
display: flex;
|
|
162
|
+
position: relative;
|
|
163
|
+
overflow: hidden;
|
|
164
|
+
background: linear-gradient(120deg, #0f172a 0%, #1e293b 100%);
|
|
165
|
+
color: #f8fafc;
|
|
166
|
+
font-family: 'OG Sans';
|
|
167
|
+
box-sizing: border-box;
|
|
168
|
+
padding: 56px;
|
|
169
|
+
"
|
|
170
|
+
>
|
|
171
|
+
<div
|
|
172
|
+
style="
|
|
173
|
+
width: 100%;
|
|
174
|
+
height: 100%;
|
|
175
|
+
border-radius: 24px;
|
|
176
|
+
border: 1px solid rgba(148, 163, 184, 0.3);
|
|
177
|
+
background: rgba(15, 23, 42, 0.68);
|
|
178
|
+
display: flex;
|
|
179
|
+
flex-direction: column;
|
|
180
|
+
justify-content: space-between;
|
|
181
|
+
padding: 42px;
|
|
182
|
+
box-sizing: border-box;
|
|
183
|
+
"
|
|
184
|
+
>
|
|
185
|
+
<div
|
|
186
|
+
style="
|
|
187
|
+
font-size: 30px;
|
|
188
|
+
color: #93c5fd;
|
|
189
|
+
font-weight: 700;
|
|
190
|
+
"
|
|
191
|
+
>
|
|
192
|
+
${input.siteTitle}
|
|
193
|
+
</div>
|
|
194
|
+
<div style="display: flex; flex-direction: column; gap: 18px;">
|
|
195
|
+
<div
|
|
196
|
+
style="
|
|
197
|
+
font-size: 56px;
|
|
198
|
+
color: #f8fafc;
|
|
199
|
+
font-weight: 700;
|
|
200
|
+
line-height: 1.2;
|
|
201
|
+
"
|
|
202
|
+
>
|
|
203
|
+
${input.title}
|
|
204
|
+
</div>
|
|
205
|
+
<div
|
|
206
|
+
style="
|
|
207
|
+
font-size: 30px;
|
|
208
|
+
color: #cbd5e1;
|
|
209
|
+
line-height: 1.45;
|
|
210
|
+
"
|
|
211
|
+
>
|
|
212
|
+
${input.description}
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
<div
|
|
216
|
+
style="
|
|
217
|
+
font-size: 24px;
|
|
218
|
+
color: #94a3b8;
|
|
219
|
+
"
|
|
220
|
+
>
|
|
221
|
+
${routeText}
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</div>`,
|
|
225
|
+
{
|
|
226
|
+
width: 1200,
|
|
227
|
+
height: 630,
|
|
228
|
+
fonts: input.fonts,
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
} catch {
|
|
232
|
+
// Fallback for environments where ultrahtml/satori-html has parser incompatibilities.
|
|
233
|
+
return `<svg width="1200" height="630" viewBox="0 0 1200 630" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
234
|
+
<defs>
|
|
235
|
+
<linearGradient id="bg" x1="0" y1="0" x2="1200" y2="630" gradientUnits="userSpaceOnUse">
|
|
236
|
+
<stop stop-color="#0f172a"/>
|
|
237
|
+
<stop offset="1" stop-color="#1e293b"/>
|
|
238
|
+
</linearGradient>
|
|
239
|
+
</defs>
|
|
240
|
+
<rect width="1200" height="630" fill="url(#bg)"/>
|
|
241
|
+
<rect x="56" y="56" width="1088" height="518" rx="24" fill="rgba(15, 23, 42, 0.68)" stroke="rgba(148, 163, 184, 0.30)"/>
|
|
242
|
+
<text x="96" y="132" fill="#93c5fd" font-size="30" font-family="Arial, sans-serif" font-weight="700">${escapeXml(input.siteTitle)}</text>
|
|
243
|
+
<text x="96" y="228" fill="#f8fafc" font-size="56" font-family="Arial, sans-serif" font-weight="700">${escapeXml(input.title)}</text>
|
|
244
|
+
<text x="96" y="304" fill="#cbd5e1" font-size="30" font-family="Arial, sans-serif" font-weight="400">${escapeXml(input.description)}</text>
|
|
245
|
+
<text x="96" y="532" fill="#94a3b8" font-size="24" font-family="Arial, sans-serif" font-weight="400">${escapeXml(routeText)}</text>
|
|
246
|
+
</svg>`;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const renderSvgToPng = (svg: string) => {
|
|
251
|
+
return new Resvg(svg, {
|
|
252
|
+
fitTo: {
|
|
253
|
+
mode: "width",
|
|
254
|
+
value: 1200,
|
|
255
|
+
},
|
|
256
|
+
})
|
|
257
|
+
.render()
|
|
258
|
+
.asPng();
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const escapeXml = (value: string) =>
|
|
262
|
+
value
|
|
263
|
+
.replaceAll("&", "&")
|
|
264
|
+
.replaceAll("<", "<")
|
|
265
|
+
.replaceAll(">", ">")
|
|
266
|
+
.replaceAll('"', """)
|
|
267
|
+
.replaceAll("'", "'");
|
|
268
|
+
|
|
269
|
+
const truncateText = (value: string, maxLength: number) => {
|
|
270
|
+
if (value.length <= maxLength) {
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return `${value.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
|
|
275
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
import DocsLayout from "../../layouts/DocsLayout.astro";
|
|
3
|
+
import HomePage from "../../components/HomePage.astro";
|
|
4
|
+
import { getCollection, render, type CollectionEntry } from "astro:content";
|
|
5
|
+
import { getResolvedSiteConfig } from "../../lib/config";
|
|
6
|
+
import { getCustomHomeModule } from "../../lib/custom-home";
|
|
7
|
+
|
|
8
|
+
export const getStaticPaths = async () => {
|
|
9
|
+
const docs = await getCollection("docs");
|
|
10
|
+
|
|
11
|
+
return docs
|
|
12
|
+
.filter((entry) => entry.id !== "index")
|
|
13
|
+
.map((entry) => ({
|
|
14
|
+
params: {
|
|
15
|
+
slug: entry.id.endsWith("/index")
|
|
16
|
+
? entry.id.slice(0, -"/index".length)
|
|
17
|
+
: entry.id,
|
|
18
|
+
},
|
|
19
|
+
props: {
|
|
20
|
+
entry,
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
entry: CollectionEntry<"docs">;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { entry } = Astro.props;
|
|
30
|
+
const { Content, headings } = await render(entry);
|
|
31
|
+
|
|
32
|
+
const resolvedConfig = getResolvedSiteConfig(Astro.url.pathname);
|
|
33
|
+
const normalizedPathname = Astro.url.pathname.endsWith("/")
|
|
34
|
+
? Astro.url.pathname
|
|
35
|
+
: `${Astro.url.pathname}/`;
|
|
36
|
+
const isLocaleHome =
|
|
37
|
+
resolvedConfig.localeKey !== "/" &&
|
|
38
|
+
normalizedPathname === resolvedConfig.localeKey;
|
|
39
|
+
const customHomeModule = isLocaleHome
|
|
40
|
+
? await getCustomHomeModule(Astro.url.pathname)
|
|
41
|
+
: null;
|
|
42
|
+
const CustomHome = customHomeModule?.default;
|
|
43
|
+
const customTitle = customHomeModule?.title;
|
|
44
|
+
const customDescription = customHomeModule?.description;
|
|
45
|
+
const customSidebar = customHomeModule?.sidebar;
|
|
46
|
+
const customContentClass = customHomeModule?.contentClass;
|
|
47
|
+
const localeHomeConfig = isLocaleHome ? resolvedConfig.homeConfig : null;
|
|
48
|
+
const hasHomeContent =
|
|
49
|
+
isLocaleHome &&
|
|
50
|
+
!customHomeModule &&
|
|
51
|
+
(Boolean(localeHomeConfig?.title) ||
|
|
52
|
+
Boolean(localeHomeConfig?.tagline) ||
|
|
53
|
+
Boolean(localeHomeConfig?.description) ||
|
|
54
|
+
(localeHomeConfig?.actions?.length || 0) > 0 ||
|
|
55
|
+
(localeHomeConfig?.features?.length || 0) > 0);
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
<DocsLayout
|
|
59
|
+
title={customTitle || entry.data.title}
|
|
60
|
+
description={customDescription || entry.data.description}
|
|
61
|
+
sidebar={typeof customSidebar === "boolean" ? customSidebar : !hasHomeContent}
|
|
62
|
+
tocHeadings={hasHomeContent || customHomeModule ? undefined : headings}
|
|
63
|
+
contentClass={customContentClass ||
|
|
64
|
+
(hasHomeContent || customHomeModule
|
|
65
|
+
? undefined
|
|
66
|
+
: "docs-content markdown-content")}
|
|
67
|
+
>
|
|
68
|
+
{
|
|
69
|
+
customHomeModule ? (
|
|
70
|
+
<CustomHome />
|
|
71
|
+
) : hasHomeContent ? (
|
|
72
|
+
<HomePage />
|
|
73
|
+
) : (
|
|
74
|
+
<Content />
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
</DocsLayout>
|
|
78
|
+
|
|
79
|
+
<style is:global>
|
|
80
|
+
.docs-content {
|
|
81
|
+
padding: 1rem;
|
|
82
|
+
}
|
|
83
|
+
</style>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
import DocsLayout from "../layouts/DocsLayout.astro";
|
|
3
|
+
import HomePage from "../components/HomePage.astro";
|
|
4
|
+
import { homeConfig } from "../lib/config";
|
|
5
|
+
import { getCustomHomeModule } from "../lib/custom-home";
|
|
6
|
+
import { getCollection, render } from "astro:content";
|
|
7
|
+
|
|
8
|
+
const customHomeModule = await getCustomHomeModule(Astro.url.pathname);
|
|
9
|
+
const pages = await getCollection("docs");
|
|
10
|
+
const homeDoc = pages.find((entry) => entry.id === "index");
|
|
11
|
+
|
|
12
|
+
if (!homeDoc && !customHomeModule) {
|
|
13
|
+
throw new Error("docs/index.md is required");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const CustomHome = customHomeModule?.default;
|
|
17
|
+
const customTitle = customHomeModule?.title;
|
|
18
|
+
const customDescription = customHomeModule?.description;
|
|
19
|
+
const customSidebar = customHomeModule?.sidebar;
|
|
20
|
+
const customContentClass = customHomeModule?.contentClass;
|
|
21
|
+
const renderedHomeDoc = homeDoc ? await render(homeDoc) : null;
|
|
22
|
+
const Content = renderedHomeDoc?.Content;
|
|
23
|
+
const hasHomeContent =
|
|
24
|
+
Boolean(homeConfig.title) ||
|
|
25
|
+
Boolean(homeConfig.tagline) ||
|
|
26
|
+
Boolean(homeConfig.description) ||
|
|
27
|
+
(homeConfig.actions?.length || 0) > 0 ||
|
|
28
|
+
(homeConfig.features?.length || 0) > 0;
|
|
29
|
+
const useCustomHome = Boolean(CustomHome);
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
<DocsLayout
|
|
33
|
+
title={customTitle || homeDoc?.data.title}
|
|
34
|
+
description={customDescription || homeDoc?.data.description}
|
|
35
|
+
sidebar={typeof customSidebar === "boolean" ? customSidebar : !hasHomeContent}
|
|
36
|
+
contentClass={customContentClass}
|
|
37
|
+
>
|
|
38
|
+
{
|
|
39
|
+
useCustomHome ? (
|
|
40
|
+
<CustomHome />
|
|
41
|
+
) : hasHomeContent ? (
|
|
42
|
+
<HomePage />
|
|
43
|
+
) : Content ? (
|
|
44
|
+
<Content />
|
|
45
|
+
) : null
|
|
46
|
+
}
|
|
47
|
+
</DocsLayout>
|