@knitli/astro-docs-template 0.1.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/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@knitli/astro-docs-template",
3
+ "version": "0.1.0",
4
+ "description": "Opinionated Astro + Starlight docs site template with Knitli branding",
5
+ "keywords": [
6
+ "knitli",
7
+ "astro",
8
+ "starlight",
9
+ "documentation",
10
+ "template"
11
+ ],
12
+ "homepage": "https://docs.knitli.com",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/knitli/knitli-site.git",
16
+ "directory": "packages/shared/astro-docs-template"
17
+ },
18
+ "license": "(MIT OR Apache-2.0) AND LicenseRef-KnitliProprietary",
19
+ "author": "Knitli Inc.",
20
+ "type": "module",
21
+ "exports": {
22
+ ".": "./src/index.ts",
23
+ "./config": "./src/config.ts"
24
+ },
25
+ "files": [
26
+ "src",
27
+ "scaffolding",
28
+ "README.md",
29
+ "CHANGELOG.md"
30
+ ],
31
+ "scripts": {
32
+ "build": "bunx tsc --noCheck",
33
+ "prepublishOnly": "bun run build",
34
+ "publish": "npm publish --access public",
35
+ "typecheck": "bunx tsc --noEmit"
36
+ },
37
+ "dependencies": {
38
+ "@astrojs/cloudflare": "catalog:astro-core",
39
+ "@astrojs/markdoc": "catalog:astro-plugins",
40
+ "@astrojs/mdx": "catalog:astro-core",
41
+ "@astrojs/sitemap": "catalog:astro-core",
42
+ "@astrojs/starlight": "catalog:astro-core",
43
+ "@knitli/docs-components": "workspace:*",
44
+ "@nuasite/llm-enhancements": "catalog:astro-plugins",
45
+ "astro": "catalog:astro-core",
46
+ "astro-cloudflare-pages-headers": "catalog:astro-plugins",
47
+ "astro-d2": "catalog:astro-plugins",
48
+ "astro-favicons": "catalog:astro-core",
49
+ "rehype-external-links": "^3.0.0",
50
+ "starlight-announcement": "catalog:starlight-plugins",
51
+ "starlight-changelogs": "catalog:starlight-plugins",
52
+ "starlight-heading-badges": "catalog:starlight-plugins",
53
+ "starlight-links-validator": "catalog:starlight-plugins",
54
+ "starlight-llms-txt": "catalog:starlight-plugins",
55
+ "starlight-page-actions": "catalog:starlight-plugins",
56
+ "starlight-plugin-icons": "catalog:starlight-plugins",
57
+ "starlight-scroll-to-top": "catalog:starlight-plugins",
58
+ "starlight-sidebar-topics": "catalog:starlight-plugins",
59
+ "starlight-tags": "catalog:starlight-plugins",
60
+ "vite-tsconfig-paths": "catalog:dev-common"
61
+ },
62
+ "devDependencies": {
63
+ "@astrojs/check": "catalog:dev-common",
64
+ "@astrojs/compiler-rs": "catalog:astro-core",
65
+ "@biomejs/biome": "catalog:dev-common",
66
+ "@knitli/tsconfig": "*",
67
+ "@types/node": "catalog:types",
68
+ "bun": "catalog:dev-common",
69
+ "lightningcss": "catalog:optimization",
70
+ "sharp": "catalog:astro-core",
71
+ "svgo": "catalog:optimization",
72
+ "typescript": "catalog:dev-common"
73
+ },
74
+ "devEngines": {
75
+ "packageManager": {
76
+ "name": "bun",
77
+ "version": "catalog:dev-common",
78
+ "onFail": "download"
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,42 @@
1
+ # {{appName}} Docs
2
+
3
+ Documentation site for {{appName}}, built with [Astro](https://astro.build) + [Starlight](https://starlight.astro.build) using the Knitli docs template.
4
+
5
+ ## Development
6
+
7
+ ```bash
8
+ bun install
9
+ bun run dev
10
+ ```
11
+
12
+ ## Deployment
13
+
14
+ Deployed to Cloudflare Pages:
15
+
16
+ ```bash
17
+ bun run deploy
18
+ ```
19
+
20
+ ## Project Structure
21
+
22
+ ```
23
+ .
24
+ ├── src/
25
+ │ ├── content/docs/ # Documentation pages (MDX/MD)
26
+ │ ├── styles/ # Site-specific CSS overrides
27
+ │ └── content.config.ts
28
+ ├── astro.config.ts # Uses @knitli/astro-docs-template createConfig
29
+ ├── wrangler.jsonc # Cloudflare Workers configuration
30
+ └── package.json
31
+ ```
32
+
33
+ ## Configuration
34
+
35
+ This site uses `createConfig()` from `@knitli/astro-docs-template` which provides:
36
+
37
+ - Starlight with Knitli branding (header, footer, theme)
38
+ - Pre-configured plugins (tags, changelogs, LLM text, link validation, etc.)
39
+ - Cloudflare Pages adapter
40
+ - Optimized Vite build settings
41
+
42
+ Override defaults by passing options to `createConfig()` in `astro.config.ts`.
@@ -0,0 +1,16 @@
1
+ // SPDX-FileCopyrightText: 2026 Knitli Inc.
2
+ //
3
+ // SPDX-License-Identifier: MIT OR Apache-2.0
4
+
5
+ import createConfig from "@knitli/astro-docs-template/config";
6
+
7
+ export default createConfig({
8
+ appName: "{{appName}}",
9
+ description: "{{description}}",
10
+ rootDir: import.meta.dirname,
11
+ llmConfig: {
12
+ llmDescription: "{{description}}",
13
+ promotePatterns: ["guides/**", "reference/**"],
14
+ demotePatterns: ["_*"],
15
+ },
16
+ });
@@ -0,0 +1,65 @@
1
+ # SPDX-FileCopyrightText: 2026 Knitli Inc.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ #:tombi schema.strict = false
6
+
7
+ [tasks.installdeps]
8
+ description = "Install dependencies for the project"
9
+ run = "bun install"
10
+ tools.bun = "latest"
11
+
12
+ [tasks.gentypes]
13
+ description = "Generate Cloudflare types for the project"
14
+ run = "bunx wrangler types"
15
+ tools.bun = "latest"
16
+ tools.wrangler = "latest"
17
+ depends = ["installdeps"]
18
+
19
+ [tasks.dev]
20
+ description = "Start the development server"
21
+ run = "bun run dev"
22
+ tools.bun = "latest"
23
+ depends = ["gentypes"]
24
+
25
+ [tasks.build]
26
+ description = "Build the project for production"
27
+ run = "bun run build"
28
+ tools.bun = "latest"
29
+ depends = ["gentypes"]
30
+
31
+ [tasks.deploy]
32
+ description = "Deploy the project to Cloudflare"
33
+ run = '''
34
+ bun run deploy
35
+ '''
36
+ tools.bun = "latest"
37
+ depends = ["build"]
38
+
39
+ [tasks.clean]
40
+ description = "Clean the project by removing the dist directory"
41
+ run = [
42
+ "rm -rf dist",
43
+ "rm -rf .wrangler",
44
+ "rm -rf node_modules",
45
+ "rm -rf .astro",
46
+ ]
47
+
48
+ [tasks.clean-install]
49
+ description = "Clean the project and install dependencies"
50
+ depends = ["clean", "installdeps"]
51
+
52
+ [tasks.fmt]
53
+ description = "Format the codebase using Biome"
54
+ run = "biome format . --write --changed"
55
+ tools.biome = "latest"
56
+
57
+ [tasks.lint]
58
+ description = "Lint the codebase using Biome"
59
+ run = "biome check . --changed"
60
+ tools.biome = "latest"
61
+
62
+ [tasks.fix]
63
+ description = "Fix linting issues using Biome"
64
+ run = "biome check . --write --changed"
65
+ tools.biome = "latest"
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "bunx astro build",
8
+ "deploy": "bunx astro build && bunx wrangler deploy",
9
+ "dev": "bunx astro dev",
10
+ "generate-types": "bunx wrangler types",
11
+ "preview": "bunx astro build && bunx astro preview"
12
+ },
13
+ "dependencies": {
14
+ "@knitli/astro-docs-template": "workspace:*",
15
+ "@knitli/docs-components": "workspace:*"
16
+ },
17
+ "devDependencies": {
18
+ "@astrojs/check": "catalog:dev-common",
19
+ "@types/node": "catalog:types",
20
+ "typescript": "catalog:dev-common",
21
+ "wrangler": "catalog:dev-common"
22
+ }
23
+ }
@@ -0,0 +1,2 @@
1
+ _worker.js
2
+ _routes.json
@@ -0,0 +1,18 @@
1
+ ---
2
+ title: Getting Started
3
+ description: Set up and start using {{appName}}.
4
+ ---
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ bun add {{name}}
10
+ ```
11
+
12
+ ## Quick Start
13
+
14
+ Get up and running in minutes.
15
+
16
+ ## Next Steps
17
+
18
+ - Check the [API Reference](/reference/) for detailed documentation
@@ -0,0 +1,28 @@
1
+ ---
2
+ title: "{{appName}} Docs"
3
+ description: "{{description}}"
4
+ template: splash
5
+ hero:
6
+ tagline: "{{description}}"
7
+ actions:
8
+ - text: Get Started
9
+ link: /{{appNameLower}}/guides/getting-started/
10
+ icon: right-arrow
11
+ - text: API Reference
12
+ link: /{{appNameLower}}/reference/
13
+ icon: open-book
14
+ variant: minimal
15
+ ---
16
+
17
+ import { Card, CardGrid } from "@astrojs/starlight/components";
18
+
19
+ ## Next steps
20
+
21
+ <CardGrid stagger>
22
+ <Card title="Getting Started" icon="rocket">
23
+ Follow the [getting started guide](./guides/getting-started/) to set up your environment.
24
+ </Card>
25
+ <Card title="Configuration" icon="setting">
26
+ See the [configuration reference](./reference/) for all available options.
27
+ </Card>
28
+ </CardGrid>
@@ -0,0 +1,6 @@
1
+ ---
2
+ title: API Reference
3
+ description: "{{appName}} API reference documentation."
4
+ ---
5
+
6
+ Add your API reference documentation here.
@@ -0,0 +1,7 @@
1
+ import { defineCollection } from "astro:content";
2
+ import { docsLoader } from "@astrojs/starlight/loaders";
3
+ import { docsSchema } from "@astrojs/starlight/schema";
4
+
5
+ export const collections = {
6
+ docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
7
+ };
@@ -0,0 +1,5 @@
1
+ type Runtime = import("@astrojs/cloudflare").Runtime<Env>;
2
+
3
+ declare namespace App {
4
+ interface Locals extends Runtime {}
5
+ }
@@ -0,0 +1,4 @@
1
+ /* Site-specific style overrides.
2
+ * This file is loaded after the shared Knitli docs theme.
3
+ * Add custom styles here without modifying shared packages.
4
+ */
@@ -0,0 +1,26 @@
1
+ # sample tags.yml for starlight-tags plugin configuration
2
+ tags:
3
+ getting-started:
4
+ label: "Getting Started"
5
+ description: "Essential first steps"
6
+ color: "#22c55e"
7
+ icon: "🚀"
8
+ priority: 100
9
+
10
+ components:
11
+ label: "Components"
12
+ description: "UI building blocks"
13
+ color: "#3b82f6"
14
+ icon: "🧩"
15
+
16
+ advanced:
17
+ label: "Advanced"
18
+ description: "For experienced users"
19
+ color: "#f59e0b"
20
+ icon: "⚡"
21
+ difficulty: advanced
22
+ prerequisites:
23
+ - getting-started
24
+
25
+ defaults:
26
+ color: "#6b7280"
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "@knitli/tsconfig/astro-tsconfig.json",
3
+ "include": [".astro/types.d.ts", "**/*"],
4
+ "exclude": ["dist"]
5
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "$schema": "node_modules/wrangler/config-schema.json",
3
+ "account_id": "5def525918a785eeda6decf66cf6b99b",
4
+ "compatibility_date": "2026-03-17",
5
+ "compatibility_flags": ["global_fetch_strictly_public", "nodejs_compat"],
6
+ "name": "{{workerName}}",
7
+ "main": "@astrojs/cloudflare/entrypoints/server",
8
+ "assets": {
9
+ "directory": "./dist",
10
+ "binding": "ASSETS",
11
+ "html_handling": "auto-trailing-slash",
12
+ "not_found_handling": "404-errors",
13
+ "run_worker_first": false
14
+ },
15
+ "observability": {
16
+ "enabled": true
17
+ },
18
+ "placement": {
19
+ "mode": "smart"
20
+ },
21
+ "preview_urls": true,
22
+ "workers_dev": true,
23
+ "routes": [
24
+ {
25
+ "pattern": "https://docs.knitli.com/{{appNameLower}}/*",
26
+ "zone_name": "knitli.com"
27
+ }
28
+ ],
29
+ "vars": {
30
+ "PUBLIC_DOCS_PRODUCT": "{{appName}}"
31
+ }
32
+ }
package/src/config.ts ADDED
@@ -0,0 +1,551 @@
1
+ // SPDX-FileCopyrightText: 2026 Knitli Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+
5
+ // @ts-check
6
+
7
+ import type { OutgoingHttpHeaders } from "node:http2";
8
+ import cloudflare from "@astrojs/cloudflare";
9
+ import markdoc from "@astrojs/markdoc";
10
+ import mdx from "@astrojs/mdx";
11
+ import sitemap from "@astrojs/sitemap";
12
+ import starlight from "@astrojs/starlight";
13
+ import { DocsAssets } from "@knitli/docs-components";
14
+ import llmEnhancements from "@nuasite/llm-enhancements";
15
+ import { defineConfig, fontProviders } from "astro/config";
16
+ import cloudflarePagesHeaders from "astro-cloudflare-pages-headers";
17
+ import astroD2 from "astro-d2";
18
+ import favicons from "astro-favicons";
19
+ import rehypeExternalLinks from "rehype-external-links";
20
+ import starlightAnnouncement from "starlight-announcement";
21
+ import starlightChangelogs from "starlight-changelogs";
22
+ import starlightHeadingBadges from "starlight-heading-badges";
23
+ import starlightLinksValidator from "starlight-links-validator";
24
+ import starlightLlmsText from "starlight-llms-txt";
25
+ import starlightPageActions from "starlight-page-actions";
26
+ import {
27
+ starlightIconsIntegration,
28
+ starlightIconsPlugin,
29
+ } from "starlight-plugin-icons";
30
+ import starlightScrollToTop from "starlight-scroll-to-top";
31
+ import starlightSidebarTopics from "starlight-sidebar-topics";
32
+ import starlightTags from "starlight-tags";
33
+ import { searchForWorkspaceRoot } from "vite";
34
+ import viteTsconfigPaths from "vite-tsconfig-paths";
35
+
36
+ type defaultIntegration =
37
+ | "sitemap"
38
+ | "astroD2"
39
+ | "markdoc"
40
+ | "mdx"
41
+ | "favicons"
42
+ | "cloudflare-pages-headers";
43
+
44
+ function nonNullable<T>(value: T): value is NonNullable<T> {
45
+ return value != null;
46
+ }
47
+
48
+ // ── Defaults (defined before interface so `typeof` references work) ──
49
+
50
+ export const {
51
+ headlineLogoDark,
52
+ headlineLogoLight,
53
+ variables,
54
+ docsStyle,
55
+ faviconIco,
56
+ faviconSvg,
57
+ } = DocsAssets;
58
+
59
+ const shikiCfg = {
60
+ themes: {
61
+ light: "catppuccin-latte" as const,
62
+ dark: "catppuccin-mocha" as const,
63
+ },
64
+ bundledLangs: [
65
+ "ansi",
66
+ "astro",
67
+ "bash",
68
+ "json",
69
+ "markdown",
70
+ "python",
71
+ "rust",
72
+ "toml",
73
+ "typescript",
74
+ "yaml",
75
+ ],
76
+ langAlias: {
77
+ js: "typescript",
78
+ md: "markdown",
79
+ py: "python",
80
+ rs: "rust",
81
+ sh: "bash",
82
+ yml: "yaml",
83
+ },
84
+ };
85
+
86
+ const imgDomains = [
87
+ { protocol: "https", hostname: "ui-avatars.com" },
88
+ { protocol: "https", hostname: "knitli.com" },
89
+ { protocol: "https", hostname: "*.knitli.com" },
90
+ { protocol: "https", hostname: "*.githubusercontent.com" },
91
+ { protocol: "https", hostname: "*.cloudflare.com" },
92
+ ];
93
+
94
+ const defaultFontConfig = [
95
+ {
96
+ provider: fontProviders.google(),
97
+ name: "DM Mono",
98
+ cssVariable: "--font-sans",
99
+ weights: [400, 500, 700] as [number, ...number[]],
100
+ styles: ["normal", "italic"] as [string, ...string[]],
101
+ subsets: ["latin"] as [string, ...string[]],
102
+ formats: ["woff2"] as [string, ...string[]],
103
+ fallbacks: [
104
+ "Roboto Mono",
105
+ "Menlo",
106
+ "Consolas",
107
+ "DejaVu Sans Mono",
108
+ "monospace",
109
+ ],
110
+ },
111
+ {
112
+ provider: fontProviders.google(),
113
+ name: "JetBrains Mono",
114
+ cssVariable: "--font-mono",
115
+ weights: [400, 500, 700] as [number, ...number[]],
116
+ styles: ["normal", "italic"] as [string, ...string[]],
117
+ subsets: ["latin"] as [string, ...string[]],
118
+ formats: ["woff2"] as [string, ...string[]],
119
+ fallbacks: [
120
+ "DM Mono",
121
+ "Roboto Mono",
122
+ "Menlo",
123
+ "Consolas",
124
+ "DejaVu Sans Mono",
125
+ "monospace",
126
+ ],
127
+ },
128
+ ];
129
+
130
+ const codeweaverFontConfig = [
131
+ { ...defaultFontConfig[1], cssVariable: "--font-sans" },
132
+ {
133
+ provider: fontProviders.google(),
134
+ name: "IBM Plex Mono",
135
+ cssVariable: "--font-mono",
136
+ weights: [400, 500, 700] as [number, ...number[]],
137
+ styles: ["normal", "italic"] as [string, ...string[]],
138
+ subsets: ["latin"] as [string, ...string[]],
139
+ formats: ["woff2"] as [string, ...string[]],
140
+ fallbacks: [
141
+ "JetBrains Mono",
142
+ "DM Mono",
143
+ "Roboto Mono",
144
+ "Menlo",
145
+ "Consolas",
146
+ "DejaVu Sans Mono",
147
+ "monospace",
148
+ ],
149
+ },
150
+ ];
151
+
152
+ // ── Public interface ──
153
+
154
+ export interface DocsTemplateOptions {
155
+ appName: string;
156
+ description: string;
157
+ llmConfig: {
158
+ llmDescription: string;
159
+ promotePatterns: string[];
160
+ demotePatterns: string[];
161
+ };
162
+ rootDir: string;
163
+ shikiConfig?: typeof shikiCfg;
164
+ logoDark?: string;
165
+ logoLight?: string;
166
+ imageDomains?: typeof imgDomains;
167
+ is_codeweaver?: boolean;
168
+ sitemapFilter?: (page: string) => boolean;
169
+ linkValidationConfig?: Record<string, unknown>;
170
+ sidebarConfig?: {
171
+ label: string;
172
+ autogenerate: { directory: string };
173
+ }[];
174
+ unwantedPlugins?: string[];
175
+ // biome-ignore lint/suspicious/noExplicitAny: plugin configs are opaque pass-throughs
176
+ pluginConfigs?: Record<string, any>;
177
+ unwantedIntegrations?: defaultIntegration[];
178
+ // biome-ignore lint/suspicious/noExplicitAny: integration configs are opaque pass-throughs
179
+ integrationConfigs?: Record<string, any>;
180
+ headersConfig?: OutgoingHttpHeaders;
181
+ }
182
+
183
+ export type IntegrationOptions = Pick<
184
+ DocsTemplateOptions,
185
+ "appName" | "sitemapFilter" | "integrationConfigs"
186
+ > & { unwantedIntegrations?: defaultIntegration[] };
187
+
188
+ const getIntegrations = (options: IntegrationOptions) => {
189
+ const {
190
+ appName,
191
+ sitemapFilter,
192
+ integrationConfigs,
193
+ unwantedIntegrations = [],
194
+ } = options;
195
+ const defaultIntegrations = [
196
+ astroD2({ skipGeneration: true }),
197
+ markdoc(),
198
+ llmEnhancements({ llmsTxt: false }),
199
+ mdx(),
200
+ favicons({
201
+ name: `${appName} Docs by Knitli`,
202
+ short_name: `${appName} Docs`,
203
+ input: {
204
+ favicons: [faviconSvg],
205
+ },
206
+ }),
207
+ cloudflarePagesHeaders({}),
208
+ sitemap({
209
+ filter: sitemapFilter || ((page) => !/\^\/(?!cdn-cgi\/)/.test(page)),
210
+ changefreq: "weekly",
211
+ priority: 0.4,
212
+ lastmod: new Date(),
213
+ namespaces: {
214
+ image: false,
215
+ video: false,
216
+ },
217
+ }),
218
+ ];
219
+
220
+ const filtered = defaultIntegrations.filter((integration) => {
221
+ const name = integration?.name;
222
+ return !name || !unwantedIntegrations.includes(name as defaultIntegration);
223
+ });
224
+
225
+ if (!integrationConfigs) return filtered;
226
+
227
+ const overrides = Object.entries(integrationConfigs)
228
+ .map(([integrationName, config]) => {
229
+ switch (integrationName) {
230
+ case "astroD2":
231
+ return astroD2(config);
232
+ case "markdoc":
233
+ return markdoc(config);
234
+ case "mdx":
235
+ return mdx(config);
236
+ case "favicons":
237
+ return favicons(config);
238
+ case "sitemap":
239
+ return sitemap(config);
240
+ default:
241
+ return null;
242
+ }
243
+ })
244
+ .filter(nonNullable);
245
+
246
+ return [...filtered, ...overrides];
247
+ };
248
+
249
+ export type PluginOptions = Pick<
250
+ DocsTemplateOptions,
251
+ "appName" | "llmConfig" | "pluginConfigs"
252
+ > & { unwantedPlugins?: string[] };
253
+
254
+ const get_plugins = (options: PluginOptions) => {
255
+ const { appName, llmConfig, pluginConfigs, unwantedPlugins = [] } = options;
256
+ const defaultPlugins = [
257
+ starlightAnnouncement(),
258
+ starlightChangelogs(),
259
+ starlightHeadingBadges(),
260
+ starlightIconsIntegration(),
261
+ starlightIconsPlugin(),
262
+ starlightLinksValidator(),
263
+ starlightPageActions({
264
+ baseUrl: `https://docs.knitli.com/${appName.toLowerCase()}`,
265
+ actions: { claude: true, chatgpt: true, markdown: true },
266
+ share: true,
267
+ }),
268
+ starlightTags({ onInlineTagsNotFound: "warn" }),
269
+ pluginConfigs?.starlightSidebarTopics
270
+ ? starlightSidebarTopics(pluginConfigs.starlightSidebarTopics)
271
+ : null,
272
+ starlightScrollToTop({ showOnHomepage: false }),
273
+ // We need to configure starlight-tags with a tags.yml.
274
+ //starlightTags(),
275
+ starlightLlmsText({
276
+ projectName: appName,
277
+ description: llmConfig.llmDescription,
278
+ promote: llmConfig.promotePatterns,
279
+ demote: llmConfig.demotePatterns,
280
+ minify: {
281
+ whitespace: true,
282
+ note: true,
283
+ details: true,
284
+ },
285
+ }),
286
+ ];
287
+
288
+ const filtered = defaultPlugins
289
+ .filter(nonNullable)
290
+ .filter((plugin) => !unwantedPlugins.includes(plugin.name));
291
+
292
+ if (!pluginConfigs) return filtered;
293
+
294
+ const overrides = Object.entries(pluginConfigs)
295
+ .map(([pluginName, config]) => {
296
+ switch (pluginName) {
297
+ case "starlightAnnouncement":
298
+ return starlightAnnouncement(config);
299
+ case "starlightIconsIntegration":
300
+ return starlightIconsIntegration(config);
301
+ case "starlightIconsPlugin":
302
+ return starlightIconsPlugin(config);
303
+ case "starlightLinksValidator":
304
+ return starlightLinksValidator(config);
305
+ case "starlightPageActions":
306
+ return starlightPageActions(config);
307
+ case "starlightTags":
308
+ return starlightTags(config);
309
+ case "starlightScrollToTop":
310
+ return starlightScrollToTop(config);
311
+ default:
312
+ return null;
313
+ }
314
+ })
315
+ .filter(nonNullable);
316
+
317
+ return [...filtered, ...overrides];
318
+ };
319
+
320
+ export default function createConfig(options: DocsTemplateOptions) {
321
+ const {
322
+ appName,
323
+ description,
324
+ llmConfig,
325
+ rootDir,
326
+ shikiConfig = shikiCfg,
327
+ logoDark = headlineLogoDark,
328
+ logoLight = headlineLogoLight,
329
+ imageDomains = imgDomains,
330
+ is_codeweaver = false,
331
+ sitemapFilter,
332
+ sidebarConfig,
333
+ pluginConfigs,
334
+ integrationConfigs,
335
+ unwantedIntegrations,
336
+ unwantedPlugins,
337
+ headersConfig,
338
+ } = options;
339
+
340
+ // https://astro.build/config
341
+ return defineConfig({
342
+ site: "https://docs.knitli.com",
343
+ base: `/${appName.toLowerCase()}/`,
344
+ adapter: cloudflare({
345
+ prerenderEnvironment: "workerd",
346
+ experimental: {
347
+ headersAndRedirectsDevModeSupport: true,
348
+ },
349
+ configPath: `${rootDir}/wrangler.jsonc`,
350
+ imageService: "compile",
351
+ }),
352
+ // Image optimization
353
+ image: {
354
+ service: {
355
+ entrypoint: "astro/assets/services/sharp",
356
+ },
357
+ responsiveStyles: true,
358
+ layout: "constrained",
359
+ remotePatterns: imageDomains,
360
+ },
361
+ compressHTML: true,
362
+ // biome-ignore lint/suspicious/noExplicitAny: Astro's FontFamily generic inference doesn't match spread patterns
363
+ fonts: (is_codeweaver ? codeweaverFontConfig : defaultFontConfig) as any,
364
+ // Build optimizations
365
+ build: {
366
+ inlineStylesheets: "auto",
367
+ assets: "_astro",
368
+ },
369
+ markdown: {
370
+ shikiConfig: Object.fromEntries(
371
+ Object.entries(shikiConfig).filter(([key]) => key !== "bundledLangs"),
372
+ ),
373
+ rehypePlugins: [
374
+ rehypeExternalLinks({
375
+ content: { type: "text", value: " 🔗" },
376
+ rel: ["nofollow"],
377
+ }),
378
+ ],
379
+ },
380
+ server: {
381
+ headers: headersConfig,
382
+ },
383
+ trailingSlash: "always",
384
+ // Vite configuration for better bundling
385
+ vite: {
386
+ server: {
387
+ fs: {
388
+ allow: [searchForWorkspaceRoot(rootDir)],
389
+ },
390
+ },
391
+ assetsInclude: [
392
+ "src/*.webp",
393
+ "src/*.png",
394
+ "src/*.jpg",
395
+ "src/*.jpeg",
396
+ "src/*.svg",
397
+ "src/*.avif",
398
+ ],
399
+ plugins: [viteTsconfigPaths({ loose: true })],
400
+ define: {
401
+ "import.meta.env.PUBLIC_DOCS_PRODUCT": JSON.stringify(appName),
402
+ },
403
+ build: {
404
+ cssMinify: "lightningcss",
405
+ minify: "esbuild",
406
+ cssCodeSplit: true,
407
+ rollupOptions: {
408
+ external: ["shiki", "shiki-esbuild"],
409
+ output: {
410
+ format: "es",
411
+ dir: `${rootDir}/dist/_astro/`,
412
+ compact: true,
413
+ interop: "esModule",
414
+ experimentalMinChunkSize: 10000,
415
+ banner: `
416
+ /* SPDX-FileCopyrightText: 2026 Knitli Inc.
417
+ * SPDX-License-Identifier: MIT OR Apache-2.0
418
+ */
419
+ `.trim(),
420
+ minifyInternalExports: true,
421
+ sourcemap: false,
422
+ generatedCode: {
423
+ arrowFunctions: true,
424
+ constBindings: true,
425
+ objectShorthand: true,
426
+ symbols: true,
427
+ },
428
+ entryFileNames: "assets/[name]-[hash].js",
429
+ chunkFileNames: "assets/[name]-[hash].js",
430
+ assetFileNames: "assets/[name]-[hash][extname]",
431
+ },
432
+ treeshake: "smallest",
433
+ },
434
+ ssr: false,
435
+ },
436
+ css: {
437
+ lightningcss: {},
438
+ },
439
+ },
440
+ prefetch: {
441
+ defaultStrategy: "viewport",
442
+ },
443
+ prerenderConflictBehavior: "warn",
444
+ experimental: {
445
+ chromeDevtoolsWorkspace: true,
446
+ clientPrerender: true,
447
+ contentIntellisense: true,
448
+ queuedRendering: {
449
+ contentCache: true,
450
+ enabled: true,
451
+ },
452
+ rustCompiler: true,
453
+ svgo: {
454
+ plugins: [
455
+ {
456
+ name: "preset-default",
457
+ params: {
458
+ overrides: {
459
+ removeMetadata: false,
460
+ },
461
+ },
462
+ },
463
+ ],
464
+ },
465
+ },
466
+
467
+ // Static site generation for Cloudflare
468
+ output: "static",
469
+ integrations: [
470
+ ...getIntegrations({
471
+ appName,
472
+ sitemapFilter,
473
+ integrationConfigs,
474
+ unwantedIntegrations,
475
+ }),
476
+ starlight({
477
+ title: `${appName} Docs`,
478
+ pagefind: true,
479
+ description: description,
480
+ logo: {
481
+ dark: logoDark,
482
+ light: logoLight,
483
+ alt: is_codeweaver ? "CodeWeaver Logo" : "Knitli Logo",
484
+ replacesTitle: true,
485
+ },
486
+ editLink: {
487
+ baseUrl: `https://github.com/knitli/${appName.toLowerCase()}/edit/main/docs-site/src`,
488
+ },
489
+ expressiveCode: {
490
+ useStarlightDarkModeSwitch: true,
491
+ themes: [shikiConfig.themes.dark, shikiConfig.themes.light],
492
+ removeUnusedThemes: true,
493
+ shiki: {
494
+ ...Object.fromEntries(
495
+ Object.entries(shikiConfig).filter(([key]) => key !== "themes"),
496
+ ),
497
+ },
498
+ },
499
+ plugins: get_plugins({
500
+ appName,
501
+ llmConfig,
502
+ pluginConfigs,
503
+ unwantedPlugins,
504
+ // biome-ignore lint/suspicious/noExplicitAny: Starlight plugin types are complex Zod inferences
505
+ }) as any[],
506
+ social: [
507
+ {
508
+ icon: "github",
509
+ label: "GitHub",
510
+ href: `https://github.com/knitli/${appName.toLowerCase()}`,
511
+ },
512
+ ],
513
+ components: {
514
+ Footer: "@knitli/docs-components/Footer.astro",
515
+ PageFrame: "@knitli/docs-components/PageFrame.astro",
516
+ },
517
+ customCss: [variables, docsStyle, `${rootDir}/src/styles/custom.css`],
518
+ head: [
519
+ {
520
+ tag: "meta",
521
+ attrs: {
522
+ property: "og:image",
523
+ content: `https://docs.knitli.com/${appName.toLowerCase()}/og-image.png`,
524
+ },
525
+ },
526
+ {
527
+ tag: "meta",
528
+ attrs: {
529
+ property: "twitter:card",
530
+ content: "summary_large_image",
531
+ },
532
+ },
533
+ ],
534
+ sidebar: sidebarConfig || [
535
+ {
536
+ label: "Guides",
537
+ autogenerate: { directory: "guides" },
538
+ },
539
+ {
540
+ label: "Examples",
541
+ autogenerate: { directory: "examples" },
542
+ },
543
+ {
544
+ label: "Reference",
545
+ autogenerate: { directory: "reference" },
546
+ },
547
+ ],
548
+ }),
549
+ ],
550
+ });
551
+ }
package/src/index.ts ADDED
@@ -0,0 +1,128 @@
1
+ // SPDX-FileCopyrightText: 2026 Knitli Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+
5
+ import {
6
+ cpSync,
7
+ existsSync,
8
+ mkdirSync,
9
+ readdirSync,
10
+ readFileSync,
11
+ statSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { dirname, join, resolve } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ export { type DocsTemplateOptions, default as createConfig } from "./config.js";
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const SCAFFOLDING_DIR = resolve(__dirname, "../scaffolding");
21
+
22
+ export interface InitOptions {
23
+ /** Display name for the product (e.g. "Recoco", "CodeWeaver") */
24
+ appName: string;
25
+ /** npm package name (e.g. "@knitli-site/recoco-docs") */
26
+ name: string;
27
+ /** Short product description */
28
+ description: string;
29
+ /** Cloudflare Worker name (e.g. "recoco-docs") */
30
+ workerName?: string;
31
+ /** Whether this is a CodeWeaver-branded site */
32
+ is_codeweaver?: boolean;
33
+ }
34
+
35
+ /** File extensions that should have placeholder substitution applied */
36
+ const TEXT_EXTENSIONS = new Set([
37
+ ".ts",
38
+ ".js",
39
+ ".mjs",
40
+ ".json",
41
+ ".jsonc",
42
+ ".md",
43
+ ".mdx",
44
+ ".yml",
45
+ ".yaml",
46
+ ".css",
47
+ ".html",
48
+ ".astro",
49
+ ".d.ts",
50
+ ]);
51
+
52
+ function isTextFile(filePath: string): boolean {
53
+ return TEXT_EXTENSIONS.has(filePath.slice(filePath.lastIndexOf(".")));
54
+ }
55
+
56
+ function buildReplacements(options: InitOptions): Record<string, string> {
57
+ return {
58
+ "{{appName}}": options.appName,
59
+ "{{appNameLower}}": options.appName.toLowerCase(),
60
+ "{{name}}": options.name,
61
+ "{{description}}": options.description,
62
+ "{{workerName}}":
63
+ options.workerName ?? `${options.appName.toLowerCase()}-docs`,
64
+ };
65
+ }
66
+
67
+ function applyReplacements(
68
+ content: string,
69
+ replacements: Record<string, string>,
70
+ ): string {
71
+ let result = content;
72
+ for (const [placeholder, value] of Object.entries(replacements)) {
73
+ result = result.replaceAll(placeholder, value);
74
+ }
75
+ return result;
76
+ }
77
+
78
+ /**
79
+ * Initialize a new Knitli docs site from the template scaffolding.
80
+ *
81
+ * Copies all files from the scaffolding directory to the target path,
82
+ * applying placeholder substitution to text files.
83
+ *
84
+ * @param targetPath - Directory to scaffold into (will be created if it doesn't exist)
85
+ * @param options - Configuration for placeholder substitution
86
+ * @returns List of files created
87
+ */
88
+ export function initDocsTemplate(
89
+ targetPath: string,
90
+ options: InitOptions,
91
+ ): string[] {
92
+ const target = resolve(targetPath);
93
+ const replacements = buildReplacements(options);
94
+ const created: string[] = [];
95
+
96
+ if (!existsSync(SCAFFOLDING_DIR)) {
97
+ throw new Error(`Scaffolding directory not found at ${SCAFFOLDING_DIR}`);
98
+ }
99
+
100
+ function copyDir(srcDir: string, destDir: string) {
101
+ if (!existsSync(destDir)) {
102
+ mkdirSync(destDir, { recursive: true });
103
+ }
104
+
105
+ for (const entry of readdirSync(srcDir)) {
106
+ const srcPath = join(srcDir, entry);
107
+ const destPath = join(destDir, entry);
108
+ const stat = statSync(srcPath);
109
+
110
+ if (stat.isDirectory()) {
111
+ copyDir(srcPath, destPath);
112
+ } else if (isTextFile(srcPath)) {
113
+ const content = readFileSync(srcPath, "utf-8");
114
+ const processed = applyReplacements(content, replacements);
115
+ mkdirSync(dirname(destPath), { recursive: true });
116
+ writeFileSync(destPath, processed, "utf-8");
117
+ created.push(destPath);
118
+ } else {
119
+ mkdirSync(dirname(destPath), { recursive: true });
120
+ cpSync(srcPath, destPath);
121
+ created.push(destPath);
122
+ }
123
+ }
124
+ }
125
+
126
+ copyDir(SCAFFOLDING_DIR, target);
127
+ return created;
128
+ }