@jsnchn/buntastic 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,96 @@
1
+ # Buntastic
2
+
3
+ A simple static site generator built with Bun.
4
+
5
+ ## Project Structure
6
+
7
+ ```
8
+ buntastic/
9
+ ├── content/ # File-based routing source
10
+ │ ├── index.md # → /
11
+ │ ├── about.md # → /about
12
+ │ ├── 404.md # → /404.html
13
+ │ └── posts/
14
+ │ ├── index.md # → /posts (with collection)
15
+ │ └── *.md # → /posts/*
16
+ ├── src/
17
+ │ ├── index.ts # Main build script
18
+ │ └── layouts/ # HTML layouts
19
+ │ ├── base.html # Root layout
20
+ │ ├── page.html # extends: base.html
21
+ │ └── post.html # extends: base.html
22
+ ├── public/ # Static assets
23
+ └── package.json
24
+ ```
25
+
26
+ ## Commands
27
+
28
+ ```bash
29
+ bun run build # Production build (excludes drafts)
30
+ bun run build:drafts # Build with drafts included
31
+ bun run dev # Watch mode + dev server
32
+ bun run preview # Serve dist folder
33
+ ```
34
+
35
+ ## Features
36
+
37
+ - **Zero dependencies** - Uses only Bun's built-in APIs
38
+ - **File-based routing** - `content/*.md` → `/`, `content/posts/*.md` → `/posts/*`
39
+ - **Layout inheritance** - Layouts can extend other layouts via `extends:` frontmatter
40
+ - **Collections** - `{{ collection }}` variable available in index pages
41
+ - **Draft mode** - `draft: true` in frontmatter, included with `--drafts` flag
42
+ - **404 page** - `content/404.md` automatically becomes `/404.html`
43
+ - **Asset co-location** - Non-markdown files in content/ are copied to dist
44
+
45
+ ## Frontmatter
46
+
47
+ ```yaml
48
+ ---
49
+ title: Page Title
50
+ date: 2024-01-15
51
+ layout: post # Uses layouts/post.html
52
+ description: Meta description
53
+ draft: false # Set true to exclude from production build
54
+ ---
55
+ ```
56
+
57
+ ## Layout Variables
58
+
59
+ | Variable | Description |
60
+ |----------|-------------|
61
+ | `{{ title }}` | Page title from frontmatter |
62
+ | `{{ content \| safe }}` | Rendered markdown HTML |
63
+ | `{{ date }}` | Page date |
64
+ | `{{ description }}` | Page description |
65
+ | `{{ url }}` | Current page URL |
66
+ | `{{ collection }}` | Array of posts in current folder (for index pages) |
67
+
68
+ ## Layout System
69
+
70
+ Layouts support inheritance via frontmatter:
71
+
72
+ ```html
73
+ <!-- layouts/post.html -->
74
+ ---
75
+ extends: base.html
76
+ ---
77
+ <article class="post">
78
+ <h1>{{ title }}</h1>
79
+ {{ content | safe }}
80
+ </article>
81
+ ```
82
+
83
+ The `{{ content | safe }}` placeholder is where child content gets injected.
84
+
85
+ ## Development
86
+
87
+ 1. Add content to `content/` folder
88
+ 2. Run `bun run dev` for development
89
+ 3. Run `bun run build` for production
90
+
91
+ ## Tech Stack
92
+
93
+ - [Bun](https://bun.sh) - JavaScript runtime
94
+ - Bun.markdown - Built-in GFM markdown parser
95
+ - Bun.serve - HTTP server
96
+ - Bun.watch - File watching
package/README.md ADDED
@@ -0,0 +1,222 @@
1
+ # Buntastic
2
+
3
+ A simple static site generator built with Bun.
4
+
5
+ ## Why Buntastic?
6
+
7
+ - **Zero dependencies** - Uses only Bun's built-in APIs
8
+ - **Fast** - Powered by Bun's native markdown parser
9
+ - **Simple** - No complex configuration needed
10
+ - **Flexible** - Layouts with inheritance, collections, drafts
11
+
12
+ ## Installation
13
+
14
+ ### As a CLI tool (recommended)
15
+
16
+ ```bash
17
+ # Install Bun (if needed)
18
+ curl -fsSL https://bun.sh/install | bash
19
+
20
+ # Install buntastic globally
21
+ bun install -g buntastic
22
+
23
+ # Or use npx without installing
24
+ npx buntastic build
25
+ ```
26
+
27
+ ### For development
28
+
29
+ ```bash
30
+ # Clone the repo
31
+ git clone https://github.com/jsnchn/buntastic.git
32
+ cd buntastic
33
+
34
+ # Start the dev server
35
+ bun run dev
36
+ ```
37
+
38
+ Visit `http://localhost:3000` to see your site.
39
+
40
+ ## Project Structure
41
+
42
+ ```
43
+ buntastic/
44
+ ├── content/ # Your markdown content
45
+ │ ├── index.md # → /
46
+ │ ├── about.md # → /about
47
+ │ └── posts/
48
+ │ └── *.md # → /posts/*
49
+ ├── src/
50
+ │ ├── index.ts # Build script
51
+ │ └── layouts/ # HTML templates
52
+ ├── public/ # Static assets (CSS, images)
53
+ └── package.json
54
+ ```
55
+
56
+ ## Commands
57
+
58
+ | Command | Description |
59
+ |---------|-------------|
60
+ | `buntastic build` | Build for production (excludes drafts) |
61
+ | `buntastic build --drafts` | Build with drafts included |
62
+ | `buntastic dev` | Watch mode + dev server |
63
+ | `buntastic preview` | Serve the built `dist/` folder |
64
+
65
+ Or with bun run (if using from source):
66
+
67
+ | Command | Description |
68
+ |---------|-------------|
69
+ | `bun run build` | Build for production (excludes drafts) |
70
+ | `bun run build:drafts` | Build with drafts included |
71
+ | `bun run dev` | Watch mode + dev server |
72
+ | `bun run preview` | Serve the built `dist/` folder |
73
+
74
+ ## Writing Content
75
+
76
+ Create markdown files in `content/`:
77
+
78
+ ```markdown
79
+ ---
80
+ title: My Post
81
+ date: 2024-01-15
82
+ layout: post
83
+ description: A short description
84
+ ---
85
+
86
+ # Hello World
87
+
88
+ Your content here...
89
+ ```
90
+
91
+ ### Frontmatter Options
92
+
93
+ | Field | Description |
94
+ |-------|-------------|
95
+ | `title` | Page title |
96
+ | `date` | Publication date |
97
+ | `layout` | Layout to use (page, post, or custom) |
98
+ | `description` | Meta description |
99
+ | `draft` | Set `true` to exclude from production |
100
+
101
+ ### File-Based Routing
102
+
103
+ | Content Path | Output URL |
104
+ |--------------|------------|
105
+ | `content/index.md` | `/` |
106
+ | `content/about.md` | `/about` |
107
+ | `content/posts/hello.md` | `/posts/hello` |
108
+
109
+ ## Layouts
110
+
111
+ ### Creating Layouts
112
+
113
+ Layouts live in `src/layouts/`. Create HTML files with frontmatter:
114
+
115
+ ```html
116
+ <!-- layouts/post.html -->
117
+ ---
118
+ extends: base.html
119
+ ---
120
+ <article class="post">
121
+ <h1>{{ title }}</h1>
122
+ <time>{{ date }}</time>
123
+ {{ content | safe }}
124
+ </article>
125
+ ```
126
+
127
+ ### Layout Variables
128
+
129
+ | Variable | Description |
130
+ |----------|-------------|
131
+ | `{{ title }}` | Page title |
132
+ | `{{ content \| safe }}` | Rendered markdown HTML |
133
+ | `{{ date }}` | Page date |
134
+ | `{{ description }}` | Meta description |
135
+ | `{{ url }}` | Current page URL |
136
+ | `{{ collection }}` | List of posts in current folder (for index pages) |
137
+
138
+ ### Layout Inheritance
139
+
140
+ Use `extends:` to build on top of other layouts:
141
+
142
+ ```html
143
+ <!-- layouts/base.html -->
144
+ <!DOCTYPE html>
145
+ <html>
146
+ <head>
147
+ <title>{{ title }}</title>
148
+ </head>
149
+ <body>
150
+ <main>{{ content | safe }}</main>
151
+ </body>
152
+ </html>
153
+ ```
154
+
155
+ ## Collections
156
+
157
+ For folder index pages (e.g., `content/posts/index.md`), use `{{ collection }}` to list all pages in that folder:
158
+
159
+ ```html
160
+ <h1>Blog Posts</h1>
161
+ <ul>
162
+ {{ collection }}
163
+ </ul>
164
+ ```
165
+
166
+ Renders as:
167
+
168
+ ```html
169
+ <li><a href="/posts/hello">Hello World</a> - <time>2024-01-15</time></li>
170
+ <li><a href="/posts/other">Other Post</a></li>
171
+ ```
172
+
173
+ ## Static Assets
174
+
175
+ Place CSS, images, or other files in `public/`:
176
+
177
+ ```
178
+ public/
179
+ └── style.css
180
+ ```
181
+
182
+ They'll be available at `/style.css` in the built site.
183
+
184
+ You can also co-locate assets with content:
185
+
186
+ ```
187
+ content/
188
+ └── posts/
189
+ └── my-post/
190
+ ├── index.md
191
+ └── image.png
192
+ ```
193
+
194
+ The image will be available at `/posts/my-post/image.png`.
195
+
196
+ ## 404 Page
197
+
198
+ Create `content/404.md` to have a custom 404 page at `/404.html`.
199
+
200
+ ## Drafts
201
+
202
+ Set `draft: true` in frontmatter to exclude a page from production builds:
203
+
204
+ ```yaml
205
+ ---
206
+ title: Work in Progress
207
+ draft: true
208
+ ---
209
+ ```
210
+
211
+ Drafts are included when running `bun run build:drafts`.
212
+
213
+ ## Tech Stack
214
+
215
+ - [Bun](https://bun.sh) - JavaScript runtime
216
+ - Bun.markdown - Built-in GFM markdown parser
217
+ - Bun.serve - HTTP server
218
+ - Bun.watch - File watching
219
+
220
+ ## License
221
+
222
+ MIT
package/content/404.md ADDED
@@ -0,0 +1,10 @@
1
+ ---
2
+ title: 404 - Not Found
3
+ layout: page
4
+ ---
5
+
6
+ # 404 - Page Not Found
7
+
8
+ The page you're looking for doesn't exist.
9
+
10
+ [Go back home](/)
@@ -0,0 +1,23 @@
1
+ ---
2
+ title: About
3
+ description: About BunPress
4
+ layout: page
5
+ ---
6
+
7
+ # About BunPress
8
+
9
+ BunPress is a minimal static site generator that uses Bun's built-in markdown parser.
10
+
11
+ ## Why BunPress?
12
+
13
+ - **Zero dependencies** - Uses only Bun's built-in APIs
14
+ - **Fast** - Powered by Bun's native markdown parser
15
+ - **Simple** - No complex configuration needed
16
+ - **Flexible** - Layouts with inheritance
17
+
18
+ ## Tech Stack
19
+
20
+ - [Bun](https://bun.sh) - JavaScript runtime
21
+ - Bun.markdown - Built-in markdown parser
22
+ - Bun.serve - HTTP server
23
+ - Bun.watch - File watching
@@ -0,0 +1,33 @@
1
+ ---
2
+ title: Welcome
3
+ description: Welcome to BunPress
4
+ ---
5
+
6
+ # Welcome to BunPress
7
+
8
+ This is a simple static site generator built with **Bun**.
9
+
10
+ ## Features
11
+
12
+ - File-based routing
13
+ - Markdown support
14
+ - Layouts with inheritance
15
+ - Collections
16
+ - Draft mode
17
+
18
+ ## Quick Start
19
+
20
+ 1. Add content to the `content/` folder
21
+ 2. Run `bun run build`
22
+ 3. View the output in `dist/`
23
+
24
+ ## Code Example
25
+
26
+ ```typescript
27
+ const greeting = "Hello, BunPress!";
28
+ console.log(greeting);
29
+ ```
30
+
31
+ ## Try It
32
+
33
+ Check out the [posts](/posts) page to see more!
@@ -0,0 +1,13 @@
1
+ ---
2
+ title: Draft Post
3
+ date: 2024-01-20
4
+ layout: post
5
+ draft: true
6
+ description: This is a draft
7
+ ---
8
+
9
+ # Draft Post
10
+
11
+ This post is a **draft** and won't appear in production builds.
12
+
13
+ Run `bun run build --drafts` to include it.
@@ -0,0 +1,42 @@
1
+ ---
2
+ title: Hello World
3
+ date: 2024-01-15
4
+ layout: post
5
+ description: My first blog post
6
+ ---
7
+
8
+ # Hello World
9
+
10
+ This is my first blog post on **BunPress**!
11
+
12
+ ## Introduction
13
+
14
+ Static site generators are great for blogs and documentation. BunPress makes it simple.
15
+
16
+ ### Features I love:
17
+
18
+ 1. Markdown support
19
+ 2. File-based routing
20
+ 3. Layouts
21
+ 4. Collections
22
+
23
+ > "Simplicity is the ultimate sophistication." - Leonardo da Vinci
24
+
25
+ ## Code
26
+
27
+ ```javascript
28
+ console.log("Hello from BunPress!");
29
+ ```
30
+
31
+ ## Tables
32
+
33
+ | Feature | Status |
34
+ |---------|--------|
35
+ | Markdown | ✅ |
36
+ | Routing | ✅ |
37
+ | Layouts | ✅ |
38
+ | Collections | ✅ |
39
+
40
+ ---
41
+
42
+ Thanks for reading!
@@ -0,0 +1,10 @@
1
+ ---
2
+ title: Blog Posts
3
+ layout: page
4
+ ---
5
+
6
+ # Blog Posts
7
+
8
+ Here are all my posts:
9
+
10
+ {{ collection }}
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@jsnchn/buntastic",
3
+ "version": "0.0.2",
4
+ "description": "A simple static site generator built with Bun",
5
+ "type": "module",
6
+ "bin": {
7
+ "buntastic": "src/index.ts"
8
+ },
9
+ "scripts": {
10
+ "build": "buntastic build",
11
+ "build:drafts": "buntastic build --drafts",
12
+ "dev": "buntastic dev",
13
+ "preview": "buntastic preview"
14
+ },
15
+ "keywords": [
16
+ "static-site-generator",
17
+ "ssg",
18
+ "blog",
19
+ "markdown",
20
+ "bun"
21
+ ],
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/jsnchn/buntastic.git"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "private": false
31
+ }
@@ -0,0 +1,136 @@
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif;
9
+ line-height: 1.6;
10
+ color: #333;
11
+ max-width: 800px;
12
+ margin: 0 auto;
13
+ padding: 2rem;
14
+ }
15
+
16
+ header {
17
+ padding: 1rem 0;
18
+ border-bottom: 1px solid #eee;
19
+ margin-bottom: 2rem;
20
+ }
21
+
22
+ nav a {
23
+ margin-right: 1rem;
24
+ color: #0066cc;
25
+ text-decoration: none;
26
+ }
27
+
28
+ nav a:hover {
29
+ text-decoration: underline;
30
+ }
31
+
32
+ main {
33
+ min-height: 60vh;
34
+ }
35
+
36
+ h1, h2, h3, h4, h5, h6 {
37
+ margin: 1.5rem 0 1rem;
38
+ line-height: 1.3;
39
+ }
40
+
41
+ h1 { font-size: 2rem; }
42
+ h2 { font-size: 1.5rem; }
43
+ h3 { font-size: 1.25rem; }
44
+
45
+ p {
46
+ margin: 1rem 0;
47
+ }
48
+
49
+ a {
50
+ color: #0066cc;
51
+ }
52
+
53
+ code {
54
+ background: #f4f4f4;
55
+ padding: 0.2rem 0.4rem;
56
+ border-radius: 3px;
57
+ font-size: 0.9em;
58
+ }
59
+
60
+ pre {
61
+ background: #f4f4f4;
62
+ padding: 1rem;
63
+ overflow-x: auto;
64
+ border-radius: 5px;
65
+ margin: 1rem 0;
66
+ }
67
+
68
+ pre code {
69
+ background: none;
70
+ padding: 0;
71
+ }
72
+
73
+ blockquote {
74
+ border-left: 4px solid #ddd;
75
+ padding-left: 1rem;
76
+ margin: 1rem 0;
77
+ color: #666;
78
+ }
79
+
80
+ ul, ol {
81
+ margin: 1rem 0;
82
+ padding-left: 2rem;
83
+ }
84
+
85
+ li {
86
+ margin: 0.5rem 0;
87
+ }
88
+
89
+ table {
90
+ width: 100%;
91
+ border-collapse: collapse;
92
+ margin: 1rem 0;
93
+ }
94
+
95
+ th, td {
96
+ border: 1px solid #ddd;
97
+ padding: 0.5rem;
98
+ text-align: left;
99
+ }
100
+
101
+ th {
102
+ background: #f4f4f4;
103
+ }
104
+
105
+ hr {
106
+ border: none;
107
+ border-top: 1px solid #eee;
108
+ margin: 2rem 0;
109
+ }
110
+
111
+ img {
112
+ max-width: 100%;
113
+ height: auto;
114
+ }
115
+
116
+ .post header {
117
+ border-bottom: none;
118
+ margin-bottom: 1rem;
119
+ }
120
+
121
+ .post time {
122
+ color: #666;
123
+ font-size: 0.9rem;
124
+ }
125
+
126
+ .page h1 {
127
+ margin-top: 0;
128
+ }
129
+
130
+ footer {
131
+ padding: 2rem 0;
132
+ border-top: 1px solid #eee;
133
+ margin-top: 3rem;
134
+ text-align: center;
135
+ color: #666;
136
+ }
package/src/index.ts ADDED
@@ -0,0 +1,452 @@
1
+ #!/usr/bin/env bun
2
+ import { mkdir, writeFile, readFile, copyFile, exists } from "fs/promises";
3
+ import { join, relative, dirname, extname } from "path";
4
+ import { Glob } from "bun";
5
+
6
+ const CONTENT_DIR = join(process.cwd(), "content");
7
+ const LAYOUTS_DIR = join(process.cwd(), "src/layouts");
8
+ const PUBLIC_DIR = join(process.cwd(), "public");
9
+ const DIST_DIR = join(process.cwd(), "dist");
10
+
11
+ interface Frontmatter {
12
+ title?: string;
13
+ date?: string;
14
+ layout?: string;
15
+ description?: string;
16
+ draft?: boolean;
17
+ extends?: string;
18
+ [key: string]: unknown;
19
+ }
20
+
21
+ interface Page {
22
+ url: string;
23
+ filePath: string;
24
+ frontmatter: Frontmatter;
25
+ content: string;
26
+ html: string;
27
+ }
28
+
29
+ interface CollectionItem {
30
+ url: string;
31
+ title: string;
32
+ date: string;
33
+ description: string;
34
+ }
35
+
36
+ function parseFrontmatter(content: string): { frontmatter: Frontmatter; content: string } {
37
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
38
+ if (!match) {
39
+ return { frontmatter: {}, content };
40
+ }
41
+
42
+ const yamlStr = match[1];
43
+ const body = match[2];
44
+
45
+ const frontmatter: Frontmatter = {};
46
+ for (const line of yamlStr.split("\n")) {
47
+ const colonIdx = line.indexOf(":");
48
+ if (colonIdx === -1) continue;
49
+ const key = line.slice(0, colonIdx).trim();
50
+ let value: unknown = line.slice(colonIdx + 1).trim();
51
+ if (value === "true") value = true;
52
+ else if (value === "false") value = false;
53
+ frontmatter[key] = value;
54
+ }
55
+
56
+ return { frontmatter, content: body };
57
+ }
58
+
59
+ function renderMarkdown(content: string): string {
60
+ return Bun.markdown.html(content, {
61
+ tables: true,
62
+ strikethrough: true,
63
+ tasklists: true,
64
+ autolinks: true,
65
+ headings: true,
66
+ });
67
+ }
68
+
69
+ async function readLayout(layoutName: string): Promise<string> {
70
+ const layoutPath = join(LAYOUTS_DIR, `${layoutName}.html`);
71
+ return await readFile(layoutPath, "utf-8");
72
+ }
73
+
74
+ async function resolveLayout(frontmatter: Frontmatter): Promise<string> {
75
+ const layoutName = frontmatter.layout || frontmatter.extends || "page";
76
+ let template = await readLayout(layoutName);
77
+
78
+ const extendsMatch = template.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
79
+ if (extendsMatch) {
80
+ const parentLayout = extendsMatch[1].match(/extends:\s*(\w+)/);
81
+ if (parentLayout) {
82
+ const parentTemplate = await resolveLayout({ extends: parentLayout[1] } as Frontmatter);
83
+ const childContent = extendsMatch[2];
84
+ return parentTemplate.replace(/\{\{\s*content\s*\|\s*safe\s*\}\}/g, childContent);
85
+ }
86
+ }
87
+
88
+ return template;
89
+ }
90
+
91
+ function applyTemplate(template: string, page: Page, collection?: CollectionItem[]): string {
92
+ let result = template;
93
+
94
+ result = result.replace(/\{\{\s*title\s*\}\}/g, page.frontmatter.title || "");
95
+ result = result.replace(/\{\{\s*date\s*\}\}/g, page.frontmatter.date || "");
96
+ result = result.replace(/\{\{\s*description\s*\}\}/g, page.frontmatter.description || "");
97
+ result = result.replace(/\{\{\s*url\s*\}\}/g, page.url);
98
+
99
+ if (collection) {
100
+ const collectionHtml = collection
101
+ .map(
102
+ (item) =>
103
+ `<li><a href="${item.url}">${item.title}</a>${item.date ? ` - <time>${item.date}</time>` : ""}</li>`
104
+ )
105
+ .join("\n");
106
+ result = result.replace(/\{\{\s*collection\s*\}\}/g, collectionHtml);
107
+ }
108
+
109
+ return result;
110
+ }
111
+
112
+ async function getCollectionForPath(filePath: string): Promise<CollectionItem[] | undefined> {
113
+ const dir = dirname(filePath);
114
+ if (dir === "." || dir === CONTENT_DIR) return undefined;
115
+
116
+ const contentDir = join(CONTENT_DIR, dir);
117
+ if (!await exists(contentDir)) return undefined;
118
+
119
+ const glob = new Glob("*.md");
120
+ const files = Array.from(glob.scanSync({ cwd: contentDir, absolute: true }));
121
+
122
+ const items: CollectionItem[] = [];
123
+ for (const mdFile of files) {
124
+ if (mdFile.endsWith("/index.md")) continue;
125
+
126
+ const content = await readFile(mdFile, "utf-8");
127
+ const { frontmatter } = parseFrontmatter(content);
128
+ if (frontmatter.draft) continue;
129
+
130
+ const relativePath = relative(CONTENT_DIR, mdFile).replace(/\.md$/, "");
131
+ const url = relativePath === "index" ? "/" : `/${relativePath}`;
132
+
133
+ items.push({
134
+ url,
135
+ title: (frontmatter.title as string) || "Untitled",
136
+ date: (frontmatter.date as string) || "",
137
+ description: (frontmatter.description as string) || "",
138
+ });
139
+ }
140
+
141
+ items.sort((a, b) => (b.date || "").localeCompare(a.date || ""));
142
+
143
+ return items;
144
+ }
145
+
146
+ async function buildPage(filePath: string, includeDrafts: boolean): Promise<Page | null> {
147
+ const content = await readFile(filePath, "utf-8");
148
+ const { frontmatter, content: mdContent } = parseFrontmatter(content);
149
+
150
+ if (!includeDrafts && frontmatter.draft) {
151
+ return null;
152
+ }
153
+
154
+ const html = renderMarkdown(mdContent);
155
+
156
+ const relativePath = relative(CONTENT_DIR, filePath).replace(/\.md$/, "");
157
+ let url = relativePath === "index" ? "/" : `/${relativePath}`;
158
+ if (url.endsWith("/index")) {
159
+ url = url.slice(0, -6) || "/";
160
+ }
161
+
162
+ return {
163
+ url,
164
+ filePath,
165
+ frontmatter,
166
+ content: mdContent,
167
+ html,
168
+ };
169
+ }
170
+
171
+ async function build(includeDrafts = false): Promise<void> {
172
+ console.log(`Building${includeDrafts ? " (with drafts)" : ""}...`);
173
+
174
+ if (await exists(DIST_DIR)) {
175
+ const { rmSync } = await import("fs");
176
+ rmSync(DIST_DIR, { recursive: true });
177
+ }
178
+
179
+ await mkdir(DIST_DIR, { recursive: true });
180
+
181
+ const glob = new Glob("**/*.md");
182
+ const files = Array.from(glob.scanSync({ cwd: CONTENT_DIR, absolute: true }));
183
+
184
+ const pages: Page[] = [];
185
+ for (const file of files) {
186
+ const page = await buildPage(file, includeDrafts);
187
+ if (page) {
188
+ pages.push(page);
189
+ }
190
+ }
191
+
192
+ for (const page of pages) {
193
+ let template = await resolveLayout(page.frontmatter);
194
+ template = template.replace(/\{\{\s*content\s*\|\s*safe\s*\}\}/g, page.html);
195
+
196
+ let collection: CollectionItem[] | undefined;
197
+ if (page.filePath.includes("/index.md")) {
198
+ const dir = dirname(page.filePath);
199
+ const contentDir = relative(CONTENT_DIR, dir);
200
+ if (contentDir && contentDir !== ".") {
201
+ const allPagesInDir = pages.filter((p) => {
202
+ const pDir = dirname(p.filePath);
203
+ return relative(CONTENT_DIR, pDir) === contentDir && !p.filePath.endsWith("/index.md");
204
+ });
205
+ collection = allPagesInDir
206
+ .map((p) => ({
207
+ url: p.url,
208
+ title: p.frontmatter.title || "Untitled",
209
+ date: p.frontmatter.date || "",
210
+ description: p.frontmatter.description || "",
211
+ }))
212
+ .sort((a, b) => (b.date || "").localeCompare(a.date || ""));
213
+ }
214
+ }
215
+
216
+ const outputHtml = applyTemplate(template, page, collection);
217
+
218
+ const outputPath = join(DIST_DIR, page.url === "/" ? "index.html" : `${page.url}/index.html`);
219
+ await mkdir(dirname(outputPath), { recursive: true });
220
+ await writeFile(outputPath, outputHtml);
221
+ }
222
+
223
+ const assetGlob = new Glob("**/*");
224
+ const assets = Array.from(assetGlob.scanSync({ cwd: CONTENT_DIR, absolute: true }));
225
+ for (const asset of assets) {
226
+ if (asset.endsWith(".md")) continue;
227
+ const relPath = relative(CONTENT_DIR, asset);
228
+ const destPath = join(DIST_DIR, relPath);
229
+ await mkdir(dirname(destPath), { recursive: true });
230
+ await copyFile(asset, destPath);
231
+ }
232
+
233
+ if (await exists(PUBLIC_DIR)) {
234
+ const publicFiles = Array.from(assetGlob.scanSync({ cwd: PUBLIC_DIR, absolute: true }));
235
+ for (const file of publicFiles) {
236
+ const relPath = relative(PUBLIC_DIR, file);
237
+ const destPath = join(DIST_DIR, relPath);
238
+ await mkdir(dirname(destPath), { recursive: true });
239
+ await copyFile(file, destPath);
240
+ }
241
+ }
242
+
243
+ if (await exists(join(CONTENT_DIR, "404.md"))) {
244
+ const page404 = await buildPage(join(CONTENT_DIR, "404.md"), includeDrafts);
245
+ if (page404) {
246
+ let template = await resolveLayout(page404.frontmatter);
247
+ template = template.replace(/\{\{\s*content\s*\|\s*safe\s*\}\}/g, page404.html);
248
+ const outputHtml = applyTemplate(template, page404);
249
+ await writeFile(join(DIST_DIR, "404.html"), outputHtml);
250
+ }
251
+ }
252
+
253
+ console.log(`Built ${pages.length} pages to ${DIST_DIR}`);
254
+ }
255
+
256
+ async function dev(): Promise<void> {
257
+ console.log("Starting dev server...");
258
+
259
+ const port = 3000;
260
+ const server = Bun.serve({
261
+ port,
262
+ async fetch(req) {
263
+ const url = new URL(req.url);
264
+ let path = url.pathname;
265
+
266
+ if (path === "/") {
267
+ path = "/index";
268
+ }
269
+
270
+ const filePath = join(DIST_DIR, `${path}.html`);
271
+ const indexPath = join(DIST_DIR, path, "index.html");
272
+
273
+ let file: any;
274
+ if (await exists(filePath)) {
275
+ file = Bun.file(filePath);
276
+ } else if (await exists(indexPath)) {
277
+ file = Bun.file(indexPath);
278
+ } else if (await exists(join(DIST_DIR, "404.html"))) {
279
+ return new Response(Bun.file(join(DIST_DIR, "404.html")), {
280
+ headers: { "Content-Type": "text/html" },
281
+ status: 404,
282
+ });
283
+ } else {
284
+ return new Response("Not Found", { status: 404 });
285
+ }
286
+
287
+ return new Response(file, {
288
+ headers: { "Content-Type": "text/html" },
289
+ });
290
+ },
291
+ });
292
+
293
+ console.log(`Dev server running at http://localhost:${server.port}`);
294
+
295
+ const watcher = Bun.watch([CONTENT_DIR, LAYOUTS_DIR, PUBLIC_DIR]);
296
+ for await (const event of watcher) {
297
+ console.log(`[${event.kind}] ${event.path} - rebuilding...`);
298
+ await build(false);
299
+ }
300
+ }
301
+
302
+ async function preview(): Promise<void> {
303
+ const port = 3000;
304
+ console.log(`Serving ${DIST_DIR} at http://localhost:${port}`);
305
+
306
+ Bun.serve({
307
+ port,
308
+ async fetch(req) {
309
+ const url = new URL(req.url);
310
+ let path = url.pathname;
311
+
312
+ if (path === "/") {
313
+ path = "/index";
314
+ }
315
+
316
+ const filePath = join(DIST_DIR, `${path}.html`);
317
+ const indexPath = join(DIST_DIR, path, "index.html");
318
+ const directPath = join(DIST_DIR, path);
319
+
320
+ let file: any;
321
+ if (await exists(filePath)) {
322
+ file = Bun.file(filePath);
323
+ } else if (await exists(indexPath)) {
324
+ file = Bun.file(indexPath);
325
+ } else if (await exists(directPath) && (await import("fs")).statSync(directPath).isDirectory()) {
326
+ file = Bun.file(join(directPath, "index.html"));
327
+ } else if (await exists(join(DIST_DIR, "404.html"))) {
328
+ return new Response(Bun.file(join(DIST_DIR, "404.html")), {
329
+ headers: { "Content-Type": "text/html" },
330
+ status: 404,
331
+ });
332
+ } else {
333
+ return new Response("Not Found", { status: 404 });
334
+ }
335
+
336
+ return new Response(file);
337
+ },
338
+ });
339
+ }
340
+
341
+ async function init(): Promise<void> {
342
+ const root = process.cwd();
343
+
344
+ const dirs = ["content/posts", "src/layouts", "public"];
345
+ for (const dir of dirs) {
346
+ await mkdir(join(root, dir), { recursive: true });
347
+ }
348
+
349
+ const baseLayout = `<!DOCTYPE html>
350
+ <html lang="en">
351
+ <head>
352
+ <meta charset="UTF-8">
353
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
354
+ <title>{{ title }}</title>
355
+ <meta name="description" content="{{ description }}">
356
+ <link rel="stylesheet" href="/style.css">
357
+ </head>
358
+ <body>
359
+ <header>
360
+ <nav>
361
+ <a href="/">Home</a>
362
+ </nav>
363
+ </header>
364
+ <main>
365
+ {{ content | safe }}
366
+ </main>
367
+ <footer>
368
+ <p>Built with BunPress</p>
369
+ </footer>
370
+ </body>
371
+ </html>`;
372
+
373
+ const pageLayout = `---
374
+ extends: base.html
375
+ ---
376
+ <article class="page">
377
+ <h1>{{ title }}</h1>
378
+ {{ content | safe }}
379
+ </article>`;
380
+
381
+ const indexMd = `---
382
+ title: Welcome
383
+ description: Welcome to my site
384
+ ---
385
+
386
+ # Welcome
387
+
388
+ This is your new BunPress site. Start editing \`content/index.md\` to get started!
389
+ `;
390
+
391
+ const packageJson = {
392
+ name: "my-site",
393
+ version: "1.0.0",
394
+ type: "module",
395
+ scripts: {
396
+ build: "buntastic build",
397
+ "build:drafts": "buntastic build --drafts",
398
+ dev: "buntastic dev",
399
+ preview: "buntastic preview",
400
+ },
401
+ };
402
+
403
+ const styleCss = `* {
404
+ margin: 0;
405
+ padding: 0;
406
+ box-sizing: border-box;
407
+ }
408
+
409
+ body {
410
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
411
+ line-height: 1.6;
412
+ max-width: 800px;
413
+ margin: 0 auto;
414
+ padding: 2rem;
415
+ }
416
+
417
+ a { color: #0066cc; }
418
+ h1, h2, h3 { margin: 1.5rem 0 1rem; }
419
+ main { min-height: 60vh; }
420
+ footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid #eee; }
421
+ `;
422
+
423
+ await writeFile(join(root, "src/layouts/base.html"), baseLayout);
424
+ await writeFile(join(root, "src/layouts/page.html"), pageLayout);
425
+ await writeFile(join(root, "content/index.md"), indexMd);
426
+ await writeFile(join(root, "package.json"), JSON.stringify(packageJson, null, 2));
427
+ await writeFile(join(root, "public/style.css"), styleCss);
428
+
429
+ console.log("Initialized BunPress project!");
430
+ console.log("Run 'buntastic dev' to start the dev server.");
431
+ }
432
+
433
+ const args = process.argv.slice(2);
434
+ const command = args[0];
435
+
436
+ if (command === "init") {
437
+ init();
438
+ } else if (command === "build") {
439
+ const drafts = args.includes("--drafts");
440
+ build(drafts);
441
+ } else if (command === "dev") {
442
+ dev();
443
+ } else if (command === "preview") {
444
+ preview();
445
+ } else {
446
+ console.log("Usage:");
447
+ console.log(" buntastic init - Initialize a new project");
448
+ console.log(" buntastic build - Build for production");
449
+ console.log(" buntastic build --drafts - Build with drafts");
450
+ console.log(" buntastic dev - Development mode");
451
+ console.log(" buntastic preview - Serve dist folder");
452
+ }
@@ -0,0 +1,25 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{ title }}</title>
7
+ <meta name="description" content="{{ description }}">
8
+ <link rel="stylesheet" href="/style.css">
9
+ </head>
10
+ <body>
11
+ <header>
12
+ <nav>
13
+ <a href="/">Home</a>
14
+ <a href="/posts">Posts</a>
15
+ <a href="/about">About</a>
16
+ </nav>
17
+ </header>
18
+ <main>
19
+ {{ content | safe }}
20
+ </main>
21
+ <footer>
22
+ <p>Built with BunPress</p>
23
+ </footer>
24
+ </body>
25
+ </html>
@@ -0,0 +1,7 @@
1
+ ---
2
+ extends: base.html
3
+ ---
4
+ <article class="page">
5
+ <h1>{{ title }}</h1>
6
+ {{ content | safe }}
7
+ </article>
@@ -0,0 +1,12 @@
1
+ ---
2
+ extends: base.html
3
+ ---
4
+ <article class="post">
5
+ <header>
6
+ <h1>{{ title }}</h1>
7
+ <time datetime="{{ date }}">{{ date }}</time>
8
+ </header>
9
+ <div class="content">
10
+ {{ content | safe }}
11
+ </div>
12
+ </article>