bunki 0.16.0 → 0.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -67,13 +67,31 @@ export default (): SiteConfig => ({
67
67
 
68
68
  ## Content & Frontmatter
69
69
 
70
- Create Markdown files in `content/YYYY/` (e.g., `content/2025/my-post.md`):
70
+ Create Markdown files in `content/YYYY/` using either pattern:
71
+
72
+ **Option 1: Single file** (traditional)
73
+ ```
74
+ content/2025/my-post.md
75
+ ```
76
+
77
+ **Option 2: Directory with README** (Obsidian-friendly)
78
+ ```
79
+ content/2025/my-post/README.md
80
+ content/2025/my-post/image.jpg
81
+ ```
82
+
83
+ Both patterns generate the same output: `dist/2025/my-post/index.html`
84
+
85
+ > [!WARNING]
86
+ > 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.
87
+
88
+ Example frontmatter:
71
89
 
72
90
  ```markdown
73
91
  ---
74
92
  title: "Post Title"
75
93
  date: 2025-01-15T09:00:00-07:00
76
- tags: [web, performance]
94
+ tags: [web-development, performance-optimization]
77
95
  excerpt: "Optional summary for listings"
78
96
  ---
79
97
 
@@ -89,13 +107,47 @@ Your content here with **markdown** support.
89
107
  </video>
90
108
  ```
91
109
 
110
+ ### Tag Format
111
+
112
+ > [!IMPORTANT]
113
+ > Tags must use hyphens instead of spaces: `web-development` NOT `"web development"`
114
+
115
+ Tags with spaces will fail validation. Use hyphenated slugs:
116
+ - ✅ `tags: [new-york-city, travel, family-friendly]`
117
+ - ❌ `tags: ["new york city", "travel", "family friendly"]`
118
+
92
119
  Optional: Define tag descriptions in `src/tags.toml`:
93
120
 
94
121
  ```toml
95
- performance = "Performance optimization and speed"
96
- web = "Web development and technology"
122
+ performance-optimization = "Performance optimization and speed"
123
+ web-development = "Web development and technology"
124
+ new-york-city = "New York City travel guides"
97
125
  ```
98
126
 
127
+ ### Internal Links (Relative Markdown Links)
128
+
129
+ Bunki automatically converts relative markdown links to absolute URLs during build time. This lets you write cross-references using familiar file paths:
130
+
131
+ **In your markdown:**
132
+ ```markdown
133
+ Check out [my earlier post](../2023/introduction.md) for context.
134
+
135
+ See also [related article](../../2020/old-post.md).
136
+ ```
137
+
138
+ **Generated HTML:**
139
+ ```html
140
+ <a href="/2023/introduction/">my earlier post</a>
141
+ <a href="/2020/old-post/">related article</a>
142
+ ```
143
+
144
+ This feature works with:
145
+ - `../YEAR/slug.md` - Single level up
146
+ - `../../YEAR/slug.md` - Multiple levels up
147
+ - Any number of `../` sequences
148
+
149
+ The links are automatically converted to absolute URLs (`/YEAR/slug/`) that match your site's URL structure.
150
+
99
151
  ### Business Location Data
100
152
 
101
153
  Add structured business/location data with automatic validation:
@@ -773,6 +825,7 @@ dist/
773
825
  ## Features
774
826
 
775
827
  - **Markdown Processing**: Frontmatter extraction, code highlighting, HTML sanitization
828
+ - **Relative Link Conversion**: Automatic conversion of relative markdown links (`../2023/post.md`) to absolute URLs (`/2023/post/`)
776
829
  - **Frontmatter Validation**: Automatic validation of business location data with clear error messages
777
830
  - **Security**: XSS protection, sanitized HTML, link hardening
778
831
  - **Performance**: Static files, optional gzip, optimized output
@@ -819,7 +872,17 @@ bunki/
819
872
 
820
873
  ## Changelog
821
874
 
822
- ### v0.15.0 (Current)
875
+ ### v0.16.0 (Current)
876
+
877
+ - **Relative Link Conversion**: Automatically convert relative markdown links to absolute URLs
878
+ - Supports `../2023/post.md` → `/2023/post/` conversion during build time
879
+ - Works with multiple parent directories (`../../`, `../../../`, etc.)
880
+ - Preserves link text and formatting
881
+ - Enables cleaner internal cross-references in markdown files
882
+ - **Comprehensive Testing**: Added 13 new tests for relative link conversion
883
+ - **Zero Configuration**: Works automatically without any setup required
884
+
885
+ ### v0.15.0
823
886
 
824
887
  - **Frontmatter Validation**: Automatic validation of business location data
825
888
  - Enforces `business:` field (rejects deprecated `location:`)
package/dist/cli.js CHANGED
@@ -28370,13 +28370,16 @@ function registerCssCommand(program2) {
28370
28370
  }
28371
28371
 
28372
28372
  // src/cli/commands/generate.ts
28373
- import path6 from "path";
28373
+ import path7 from "path";
28374
28374
 
28375
28375
  // src/site-generator.ts
28376
28376
  var import_nunjucks = __toESM(require_nunjucks(), 1);
28377
28377
  var import_slugify = __toESM(require_slugify(), 1);
28378
28378
  var {Glob: Glob2 } = globalThis.Bun;
28379
28379
  import fs2 from "fs";
28380
+ import path6 from "path";
28381
+
28382
+ // src/parser.ts
28380
28383
  import path5 from "path";
28381
28384
 
28382
28385
  // src/utils/file-utils.ts
@@ -28423,7 +28426,12 @@ async function readFileAsText(filePath) {
28423
28426
  }
28424
28427
  }
28425
28428
  function getBaseFilename(filePath, extension = ".md") {
28426
- return path4.basename(filePath, extension);
28429
+ const basename = path4.basename(filePath, extension);
28430
+ if (basename.toLowerCase() === "readme") {
28431
+ const dir = path4.dirname(filePath);
28432
+ return path4.basename(dir);
28433
+ }
28434
+ return basename;
28427
28435
  }
28428
28436
  async function createDir(dirPath) {
28429
28437
  try {
@@ -32951,6 +32959,11 @@ function createMarked(cdnConfig) {
32951
32959
  walkTokens(token) {
32952
32960
  if (token.type === "link") {
32953
32961
  token.href = token.href || "";
32962
+ let relativeMatch = token.href.match(/^(\.\.\/)+(\d{4})\/([a-zA-Z0-9_-]+?)(?:\.md)?(?:\/)?$/);
32963
+ if (relativeMatch) {
32964
+ const [, , year, slug] = relativeMatch;
32965
+ token.href = `/${year}/${slug}/`;
32966
+ }
32954
32967
  const isExternal = token.href && (token.href.startsWith("http://") || token.href.startsWith("https://") || token.href.startsWith("//"));
32955
32968
  if (isExternal) {
32956
32969
  token.isExternalLink = true;
@@ -33214,6 +33227,20 @@ async function parseMarkdownFile(filePath, cdnConfig) {
33214
33227
  };
33215
33228
  }
33216
33229
  }
33230
+ if (data.tags && Array.isArray(data.tags)) {
33231
+ const tagsWithSpaces = data.tags.filter((tag) => tag.includes(" "));
33232
+ if (tagsWithSpaces.length > 0) {
33233
+ return {
33234
+ post: null,
33235
+ error: {
33236
+ file: filePath,
33237
+ type: "validation",
33238
+ message: `Tags must not contain spaces. Found: ${tagsWithSpaces.map((t) => `"${t}"`).join(", ")}`,
33239
+ suggestion: `Use hyphens instead of spaces. Example: "new-york-city" instead of "new york city"`
33240
+ }
33241
+ };
33242
+ }
33243
+ }
33217
33244
  let slug = getBaseFilename(filePath);
33218
33245
  const sanitizedHtml = convertMarkdownToHtml(content, cdnConfig);
33219
33246
  const pacificDate = toPacificTime(data.date);
@@ -33277,14 +33304,55 @@ async function parseMarkdownFile(filePath, cdnConfig) {
33277
33304
  }
33278
33305
 
33279
33306
  // src/parser.ts
33307
+ function detectFileConflicts(files) {
33308
+ const errors = [];
33309
+ const slugMap = new Map;
33310
+ for (const filePath of files) {
33311
+ const slug = getBaseFilename(filePath);
33312
+ const dir = path5.dirname(filePath);
33313
+ const year = path5.basename(dir);
33314
+ const key = `${year}/${slug}`;
33315
+ if (!slugMap.has(key)) {
33316
+ slugMap.set(key, []);
33317
+ }
33318
+ slugMap.get(key).push(filePath);
33319
+ }
33320
+ for (const [key, paths] of slugMap.entries()) {
33321
+ if (paths.length > 1) {
33322
+ errors.push({
33323
+ file: paths[0],
33324
+ type: "validation",
33325
+ message: `Conflicting files for '${key}': ${paths.map((p) => path5.relative(process.cwd(), p)).join(" AND ")}`,
33326
+ suggestion: `Remove one of the files. Keep either the .md file OR the /README.md file, not both.`
33327
+ });
33328
+ }
33329
+ }
33330
+ return errors;
33331
+ }
33280
33332
  async function parseMarkdownDirectory(contentDir, strictMode = false, cdnConfig) {
33281
33333
  try {
33282
33334
  const markdownFiles = await findFilesByPattern("**/*.md", contentDir, true);
33283
33335
  console.log(`Found ${markdownFiles.length} markdown files`);
33336
+ const conflictErrors = detectFileConflicts(markdownFiles);
33337
+ if (conflictErrors.length > 0) {
33338
+ console.error(`
33339
+ \u26A0\uFE0F Found ${conflictErrors.length} file conflict(s):
33340
+ `);
33341
+ conflictErrors.forEach((e) => {
33342
+ console.error(` \u274C ${e.message}`);
33343
+ if (e.suggestion) {
33344
+ console.error(` \uD83D\uDCA1 ${e.suggestion}`);
33345
+ }
33346
+ });
33347
+ console.error("");
33348
+ if (strictMode) {
33349
+ throw new Error(`File conflicts detected. Fix conflicts before building.`);
33350
+ }
33351
+ }
33284
33352
  const resultsPromises = markdownFiles.map((filePath) => parseMarkdownFile(filePath, cdnConfig));
33285
33353
  const results = await Promise.all(resultsPromises);
33286
33354
  const posts = [];
33287
- const errors = [];
33355
+ const errors = [...conflictErrors];
33288
33356
  for (const result of results) {
33289
33357
  if (result.post) {
33290
33358
  posts.push(result.post);
@@ -33618,7 +33686,7 @@ class SiteGenerator {
33618
33686
  setNoFollowExceptions(this.options.config.noFollowExceptions);
33619
33687
  }
33620
33688
  let tagDescriptions = {};
33621
- const tagsTomlPath = path5.join(process.cwd(), "src", "tags.toml");
33689
+ const tagsTomlPath = path6.join(process.cwd(), "src", "tags.toml");
33622
33690
  const tagsTomlFile = Bun.file(tagsTomlPath);
33623
33691
  if (await tagsTomlFile.exists()) {
33624
33692
  try {
@@ -33686,7 +33754,7 @@ class SiteGenerator {
33686
33754
  const notFoundHtml = import_nunjucks.default.render("404.njk", {
33687
33755
  site: this.options.config
33688
33756
  });
33689
- await Bun.write(path5.join(this.options.outputDir, "404.html"), notFoundHtml);
33757
+ await Bun.write(path6.join(this.options.outputDir, "404.html"), notFoundHtml);
33690
33758
  console.log("Generated 404.html");
33691
33759
  } catch (error) {
33692
33760
  if (error instanceof Error && error.message.includes("404.njk")) {
@@ -33698,7 +33766,7 @@ class SiteGenerator {
33698
33766
  }
33699
33767
  async generateYearArchives() {
33700
33768
  for (const [year, yearPosts] of Object.entries(this.site.postsByYear)) {
33701
- const yearDir = path5.join(this.options.outputDir, year);
33769
+ const yearDir = path6.join(this.options.outputDir, year);
33702
33770
  await ensureDir(yearDir);
33703
33771
  const pageSize = 10;
33704
33772
  const totalPages = Math.ceil(yearPosts.length / pageSize);
@@ -33737,11 +33805,11 @@ class SiteGenerator {
33737
33805
  jsonLd
33738
33806
  });
33739
33807
  if (page === 1) {
33740
- await Bun.write(path5.join(yearDir, "index.html"), yearPageHtml);
33808
+ await Bun.write(path6.join(yearDir, "index.html"), yearPageHtml);
33741
33809
  } else {
33742
- const pageDir = path5.join(yearDir, "page", page.toString());
33810
+ const pageDir = path6.join(yearDir, "page", page.toString());
33743
33811
  await ensureDir(pageDir);
33744
- await Bun.write(path5.join(pageDir, "index.html"), yearPageHtml);
33812
+ await Bun.write(path6.join(pageDir, "index.html"), yearPageHtml);
33745
33813
  }
33746
33814
  }
33747
33815
  }
@@ -33771,18 +33839,18 @@ class SiteGenerator {
33771
33839
  noindex: page > 2
33772
33840
  });
33773
33841
  if (page === 1) {
33774
- await Bun.write(path5.join(this.options.outputDir, "index.html"), pageHtml);
33842
+ await Bun.write(path6.join(this.options.outputDir, "index.html"), pageHtml);
33775
33843
  } else {
33776
- const pageDir = path5.join(this.options.outputDir, "page", page.toString());
33844
+ const pageDir = path6.join(this.options.outputDir, "page", page.toString());
33777
33845
  await ensureDir(pageDir);
33778
- await Bun.write(path5.join(pageDir, "index.html"), pageHtml);
33846
+ await Bun.write(path6.join(pageDir, "index.html"), pageHtml);
33779
33847
  }
33780
33848
  }
33781
33849
  }
33782
33850
  async generatePostPages() {
33783
33851
  for (const post of this.site.posts) {
33784
33852
  const postPath = post.url.substring(1);
33785
- const postDir = path5.join(this.options.outputDir, postPath);
33853
+ const postDir = path6.join(this.options.outputDir, postPath);
33786
33854
  await ensureDir(postDir);
33787
33855
  const imageUrl = extractFirstImageUrl(post.html, this.options.config.baseUrl);
33788
33856
  const schemas = generatePostPageSchemas({
@@ -33797,19 +33865,19 @@ class SiteGenerator {
33797
33865
  post,
33798
33866
  jsonLd
33799
33867
  });
33800
- await Bun.write(path5.join(postDir, "index.html"), postHtml);
33868
+ await Bun.write(path6.join(postDir, "index.html"), postHtml);
33801
33869
  }
33802
33870
  }
33803
33871
  async generateTagPages() {
33804
- const tagsDir = path5.join(this.options.outputDir, "tags");
33872
+ const tagsDir = path6.join(this.options.outputDir, "tags");
33805
33873
  await ensureDir(tagsDir);
33806
33874
  const tagIndexHtml = import_nunjucks.default.render("tags.njk", {
33807
33875
  site: this.options.config,
33808
33876
  tags: this.getSortedTags()
33809
33877
  });
33810
- await Bun.write(path5.join(tagsDir, "index.html"), tagIndexHtml);
33878
+ await Bun.write(path6.join(tagsDir, "index.html"), tagIndexHtml);
33811
33879
  for (const [tagName, tagData] of Object.entries(this.site.tags)) {
33812
- const tagDir = path5.join(tagsDir, tagData.slug);
33880
+ const tagDir = path6.join(tagsDir, tagData.slug);
33813
33881
  await ensureDir(tagDir);
33814
33882
  const pageSize = 10;
33815
33883
  const totalPages = Math.ceil(tagData.posts.length / pageSize);
@@ -33852,24 +33920,24 @@ class SiteGenerator {
33852
33920
  jsonLd
33853
33921
  });
33854
33922
  if (page === 1) {
33855
- await Bun.write(path5.join(tagDir, "index.html"), tagPageHtml);
33923
+ await Bun.write(path6.join(tagDir, "index.html"), tagPageHtml);
33856
33924
  } else {
33857
- const pageDir = path5.join(tagDir, "page", page.toString());
33925
+ const pageDir = path6.join(tagDir, "page", page.toString());
33858
33926
  await ensureDir(pageDir);
33859
- await Bun.write(path5.join(pageDir, "index.html"), tagPageHtml);
33927
+ await Bun.write(path6.join(pageDir, "index.html"), tagPageHtml);
33860
33928
  }
33861
33929
  }
33862
33930
  }
33863
33931
  }
33864
33932
  async generateMapPage() {
33865
33933
  try {
33866
- const mapDir = path5.join(this.options.outputDir, "map");
33934
+ const mapDir = path6.join(this.options.outputDir, "map");
33867
33935
  await ensureDir(mapDir);
33868
33936
  const mapHtml = import_nunjucks.default.render("map.njk", {
33869
33937
  site: this.options.config,
33870
33938
  posts: this.site.posts
33871
33939
  });
33872
- await Bun.write(path5.join(mapDir, "index.html"), mapHtml);
33940
+ await Bun.write(path6.join(mapDir, "index.html"), mapHtml);
33873
33941
  console.log("Generated map page");
33874
33942
  } catch (error) {
33875
33943
  if (error instanceof Error && error.message.includes("map.njk")) {
@@ -33899,7 +33967,7 @@ class SiteGenerator {
33899
33967
  }
33900
33968
  }
33901
33969
  async fallbackCSSGeneration(cssConfig) {
33902
- const cssFilePath = path5.resolve(process.cwd(), cssConfig.input);
33970
+ const cssFilePath = path6.resolve(process.cwd(), cssConfig.input);
33903
33971
  const cssFile = Bun.file(cssFilePath);
33904
33972
  if (!await cssFile.exists()) {
33905
33973
  console.warn(`CSS input file not found: ${cssFilePath}`);
@@ -33907,8 +33975,8 @@ class SiteGenerator {
33907
33975
  }
33908
33976
  try {
33909
33977
  const cssContent = await cssFile.text();
33910
- const outputPath = path5.resolve(this.options.outputDir, cssConfig.output);
33911
- const outputDir = path5.dirname(outputPath);
33978
+ const outputPath = path6.resolve(this.options.outputDir, cssConfig.output);
33979
+ const outputDir = path6.dirname(outputPath);
33912
33980
  await ensureDir(outputDir);
33913
33981
  await Bun.write(outputPath, cssContent);
33914
33982
  console.log("\u2705 CSS file copied successfully (fallback mode)");
@@ -33917,8 +33985,8 @@ class SiteGenerator {
33917
33985
  }
33918
33986
  }
33919
33987
  async copyStaticAssets() {
33920
- const assetsDir = path5.join(this.options.templatesDir, "assets");
33921
- const publicDir = path5.join(process.cwd(), "public");
33988
+ const assetsDir = path6.join(this.options.templatesDir, "assets");
33989
+ const publicDir = path6.join(process.cwd(), "public");
33922
33990
  async function dirExists(p) {
33923
33991
  try {
33924
33992
  const stat = await fs2.promises.stat(p);
@@ -33930,15 +33998,15 @@ class SiteGenerator {
33930
33998
  const assetsDirFile = Bun.file(assetsDir);
33931
33999
  if (await assetsDirFile.exists() && await dirExists(assetsDir)) {
33932
34000
  const assetGlob = new Glob2("**/*.*");
33933
- const assetsOutputDir = path5.join(this.options.outputDir, "assets");
34001
+ const assetsOutputDir = path6.join(this.options.outputDir, "assets");
33934
34002
  await ensureDir(assetsOutputDir);
33935
34003
  for await (const file of assetGlob.scan({
33936
34004
  cwd: assetsDir,
33937
34005
  absolute: true
33938
34006
  })) {
33939
- const relativePath = path5.relative(assetsDir, file);
33940
- const targetPath = path5.join(assetsOutputDir, relativePath);
33941
- const targetDir = path5.dirname(targetPath);
34007
+ const relativePath = path6.relative(assetsDir, file);
34008
+ const targetPath = path6.join(assetsOutputDir, relativePath);
34009
+ const targetDir = path6.dirname(targetPath);
33942
34010
  await ensureDir(targetDir);
33943
34011
  await copyFile(file, targetPath);
33944
34012
  }
@@ -33949,9 +34017,9 @@ class SiteGenerator {
33949
34017
  withFileTypes: true
33950
34018
  });
33951
34019
  for (const entry of entries) {
33952
- const srcPath = path5.join(srcDir, entry.name);
33953
- const relativePath = path5.relative(publicDir, srcPath);
33954
- const destPath = path5.join(this.options.outputDir, relativePath);
34020
+ const srcPath = path6.join(srcDir, entry.name);
34021
+ const relativePath = path6.relative(publicDir, srcPath);
34022
+ const destPath = path6.join(this.options.outputDir, relativePath);
33955
34023
  if (!relativePath)
33956
34024
  continue;
33957
34025
  if (entry.isDirectory()) {
@@ -33960,7 +34028,7 @@ class SiteGenerator {
33960
34028
  } else if (entry.isFile()) {
33961
34029
  const targetFile = Bun.file(destPath);
33962
34030
  if (!await targetFile.exists()) {
33963
- const targetDir = path5.dirname(destPath);
34031
+ const targetDir = path6.dirname(destPath);
33964
34032
  await ensureDir(targetDir);
33965
34033
  await copyFile(srcPath, destPath);
33966
34034
  }
@@ -34060,7 +34128,7 @@ ${channelXml}
34060
34128
  ${rssItems}
34061
34129
  </channel>
34062
34130
  </rss>`;
34063
- await Bun.write(path5.join(this.options.outputDir, "feed.xml"), rssContent);
34131
+ await Bun.write(path6.join(this.options.outputDir, "feed.xml"), rssContent);
34064
34132
  }
34065
34133
  async generateSitemap() {
34066
34134
  const currentDate = toPacificTime(new Date).toISOString();
@@ -34179,7 +34247,7 @@ ${rssItems}
34179
34247
  }
34180
34248
  }
34181
34249
  sitemapContent += `</urlset>`;
34182
- await Bun.write(path5.join(this.options.outputDir, "sitemap.xml"), sitemapContent);
34250
+ await Bun.write(path6.join(this.options.outputDir, "sitemap.xml"), sitemapContent);
34183
34251
  console.log("Generated sitemap.xml");
34184
34252
  const urlCount = this.site.posts.length + Object.keys(this.site.tags).length + 10;
34185
34253
  const sitemapSize = sitemapContent.length;
@@ -34199,7 +34267,7 @@ ${rssItems}
34199
34267
  </sitemap>
34200
34268
  `;
34201
34269
  sitemapIndexContent += `</sitemapindex>`;
34202
- await Bun.write(path5.join(this.options.outputDir, "sitemap_index.xml"), sitemapIndexContent);
34270
+ await Bun.write(path6.join(this.options.outputDir, "sitemap_index.xml"), sitemapIndexContent);
34203
34271
  console.log("Generated sitemap_index.xml");
34204
34272
  }
34205
34273
  async generateRobotsTxt() {
@@ -34221,7 +34289,7 @@ Sitemap: ${config.baseUrl}/sitemap.xml
34221
34289
  # Disallow: /admin/
34222
34290
  # Disallow: /api/
34223
34291
  `;
34224
- await Bun.write(path5.join(this.options.outputDir, "robots.txt"), robotsTxtContent);
34292
+ await Bun.write(path6.join(this.options.outputDir, "robots.txt"), robotsTxtContent);
34225
34293
  console.log("Generated robots.txt");
34226
34294
  }
34227
34295
  }
@@ -34235,10 +34303,10 @@ var defaultDeps2 = {
34235
34303
  };
34236
34304
  async function handleGenerateCommand(options2, deps = defaultDeps2) {
34237
34305
  try {
34238
- const configPath = path6.resolve(options2.config);
34239
- const contentDir = path6.resolve(options2.content);
34240
- const outputDir = path6.resolve(options2.output);
34241
- const templatesDir = path6.resolve(options2.templates);
34306
+ const configPath = path7.resolve(options2.config);
34307
+ const contentDir = path7.resolve(options2.content);
34308
+ const outputDir = path7.resolve(options2.output);
34309
+ const templatesDir = path7.resolve(options2.templates);
34242
34310
  deps.logger.log("Generating site with:");
34243
34311
  deps.logger.log(`- Config file: ${configPath}`);
34244
34312
  deps.logger.log(`- Content directory: ${contentDir}`);
@@ -34266,11 +34334,11 @@ function registerGenerateCommand(program2) {
34266
34334
  }
34267
34335
 
34268
34336
  // src/utils/image-uploader.ts
34269
- import path8 from "path";
34337
+ import path9 from "path";
34270
34338
 
34271
34339
  // src/utils/s3-uploader.ts
34272
34340
  var {S3Client } = globalThis.Bun;
34273
- import path7 from "path";
34341
+ import path8 from "path";
34274
34342
 
34275
34343
  class S3Uploader {
34276
34344
  s3Config;
@@ -34393,8 +34461,8 @@ class S3Uploader {
34393
34461
  let failedCount = 0;
34394
34462
  const uploadTasks = imageFiles.map((imageFile) => async () => {
34395
34463
  try {
34396
- const imagePath = path7.join(imagesDir, imageFile);
34397
- const filename = path7.basename(imagePath);
34464
+ const imagePath = path8.join(imagesDir, imageFile);
34465
+ const filename = path8.basename(imagePath);
34398
34466
  const file = Bun.file(imagePath);
34399
34467
  const contentType = file.type;
34400
34468
  if (process.env.BUNKI_DRY_RUN === "true") {} else {
@@ -34428,10 +34496,10 @@ function createUploader(config) {
34428
34496
  }
34429
34497
 
34430
34498
  // src/utils/image-uploader.ts
34431
- var DEFAULT_IMAGES_DIR = path8.join(process.cwd(), "assets");
34499
+ var DEFAULT_IMAGES_DIR = path9.join(process.cwd(), "assets");
34432
34500
  async function uploadImages(options2 = {}) {
34433
34501
  try {
34434
- const imagesDir = path8.resolve(options2.images || DEFAULT_IMAGES_DIR);
34502
+ const imagesDir = path9.resolve(options2.images || DEFAULT_IMAGES_DIR);
34435
34503
  if (!await fileExists(imagesDir)) {
34436
34504
  console.log(`Creating images directory at ${imagesDir}...`);
34437
34505
  await ensureDir(imagesDir);
@@ -34468,7 +34536,7 @@ async function uploadImages(options2 = {}) {
34468
34536
  const uploader = createUploader(s3Config);
34469
34537
  const imageUrlMap = await uploader.uploadImages(imagesDir, options2.minYear);
34470
34538
  if (options2.outputJson) {
34471
- const outputFile = path8.resolve(options2.outputJson);
34539
+ const outputFile = path9.resolve(options2.outputJson);
34472
34540
  await Bun.write(outputFile, JSON.stringify(imageUrlMap, null, 2));
34473
34541
  console.log(`Image URL mapping saved to ${outputFile}`);
34474
34542
  }
@@ -34514,7 +34582,7 @@ function registerImagesPushCommand(program2) {
34514
34582
  }
34515
34583
 
34516
34584
  // src/cli/commands/init.ts
34517
- import path9 from "path";
34585
+ import path10 from "path";
34518
34586
  var defaultDependencies = {
34519
34587
  createDefaultConfig,
34520
34588
  ensureDir,
@@ -34524,7 +34592,7 @@ var defaultDependencies = {
34524
34592
  };
34525
34593
  async function handleInitCommand(options2, deps = defaultDependencies) {
34526
34594
  try {
34527
- const configPath = path9.resolve(options2.config);
34595
+ const configPath = path10.resolve(options2.config);
34528
34596
  const configCreated = await deps.createDefaultConfig(configPath);
34529
34597
  if (!configCreated) {
34530
34598
  deps.logger.log(`
@@ -34533,19 +34601,19 @@ Skipped initialization because the config file already exists`);
34533
34601
  }
34534
34602
  deps.logger.log("Creating directory structure...");
34535
34603
  const baseDir = process.cwd();
34536
- const contentDir = path9.join(baseDir, "content");
34537
- const templatesDir = path9.join(baseDir, "templates");
34538
- const stylesDir = path9.join(templatesDir, "styles");
34539
- const publicDir = path9.join(baseDir, "public");
34604
+ const contentDir = path10.join(baseDir, "content");
34605
+ const templatesDir = path10.join(baseDir, "templates");
34606
+ const stylesDir = path10.join(templatesDir, "styles");
34607
+ const publicDir = path10.join(baseDir, "public");
34540
34608
  await deps.ensureDir(contentDir);
34541
34609
  await deps.ensureDir(templatesDir);
34542
34610
  await deps.ensureDir(stylesDir);
34543
34611
  await deps.ensureDir(publicDir);
34544
34612
  for (const [filename, content] of Object.entries(getDefaultTemplates())) {
34545
- await deps.writeFile(path9.join(templatesDir, filename), content);
34613
+ await deps.writeFile(path10.join(templatesDir, filename), content);
34546
34614
  }
34547
- await deps.writeFile(path9.join(stylesDir, "main.css"), getDefaultCss());
34548
- await deps.writeFile(path9.join(contentDir, "welcome.md"), getSamplePost());
34615
+ await deps.writeFile(path10.join(stylesDir, "main.css"), getDefaultCss());
34616
+ await deps.writeFile(path10.join(contentDir, "welcome.md"), getSamplePost());
34549
34617
  deps.logger.log(`
34550
34618
  Initialization complete! Here are the next steps:`);
34551
34619
  deps.logger.log("1. Edit bunki.config.ts to configure your site");
@@ -35037,7 +35105,7 @@ function hello() {
35037
35105
  }
35038
35106
 
35039
35107
  // src/cli/commands/new-post.ts
35040
- import path10 from "path";
35108
+ import path11 from "path";
35041
35109
  var defaultDeps4 = {
35042
35110
  writeFile: (filePath, data) => Bun.write(filePath, data),
35043
35111
  now: () => new Date,
@@ -35061,7 +35129,7 @@ async function handleNewCommand(title, options2, deps = defaultDeps4) {
35061
35129
  ` + `# ${title}
35062
35130
 
35063
35131
  `;
35064
- const filePath = path10.join(DEFAULT_CONTENT_DIR, `${slug}.md`);
35132
+ const filePath = path11.join(DEFAULT_CONTENT_DIR, `${slug}.md`);
35065
35133
  await deps.writeFile(filePath, frontmatter);
35066
35134
  deps.logger.log(`Created new post: ${filePath}`);
35067
35135
  return filePath;
@@ -35078,11 +35146,11 @@ function registerNewCommand(program2) {
35078
35146
  }
35079
35147
 
35080
35148
  // src/cli/commands/serve.ts
35081
- import path12 from "path";
35149
+ import path13 from "path";
35082
35150
 
35083
35151
  // src/server.ts
35084
35152
  import fs3 from "fs";
35085
- import path11 from "path";
35153
+ import path12 from "path";
35086
35154
  async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
35087
35155
  try {
35088
35156
  const stats = await fs3.promises.stat(outputDir);
@@ -35117,18 +35185,18 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
35117
35185
  let filePath = "";
35118
35186
  if (homePaginationMatch) {
35119
35187
  const pageNumber = homePaginationMatch[1];
35120
- filePath = path11.join(outputDir, "page", pageNumber, "index.html");
35188
+ filePath = path12.join(outputDir, "page", pageNumber, "index.html");
35121
35189
  } else if (tagPaginationMatch) {
35122
35190
  const tagSlug = tagPaginationMatch[1];
35123
35191
  const pageNumber = tagPaginationMatch[2];
35124
- filePath = path11.join(outputDir, "tags", tagSlug, "page", pageNumber, "index.html");
35192
+ filePath = path12.join(outputDir, "tags", tagSlug, "page", pageNumber, "index.html");
35125
35193
  } else if (yearPaginationMatch) {
35126
35194
  const year = yearPaginationMatch[1];
35127
35195
  const pageNumber = yearPaginationMatch[2];
35128
- filePath = path11.join(outputDir, year, "page", pageNumber, "index.html");
35196
+ filePath = path12.join(outputDir, year, "page", pageNumber, "index.html");
35129
35197
  } else {
35130
- const directPath = path11.join(outputDir, pathname);
35131
- const withoutSlash = path11.join(outputDir, pathname + ".html");
35198
+ const directPath = path12.join(outputDir, pathname);
35199
+ const withoutSlash = path12.join(outputDir, pathname + ".html");
35132
35200
  const withHtml = pathname.endsWith(".html") ? directPath : withoutSlash;
35133
35201
  const bunFileDirect = Bun.file(directPath);
35134
35202
  const bunFileHtml = Bun.file(withHtml);
@@ -35137,7 +35205,7 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
35137
35205
  } else if (await bunFileHtml.exists()) {
35138
35206
  filePath = withHtml;
35139
35207
  } else {
35140
- const indexPath = path11.join(outputDir, pathname, "index.html");
35208
+ const indexPath = path12.join(outputDir, pathname, "index.html");
35141
35209
  const bunFileIndex = Bun.file(indexPath);
35142
35210
  if (await bunFileIndex.exists()) {
35143
35211
  filePath = indexPath;
@@ -35151,7 +35219,7 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
35151
35219
  }
35152
35220
  }
35153
35221
  console.log(`Serving file: ${filePath}`);
35154
- const extname = path11.extname(filePath);
35222
+ const extname = path12.extname(filePath);
35155
35223
  let contentType = "text/html";
35156
35224
  switch (extname) {
35157
35225
  case ".js":
@@ -35210,7 +35278,7 @@ var defaultDeps5 = {
35210
35278
  };
35211
35279
  async function handleServeCommand(options2, deps = defaultDeps5) {
35212
35280
  try {
35213
- const outputDir = path12.resolve(options2.output);
35281
+ const outputDir = path13.resolve(options2.output);
35214
35282
  const port = parseInt(options2.port, 10);
35215
35283
  await deps.startServer(outputDir, port);
35216
35284
  } catch (error) {
@@ -35251,6 +35319,12 @@ function registerValidateCommand(program2) {
35251
35319
  }
35252
35320
 
35253
35321
  // src/cli.ts
35322
+ import { readFileSync } from "fs";
35323
+ import { join, dirname } from "path";
35324
+ import { fileURLToPath } from "url";
35325
+ var __dirname2 = dirname(fileURLToPath(import.meta.url));
35326
+ var packageJson = JSON.parse(readFileSync(join(__dirname2, "../package.json"), "utf-8"));
35327
+ var VERSION = packageJson.version;
35254
35328
  var program2 = new Command;
35255
35329
  registerInitCommand(program2);
35256
35330
  registerNewCommand(program2);
@@ -35259,7 +35333,7 @@ registerServeCommand(program2);
35259
35333
  registerCssCommand(program2);
35260
35334
  registerImagesPushCommand(program2);
35261
35335
  registerValidateCommand(program2);
35262
- program2.name("bunki").description("An opinionated static site generator built with Bun").version("0.10.0");
35336
+ program2.name("bunki").description("An opinionated static site generator built with Bun").version(VERSION);
35263
35337
  var currentFile = import.meta.url.replace("file://", "");
35264
35338
  var mainFile = Bun.main;
35265
35339
  if (currentFile === mainFile || currentFile.endsWith(mainFile)) {