@rahuldshetty/inscribe 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/README.md ADDED
@@ -0,0 +1,65 @@
1
+ <div align="center">
2
+ <img src="https://via.placeholder.com/150x150.png?text=Inscribe" alt="Inscribe Logo" width="120" />
3
+ <h1>Inscribe</h1>
4
+ <p><strong>A minimalist, high-performance Static Site Generator (SSG)</strong></p>
5
+ <p>
6
+ <img src="https://img.shields.io/badge/status-under%20development-orange" alt="Project Status" />
7
+ <img src="https://img.shields.io/badge/license-MIT-blue" alt="License" />
8
+ <img src="https://img.shields.io/badge/built%20with-Bun-black" alt="Built with Bun" />
9
+ </p>
10
+ </div>
11
+
12
+ ---
13
+
14
+ Inscribe is a modern static site generator built with **Bun**, **MDX**, and **Preact**. It's designed to be fast, simple, and developer-friendly, making it perfect for blogs, documentation, and personal portfolios.
15
+
16
+ > [!IMPORTANT]
17
+ > This project is currently **under active development**. Features and APIs are subject to change.
18
+
19
+ ## ✨ Features
20
+
21
+ - [x] **CLI** – Simple commands to scaffold, develop, and build your site.
22
+ - [x] **Dev Server** – Local development server with instant live reload.
23
+ - [x] **MDX & Markdown** – Write content using powerful MDX and standard Markdown.
24
+ - [ ] **Search** – Integrated full-text search.
25
+ - [ ] **Themes** – Customizable and extensible theme system.
26
+ - [ ] **Plugins** – Flexible plugin architecture for extending functionality.
27
+
28
+ ## 🚀 Quick Start
29
+
30
+ ### Installation
31
+
32
+ ```bash
33
+ # Using Bun (Recommended)
34
+ bun install -g inscribe-ssg
35
+
36
+ # Using npm
37
+ npm install -g inscribe-ssg
38
+ ```
39
+
40
+ ### Usage
41
+
42
+ Scaffold a new project, start the development server, or build for production.
43
+
44
+ ```bash
45
+ # Initialize a new project in the current directory
46
+ inscribe init
47
+
48
+ # Run the development server with live reload
49
+ inscribe dev
50
+
51
+ # Build the static site for production
52
+ inscribe build
53
+ ```
54
+
55
+ ## 🛠️ Commands
56
+
57
+ | Command | Description |
58
+ | :------------- | :-------------------------------------------------------------- |
59
+ | `init` | Create a new scaffold for your blog or portfolio. |
60
+ | `dev [path]` | Start the local development server (default: `.` port `3000`). |
61
+ | `build [path]` | Generate a production-ready static website (default: `./dist`). |
62
+
63
+ ---
64
+
65
+ <p align="center">Made with ❤️ by <a href="https://github.com/rahuldshetty">rahuldshetty</a></p>
@@ -0,0 +1,145 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import { minifyHtml } from "../utils/minifier";
4
+ import { parseBlogPost, renderSectionPage, renderSectionIndexPage, renderHomePage, NavState } from "./renderer";
5
+ import { Blog } from "../schemas/blog";
6
+ import { InscribeConfig } from "../schemas/inscribe";
7
+ import { readInscribeFile } from "./inscribe_reader";
8
+ import { parseFolderMetadata } from "../utils/markdown";
9
+ import { FolderMetadata } from "../schemas/folder";
10
+
11
+ export interface BuildOptions {
12
+ sourceDir: string;
13
+ outputDir: string;
14
+ env: string;
15
+ }
16
+
17
+ const buildSection = async (
18
+ type: 'blog' | 'doc',
19
+ sourceDir: string,
20
+ outputDir: string,
21
+ files: string[],
22
+ isRelease: boolean,
23
+ inscribe: InscribeConfig,
24
+ navState: NavState
25
+ ): Promise<{ posts: Blog[]; folderMetadata: Record<string, FolderMetadata> }> => {
26
+ const posts: Blog[] = [];
27
+ const singularFolder = type === 'blog' ? 'blog' : 'doc';
28
+ const sectionRoot = path.join(sourceDir, type === 'doc' ? (inscribe.doc_path || 'docs') : (inscribe.blog_path || 'blog'));
29
+
30
+ await fs.ensureDir(path.join(outputDir, singularFolder));
31
+
32
+ const folderMetadata: Record<string, FolderMetadata> = {};
33
+
34
+ for (const filePath of files) {
35
+ if (filePath.endsWith("index.md")) {
36
+ const dir = path.dirname(filePath);
37
+ const relativeDir = path.relative(sectionRoot, dir);
38
+ folderMetadata[relativeDir || ''] = parseFolderMetadata(dir);
39
+ continue;
40
+ }
41
+
42
+ const post = await parseBlogPost(filePath);
43
+ (post as any).relativePath = path.relative(sectionRoot, filePath);
44
+ posts.push(post);
45
+ }
46
+
47
+ posts.sort((a, b) => {
48
+ const weightA = a.metadata.weight ?? 0;
49
+ const weightB = b.metadata.weight ?? 0;
50
+ if (weightA !== weightB) return weightA - weightB;
51
+ return a.metadata.slug.localeCompare(b.metadata.slug);
52
+ });
53
+
54
+ for (const post of posts) {
55
+ let fullHtml = await renderSectionPage(type, post, posts, folderMetadata, inscribe, sourceDir, navState);
56
+ if (isRelease) fullHtml = await minifyHtml(fullHtml);
57
+ await fs.writeFile(path.join(outputDir, singularFolder, `${post.metadata.slug}.html`), fullHtml);
58
+ }
59
+
60
+ return { posts, folderMetadata };
61
+ }
62
+
63
+ export async function build(options: BuildOptions) {
64
+ const { sourceDir, outputDir, env } = options;
65
+ const isRelease = env === "release";
66
+
67
+ // Read inscribe config
68
+ const inscribe = await readInscribeFile(sourceDir);
69
+
70
+ const blogPath = inscribe.blog_path === "null" || !inscribe.blog_path ? null : inscribe.blog_path;
71
+ const docPath = inscribe.doc_path === "null" || !inscribe.doc_path ? null : inscribe.doc_path;
72
+ const blogDir = blogPath ? path.join(sourceDir, blogPath) : null;
73
+ const docDir = docPath ? path.join(sourceDir, docPath) : null;
74
+
75
+ const hasBlog = blogDir ? fs.existsSync(blogDir) : false;
76
+ const hasDocs = docDir ? fs.existsSync(docDir) : false;
77
+ const hasHome = inscribe.show_home !== false;
78
+
79
+ if (!hasBlog && !hasDocs && !hasHome) {
80
+ throw new Error("At least one section (home, blog, or doc) must be active to generate the site.");
81
+ }
82
+
83
+ const navState: NavState = { hasHome, hasBlog, hasDocs };
84
+
85
+ // Ensure output directory exists
86
+ await fs.ensureDir(outputDir);
87
+
88
+ let redirectUrl = "";
89
+
90
+ // Build blogs
91
+ if (hasBlog && blogDir) {
92
+ const blogFiles = fs.readdirSync(blogDir, { recursive: true })
93
+ .filter((f): f is string => typeof f === "string" && (f.endsWith(".md") || f.endsWith(".mdx")))
94
+ .map((file) => path.join(blogDir, file));
95
+
96
+ console.log('No. of blog pages identified:', blogFiles.length);
97
+
98
+ const { posts: blogs } = await buildSection('blog', sourceDir, outputDir, blogFiles, isRelease, inscribe, navState);
99
+
100
+ await fs.ensureDir(path.join(outputDir, "blogs"));
101
+ let blogIndex = renderSectionIndexPage('blog', blogs, {}, inscribe, sourceDir, navState);
102
+ if (isRelease) blogIndex = await minifyHtml(blogIndex);
103
+ await fs.writeFile(path.join(outputDir, "blogs", "index.html"), blogIndex);
104
+
105
+ if (!redirectUrl) redirectUrl = "/blogs/";
106
+ }
107
+
108
+ // Build docs
109
+ if (hasDocs && docDir) {
110
+ const docFiles = fs.readdirSync(docDir, { recursive: true })
111
+ .filter((f): f is string => typeof f === "string" && (f.endsWith(".md") || f.endsWith(".mdx")))
112
+ .map((file) => path.join(docDir, file));
113
+
114
+ console.log('No. of doc pages identified:', docFiles.length);
115
+
116
+ // folderMetadata is computed once inside buildSection and reused for the index page
117
+ const { posts: docs, folderMetadata } = await buildSection('doc', sourceDir, outputDir, docFiles, isRelease, inscribe, navState);
118
+
119
+ await fs.ensureDir(path.join(outputDir, "docs"));
120
+ if (docs.length > 0) {
121
+ const firstLevelDoc = docs.find(p => !((p as any).relativePath).includes('/') && !((p as any).relativePath).includes('\\'));
122
+ const firstDocSlug = (firstLevelDoc || docs[0]).metadata.slug;
123
+ const redirectHtml = `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=/doc/${firstDocSlug}"></head><body>Redirecting...</body></html>`;
124
+ await fs.writeFile(path.join(outputDir, "docs", "index.html"), redirectHtml);
125
+ }
126
+
127
+ if (!redirectUrl) redirectUrl = "/docs/";
128
+ }
129
+
130
+ // Generate index.html
131
+ let indexPage = "";
132
+ if (hasHome) {
133
+ indexPage = renderHomePage(inscribe, sourceDir, navState);
134
+ } else if (redirectUrl) {
135
+ indexPage = `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${redirectUrl}"></head><body>Redirecting...</body></html>`;
136
+ } else {
137
+ indexPage = `<!DOCTYPE html><html><body>No content available.</body></html>`;
138
+ }
139
+
140
+ if (isRelease && hasHome) {
141
+ indexPage = await minifyHtml(indexPage);
142
+ }
143
+
144
+ await fs.writeFile(path.join(outputDir, "index.html"), indexPage);
145
+ }
@@ -0,0 +1,14 @@
1
+ import fs from "fs-extra";
2
+ import { parse as yamlParse } from 'yaml';
3
+ import { InscribeSchema, InscribeConfig } from "../schemas/inscribe";
4
+ import path from "path";
5
+
6
+ export const readInscribeFile = async (rootDir: string): Promise<InscribeConfig> => {
7
+ const filePath = path.join(rootDir, "inscribe.yaml");
8
+ if (!fs.existsSync(filePath)) {
9
+ return InscribeSchema.parse({});
10
+ }
11
+ const content = fs.readFileSync(filePath, "utf-8");
12
+ const parsedYaml = yamlParse(content);
13
+ return InscribeSchema.parse(parsedYaml);
14
+ }
@@ -0,0 +1,199 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import nunjucks from "nunjucks";
4
+ import { markdown2HTML, parseFrontMatter } from "../utils/markdown";
5
+ import { Blog, BlogScehma } from "../schemas/blog";
6
+ import { InscribeConfig } from "../schemas/inscribe";
7
+ import { resolveThemeCSS } from "./theme_resolver";
8
+
9
+ export const parseBlogPost = async (filePath: string) => {
10
+ const content = fs.readFileSync(filePath, "utf-8");
11
+ const { data, body } = parseFrontMatter(content);
12
+ const isMDX = filePath.endsWith(".mdx");
13
+
14
+ // Validate with Zod
15
+ const validated = BlogScehma.parse({
16
+ metadata: data,
17
+ markdown: body,
18
+ isMDX
19
+ });
20
+
21
+ return validated;
22
+ };
23
+
24
+ /**
25
+ * Configure Nunjucks with a search path:
26
+ * 1. User project layouts
27
+ * 2. CLI built-in layouts
28
+ */
29
+ const getRenderer = (sourceDir: string) => {
30
+ const userLayouts = path.resolve(sourceDir, "layouts");
31
+ const builtInLayouts = path.resolve(__dirname, "../../template/layouts");
32
+
33
+ const searchPaths = [userLayouts, builtInLayouts];
34
+
35
+ return new nunjucks.Environment(
36
+ new nunjucks.FileSystemLoader(searchPaths),
37
+ { autoescape: true }
38
+ );
39
+ };
40
+
41
+ import { FolderMetadata } from "../schemas/folder";
42
+
43
+ export interface NavState {
44
+ hasHome: boolean;
45
+ hasBlog: boolean;
46
+ hasDocs: boolean;
47
+ }
48
+
49
+ export type SidebarItem =
50
+ | { type: 'folder'; node: SidebarNode }
51
+ | { type: 'file'; post: Blog };
52
+
53
+ export interface SidebarNode {
54
+ title: string;
55
+ path: string;
56
+ weight: number;
57
+ items: SidebarItem[];
58
+ }
59
+
60
+ const getSidebarTree = (posts: Blog[], folderMetadata: Record<string, FolderMetadata> = {}): SidebarNode[] => {
61
+ // Root node — holds top-level files and sub-folder nodes
62
+ const root: SidebarNode = { title: '', path: '', weight: 0, items: [] };
63
+
64
+ // Map from path string to its SidebarNode (for quick lookup)
65
+ const nodeMap = new Map<string, SidebarNode>();
66
+ nodeMap.set('', root);
67
+
68
+ // Helper: ensure all ancestor nodes exist for a given path
69
+ const ensureNode = (dirPath: string): SidebarNode => {
70
+ if (nodeMap.has(dirPath)) return nodeMap.get(dirPath)!;
71
+
72
+ // Ensure parent exists first
73
+ const parentPath = path.dirname(dirPath).replace(/\\/g, '/');
74
+ const parent = ensureNode(parentPath === '.' ? '' : parentPath);
75
+
76
+ const meta = folderMetadata[dirPath] || { title: path.basename(dirPath), weight: 0 };
77
+ const node: SidebarNode = {
78
+ title: meta.title || path.basename(dirPath),
79
+ path: dirPath,
80
+ weight: meta.weight || 0,
81
+ items: [],
82
+ };
83
+ nodeMap.set(dirPath, node);
84
+ parent.items.push({ type: 'folder', node });
85
+ return node;
86
+ };
87
+
88
+ // Place each post in the correct node
89
+ for (const post of posts) {
90
+ let dir = path.dirname((post as any).relativePath || '').replace(/\\/g, '/');
91
+ if (dir === '.') dir = '';
92
+ const node = ensureNode(dir);
93
+ node.items.push({ type: 'file', post });
94
+ }
95
+
96
+ // Recursively sort items within each node
97
+ const sortNode = (node: SidebarNode) => {
98
+ node.items.sort((a, b) => {
99
+ const wA = a.type === 'folder' ? a.node.weight : (a.post.metadata.weight ?? 0);
100
+ const wB = b.type === 'folder' ? b.node.weight : (b.post.metadata.weight ?? 0);
101
+
102
+ if (wA !== wB) return wA - wB;
103
+
104
+ const tA = a.type === 'folder' ? a.node.title : a.post.metadata.title;
105
+ const tB = b.type === 'folder' ? b.node.title : b.post.metadata.title;
106
+ return tA.localeCompare(tB);
107
+ });
108
+
109
+ for (const item of node.items) {
110
+ if (item.type === 'folder') {
111
+ sortNode(item.node);
112
+ }
113
+ }
114
+ };
115
+ sortNode(root);
116
+
117
+ return root.items.length > 0 ? [root] : [];
118
+ };
119
+
120
+ export const renderSectionPage = async (
121
+ type: 'blog' | 'doc',
122
+ post: Blog,
123
+ allPosts: Blog[],
124
+ folderMetadata: Record<string, FolderMetadata>,
125
+ inscribe: InscribeConfig,
126
+ sourceDir: string,
127
+ navState: NavState,
128
+ isDev: boolean = false
129
+ ) => {
130
+ const html = await markdown2HTML(post.markdown, post.isMDX);
131
+ const env = getRenderer(sourceDir);
132
+ const themeCSS = resolveThemeCSS(inscribe.theme ?? 'default', sourceDir);
133
+
134
+ const template = type === 'blog' ? "blog.njk" : "doc.njk";
135
+
136
+ const sidebarTree = type === 'doc' ? getSidebarTree(allPosts, folderMetadata) : [];
137
+ const currentDirPath = path.dirname((post as any).relativePath || '').replace(/\\/g, '/').replace(/^\.$/, '');
138
+
139
+ return env.render(template, {
140
+ post, // rename to post instead of blog to be generic
141
+ allPosts,
142
+ sidebarTree,
143
+ currentDirPath,
144
+ blog: post,
145
+ doc: post,
146
+ config: inscribe,
147
+ content: html,
148
+ themeCSS,
149
+ navState,
150
+ isDev
151
+ });
152
+ };
153
+
154
+ export const renderSectionIndexPage = (
155
+ type: 'blog' | 'doc',
156
+ posts: Blog[],
157
+ folderMetadata: Record<string, FolderMetadata>,
158
+ inscribe: InscribeConfig,
159
+ sourceDir: string,
160
+ navState: NavState,
161
+ isDev: boolean = false
162
+ ) => {
163
+ const env = getRenderer(sourceDir);
164
+ const themeCSS = resolveThemeCSS(inscribe.theme ?? 'default', sourceDir);
165
+
166
+ const template = type === 'blog' ? "blog_index.njk" : "doc_index.njk";
167
+
168
+ const sidebarTree = type === 'doc' ? getSidebarTree(posts, folderMetadata) : [];
169
+
170
+ return env.render(template, {
171
+ posts, // rename to posts
172
+ allPosts: posts,
173
+ sidebarTree,
174
+ currentDirPath: '',
175
+ blogs: posts,
176
+ docs: posts,
177
+ config: inscribe,
178
+ themeCSS,
179
+ navState,
180
+ isDev
181
+ });
182
+ };
183
+
184
+ export const renderHomePage = (
185
+ inscribe: InscribeConfig,
186
+ sourceDir: string,
187
+ navState: NavState,
188
+ isDev: boolean = false
189
+ ) => {
190
+ const env = getRenderer(sourceDir);
191
+ const themeCSS = resolveThemeCSS(inscribe.theme ?? 'default', sourceDir);
192
+
193
+ return env.render("home.njk", {
194
+ config: inscribe,
195
+ themeCSS,
196
+ navState,
197
+ isDev
198
+ });
199
+ };
@@ -0,0 +1,144 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import { parseBlogPost, renderSectionPage, renderSectionIndexPage, renderHomePage, NavState } from "./renderer";
4
+ import { readInscribeFile } from "./inscribe_reader";
5
+ import { parseFolderMetadata } from "../utils/markdown";
6
+ import { Blog } from "../schemas/blog";
7
+
8
+ export const LocalServer = (sourceDir: string, isDev: boolean = false, port = 3000) => {
9
+ return {
10
+ port: port,
11
+ async fetch(req: Request, server: any) {
12
+ const url = new URL(req.url);
13
+
14
+ // Handle WebSocket upgrade
15
+ if (isDev && url.pathname === "/_reload") {
16
+ const success = server.upgrade(req);
17
+ if (success) return undefined;
18
+ }
19
+
20
+ // Read inscribe config
21
+ const inscribe = await readInscribeFile(sourceDir);
22
+
23
+ const blogPath = inscribe.blog_path === "null" || !inscribe.blog_path ? null : inscribe.blog_path;
24
+ const docPath = inscribe.doc_path === "null" || !inscribe.doc_path ? null : inscribe.doc_path;
25
+ const blogDir = blogPath ? path.join(sourceDir, blogPath) : null;
26
+ const docDir = docPath ? path.join(sourceDir, docPath) : null;
27
+
28
+ const hasBlog = blogDir ? fs.existsSync(blogDir) : false;
29
+ const hasDocs = docDir ? fs.existsSync(docDir) : false;
30
+ const hasHome = inscribe.show_home !== false;
31
+ const navState: NavState = { hasHome, hasBlog, hasDocs };
32
+
33
+ // Helper: collect relative .md/.mdx file paths under a directory
34
+ const getFiles = (dir: string | null): string[] =>
35
+ dir && fs.existsSync(dir)
36
+ ? (fs.readdirSync(dir, { recursive: true }) as string[]).filter(
37
+ (f) => f.endsWith(".md") || f.endsWith(".mdx")
38
+ )
39
+ : [];
40
+
41
+ // Helper: parse all posts in a directory, attaching relativePath and sorting
42
+ const getAllPosts = async (dir: string, files: string[]): Promise<Blog[]> => {
43
+ const posts = await Promise.all(
44
+ files
45
+ .filter((f) => !f.endsWith("index.md"))
46
+ .map(async (f) => {
47
+ const post = await parseBlogPost(path.join(dir, f));
48
+ (post as any).relativePath = f;
49
+ return post;
50
+ })
51
+ );
52
+ return posts.sort((a, b) => {
53
+ const wA = a.metadata.weight ?? 0;
54
+ const wB = b.metadata.weight ?? 0;
55
+ if (wA !== wB) return wA - wB;
56
+ return a.metadata.slug.localeCompare(b.metadata.slug);
57
+ });
58
+ };
59
+
60
+ // Helper: build folder-metadata map from index.md files
61
+ const getFolderMetadata = (dir: string, files: string[]): Record<string, any> => {
62
+ const meta: Record<string, any> = {};
63
+ for (const f of files) {
64
+ if (f.endsWith("index.md")) {
65
+ const absDir = path.dirname(path.join(dir, f));
66
+ const relDir = path.relative(dir, absDir);
67
+ meta[relDir.replace(/\\/g, '/') || ''] = parseFolderMetadata(absDir);
68
+ }
69
+ }
70
+ return meta;
71
+ };
72
+
73
+ const blogFiles = getFiles(blogDir);
74
+ const docFilesRaw = getFiles(docDir);
75
+
76
+ // Home page
77
+ if (url.pathname === "/") {
78
+ if (hasHome) {
79
+ const html = renderHomePage(inscribe, sourceDir, navState, isDev);
80
+ return new Response(html, { headers: { "Content-Type": "text/html" } });
81
+ }
82
+ const redirectUrl = hasBlog ? "/blogs/" : hasDocs ? "/docs/" : "";
83
+ if (redirectUrl) return Response.redirect(`http://${url.host}${redirectUrl}`, 302);
84
+ return new Response("No content available", { status: 404 });
85
+ }
86
+
87
+ // Blogs index
88
+ if ((url.pathname === "/blogs" || url.pathname === "/blogs/") && hasBlog && blogDir) {
89
+ const blogs = await getAllPosts(blogDir, blogFiles);
90
+ const html = renderSectionIndexPage('blog', blogs, {}, inscribe, sourceDir, navState, isDev);
91
+ return new Response(html, { headers: { "Content-Type": "text/html" } });
92
+ }
93
+
94
+ // Docs index
95
+ if ((url.pathname === "/docs" || url.pathname === "/docs/") && hasDocs && docDir) {
96
+ const docs = await getAllPosts(docDir, docFilesRaw);
97
+ if (docs.length > 0) {
98
+ const firstLevelDoc = docs.find(p => !((p as any).relativePath).includes('/') && !((p as any).relativePath).includes('\\'));
99
+ const firstDocSlug = (firstLevelDoc || docs[0]).metadata.slug;
100
+ return Response.redirect(`http://${url.host}/doc/${firstDocSlug}`, 302);
101
+ }
102
+ const docFolderMetadata = getFolderMetadata(docDir, docFilesRaw);
103
+ const html = renderSectionIndexPage('doc', docs, docFolderMetadata, inscribe, sourceDir, navState, isDev);
104
+ return new Response(html, { headers: { "Content-Type": "text/html" } });
105
+ }
106
+
107
+ // Blog page — match by frontmatter slug, not filename
108
+ if (url.pathname.startsWith("/blog/") && blogDir) {
109
+ const slug = url.pathname.replace("/blog/", "").replace(/\/$/, "");
110
+ const blogs = await getAllPosts(blogDir, blogFiles);
111
+ const blog = blogs.find((p) => p.metadata.slug === slug);
112
+ if (blog) {
113
+ const fullHtml = await renderSectionPage('blog', blog, blogs, {}, inscribe, sourceDir, navState, isDev);
114
+ return new Response(fullHtml, { headers: { "Content-Type": "text/html" } });
115
+ }
116
+ }
117
+
118
+ // Doc page — match by frontmatter slug, not filename
119
+ if (url.pathname.startsWith("/doc/") && docDir) {
120
+ const slug = url.pathname.replace("/doc/", "").replace(/\/$/, "");
121
+ const docs = await getAllPosts(docDir, docFilesRaw);
122
+ const docFolderMetadata = getFolderMetadata(docDir, docFilesRaw);
123
+ const doc = docs.find((p) => p.metadata.slug === slug);
124
+ if (doc) {
125
+ const fullHtml = await renderSectionPage('doc', doc, docs, docFolderMetadata, inscribe, sourceDir, navState, isDev);
126
+ return new Response(fullHtml, { headers: { "Content-Type": "text/html" } });
127
+ }
128
+ }
129
+
130
+ return new Response("Not Found", { status: 404 });
131
+ },
132
+ websocket: {
133
+ message(ws: any, message: string) { },
134
+ open(ws: any) {
135
+ ws.subscribe("reload");
136
+ },
137
+ close(ws: any) {
138
+ ws.unsubscribe("reload");
139
+ },
140
+ },
141
+ }
142
+ }
143
+
144
+
@@ -0,0 +1,31 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+
4
+ /**
5
+ * Resolves a theme name to its CSS content.
6
+ *
7
+ * Priority:
8
+ * 1. <sourceDir>/themes/<themeName>.css (user override)
9
+ * 2. <builtInThemesDir>/<themeName>.css (built-in preset)
10
+ *
11
+ * Throws a descriptive error if neither location has the file.
12
+ */
13
+ export const resolveThemeCSS = (themeName: string, sourceDir: string): string => {
14
+ const userThemePath = path.resolve(sourceDir, "themes", `${themeName}.css`);
15
+ const builtInThemePath = path.resolve(__dirname, "../../template/themes", `${themeName}.css`);
16
+
17
+ if (fs.existsSync(userThemePath)) {
18
+ return fs.readFileSync(userThemePath, "utf-8");
19
+ }
20
+
21
+ if (fs.existsSync(builtInThemePath)) {
22
+ return fs.readFileSync(builtInThemePath, "utf-8");
23
+ }
24
+
25
+ throw new Error(
26
+ `Theme "${themeName}" not found.\n` +
27
+ ` Checked: ${userThemePath}\n` +
28
+ ` Checked: ${builtInThemePath}\n` +
29
+ ` Place a "${themeName}.css" file in your project's "themes/" folder, or use a built-in theme name.`
30
+ );
31
+ };