bunki 0.21.0 → 0.22.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/README.md CHANGED
@@ -4,1409 +4,209 @@
4
4
  [![Coverage Status](https://coveralls.io/repos/github/kahwee/bunki/badge.svg?branch=main)](https://coveralls.io/github/kahwee/bunki?branch=main)
5
5
  [![npm version](https://badge.fury.io/js/bunki.svg)](https://badge.fury.io/js/bunki)
6
6
 
7
- Fast static site generator for blogs and documentation built with Bun. Supports Markdown + frontmatter, tags, year-based archives, pagination, RSS feeds, sitemaps, JSON-LD structured data for SEO, secure HTML sanitization, syntax highlighting, PostCSS pipelines, media uploads (images & videos to S3/R2), incremental uploads with year filtering, and Nunjucks templating.
7
+ Bunki turns Markdown into a blog, a collection of notes, or a documentation site. Built with [Bun](https://bun.sh), it pairs editable Nunjucks templates with static HTML you can host wherever you like.
8
8
 
9
- ## Install
9
+ ![Bunki — Your words, on your own site.](docs/assets/bunki-banner.svg)
10
10
 
11
- Requires **Bun v1.3.0+** (recommended) or Node.js v18+
11
+ [Get started](#quick-start) · [Read the reference](docs/reference.md) · [Migration guide](docs/migration.md) · [Contribute](CONTRIBUTING.md)
12
12
 
13
- ```bash
14
- # Install globally with Bun
15
- bun install -g bunki
13
+ ## Quick start
14
+
15
+ Requires Bun 1.4.4 or newer. Run `bun upgrade` to update an existing Bun installation.
16
16
 
17
- # Or with npm
18
- npm install -g bunki
17
+ ```bash
18
+ mkdir my-blog
19
+ cd my-blog
20
+ bun init -y
21
+ bun add bunki
19
22
 
20
- # Or in your project
21
- bun install bunki
23
+ bunx bunki init
24
+ bunx bunki new "My First Post" --tags web,notes
25
+ bunx bunki generate
26
+ bunx bunki serve --port 3000
22
27
  ```
23
28
 
24
- ## Quick Start
29
+ Open <http://localhost:3000>. Edit `bunki.config.ts` for your site details, `content/` for posts, and `templates/` for layout. Files in `public/` are copied into the generated site in `dist/`, which you can deploy to a static host.
25
30
 
26
- ```bash
27
- bunki init # Create new site
28
- bunki new "My First Post" --tags web,notes # Add content
29
- bunki generate # Build static site
30
- bunki serve --port 3000 # Preview locally
31
+ ```text
32
+ my-blog/
33
+ ├── bunki.config.ts Site settings
34
+ ├── content/ Your Markdown posts
35
+ ├── templates/ Layouts and styles
36
+ ├── public/ Files to copy as-is
37
+ └── dist/ Generated site
31
38
  ```
32
39
 
33
- This creates a fully functional site with Markdown content, responsive templates, and all assets in `dist/`.
40
+ For a global CLI, use `bun add -g bunki` and run `bunki` in place of `bunx bunki`.
34
41
 
35
42
  ## Configuration
36
43
 
37
- Create `bunki.config.ts` in your project root:
38
-
39
44
  ```typescript
40
- import { SiteConfig } from "bunki";
45
+ import { defineConfig } from "bunki";
41
46
 
42
- export default (): SiteConfig => ({
43
- title: "My Blog",
44
- description: "My thoughts and ideas",
47
+ export default defineConfig({
48
+ title: "Field Notes",
49
+ description: "Things I build, places I go, and what I learn.",
45
50
  baseUrl: "https://example.com",
46
51
  domain: "example.com",
47
-
48
- // Optional: PostCSS/Tailwind CSS support
49
- css: {
50
- input: "templates/styles/main.css",
51
- output: "css/style.css",
52
- postcssConfig: "postcss.config.js",
53
- enabled: true,
54
- },
55
-
56
- // Optional: Image upload to Cloudflare R2 or S3
57
- s3: {
58
- accessKeyId: process.env.R2_ACCESS_KEY_ID || "",
59
- secretAccessKey: process.env.R2_SECRET_ACCESS_KEY || "",
60
- bucket: process.env.R2_BUCKET || "",
61
- endpoint: process.env.R2_ENDPOINT,
62
- region: process.env.R2_REGION || "auto",
63
- publicUrl: process.env.R2_PUBLIC_URL || "",
64
- },
52
+ authorName: "Your Name",
53
+ authorEmail: "you@example.com",
54
+ rssLanguage: "en-US",
65
55
  });
66
56
  ```
67
57
 
68
- ## Content & Frontmatter
58
+ See [configuration types](src/types.ts) for available options and [configuration defaults](src/config.ts) for loading behavior.
69
59
 
70
- Create Markdown files in `content/YYYY/` using either pattern:
60
+ The [reference](docs/reference.md) covers path overrides, template data, validation, and cache limitations.
71
61
 
72
- **Option 1: Single file** (traditional)
73
-
74
- ```
75
- content/2025/my-post.md
76
- ```
77
-
78
- **Option 2: Directory with README** (Obsidian-friendly)
79
-
80
- ```
81
- content/2025/my-post/README.md
82
- content/2025/my-post/image.jpg
83
- ```
62
+ ## Write a post
84
63
 
85
- Both patterns generate the same output: `dist/2025/my-post/index.html`
86
-
87
- > [!WARNING]
88
- > You cannot have both patterns for the same slug. Bunki will throw a validation error if both `content/2025/my-post.md` AND `content/2025/my-post/README.md` exist.
89
-
90
- Example frontmatter:
64
+ Posts live under `content/`. Use either `content/2026/my-post.md` or `content/2026/my-post/README.md`, but avoid both for the same slug.
91
65
 
92
66
  ```markdown
93
67
  ---
94
- title: "Post Title"
95
- date: 2025-01-15T09:00:00-07:00
96
- tags: [web-development, performance-optimization]
97
- excerpt: "Optional summary for listings"
68
+ title: "A place for small discoveries"
69
+ date: 2026-09-11T09:00:00-07:00
70
+ tags: [web-development, notes]
71
+ excerpt: "Notes worth keeping, one post at a time."
98
72
  ---
99
73
 
100
- # Post Title
101
-
102
- Your content here with **markdown** support.
103
-
104
- ![Image alt text](/images/my-image.jpg)
74
+ Today I started keeping a few **field notes**.
105
75
 
106
- <video controls width="640" height="360">
107
- <source src="video.mp4" type="video/mp4">
108
- Your browser does not support HTML5 video.
109
- </video>
76
+ ![A photo](/images/photo.jpg)
110
77
  ```
111
78
 
112
- ### Tag Format
113
-
114
- > [!IMPORTANT]
115
- > Tags must use hyphens instead of spaces: `web-development` NOT `"web development"`
116
-
117
- Tags with spaces will fail validation. Use hyphenated slugs:
118
-
119
- - ✅ `tags: [new-york-city, travel, family-friendly]`
120
- - ❌ `tags: ["new york city", "travel", "family friendly"]`
79
+ Saved as `my-post.md`, the example generates `/2026/my-post/`. The URL year comes from the frontmatter date in Pacific time, not the directory name. Use timezone-aware dates and hyphenated tag slugs such as `web-development`.
121
80
 
122
- Optional: Define tag descriptions in `src/tags.toml`:
81
+ Relative Markdown links such as `../2025/earlier-post.md` become site links such as `/2025/earlier-post/`. Optional tag descriptions go in `src/tags.toml`:
123
82
 
124
83
  ```toml
125
- performance-optimization = "Performance optimization and speed"
126
- web-development = "Web development and technology"
127
- new-york-city = "New York City travel guides"
84
+ web-development = "Notes on building for the web"
128
85
  ```
129
86
 
130
- ### Internal Links (Relative Markdown Links)
131
-
132
- Bunki automatically converts relative markdown links to absolute URLs during build time. This lets you write cross-references using familiar file paths:
133
-
134
- **In your markdown:**
135
-
136
- ```markdown
137
- Check out [my earlier post](../2023/introduction.md) for context.
138
-
139
- See also [related article](../../2020/old-post.md).
140
- ```
87
+ For business or location metadata, add `business` to frontmatter:
141
88
 
142
- **Generated HTML:**
143
-
144
- ```html
145
- <a href="/2023/introduction/">my earlier post</a>
146
- <a href="/2020/old-post/">related article</a>
147
- ```
148
-
149
- This feature works with:
150
-
151
- - `../YEAR/slug.md` - Single level up
152
- - `../../YEAR/slug.md` - Multiple levels up
153
- - Any number of `../` sequences
154
-
155
- The links are automatically converted to absolute URLs (`/YEAR/slug/`) that match your site's URL structure.
156
-
157
- ### Business Location Data
158
-
159
- Add structured business/location data with automatic validation:
160
-
161
- ```markdown
162
- ---
163
- title: "Restaurant Review"
164
- date: 2025-01-15T09:00:00-07:00
165
- tags: [food, review]
89
+ ```yaml
166
90
  business:
167
91
  - type: Restaurant
168
- name: "Blue Bottle Coffee"
169
- address: "123 Main St, San Francisco, CA 94102"
92
+ name: "Example Cafe"
93
+ address: "123 Main St, San Francisco, CA"
170
94
  lat: 37.7749
171
95
  lng: -122.4194
172
- ---
173
- ```
174
-
175
- **Required fields:** `type`, `name`, `lat`, `lng`
176
- **Optional fields:** `address`, `cuisine`, `priceRange`, `telephone`, `url`, `openingHours`
177
-
178
- The validator enforces:
179
-
180
- - Use `business:` (not deprecated `location:`)
181
- - Use `lat:`/`lng:` (not deprecated `latitude:`/`longitude:`)
182
- - All required fields must be present
183
-
184
- Validation runs automatically during `bunki generate` and `bunki validate`.
185
-
186
- ## CSS & Tailwind
187
-
188
- To use Tailwind CSS:
189
-
190
- ```bash
191
- bun add -D tailwindcss @tailwindcss/postcss @tailwindcss/typography
192
- ```
193
-
194
- Create `postcss.config.js`:
195
-
196
- ```javascript
197
- module.exports = {
198
- plugins: [require("@tailwindcss/postcss")],
199
- };
200
- ```
201
-
202
- Create `templates/styles/main.css`:
203
-
204
- ```css
205
- @tailwind base;
206
- @tailwind components;
207
- @tailwind utilities;
208
- ```
209
-
210
- CSS is processed automatically during `bunki generate`.
211
-
212
- ## JSON-LD Structured Data for SEO
213
-
214
- Bunki automatically generates [JSON-LD](https://json-ld.org/) structured data markup for enhanced SEO and search engine visibility. JSON-LD (JavaScript Object Notation for Linked Data) is Google's recommended format for structured data.
215
-
216
- ### What is JSON-LD?
217
-
218
- JSON-LD helps search engines better understand your content by providing explicit, structured information about your pages. This can lead to:
219
-
220
- - **Rich snippets** in search results (article previews, star ratings, etc.)
221
- - **Better content indexing** and understanding by search engines
222
- - **Improved click-through rates** from search results
223
- - **Knowledge graph integration** with Google, Bing, and other search engines
224
-
225
- ### Automatic Schema Generation
226
-
227
- Bunki automatically generates appropriate schemas for different page types:
228
-
229
- #### Blog Posts (BlogPosting Schema)
230
-
231
- Every blog post includes comprehensive `BlogPosting` schema with:
232
-
233
- - Headline and description
234
- - Publication and modification dates
235
- - Author information
236
- - Publisher details
237
- - Article keywords (from tags)
238
- - Word count
239
- - Featured image (automatically extracted)
240
- - Language information
241
-
242
- Example output in your HTML:
243
-
244
- ```html
245
- <script type="application/ld+json">
246
- {
247
- "@context": "https://schema.org",
248
- "@type": "BlogPosting",
249
- "headline": "Getting Started with Bun",
250
- "description": "Learn how to get started with Bun, the fast JavaScript runtime.",
251
- "url": "https://example.com/2025/getting-started-with-bun/",
252
- "datePublished": "2025-01-15T10:30:00.000Z",
253
- "dateModified": "2025-01-15T10:30:00.000Z",
254
- "author": {
255
- "@type": "Person",
256
- "name": "John Doe",
257
- "email": "john@example.com"
258
- },
259
- "publisher": {
260
- "@type": "Organization",
261
- "name": "My Blog",
262
- "url": "https://example.com"
263
- },
264
- "keywords": "bun, javascript, performance",
265
- "image": "https://example.com/images/bun-logo.png"
266
- }
267
- </script>
268
- ```
269
-
270
- #### Homepage (WebSite & Organization Schemas)
271
-
272
- The homepage includes dual schemas:
273
-
274
- 1. **WebSite Schema**: Defines the website entity
275
- 2. **Organization Schema**: Defines the publisher/organization
276
-
277
- ```html
278
- <script type="application/ld+json">
279
- {
280
- "@context": "https://schema.org",
281
- "@type": "WebSite",
282
- "name": "My Blog",
283
- "url": "https://example.com",
284
- "description": "My thoughts and ideas",
285
- "potentialAction": {
286
- "@type": "SearchAction",
287
- "target": {
288
- "@type": "EntryPoint",
289
- "urlTemplate": "https://example.com/search?q={search_term_string}"
290
- }
291
- }
292
- }
293
- </script>
294
- ```
295
-
296
- #### Breadcrumbs (BreadcrumbList Schema)
297
-
298
- All pages include breadcrumb navigation for better site hierarchy understanding:
299
-
300
- ```html
301
- <script type="application/ld+json">
302
- {
303
- "@context": "https://schema.org",
304
- "@type": "BreadcrumbList",
305
- "itemListElement": [
306
- {
307
- "@type": "ListItem",
308
- "position": 1,
309
- "name": "Home",
310
- "item": "https://example.com"
311
- },
312
- {
313
- "@type": "ListItem",
314
- "position": 2,
315
- "name": "Getting Started with Bun",
316
- "item": "https://example.com/2025/getting-started-with-bun/"
317
- }
318
- ]
319
- }
320
- </script>
321
- ```
322
-
323
- ### Configuration for SEO
324
-
325
- Enhance your JSON-LD output by providing complete author and site information in `bunki.config.ts`:
326
-
327
- ```typescript
328
- import { SiteConfig } from "bunki";
329
-
330
- export default (): SiteConfig => ({
331
- title: "My Blog",
332
- description: "My thoughts and ideas on web development",
333
- baseUrl: "https://example.com",
334
- domain: "example.com",
335
-
336
- // Author information (used in BlogPosting schema)
337
- authorName: "John Doe",
338
- authorEmail: "john@example.com",
339
-
340
- // RSS/SEO configuration
341
- rssLanguage: "en-US", // Language code for content
342
- copyright: "Copyright © 2025 My Blog",
343
-
344
- // ... other config
345
- });
346
- ```
347
-
348
- ### Testing Your JSON-LD
349
-
350
- You can validate your structured data using these tools:
351
-
352
- 1. **[Google Rich Results Test](https://search.google.com/test/rich-results)** - Test how Google sees your structured data
353
- 2. **[Schema.org Validator](https://validator.schema.org/)** - Validate JSON-LD syntax
354
- 3. **[Structured Data Linter](http://linter.structured-data.org/)** - Check for errors and warnings
355
-
356
- ### Supported Schema Types
357
-
358
- Bunki currently supports these Schema.org types:
359
-
360
- - **BlogPosting** - Individual blog posts and articles
361
- - **WebSite** - Homepage and site-wide metadata
362
- - **Organization** - Publisher/organization information
363
- - **Person** - Author information
364
- - **BreadcrumbList** - Navigation breadcrumbs
365
-
366
- ### How It Works
367
-
368
- JSON-LD generation is completely automatic:
369
-
370
- 1. **Post Creation**: When you write a post with frontmatter, Bunki extracts metadata
371
- 2. **Site Generation**: During `bunki generate`, appropriate schemas are created
372
- 3. **Template Injection**: JSON-LD scripts are automatically injected into `<head>`
373
- 4. **Image Extraction**: The first image in your post content is automatically used as the featured image
374
-
375
- No manual configuration needed - just run `bunki generate` and your site will have complete structured data!
376
-
377
- ### Best Practices
378
-
379
- To maximize SEO benefits:
380
-
381
- 1. **Use descriptive titles** - Your post title becomes the schema headline
382
- 2. **Write good excerpts** - These become schema descriptions
383
- 3. **Include images** - First image in content is used as featured image
384
- 4. **Tag your posts** - Tags become schema keywords
385
- 5. **Set author info** - Complete `authorName` and `authorEmail` in config
386
- 6. **Use ISO 8601 dates** - Format: `2025-01-15T10:30:00-07:00`
387
-
388
- ### Further Reading
389
-
390
- - [Schema.org Documentation](https://schema.org/)
391
- - [Google Search Central - Structured Data](https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data)
392
- - [JSON-LD Official Spec](https://json-ld.org/)
393
-
394
- ## Template Fragments
395
-
396
- Bunki ships reusable Nunjucks macros called **fragments**. They're registered as a second template search path alongside your site's `templates/` directory, so you can import them from any template without any configuration.
397
-
398
- ### Available fragments
399
-
400
- | File | Macros | Purpose |
401
- |------|--------|---------|
402
- | `og-image.njk` | `og_image(post, site)`, `twitter_image(post, site)` | Open Graph and Twitter Card image meta tags |
403
- | `json-ld.njk` | `blog_posting_schema(post, site)`, `local_business_schema(post, site)` | Schema.org structured data scripts |
404
- | `share-buttons.njk` | `share_buttons(post, site)` | X / Facebook / LinkedIn / Email share button row (Tailwind) |
405
- | `pagination.njk` | `pagination_nav(pagination)` | Previous / Next pagination nav (Tailwind) |
406
-
407
- ### Using fragments
408
-
409
- Import macros at the top of any template with `{% from %}`:
410
-
411
- ```nunjucks
412
- {# post.njk #}
413
- {% from "og-image.njk" import og_image, twitter_image %}
414
- {% from "json-ld.njk" import blog_posting_schema %}
415
-
416
- {% block og_image %}{{ og_image(post, site) }}{% endblock %}
417
- {% block twitter_image %}{{ twitter_image(post, site) }}{% endblock %}
418
-
419
- {% block head %}
420
- {{ blog_posting_schema(post, site) }}
421
- {% endblock %}
422
- ```
423
-
424
- ```nunjucks
425
- {# index.njk, tag.njk, archive.njk #}
426
- {% from "pagination.njk" import pagination_nav %}
427
-
428
- {{ pagination_nav(pagination) }}
429
96
  ```
430
97
 
431
- The default templates generated by `bunki init` use `og-image.njk` and `json-ld.njk` automatically. The `share-buttons.njk` and `pagination.njk` fragments use Tailwind CSS utility classes and are intended for sites that include Tailwind in their build.
98
+ Use `business`, `lat`, and `lng`; the older `location`, `latitude`, and `longitude` fields fail validation. Check content with `bunx bunki validate` and media with `bunx bunki validate:media`.
432
99
 
433
- ### Writing your own fragments
434
-
435
- Fragments are standard Nunjucks macro files. You can place your own in your site's `templates/` directory and import them the same way — site templates take priority over built-in fragments if names collide.
436
-
437
- ```nunjucks
438
- {# templates/my-macros.njk #}
439
- {% macro breadcrumbs(post, site) %}
440
- <nav aria-label="Breadcrumb">
441
- <a href="/">Home</a> /
442
- <span>{{ post.title }}</span>
443
- </nav>
444
- {% endmacro %}
445
- ```
100
+ ## Templates and uploads
446
101
 
447
- ```nunjucks
448
- {# templates/post.njk #}
449
- {% from "my-macros.njk" import breadcrumbs %}
450
- {{ breadcrumbs(post, site) }}
451
- ```
452
-
453
- ### Template variables reference
454
-
455
- | Variable | Available in | Description |
456
- |----------|-------------|-------------|
457
- | `site` | All templates | Site config: `title`, `description`, `baseUrl`, `author` |
458
- | `post` | `post.njk` | Post data: `title`, `excerpt`, `html`, `url`, `date`, `tags`, `tagSlugs`, `image`, `business` |
459
- | `posts` | `index.njk`, `archive.njk` | Array of post objects |
460
- | `pagination` | `index.njk`, `tag.njk`, `archive.njk` | `currentPage`, `totalPages`, `hasPrevPage`, `hasNextPage`, `prevPage`, `nextPage`, `pagePath` |
461
- | `tag` | `tag.njk` | Tag object: `name`, `slug`, `description`, `posts` |
462
- | `tags` | All templates | Array of tag objects: `name`, `slug`, `count`, `description` |
463
- | `year` | `archive.njk` | Year string, e.g. `"2025"` |
464
-
465
- ## Image Management
466
-
467
- ### Overview
468
-
469
- The `images:push` command uploads local media (images and videos) to Cloudflare R2, AWS S3, or any S3-compatible storage provider. Media files are organized by year in the `images/` directory and uploaded with their full directory structure preserved.
470
-
471
- **Supported formats:**
472
-
473
- - **Images:** JPG, JPEG, PNG, GIF, WebP, SVG
474
- - **Video:** MP4
475
-
476
- ### Directory Structure
477
-
478
- Organize images by year and post slug:
479
-
480
- ```
481
- images/
482
- ├── 2023/
483
- │ ├── post-slug-1/
484
- │ │ ├── image-1.jpg
485
- │ │ └── image-2.png
486
- │ └── post-slug-2/
487
- │ └── photo.webp
488
- ├── 2024/
489
- │ └── travel-guide/
490
- │ ├── paris-1.jpg
491
- │ ├── london-2.jpg
492
- │ ├── tokyo-3.png
493
- │ └── travel-vlog.mp4
494
- └── 2025/
495
- └── new-post/
496
- ├── screenshot.jpg
497
- └── demo-video.mp4
498
- ```
102
+ Edit Nunjucks templates and CSS for your layout. The [reference](docs/reference.md)
103
+ covers template data, PostCSS, S3-compatible uploads, and their configuration.
104
+ Uploads run separately from site generation.
499
105
 
500
- The directory structure is preserved when uploading to cloud storage.
106
+ ## Commands
501
107
 
502
- ### Configuration
108
+ Run `bunx bunki <command> --help` for all options.
503
109
 
504
- Add S3/R2 configuration to `bunki.config.ts`:
505
-
506
- ```typescript
507
- import { SiteConfig } from "bunki";
508
-
509
- export default (): SiteConfig => ({
510
- title: "My Blog",
511
- // ... other config
512
-
513
- // Image upload configuration
514
- s3: {
515
- accessKeyId: process.env.S3_ACCESS_KEY_ID || "",
516
- secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || "",
517
- bucket: process.env.S3_BUCKET || "",
518
- endpoint: process.env.S3_ENDPOINT, // Optional: for R2, etc.
519
- region: process.env.S3_REGION || "auto",
520
- publicUrl: process.env.S3_PUBLIC_URL || "",
521
- },
522
- });
523
- ```
524
-
525
- ### Environment Variables
526
-
527
- Set these in your `.env` file or export them in your shell:
528
-
529
- ```bash
530
- # Required
531
- export S3_ACCESS_KEY_ID="your-access-key"
532
- export S3_SECRET_ACCESS_KEY="your-secret-key"
533
- export S3_BUCKET="your-bucket-name"
534
- export S3_PUBLIC_URL="https://cdn.example.com"
535
-
536
- # Optional (for Cloudflare R2 or custom endpoints)
537
- export S3_ENDPOINT="https://r2.cloudflarestorage.com"
538
- export S3_REGION="auto"
539
-
540
- # Optional (custom domain per bucket)
541
- export S3_CUSTOM_DOMAIN_YOUR_BUCKET="cdn.example.com"
542
- ```
543
-
544
- ### Basic Usage
545
-
546
- Upload all images:
547
-
548
- ```bash
549
- bunki images:push
550
- ```
551
-
552
- This command:
553
-
554
- 1. Scans the `images/` directory recursively
555
- 2. Uploads all supported image formats
556
- 3. Preserves the directory structure (year/slug/filename)
557
- 4. Generates public URLs for each image
558
-
559
- ### Command Options
560
-
561
- #### `--images <dir>`
562
-
563
- Specify a custom images directory (default: `./images`)
564
-
565
- ```bash
566
- bunki images:push --images ./assets/images
567
- ```
110
+ | Command | Purpose |
111
+ | ------------------------------ | --------------------------------------------------- |
112
+ | `init` | Create configuration, templates, and sample content |
113
+ | `new <title> --tags web,notes` | Create a Markdown post |
114
+ | `generate` | Build the site in `dist/` |
115
+ | `generate --incremental` | Reuse cached posts and unchanged CSS |
116
+ | `serve --port 3000` | Preview generated files locally |
117
+ | `css --watch` | Process CSS and watch for changes |
118
+ | `validate` | Check Markdown and frontmatter |
119
+ | `validate:media` | Check media files |
120
+ | `images:push` | Upload media to S3-compatible storage |
568
121
 
569
- #### `--domain <domain>`
122
+ `generate` accepts `--config`, `--content`, `--output`, and `--templates` to override paths.
570
123
 
571
- Set a custom domain for bucket identification (optional)
572
-
573
- ```bash
574
- bunki images:push --domain my-blog
575
- ```
576
-
577
- #### `--output-json <file>`
578
-
579
- Export a JSON mapping of filenames to their public URLs
580
-
581
- ```bash
582
- bunki images:push --output-json image-urls.json
583
- ```
584
-
585
- This creates a JSON file with the structure:
586
-
587
- ```json
588
- {
589
- "2023/post-slug/image.jpg": "https://cdn.example.com/2023/post-slug/image.jpg",
590
- "2024/travel/paris.jpg": "https://cdn.example.com/2024/travel/paris.jpg"
591
- }
592
- ```
593
-
594
- #### `--min-year <year>`
595
-
596
- Upload only images from the specified year onwards
597
-
598
- ```bash
599
- # Upload only 2023 and 2024 images (skip 2021, 2022)
600
- bunki images:push --min-year 2023
601
-
602
- # Upload only 2024 and newer images
603
- bunki images:push --min-year 2024
604
-
605
- # Upload from 2022 onwards (all images in this example)
606
- bunki images:push --min-year 2022
607
- ```
608
-
609
- This is useful for:
610
-
611
- - Incremental uploads (upload only new images)
612
- - Testing uploads for specific years
613
- - Managing large image collections across multiple uploads
614
-
615
- #### `--content-assets`
616
-
617
- Upload images that are co-located with markdown files instead of the top-level `assets/` directory.
618
-
619
- When content is organized with images living alongside the markdown:
620
-
621
- ```
622
- content/
623
- ├── 2024/
624
- │ └── _assets/
625
- │ ├── paris-cafe.webp
626
- │ └── eiffel-tower.webp
627
- └── 2025/
628
- └── _assets/
629
- └── tokyo-ramen.webp
630
- ```
631
-
632
- Run:
633
-
634
- ```bash
635
- bunki images:push --content-assets
636
- ```
637
-
638
- This uploads each file with the S3 key `{year}/{filename}` — the `_assets/` segment is stripped. For example, `content/2024/_assets/paris-cafe.webp` becomes key `2024/paris-cafe.webp`, accessible at `https://cdn.example.com/2024/paris-cafe.webp`.
639
-
640
- > [!IMPORTANT]
641
- > Always use CDN URLs in your markdown, not relative `_assets/` paths. Relative paths cause the image files to be bundled into your Cloudflare Workers deployment instead of served from R2.
642
-
643
- ```markdown
644
- <!-- ❌ Causes image to be bundled into Workers -->
645
- ![Paris](../2024/_assets/paris-cafe.webp)
646
-
647
- <!-- ✅ Served from R2 CDN -->
648
- ![Paris](https://cdn.example.com/2024/paris-cafe.webp)
649
- ```
650
-
651
- #### `--content-assets-dir <dir>`
652
-
653
- Override the assets subdirectory name. Defaults to `_assets` (or `contentAssets.assetsDir` in `bunki.config.ts`).
654
-
655
- ```bash
656
- # Use _images instead of _assets
657
- bunki images:push --content-assets --content-assets-dir _images
658
-
659
- # Use any custom name
660
- bunki images:push --content-assets --content-assets-dir media
661
- ```
662
-
663
- ### Complete Examples
664
-
665
- #### Cloudflare R2 Setup
666
-
667
- 1. **Create R2 bucket and API token** in Cloudflare dashboard
668
-
669
- 2. **Set environment variables:**
670
-
671
- ```bash
672
- export S3_ACCESS_KEY_ID="your-r2-api-token-id"
673
- export S3_SECRET_ACCESS_KEY="your-r2-api-token-secret"
674
- export S3_BUCKET="my-blog-images"
675
- export S3_ENDPOINT="https://r2.cloudflarestorage.com"
676
- export S3_REGION="auto"
677
- export S3_PUBLIC_URL="https://cdn.example.com"
678
- ```
679
-
680
- 3. **Upload images:**
681
-
682
- ```bash
683
- bunki images:push --output-json image-urls.json
684
- ```
685
-
686
- #### AWS S3 Setup
687
-
688
- 1. **Create S3 bucket and IAM user** in AWS Console
689
-
690
- 2. **Set environment variables:**
691
-
692
- ```bash
693
- export S3_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
694
- export S3_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
695
- export S3_BUCKET="my-blog-bucket"
696
- export S3_REGION="us-east-1"
697
- export S3_PUBLIC_URL="https://my-blog-bucket.s3.amazonaws.com"
698
- ```
699
-
700
- 3. **Upload images:**
701
-
702
- ```bash
703
- bunki images:push
704
- ```
705
-
706
- #### Incremental Upload (Year-Based)
707
-
708
- If you have thousands of images and want to upload them incrementally:
709
-
710
- ```bash
711
- # First, upload all 2023 images
712
- bunki images:push --min-year 2023 --max-year 2023
713
-
714
- # Next, upload 2024 images
715
- bunki images:push --min-year 2024 --max-year 2024
716
-
717
- # Finally, upload 2025 images
718
- bunki images:push --min-year 2025
719
- ```
720
-
721
- ### Using Uploaded Images in Markdown
722
-
723
- After uploading, reference images in your Markdown posts:
724
-
725
- ```markdown
726
- ---
727
- title: "Paris Trip"
728
- date: 2024-06-15T10:00:00
729
- tags: [travel, france]
730
- ---
731
-
732
- # My Trip to Paris
733
-
734
- ![Eiffel Tower at sunset](https://cdn.example.com/2024/paris-trip/eiffel-tower.jpg)
735
-
736
- ![Louvre Museum](https://cdn.example.com/2024/paris-trip/louvre.jpg)
737
-
738
- ## Evening Stroll
739
-
740
- The Parisian streets at night are magical.
741
-
742
- ![Seine River at night](https://cdn.example.com/2024/paris-trip/seine-night.jpg)
743
- ```
744
-
745
- ### Using Uploaded Videos in Markdown
746
-
747
- Upload MP4 videos alongside your images and embed them in your posts:
748
-
749
- ```markdown
750
- ---
751
- title: "Travel Vlog"
752
- date: 2024-06-15T10:00:00
753
- tags: [travel, video]
754
- ---
755
-
756
- # My Paris Adventure
757
-
758
- Watch my trip to Paris:
759
-
760
- <video controls width="640" height="360">
761
- <source src="https://cdn.example.com/2024/paris-trip/travel-vlog.mp4" type="video/mp4">
762
- Your browser does not support HTML5 video.
763
- </video>
764
-
765
- ## Behind the Scenes
766
-
767
- Check out the making of the vlog:
768
-
769
- <video controls width="640" height="360">
770
- <source src="https://cdn.example.com/2024/paris-trip/behind-scenes.mp4" type="video/mp4">
771
- Your browser does not support HTML5 video.
772
- </video>
773
- ```
774
-
775
- **Video Upload Example:**
776
-
777
- ```bash
778
- # Upload all images and videos (including MP4 files)
779
- bunki images:push
780
-
781
- # Upload only 2024 videos and images
782
- bunki images:push --min-year 2024
783
-
784
- # Preview what would be uploaded without actually uploading
785
- BUNKI_DRY_RUN=true bunki images:push --min-year 2024
786
- ```
787
-
788
- **Video File Organization:**
789
-
790
- Keep videos organized the same way as images for consistency:
791
-
792
- ```
793
- images/
794
- ├── 2024/
795
- │ └── travel-vlog/
796
- │ ├── intro.mp4
797
- │ ├── highlights.mp4
798
- │ ├── thumbnail.jpg
799
- │ └── poster.jpg
800
- └── 2025/
801
- └── tutorial/
802
- ├── part-1.mp4
803
- ├── part-2.mp4
804
- └── preview.jpg
805
- ```
806
-
807
- **Video Tips:**
808
-
809
- 1. **File Size**: Keep MP4 files optimized (under 50MB recommended)
810
- - Use tools like FFmpeg to compress before uploading
811
- - Example: `ffmpeg -i input.mp4 -crf 28 output.mp4`
812
-
813
- 2. **Format & Codec**:
814
- - Use H.264 video codec for best compatibility
815
- - Use AAC audio codec
816
- - Container: MP4 (.mp4 extension)
817
-
818
- 3. **Video Dimensions**:
819
- - Keep 16:9 aspect ratio for web
820
- - Common resolutions: 640x360, 1280x720, 1920x1080
821
-
822
- 4. **Hosting**:
823
- - MP4s benefit from CDN caching via S3/R2
824
- - Cloudflare R2 provides excellent video delivery
825
- - AWS S3 with CloudFront for additional acceleration
826
-
827
- ### Dry Run Mode
828
-
829
- Test the upload process without actually uploading:
830
-
831
- ```bash
832
- # Preview what would be uploaded (no actual upload)
833
- BUNKI_DRY_RUN=true bunki images:push
834
- ```
835
-
836
- This shows:
837
-
838
- - Which images would be uploaded
839
- - The directory structure that would be created
840
- - Generated public URLs
841
-
842
- ### Troubleshooting
843
-
844
- #### "Missing S3 configuration"
845
-
846
- Ensure all required environment variables are set. Check `bunki.config.ts` and your `.env` file.
847
-
848
- #### "No image files found"
849
-
850
- - Verify images exist in `images/` directory
851
- - Check that files have supported extensions (.jpg, .png, .gif, .webp, .svg)
852
- - Ensure the directory structure is correct (e.g., `images/2024/post-slug/image.jpg`)
853
-
854
- #### "Unauthorized" or "Access Denied"
855
-
856
- - Verify S3 credentials (access key and secret key)
857
- - Check that the IAM user/API token has S3 permissions
858
- - Confirm the bucket name is correct
859
-
860
- #### "Invalid bucket name"
861
-
862
- - S3 bucket names must be globally unique
863
- - Use only lowercase letters, numbers, and hyphens
864
- - Bucket names must be 3-63 characters long
865
-
866
- ### Advanced Configuration
867
-
868
- #### Content Assets Configuration
869
-
870
- Configure co-located content assets in `bunki.config.ts`:
871
-
872
- ```typescript
873
- import { SiteConfig } from "bunki";
874
-
875
- export default (): SiteConfig => ({
876
- title: "My Blog",
877
- baseUrl: "https://example.com",
878
- domain: "example.com",
879
-
880
- // Default S3 config (used by bunki images:push)
881
- s3: {
882
- accessKeyId: process.env.S3_ACCESS_KEY_ID || "",
883
- secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || "",
884
- bucket: "my-site-assets",
885
- endpoint: process.env.S3_ENDPOINT,
886
- region: "auto",
887
- publicUrl: "https://assets.example.com",
888
- },
889
-
890
- // Content assets: images stored alongside markdown in content/{year}/_images/
891
- contentAssets: {
892
- // Directory name within content/{year}/ (default: "_assets")
893
- assetsDir: "_images",
894
-
895
- // Optional: use a separate R2 bucket for content assets
896
- s3: {
897
- accessKeyId: process.env.IMG_ACCESS_KEY_ID || "",
898
- secretAccessKey: process.env.IMG_SECRET_ACCESS_KEY || "",
899
- bucket: "my-blog-images",
900
- endpoint: process.env.S3_ENDPOINT,
901
- region: "auto",
902
- publicUrl: "https://img.example.com",
903
- },
904
- },
905
- });
906
- ```
907
-
908
- Then upload content assets:
909
-
910
- ```bash
911
- # Uses contentAssets.assetsDir and contentAssets.s3 from config
912
- bunki images:push --content-assets
913
-
914
- # Override the directory name at the CLI level
915
- bunki images:push --content-assets --content-assets-dir _media
916
- ```
917
-
918
- #### Custom Domain per Bucket
919
-
920
- If you have multiple S3 buckets with different custom domains:
921
-
922
- ```bash
923
- export S3_CUSTOM_DOMAIN_MY_BUCKET="cdn1.example.com"
924
- export S3_CUSTOM_DOMAIN_BACKUP_BUCKET="cdn2.example.com"
925
- ```
926
-
927
- The bucket name is converted to uppercase and hyphens to underscores for the environment variable name.
928
-
929
- #### Direct CDN URLs
930
-
931
- Configure public URLs with custom domains:
932
-
933
- ```typescript
934
- // bunki.config.ts
935
- s3: {
936
- // ... other config
937
- publicUrl: "https://img.example.com",
938
- }
939
- ```
940
-
941
- Or via environment variable:
942
-
943
- ```bash
944
- export S3_PUBLIC_URL="https://img.example.com"
945
- ```
946
-
947
- ### Performance Tips
948
-
949
- 1. **Use year-based filtering** for large image collections:
950
-
951
- ```bash
952
- bunki images:push --min-year 2024 # Only newest images
953
- ```
954
-
955
- 2. **Organize by post slug** for better directory structure:
956
-
957
- ```
958
- images/2024/post-title/image.jpg
959
- images/2024/post-title/photo.jpg
960
- ```
961
-
962
- 3. **Compress images before uploading** to save storage:
963
- - Use tools like `imagemin` or built-in OS utilities
964
- - Aim for 500KB or smaller per image
965
-
966
- 4. **Use modern formats** (WebP) for better compression:
967
- - JPG/PNG for screenshots
968
- - WebP for photos
969
- - SVG for icons/graphics
970
-
971
- ## Incremental Builds
972
-
973
- Bunki supports incremental builds for significantly faster rebuild times during development. When enabled, only changed content is reprocessed while unchanged files are loaded from cache.
974
-
975
- ### Performance Impact
976
-
977
- **Large site example (455 posts):**
978
-
979
- - Full build: 3,128ms
980
- - Incremental build (no changes): 985ms (**3.2x faster**)
981
-
982
- **Speedup breakdown:**
983
-
984
- - Markdown parsing: 1,202ms → 55ms (**22x faster**)
985
- - CSS processing: 1,024ms → 1ms (**1024x faster**)
986
- - Overall: **68% faster builds**
987
-
988
- ### Usage
989
-
990
- ```bash
991
- # Enable incremental builds
992
- bunki generate --incremental
993
-
994
- # First run (creates cache)
995
- # Config changed, full rebuild required
996
- # Total: 3,128ms (same as normal build)
997
-
998
- # Subsequent runs (no changes)
999
- # No content changes detected, using cached posts
1000
- # ✨ Loaded 455 posts from cache (0ms parsing)
1001
- # ⏭️ Skipping CSS (unchanged)
1002
- # Total: 985ms (3.2x faster!)
1003
-
1004
- # When one file changes
1005
- # 📦 Incremental build: 1/456 files changed (~2730ms saved)
1006
- # Parsed: 1 new/changed, loaded: 455 from cache
1007
- # Total: ~1,000ms
1008
- ```
1009
-
1010
- ### How It Works
1011
-
1012
- 1. **First build** creates `.bunki-cache.json` with:
1013
- - File hashes and modification times
1014
- - Parsed post data (title, content, metadata)
1015
- - CSS file checksums
1016
- - Config file hash
1017
-
1018
- 2. **Subsequent builds** detect changes by comparing:
1019
- - Config file hash (triggers full rebuild if changed)
1020
- - Markdown file hashes/mtimes
1021
- - CSS file hashes
1022
-
1023
- 3. **Selective processing**:
1024
- - Only parse changed markdown files
1025
- - Load unchanged posts from cache
1026
- - Skip CSS if unchanged
1027
- - Regenerate all pages (currently not selective)
1028
-
1029
- ### Cache Management
1030
-
1031
- The cache is stored in `.bunki-cache.json` at your project root:
1032
-
1033
- ```bash
1034
- # View cache status
1035
- cat .bunki-cache.json | jq '.version, .configHash'
1036
-
1037
- # Clear cache (force full rebuild)
1038
- rm .bunki-cache.json
1039
-
1040
- # Exclude from version control
1041
- echo ".bunki-cache.json" >> .gitignore
1042
- ```
1043
-
1044
- ### When to Use
1045
-
1046
- **Recommended for:**
1047
-
1048
- - Large sites (100+ posts)
1049
- - Development workflow with frequent rebuilds
1050
- - Sites with slow CSS processing (Tailwind, PostCSS)
1051
-
1052
- **Not needed for:**
1053
-
1054
- - Small sites (<50 posts) - already fast enough
1055
- - CI/CD builds - prefer clean full builds
1056
- - Production deployments - always use full builds
1057
-
1058
- ### Cache Format
1059
-
1060
- Version 2.0.0 cache structure:
1061
-
1062
- ```json
1063
- {
1064
- "version": "2.0.0",
1065
- "configHash": "abc123",
1066
- "files": {
1067
- "/path/to/post.md": {
1068
- "hash": "def456",
1069
- "mtime": 1771720766417,
1070
- "post": {
1071
- "title": "Post Title",
1072
- "date": "2024-01-01",
1073
- "content": "...",
1074
- "html": "..."
1075
- }
1076
- },
1077
- "/path/to/main.css": {
1078
- "hash": "ghi789",
1079
- "mtime": 1771720800000
1080
- }
1081
- }
1082
- }
1083
- ```
1084
-
1085
- ### Future Optimizations
1086
-
1087
- Current implementation (v0.18.0) optimizes parsing and CSS processing. Future versions may add:
1088
-
1089
- - Selective page regeneration (only rebuild changed posts)
1090
- - Incremental sitemap/RSS updates
1091
- - Smart index page regeneration
1092
-
1093
- ## CLI Commands
1094
-
1095
- ```bash
1096
- bunki init [--config FILE] # Initialize new site
1097
- bunki new <TITLE> [--tags TAG1,TAG2] # Create new post
1098
- bunki generate [--config FILE] # Build static site (full)
1099
- bunki generate --incremental # Build with caching (3x faster)
1100
- bunki validate [--config FILE] # Validate frontmatter
1101
- bunki serve [--port 3000] # Start dev server
1102
- bunki css [--watch] # Process CSS
1103
- bunki images:push [--domain DOMAIN] # Upload images to cloud
1104
- ```
1105
-
1106
- ## Output Structure
1107
-
1108
- ```
1109
- dist/
1110
- ├── index.html # Homepage
1111
- ├── feed.xml # RSS feed
1112
- ├── sitemap.xml # XML sitemap
1113
- ├── css/style.css # Processed stylesheet
1114
- ├── 2025/
1115
- │ └── my-post/
1116
- │ └── index.html # Post page
1117
- ├── tags/
1118
- │ └── web/
1119
- │ └── index.html # Tag page
1120
- └── page/
1121
- └── 2/index.html # Paginated content
1122
- ```
1123
-
1124
- ## Architecture
1125
-
1126
- Bunki follows a **modular architecture** with single responsibility modules for maintainability and performance:
1127
-
1128
- ### Core Orchestrator
1129
-
1130
- - **site-generator.ts** (282 lines) - Clean orchestrator coordinating all generation tasks
1131
- - Uses dependency injection for testability
1132
- - Parallel processing with Promise.all()
1133
- - Minimal business logic (delegates to generators)
1134
-
1135
- ### Modular Generators
1136
-
1137
- - **generators/feeds.ts** (285 lines) - RSS feed, sitemap, and robots.txt generation
1138
- - **generators/pages.ts** (357 lines) - HTML page generation with batched processing
1139
- - **generators/assets.ts** (115 lines) - CSS processing and static file copying
1140
-
1141
- ### Markdown Processing
1142
-
1143
- - **utils/markdown/constants.ts** (71 lines) - Pre-compiled regex patterns, Schema.org types, icons
1144
- - **utils/markdown/validators.ts** (139 lines) - Frontmatter and business location validation
1145
- - **utils/markdown/parser.ts** (308 lines) - Markdown to HTML conversion with sanitization
1146
-
1147
- ### Reusable Utilities
1148
-
1149
- - **utils/pagination.ts** (67 lines) - Pagination logic used across index, tags, and archives
1150
- - **utils/xml-builder.ts** (117 lines) - DRY XML/RSS building utilities
1151
- - **utils/markdown-utils.ts** (177 lines) - Main export file for backward compatibility
1152
-
1153
- ### Dependency Graph
1154
-
1155
- ```
1156
- site-generator.ts (orchestrator)
1157
- ├── generators/feeds.ts → utils/xml-builder.ts
1158
- ├── generators/pages.ts → utils/pagination.ts
1159
- ├── generators/assets.ts
1160
- └── utils/markdown/
1161
- ├── constants.ts
1162
- ├── validators.ts
1163
- └── parser.ts
1164
- ```
1165
-
1166
- ### Performance Optimizations
1167
-
1168
- 1. **Parallel Processing**: Independent tasks run simultaneously with Promise.all()
1169
- 2. **Batched Operations**: Posts processed in batches of 10 for optimal throughput
1170
- 3. **Pre-compiled Patterns**: Regex compiled once at module load, not on every parse
1171
- 4. **O(1) Lookups**: Set-based validation instead of array.includes()
1172
- 5. **Zero-Copy I/O**: Bun native APIs for kernel-level file transfers
1173
- 6. **Content Hashing**: Bun.hash() for CSS cache busting without external dependencies
1174
-
1175
- ### Benefits
1176
-
1177
- - ✅ **Clarity** - Easy to find and understand code
1178
- - ✅ **Testability** - Each module tested in isolation (424 tests)
1179
- - ✅ **Maintainability** - Changes isolated to specific modules
1180
- - ✅ **Reusability** - Modules can be imported independently
1181
- - ✅ **Performance** - Optimized at module level with Bun native APIs
1182
-
1183
- ## Features
1184
-
1185
- - **Markdown Processing**: Frontmatter extraction, code highlighting, HTML sanitization
1186
- - **Relative Link Conversion**: Automatic conversion of relative markdown links (`../2023/post.md`) to absolute URLs (`/2023/post/`)
1187
- - **Frontmatter Validation**: Automatic validation of business location data with clear error messages
1188
- - **Security**: XSS protection, sanitized HTML, link hardening
1189
- - **High Performance**:
1190
- - **Incremental builds** with smart caching (3.2x faster, 68% speedup)
1191
- - Parallel page generation (40-60% faster builds)
1192
- - Batched post processing (10x faster for 100+ posts)
1193
- - Pre-compiled regex patterns (2-3x faster parsing)
1194
- - O(1) Set-based validation (35x faster)
1195
- - Zero-copy file operations (50% faster, lower memory)
1196
- - Bun native APIs for optimal performance
1197
- - **Templating**: Nunjucks with custom filters and macros
1198
- - **Styling**: Built-in PostCSS support for modern CSS frameworks with content-based cache busting
1199
- - **Media Management**: Direct S3/R2 uploads for images and MP4 videos with URL mapping
1200
- - **Incremental Uploads**: Year-based filtering (`--min-year`) for large media collections
1201
- - **SEO**: Automatic RSS feeds, sitemaps, meta tags, and JSON-LD structured data
1202
- - **JSON-LD Structured Data**: Automatic Schema.org markup (BlogPosting, WebSite, Organization, BreadcrumbList)
1203
- - **Pagination**: Configurable posts per page with reusable pagination utilities
1204
- - **Archives**: Year-based and tag-based organization
1205
- - **Modular Architecture**: Single responsibility modules with comprehensive test coverage (424 tests)
124
+ Incremental builds store parsed posts and file checks in `.bunki-cache.json`; HTML pages are still regenerated. Full and incremental builds use the same validation and date ordering. Config changes invalidate cached posts; content changes also rebuild CSS. Add the cache to `.gitignore`. Delete that cache file to reset it, or omit `--incremental` for a full build.
1206
125
 
1207
126
  ## Development
1208
127
 
128
+ Bug reports, documentation improvements, and focused contributions are welcome. Start with the [contribution guide](CONTRIBUTING.md) or [open an issue](https://github.com/kahwee/bunki/issues).
129
+
1209
130
  ```bash
1210
- git clone git@github.com:kahwee/bunki.git
131
+ git clone https://github.com/kahwee/bunki.git
1211
132
  cd bunki
1212
- bun install
1213
-
1214
- bun run build # Build distribution
1215
- bun test # Run test suite
1216
- bun test:coverage # Test coverage report
1217
- bun run typecheck # TypeScript validation
1218
- bun run format # Biome formatting
133
+ bun install --frozen-lockfile
134
+ bun run test
135
+ bun run typecheck
136
+ bun run format:check
137
+ bun run build
138
+ bun run benchmark -- 1000 # Measure cold, cached, and single-post-edit builds
1219
139
  ```
1220
140
 
1221
- ## Project Structure
141
+ Source lives in `src/`, tests in `test/`, and test data in `fixtures/`. See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow and [package.json](package.json) for additional scripts.
1222
142
 
1223
- ```
1224
- bunki/
1225
- ├── src/
1226
- │ ├── cli.ts # CLI interface
1227
- │ ├── config.ts # Configuration management
1228
- │ ├── site-generator.ts # Orchestrator (282 lines, was 957)
1229
- │ ├── server.ts # Development server
1230
- │ ├── parser.ts # Markdown parsing
1231
- │ ├── types.ts # TypeScript types
1232
- │ ├── generators/ # Modular generation (NEW)
1233
- │ │ ├── feeds.ts # RSS, sitemap, robots.txt (285 lines)
1234
- │ │ ├── pages.ts # HTML generation with batching (357 lines)
1235
- │ │ └── assets.ts # CSS & static file copying (115 lines)
1236
- │ └── utils/ # Utility modules
1237
- │ ├── markdown/ # Markdown processing (NEW)
1238
- │ │ ├── constants.ts # Pre-compiled patterns (71 lines)
1239
- │ │ ├── validators.ts # Frontmatter validation (139 lines)
1240
- │ │ └── parser.ts # Markdown → HTML (308 lines)
1241
- │ ├── pagination.ts # Pagination utilities (67 lines, NEW)
1242
- │ ├── xml-builder.ts # XML/RSS builders (117 lines, NEW)
1243
- │ ├── markdown-utils.ts # Main export file (177 lines, was 576)
1244
- │ ├── css-processor.ts # PostCSS + Bun.hash()
1245
- │ ├── file-utils.ts # Bun native file ops
1246
- │ ├── date-utils.ts # Date/time utilities
1247
- │ ├── json-ld.ts # JSON-LD schema generation
1248
- │ ├── image-uploader.ts # Image upload logic
1249
- │ └── s3-uploader.ts # S3/R2 client
1250
- ├── test/ # Test suite (424 tests, mirrors src/)
1251
- │ ├── utils/
1252
- │ │ ├── markdown/ # Modular tests (NEW)
1253
- │ │ │ ├── constants.test.ts (25 tests)
1254
- │ │ │ ├── validators.test.ts (21 tests)
1255
- │ │ │ └── parser.test.ts (17 tests)
1256
- │ │ ├── pagination.test.ts (15 tests, NEW)
1257
- │ │ ├── xml-builder.test.ts (13 tests, NEW)
1258
- │ │ └── ...
1259
- │ ├── cli/commands/
1260
- │ ├── security/
1261
- │ └── ...
1262
- ├── templates/ # Example templates
1263
- ├── fixtures/ # Test fixtures
1264
- └── dist/ # Built output
1265
- ```
1266
-
1267
- ## Changelog
1268
-
1269
- ### v0.19.0 (Current)
1270
-
1271
- - **Template Fragments**: Built-in reusable Nunjucks macros available in all site templates
1272
- - `og-image.njk` — `og_image(post, site)` and `twitter_image(post, site)` for Open Graph / Twitter Card image tags
1273
- - `json-ld.njk` — `blog_posting_schema(post, site)` and `local_business_schema(post, site)` for Schema.org structured data
1274
- - `share-buttons.njk` — `share_buttons(post, site)` for X / Facebook / LinkedIn / Email share buttons (Tailwind)
1275
- - `pagination.njk` — `pagination_nav(pagination)` for previous/next navigation (Tailwind)
1276
- - Fragments are registered as a second Nunjucks search path; site templates take priority over built-in fragments
1277
- - **Improved init templates**: `bunki init` now generates templates with canonical URLs, Open Graph meta tags, Twitter Cards, RSS feed link, share buttons, and JSON-LD structured data out of the box
1278
- - **Template variables reference**: Added documentation for all variables available in each template type
1279
-
1280
- ### v0.18.1
1281
-
1282
- - **Page Generation Optimization**: Cache JSON-LD schemas and metadata during initialization
1283
- - Eliminates 910 redundant operations per build (455 posts × 2)
1284
- - `extractFirstImageUrl()` called once during initialization, cached in `post.image`
1285
- - Word count calculated once, cached in `post.wordCount`
1286
- - JSON-LD schemas pre-generated and cached in `post.jsonLd`
1287
- - Removed duplicate schema generation from page and feed generators
1288
- - **Deployment Optimization**: All deploy commands now use incremental builds by default
1289
- - `deploy:all` now builds incrementally for faster deployments
1290
- - Individual site deployments also use incremental builds
1291
- - Full builds available via `build:full` when needed
1292
-
1293
- ### v0.18.0
1294
-
1295
- - **Incremental Builds**: Smart caching for 3.2x faster development builds
1296
- - File change detection using content hashing and modification times
1297
- - Selective markdown parsing (only parse changed files)
1298
- - CSS caching (skip processing if unchanged)
1299
- - Cache format v2.0.0 stores full parsed post data
1300
- - Automatic config change detection triggers full rebuilds
1301
- - `.bunki-cache.json` stores file hashes, mtimes, and parsed posts
1302
- - **Performance Results** (455 posts):
1303
- - Full build: 3,128ms
1304
- - Incremental (no changes): 985ms (68% faster)
1305
- - Markdown parsing: 1,202ms → 55ms (22x faster)
1306
- - CSS processing: 1,024ms → 1ms (1024x faster)
1307
- - **CLI Enhancement**: New `--incremental` flag for `bunki generate`
1308
- - **Code Cleanup**: Removed unused imports, reverted template extraction
1309
-
1310
- ### v0.17.0
1311
-
1312
- - **Major Architecture Refactoring**: Modular design with single responsibility modules
1313
- - Split `site-generator.ts` from 957 to 282 lines (-70%)
1314
- - Split `markdown-utils.ts` from 576 to 177 lines (-69%)
1315
- - Created 3 new generator modules: feeds, pages, assets
1316
- - Created 3 new markdown modules: constants, validators, parser
1317
- - Created 2 new utility modules: pagination, xml-builder
1318
- - **Performance Improvements**:
1319
- - Parallel page generation with Promise.all() (40-60% faster builds)
1320
- - Batched post processing (10x faster for 100+ posts)
1321
- - Pre-compiled regex patterns (2-3x faster markdown parsing)
1322
- - O(1) Set-based validation (35x faster validation)
1323
- - Zero-copy file operations using Bun native APIs (50% faster, lower memory)
1324
- - Content-based CSS cache busting with Bun.hash()
1325
- - **DRY Improvements**:
1326
- - Extracted pagination utilities (saved 80+ lines)
1327
- - Created XML builder utilities (saved 150+ lines)
1328
- - Reusable page writing utilities (saved 40+ lines)
1329
- - **Enhanced Test Coverage**:
1330
- - Added 87 new tests (337 → 424 tests total)
1331
- - Modular test organization mirroring source structure
1332
- - 100% backward compatible with existing API
1333
- - **Code Reduction**: Eliminated 1,074 lines while adding 11 focused modules
1334
-
1335
- ### v0.16.0
1336
-
1337
- - **Relative Link Conversion**: Automatically convert relative markdown links to absolute URLs
1338
- - Supports `../2023/post.md` → `/2023/post/` conversion during build time
1339
- - Works with multiple parent directories (`../../`, `../../../`, etc.)
1340
- - Preserves link text and formatting
1341
- - Enables cleaner internal cross-references in markdown files
1342
- - **Comprehensive Testing**: Added 13 new tests for relative link conversion
1343
- - **Zero Configuration**: Works automatically without any setup required
143
+ GitHub Actions runs typechecking, formatting, coverage tests, a build, and a site-generation smoke test. Compatibility checks cover the minimum supported Bun version and the latest release.
1344
144
 
1345
- ### v0.15.0
145
+ The benchmark creates and removes a temporary site, runs each scenario three times, and reports median wall-clock times. It uses synthetic Markdown and minimal templates with CSS disabled, so it measures parsing, caching, and site generation rather than PostCSS or network uploads. The report includes the Bun version, platform,
146
+ HTML page count, and an SHA-256 fingerprint of final HTML paths and contents for
147
+ before/after output comparisons. Fingerprinting runs outside the timed builds.
148
+ Repeated tag names are normalized once per site model; the cache is discarded after
149
+ that model is created. In a local 1,000-post fixture with 21 distinct tags, this
150
+ reduced slug normalization calls from 2,000 to 21 and preserved all 1,404 HTML files.
151
+ This reduces repeated CPU work; whole-build timings also depend on parsing,
152
+ rendering, filesystem performance, and other workloads.
1346
153
 
1347
- - **Frontmatter Validation**: Automatic validation of business location data
1348
- - Enforces `business:` field (rejects deprecated `location:`)
1349
- - Enforces `lat:`/`lng:` coordinates (rejects deprecated `latitude:`/`longitude:`)
1350
- - Validates required fields (type, name, lat, lng)
1351
- - Clear error messages with suggestions for fixes
1352
- - New `bunki validate` command for standalone validation
1353
- - **Enhanced Testing**: 47 tests for markdown parsing and validation
1354
- - **Breaking Change**: Deprecated `location:`, `latitude:`, and `longitude:` fields now rejected
154
+ ## Cloudflare hosting
1355
155
 
1356
- ### v0.8.0
1357
-
1358
- - **JSON-LD Structured Data**: Automatic Schema.org markup generation for enhanced SEO
1359
- - BlogPosting schema for individual blog posts with author, keywords, images
1360
- - WebSite schema for homepage with search action
1361
- - Organization schema for publisher information
1362
- - BreadcrumbList schema for navigation hierarchy
1363
- - Automatic featured image extraction from post content
1364
- - **Comprehensive SEO**: Complete structured data support following Google best practices
1365
- - **Zero configuration**: JSON-LD automatically generated during site build
1366
- - **Well documented**: Extensive README section with examples and validation tools
1367
- - **Fully tested**: 60+ new tests covering all JSON-LD schema types
1368
-
1369
- ### v0.7.0
1370
-
1371
- - **Media uploads**: Added MP4 video support alongside image uploads
1372
- - **Incremental uploads**: Year-based filtering with `--min-year` option
1373
- - **Enhanced documentation**: Comprehensive video upload guide with examples
1374
- - **Test coverage**: Added 10+ tests for image/video uploader functionality
1375
- - **Fixed timestamps**: Stable dates in test fixtures to prevent flipping
1376
-
1377
- ### v0.6.1
1378
-
1379
- - Version bump and welcome date stabilization
1380
- - Test formatting improvements
1381
- - Code style consistency updates
1382
-
1383
- ### v0.5.3
1384
-
1385
- - Modularized CLI commands with dependency injection
1386
- - Enhanced test coverage (130+ tests, 539+ assertions)
1387
- - Fixed CLI entry point detection (Bun.main compatibility)
1388
- - Added comprehensive server tests using Bun.serve()
1389
- - Improved CSS processor with fallback support
1390
-
1391
- ### v0.3.0
1392
-
1393
- - PostCSS integration with CSS processing command
1394
- - Framework-agnostic CSS support (Tailwind, etc.)
1395
- - CSS watch mode for development
1396
- - Better error handling and recovery
1397
-
1398
- ## Contributing
1399
-
1400
- Contributions welcome! Areas for improvement:
1401
-
1402
- - Bug fixes and error handling
1403
- - Documentation and examples
1404
- - Test coverage expansion
1405
- - Performance optimizations
1406
- - New features and plugins
156
+ Build the site with Bun, then serve the generated files with Workers Static Assets.
157
+ See [Cloudflare Workers and Bun](docs/cloudflare.md) for runtime compatibility,
158
+ deployment configuration, routing, and caching guidance.
1407
159
 
1408
160
  ## License
1409
161
 
1410
- MIT © [KahWee Teng](https://github.com/kahwee)
1411
-
1412
- Built with [Bun](https://bun.sh)
162
+ [MIT](LICENSE) © [KahWee Teng](https://github.com/kahwee)
163
+
164
+ ## CI maintenance
165
+
166
+ [GitHub Actions maintenance](.github/ACTIONS.md) covers workflows, parallel checks, action versions, and weekly updates.
167
+
168
+ Bun 1.4.4 owns formatting (`bun run format`, `bun run format:check`) and type checking
169
+ (`bun run typecheck`). Biome has been removed. Bun formatting is not a replacement
170
+ for Biome’s semantic lint rules; tests, strict type checks, and package builds remain
171
+ the verification gates. TypeScript remains installed for declaration generation.
172
+
173
+ `bun run check:source` runs formatting, Bun type checking, and tests in parallel.
174
+ `bun run check` waits for those checks before building package artifacts.
175
+ `bun run build` checks types before building. CI calls `build:artifacts` only after
176
+ a successful Bun check; TypeScript emits declarations with `--noCheck` to avoid
177
+ checking the same source again. Do not use `build:artifacts` as a validation command.
178
+
179
+ On the session’s Linux executor, the median of three project-check runs was
180
+ 0.152s with Bun versus 0.700s with `bun x --no-install tsc --noEmit`.
181
+ These include command startup and describe this checkout, not a CI speed guarantee.
182
+ Declaration emission measured 0.640s with `--noCheck` versus
183
+ 0.728s with checking; generated `.d.ts` contents matched.
184
+
185
+ Plain CSS uses Bun’s native bundler with no PostCSS subprocess. Local `@import` rules
186
+ are bundled and referenced assets may be embedded as data URLs. CSS is transformed,
187
+ so output is not byte-for-byte identical to the input. Existing project PostCSS
188
+ configs or an explicit `postcssConfig` continue to select PostCSS automatically.
189
+ Set `css.processor` to `"bun"` or `"postcss"` to select an engine explicitly.
190
+
191
+ Bun does not compile Tailwind directives or run arbitrary PostCSS plugins. Those
192
+ projects still need PostCSS and their own plugins; see [CSS configuration](docs/reference.md#css-configuration).
193
+ The repository’s unused Tailwind and Autoprefixer development setup has been removed.
194
+ PostCSS and its CLI remain installed for existing plugin-based sites. The PostCSS
195
+ path needs `bun` on PATH, including when using a compiled executable. A failed
196
+ PostCSS plugin stops generation so its transformation is not silently omitted;
197
+ plain CSS retains the raw-CSS fallback. Both engines support content hashing.
198
+ Existing configuration files that cannot be loaded also stop generation.
199
+
200
+ A local Bun 1.4.4 benchmark using `templates/styles/main.css` measured median CSS-phase
201
+ times of 10.2 ms with native Bun and 204.6 ms with the Bun-hosted PostCSS CLI and no
202
+ plugins (one warm-up per engine, then five alternating samples). Native CSS output
203
+ is transformed; this is not an output-identity comparison or a full-site speedup.
204
+
205
+ See [build performance](docs/build-performance.md) for the Bun-hosted build,
206
+ measured artifact timings, output verification, and benchmark scope.
207
+
208
+ To experiment locally with the same canary for source, bundled and AOT builds, run
209
+ `python3 scripts/benchmark-aot.py /absolute/path/to/bun 5` on Linux after installing
210
+ dependencies. The benchmark uses temporary synthetic fixtures and checks output,
211
+ config, plugin and failure behavior. AOT is experimental; the benchmark enables
212
+ disk-code loading for user configs and plugins and does not change the stable runtime.