@farming-labs/docs 0.1.1-beta.4 → 0.1.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.
@@ -0,0 +1,12 @@
1
+ //#region src/cli/index.d.ts
2
+ declare const UPGRADE_TAGS: readonly ["latest", "beta"];
3
+ type UpgradeTag = (typeof UPGRADE_TAGS)[number];
4
+ /** Normalize command aliases like `upgrade@beta` into the base command + dist-tag. */
5
+ declare function parseCommandAlias(rawCommand?: string): {
6
+ command?: string;
7
+ tag?: UpgradeTag;
8
+ };
9
+ /** Parse flags like --template next, --name my-docs, --theme concrete, --entry docs, --framework astro (exported for tests). */
10
+ declare function parseFlags(argv: string[]): Record<string, string | boolean | undefined>;
11
+ //#endregion
12
+ export { parseCommandAlias, parseFlags };
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ import pc from "picocolors";
3
+
4
+ //#region src/cli/index.ts
5
+ const args = process.argv.slice(2);
6
+ const command = args[0];
7
+ const UPGRADE_TAGS = ["latest", "beta"];
8
+ /** Normalize command aliases like `upgrade@beta` into the base command + dist-tag. */
9
+ function parseCommandAlias(rawCommand) {
10
+ if (!rawCommand) return {};
11
+ const [baseCommand, rawTag] = rawCommand.split("@");
12
+ if (baseCommand === "upgrade" && rawTag && UPGRADE_TAGS.includes(rawTag)) return {
13
+ command: "upgrade",
14
+ tag: rawTag
15
+ };
16
+ return { command: rawCommand };
17
+ }
18
+ /** Parse flags like --template next, --name my-docs, --theme concrete, --entry docs, --framework astro (exported for tests). */
19
+ function parseFlags(argv) {
20
+ const flags = {};
21
+ const booleanFlags = new Set(["api-reference"]);
22
+ for (let i = 0; i < argv.length; i++) {
23
+ const arg = argv[i];
24
+ if (arg.startsWith("--") && arg.includes("=")) {
25
+ const [key, value] = arg.slice(2).split("=");
26
+ if (key.startsWith("no-")) flags[key.slice(3)] = false;
27
+ else if (booleanFlags.has(key) && value === "true") flags[key] = true;
28
+ else if (booleanFlags.has(key) && value === "false") flags[key] = false;
29
+ else flags[key] = value;
30
+ } else if (arg.startsWith("--") && argv[i + 1] && !argv[i + 1].startsWith("--")) {
31
+ flags[arg.slice(2)] = argv[i + 1];
32
+ i++;
33
+ } else if (arg.startsWith("--no-")) flags[arg.slice(5)] = false;
34
+ else if (arg.startsWith("--") && booleanFlags.has(arg.slice(2))) flags[arg.slice(2)] = true;
35
+ }
36
+ return flags;
37
+ }
38
+ async function main() {
39
+ const flags = parseFlags(args);
40
+ const parsedCommand = parseCommandAlias(command);
41
+ const initOptions = {
42
+ template: typeof flags.template === "string" ? flags.template : void 0,
43
+ name: typeof flags.name === "string" ? flags.name : void 0,
44
+ theme: typeof flags.theme === "string" ? flags.theme : void 0,
45
+ entry: typeof flags.entry === "string" ? flags.entry : void 0,
46
+ apiReference: typeof flags["api-reference"] === "boolean" ? flags["api-reference"] : void 0,
47
+ apiRouteRoot: typeof flags["api-route-root"] === "string" ? flags["api-route-root"] : void 0
48
+ };
49
+ const mcpOptions = { configPath: typeof flags.config === "string" ? flags.config : void 0 };
50
+ if (!parsedCommand.command || parsedCommand.command === "init") {
51
+ const { init } = await import("../init-N0bZQFRd.mjs");
52
+ await init(initOptions);
53
+ } else if (parsedCommand.command === "mcp") {
54
+ const { runMcp } = await import("../mcp-8rCBy2-U.mjs");
55
+ await runMcp(mcpOptions);
56
+ } else if (parsedCommand.command === "upgrade") {
57
+ const { upgrade } = await import("../upgrade-BbEyR_JB.mjs");
58
+ await upgrade({
59
+ framework: (typeof flags.framework === "string" ? flags.framework : void 0) ?? (args[1] && !args[1].startsWith("--") ? args[1] : void 0),
60
+ tag: args.includes("--beta") ? "beta" : args.includes("--latest") ? "latest" : parsedCommand.tag ?? "latest"
61
+ });
62
+ } else if (parsedCommand.command === "--help" || parsedCommand.command === "-h") printHelp();
63
+ else if (parsedCommand.command === "--version" || parsedCommand.command === "-v") printVersion();
64
+ else {
65
+ console.error(pc.red(`Unknown command: ${command}`));
66
+ console.error();
67
+ printHelp();
68
+ process.exit(1);
69
+ }
70
+ }
71
+ function printHelp() {
72
+ console.log(`
73
+ ${pc.bold("@farming-labs/docs")} — Documentation framework CLI
74
+
75
+ ${pc.dim("Usage:")}
76
+ npx @farming-labs/docs@latest ${pc.cyan("<command>")}
77
+
78
+ ${pc.dim("Commands:")}
79
+ ${pc.cyan("init")} Scaffold docs in your project (default)
80
+ ${pc.cyan("mcp")} Run the built-in docs MCP server over stdio
81
+ ${pc.cyan("upgrade")} Upgrade @farming-labs/* packages to latest (auto-detect or use --framework)
82
+
83
+ ${pc.dim("Supported frameworks:")}
84
+ Next.js, TanStack Start, SvelteKit, Astro, Nuxt
85
+
86
+ ${pc.dim("Options for init:")}
87
+ ${pc.cyan("--template <name>")} Bootstrap a project (${pc.dim("next")}, ${pc.dim("nuxt")}, ${pc.dim("sveltekit")}, ${pc.dim("astro")}, ${pc.dim("tanstack-start")}); use with ${pc.cyan("--name")}
88
+ ${pc.cyan("--name <project>")} Project folder name when using ${pc.cyan("--template")}; prompt if omitted (e.g. ${pc.dim("my-docs")})
89
+ ${pc.cyan("--theme <name>")} Skip theme prompt (e.g. ${pc.dim("darksharp")}, ${pc.dim("concrete")})
90
+ ${pc.cyan("--entry <path>")} Skip entry path prompt (e.g. ${pc.dim("docs")})
91
+ ${pc.cyan("--api-reference")} Scaffold API reference support during ${pc.cyan("init")}
92
+ ${pc.cyan("--no-api-reference")} Skip API reference scaffold during ${pc.cyan("init")}
93
+ ${pc.cyan("--api-route-root <path>")} Override the API route root scanned by ${pc.cyan("apiReference.routeRoot")} (e.g. ${pc.dim("api")}, ${pc.dim("internal-api")})
94
+
95
+ ${pc.dim("Options for mcp:")}
96
+ ${pc.cyan("--config <path>")} Use a custom docs config path instead of ${pc.dim("docs.config.ts[x]")}
97
+
98
+ ${pc.dim("Options for upgrade:")}
99
+ ${pc.cyan("--framework <name>")} Explicit framework (${pc.dim("next")}, ${pc.dim("tanstack-start")}, ${pc.dim("nuxt")}, ${pc.dim("sveltekit")}, ${pc.dim("astro")}); omit to auto-detect
100
+ ${pc.cyan("--latest")} Install latest stable (default)
101
+ ${pc.cyan("--beta")} Install beta versions
102
+ ${pc.cyan("upgrade@beta")} Shortcut for ${pc.cyan("upgrade --beta")}
103
+ ${pc.cyan("upgrade@latest")} Shortcut for ${pc.cyan("upgrade --latest")}
104
+
105
+ ${pc.cyan("-h, --help")} Show this help message
106
+ ${pc.cyan("-v, --version")} Show version
107
+ `);
108
+ }
109
+ function printVersion() {
110
+ console.log("0.1.0");
111
+ }
112
+ main().catch((err) => {
113
+ console.error(pc.red("An unexpected error occurred:"));
114
+ console.error(err);
115
+ process.exit(1);
116
+ });
117
+
118
+ //#endregion
119
+ export { parseCommandAlias, parseFlags };
@@ -0,0 +1,100 @@
1
+ import { A as SidebarConfig, C as OpenGraphImage, D as PageOpenGraph, E as PageFrontmatter, F as ThemeToggleConfig, I as TypographyConfig, L as UIConfig, M as SidebarNode, N as SidebarPageNode, O as PageTwitter, P as SidebarTree, S as OpenDocsProvider, T as PageActionsConfig, _ as GithubConfig, a as CopyMarkdownConfig, b as OGConfig, c as DocsFeedbackValue, d as DocsMcpToolsConfig, f as DocsMetadata, g as FontStyle, h as FeedbackConfig, i as CodeBlockCopyData, j as SidebarFolderNode, k as SidebarComponentProps, l as DocsI18nConfig, m as DocsTheme, n as ApiReferenceConfig, o as DocsConfig, p as DocsNav, r as BreadcrumbConfig, s as DocsFeedbackData, t as AIConfig, u as DocsMcpConfig, v as LastUpdatedConfig, w as OrderingItem, x as OpenDocsConfig, y as LlmsTxtConfig } from "./types-dqnMXLdw.mjs";
2
+
3
+ //#region src/define-docs.d.ts
4
+ /**
5
+ * Define docs configuration. Validates and returns the config.
6
+ */
7
+ declare function defineDocs(config: DocsConfig): DocsConfig;
8
+ //#endregion
9
+ //#region src/utils.d.ts
10
+ /**
11
+ * Deep merge utility for theme overrides.
12
+ * Merges objects recursively; later values override earlier ones.
13
+ */
14
+ declare function deepMerge<T extends Record<string, unknown>>(target: T, ...sources: Partial<T>[]): T;
15
+ //#endregion
16
+ //#region src/create-theme.d.ts
17
+ /**
18
+ * Create a theme preset factory.
19
+ *
20
+ * Returns a function that accepts optional overrides and deep-merges them
21
+ * with the base theme defaults. This is the same pattern used by the
22
+ * built-in `fumadocs()`, `darksharp()`, and `pixelBorder()` presets.
23
+ *
24
+ * @param baseTheme - The default theme configuration
25
+ * @returns A factory function `(overrides?) => DocsTheme`
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * import { createTheme } from "@farming-labs/docs";
30
+ *
31
+ * export const myTheme = createTheme({
32
+ * name: "my-theme",
33
+ * ui: {
34
+ * colors: { primary: "#6366f1" },
35
+ * layout: { contentWidth: 800 },
36
+ * },
37
+ * });
38
+ * ```
39
+ */
40
+ declare function createTheme(baseTheme: DocsTheme): (overrides?: Partial<DocsTheme>) => DocsTheme;
41
+ /**
42
+ * Extend an existing theme preset with additional defaults.
43
+ *
44
+ * Useful when you want to build on top of an existing theme (e.g. fumadocs)
45
+ * rather than starting from scratch.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * import { extendTheme } from "@farming-labs/docs";
50
+ * import { fumadocs } from "@farming-labs/theme/default";
51
+ *
52
+ * // Start with fumadocs defaults, override some values
53
+ * export const myTheme = extendTheme(fumadocs(), {
54
+ * name: "my-custom-fumadocs",
55
+ * ui: { colors: { primary: "#22c55e" } },
56
+ * });
57
+ * ```
58
+ */
59
+ declare function extendTheme(baseTheme: DocsTheme, extensions: Partial<DocsTheme>): DocsTheme;
60
+ //#endregion
61
+ //#region src/i18n.d.ts
62
+ interface ResolvedDocsI18n {
63
+ locales: string[];
64
+ defaultLocale: string;
65
+ }
66
+ interface DocsPathMatch {
67
+ /** Slug path relative to the docs root (no leading slash). */
68
+ slug: string;
69
+ /** Entry path used for URLs (no locale segment). */
70
+ entryPath: string;
71
+ }
72
+ declare function resolveDocsI18n(config?: DocsI18nConfig | null): ResolvedDocsI18n | null;
73
+ declare function resolveDocsLocale(searchParams: URLSearchParams, i18n?: ResolvedDocsI18n | null): string | undefined;
74
+ declare function resolveDocsPath(pathname: string, entry: string): DocsPathMatch;
75
+ //#endregion
76
+ //#region src/metadata.d.ts
77
+ /**
78
+ * Resolve page title using metadata titleTemplate.
79
+ * %s is replaced with page title.
80
+ */
81
+ declare function resolveTitle(pageTitle: string, metadata?: DocsMetadata): string;
82
+ /**
83
+ * Resolve OG image URL for a page.
84
+ * Prefers page.openGraph.images[0], then page.ogImage, then config endpoint/default.
85
+ */
86
+ declare function resolveOGImage(page: PageFrontmatter, ogConfig?: OGConfig, baseUrl?: string): string | undefined;
87
+ /**
88
+ * Build the Open Graph metadata object for a page.
89
+ * When the page has openGraph in frontmatter, uses it (with title/description filled from page if omitted).
90
+ * Otherwise uses ogImage or config (dynamic endpoint / defaultImage).
91
+ */
92
+ declare function buildPageOpenGraph(page: Pick<PageFrontmatter, "title" | "description" | "ogImage" | "openGraph">, ogConfig?: OGConfig, baseUrl?: string): PageOpenGraph | undefined;
93
+ /**
94
+ * Build the Twitter card metadata object for a page.
95
+ * When the page has twitter in frontmatter, uses it.
96
+ * Otherwise builds from ogImage or config (dynamic endpoint).
97
+ */
98
+ declare function buildPageTwitter(page: Pick<PageFrontmatter, "title" | "description" | "ogImage" | "openGraph" | "twitter">, ogConfig?: OGConfig, baseUrl?: string): PageTwitter | undefined;
99
+ //#endregion
100
+ export { type AIConfig, type ApiReferenceConfig, type BreadcrumbConfig, type CodeBlockCopyData, type CopyMarkdownConfig, type DocsConfig, type DocsFeedbackData, type DocsFeedbackValue, type DocsI18nConfig, type DocsMcpConfig, type DocsMcpToolsConfig, type DocsMetadata, type DocsNav, type DocsPathMatch, type DocsTheme, type FeedbackConfig, type FontStyle, type GithubConfig, type LastUpdatedConfig, type LlmsTxtConfig, type OGConfig, type OpenDocsConfig, type OpenDocsProvider, type OpenGraphImage, type OrderingItem, type PageActionsConfig, type PageFrontmatter, type PageOpenGraph, type PageTwitter, type ResolvedDocsI18n, type SidebarComponentProps, type SidebarConfig, type SidebarFolderNode, type SidebarNode, type SidebarPageNode, type SidebarTree, type ThemeToggleConfig, type TypographyConfig, type UIConfig, buildPageOpenGraph, buildPageTwitter, createTheme, deepMerge, defineDocs, extendTheme, resolveDocsI18n, resolveDocsLocale, resolveDocsPath, resolveOGImage, resolveTitle };
package/dist/index.mjs ADDED
@@ -0,0 +1,227 @@
1
+ //#region src/define-docs.ts
2
+ /**
3
+ * Define docs configuration. Validates and returns the config.
4
+ */
5
+ function defineDocs(config) {
6
+ return {
7
+ entry: config.entry ?? "docs",
8
+ contentDir: config.contentDir,
9
+ i18n: config.i18n,
10
+ theme: config.theme,
11
+ nav: config.nav,
12
+ github: config.github,
13
+ themeToggle: config.themeToggle,
14
+ breadcrumb: config.breadcrumb,
15
+ sidebar: config.sidebar,
16
+ components: config.components,
17
+ onCopyClick: config.onCopyClick,
18
+ feedback: config.feedback,
19
+ mcp: config.mcp,
20
+ icons: config.icons,
21
+ pageActions: config.pageActions,
22
+ lastUpdated: config.lastUpdated,
23
+ llmsTxt: config.llmsTxt,
24
+ ai: config.ai,
25
+ ordering: config.ordering,
26
+ metadata: config.metadata,
27
+ og: config.og,
28
+ apiReference: config.apiReference
29
+ };
30
+ }
31
+
32
+ //#endregion
33
+ //#region src/utils.ts
34
+ /**
35
+ * Deep merge utility for theme overrides.
36
+ * Merges objects recursively; later values override earlier ones.
37
+ */
38
+ function deepMerge(target, ...sources) {
39
+ if (!sources.length) return target;
40
+ const source = sources.shift();
41
+ if (!source) return target;
42
+ const result = { ...target };
43
+ for (const key of Object.keys(source)) {
44
+ const sourceVal = source[key];
45
+ const targetVal = result[key];
46
+ if (sourceVal && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal && typeof targetVal === "object" && !Array.isArray(targetVal)) result[key] = deepMerge(targetVal, sourceVal);
47
+ else if (sourceVal !== void 0) result[key] = sourceVal;
48
+ }
49
+ if (sources.length) return deepMerge(result, ...sources);
50
+ return result;
51
+ }
52
+
53
+ //#endregion
54
+ //#region src/create-theme.ts
55
+ /**
56
+ * Create a theme preset factory.
57
+ *
58
+ * Returns a function that accepts optional overrides and deep-merges them
59
+ * with the base theme defaults. This is the same pattern used by the
60
+ * built-in `fumadocs()`, `darksharp()`, and `pixelBorder()` presets.
61
+ *
62
+ * @param baseTheme - The default theme configuration
63
+ * @returns A factory function `(overrides?) => DocsTheme`
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * import { createTheme } from "@farming-labs/docs";
68
+ *
69
+ * export const myTheme = createTheme({
70
+ * name: "my-theme",
71
+ * ui: {
72
+ * colors: { primary: "#6366f1" },
73
+ * layout: { contentWidth: 800 },
74
+ * },
75
+ * });
76
+ * ```
77
+ */
78
+ function createTheme(baseTheme) {
79
+ return function themeFactory(overrides = {}) {
80
+ const merged = deepMerge(baseTheme, overrides);
81
+ if (overrides.ui?.colors) merged._userColorOverrides = { ...overrides.ui.colors };
82
+ return merged;
83
+ };
84
+ }
85
+ /**
86
+ * Extend an existing theme preset with additional defaults.
87
+ *
88
+ * Useful when you want to build on top of an existing theme (e.g. fumadocs)
89
+ * rather than starting from scratch.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * import { extendTheme } from "@farming-labs/docs";
94
+ * import { fumadocs } from "@farming-labs/theme/default";
95
+ *
96
+ * // Start with fumadocs defaults, override some values
97
+ * export const myTheme = extendTheme(fumadocs(), {
98
+ * name: "my-custom-fumadocs",
99
+ * ui: { colors: { primary: "#22c55e" } },
100
+ * });
101
+ * ```
102
+ */
103
+ function extendTheme(baseTheme, extensions) {
104
+ return deepMerge(baseTheme, extensions);
105
+ }
106
+
107
+ //#endregion
108
+ //#region src/i18n.ts
109
+ function normalizeSegment(value) {
110
+ return value.replace(/^\/+|\/+$/g, "");
111
+ }
112
+ function splitSegments(value) {
113
+ const cleaned = normalizeSegment(value);
114
+ return cleaned ? cleaned.split("/").filter(Boolean) : [];
115
+ }
116
+ function resolveDocsI18n(config) {
117
+ if (!config || !Array.isArray(config.locales)) return null;
118
+ const locales = Array.from(new Set(config.locales.map((l) => l.trim()).filter(Boolean)));
119
+ if (locales.length === 0) return null;
120
+ return {
121
+ locales,
122
+ defaultLocale: config.defaultLocale && locales.includes(config.defaultLocale) ? config.defaultLocale : locales[0]
123
+ };
124
+ }
125
+ function resolveDocsLocale(searchParams, i18n) {
126
+ if (!i18n) return void 0;
127
+ const raw = searchParams.get("lang") ?? searchParams.get("locale");
128
+ if (!raw) return void 0;
129
+ if (i18n.locales.includes(raw)) return raw;
130
+ return i18n.defaultLocale;
131
+ }
132
+ function resolveDocsPath(pathname, entry) {
133
+ const entryBase = normalizeSegment(entry || "docs") || "docs";
134
+ const entryParts = splitSegments(entryBase);
135
+ const pathParts = splitSegments(pathname);
136
+ let rest = pathParts;
137
+ if (entryParts.length > 0) {
138
+ if (pathParts.slice(0, entryParts.length).join("/") === entryParts.join("/")) rest = pathParts.slice(entryParts.length);
139
+ }
140
+ return {
141
+ slug: rest.join("/"),
142
+ entryPath: entryBase
143
+ };
144
+ }
145
+
146
+ //#endregion
147
+ //#region src/metadata.ts
148
+ /**
149
+ * Resolve page title using metadata titleTemplate.
150
+ * %s is replaced with page title.
151
+ */
152
+ function resolveTitle(pageTitle, metadata) {
153
+ return (metadata?.titleTemplate ?? "%s").replace("%s", pageTitle);
154
+ }
155
+ /**
156
+ * Resolve OG image URL for a page.
157
+ * Prefers page.openGraph.images[0], then page.ogImage, then config endpoint/default.
158
+ */
159
+ function resolveOGImage(page, ogConfig, baseUrl) {
160
+ if (page.openGraph?.images?.length) return resolveImageUrl(page.openGraph.images[0].url, baseUrl);
161
+ if (!ogConfig?.enabled) return void 0;
162
+ if (page.ogImage) return resolveImageUrl(page.ogImage, baseUrl);
163
+ if (ogConfig.type === "dynamic" && ogConfig.endpoint) return `${baseUrl ?? ""}${ogConfig.endpoint}`;
164
+ return ogConfig.defaultImage;
165
+ }
166
+ function resolveImageUrl(url, baseUrl) {
167
+ if (url.startsWith("/") || url.startsWith("http")) return url;
168
+ const base = baseUrl ?? "";
169
+ return `${base}${base.length > 0 && !base.endsWith("/") ? "/" : ""}${url}`;
170
+ }
171
+ /**
172
+ * Build the Open Graph metadata object for a page.
173
+ * When the page has openGraph in frontmatter, uses it (with title/description filled from page if omitted).
174
+ * Otherwise uses ogImage or config (dynamic endpoint / defaultImage).
175
+ */
176
+ function buildPageOpenGraph(page, ogConfig, baseUrl) {
177
+ if (page.openGraph) {
178
+ const images = page.openGraph.images?.length ? page.openGraph.images.map((img) => ({
179
+ url: resolveImageUrl(img.url, baseUrl),
180
+ width: img.width ?? 1200,
181
+ height: img.height ?? 630
182
+ })) : void 0;
183
+ return {
184
+ title: page.openGraph.title ?? page.title,
185
+ description: page.openGraph.description ?? page.description,
186
+ ...images && { images }
187
+ };
188
+ }
189
+ const url = resolveOGImage(page, ogConfig, baseUrl);
190
+ if (!url) return void 0;
191
+ return {
192
+ title: page.title,
193
+ ...page.description && { description: page.description },
194
+ images: [{
195
+ url,
196
+ width: 1200,
197
+ height: 630
198
+ }]
199
+ };
200
+ }
201
+ /**
202
+ * Build the Twitter card metadata object for a page.
203
+ * When the page has twitter in frontmatter, uses it.
204
+ * Otherwise builds from ogImage or config (dynamic endpoint).
205
+ */
206
+ function buildPageTwitter(page, ogConfig, baseUrl) {
207
+ if (page.twitter) {
208
+ const images = page.twitter.images?.length ? page.twitter.images.map((url) => resolveImageUrl(url, baseUrl)) : void 0;
209
+ return {
210
+ ...page.twitter.card && { card: page.twitter.card },
211
+ ...page.twitter.title !== void 0 && { title: page.twitter.title },
212
+ ...page.twitter.description !== void 0 && { description: page.twitter.description },
213
+ ...images && { images }
214
+ };
215
+ }
216
+ const url = resolveOGImage(page, ogConfig, baseUrl);
217
+ if (!url) return void 0;
218
+ return {
219
+ card: "summary_large_image",
220
+ title: page.title,
221
+ ...page.description && { description: page.description },
222
+ images: [url]
223
+ };
224
+ }
225
+
226
+ //#endregion
227
+ export { buildPageOpenGraph, buildPageTwitter, createTheme, deepMerge, defineDocs, extendTheme, resolveDocsI18n, resolveDocsLocale, resolveDocsPath, resolveOGImage, resolveTitle };