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