bunki 0.17.0 → 0.18.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/dist/cli.js CHANGED
@@ -28478,12 +28478,12 @@ function registerCssCommand(program2) {
28478
28478
  }
28479
28479
 
28480
28480
  // src/cli/commands/generate.ts
28481
- import path9 from "path";
28481
+ import path10 from "path";
28482
28482
 
28483
28483
  // src/site-generator.ts
28484
28484
  var import_nunjucks2 = __toESM(require_nunjucks(), 1);
28485
28485
  var import_slugify = __toESM(require_slugify(), 1);
28486
- import path8 from "path";
28486
+ import path9 from "path";
28487
28487
 
28488
28488
  // src/parser.ts
28489
28489
  import path5 from "path";
@@ -33368,6 +33368,20 @@ function detectFileConflicts(files) {
33368
33368
  }
33369
33369
  return errors;
33370
33370
  }
33371
+ async function parseMarkdownFiles(filePaths, cdnConfig) {
33372
+ const resultsPromises = filePaths.map((filePath) => parseMarkdownFile(filePath, cdnConfig).then((result) => ({
33373
+ result,
33374
+ filePath
33375
+ })));
33376
+ const results = await Promise.all(resultsPromises);
33377
+ const postsWithPaths = [];
33378
+ for (const { result, filePath } of results) {
33379
+ if (result.post) {
33380
+ postsWithPaths.push({ post: result.post, filePath });
33381
+ }
33382
+ }
33383
+ return postsWithPaths;
33384
+ }
33371
33385
  async function parseMarkdownDirectory(contentDir, strictMode = false, cdnConfig) {
33372
33386
  try {
33373
33387
  const markdownFiles = await findFilesByPattern("**/*.md", contentDir, true);
@@ -33638,6 +33652,165 @@ function generateHomePageSchemas(options2) {
33638
33652
  return schemas;
33639
33653
  }
33640
33654
 
33655
+ // src/utils/build-cache.ts
33656
+ var {hash: hash2 } = globalThis.Bun;
33657
+ import path6 from "path";
33658
+ var CACHE_VERSION = "2.0.0";
33659
+ var CACHE_FILENAME = ".bunki-cache.json";
33660
+ async function hashFile(filePath) {
33661
+ try {
33662
+ const file = Bun.file(filePath);
33663
+ const content = await file.arrayBuffer();
33664
+ return hash2(content).toString(36);
33665
+ } catch (error) {
33666
+ return "";
33667
+ }
33668
+ }
33669
+ async function getFileMtime(filePath) {
33670
+ try {
33671
+ const stat = await Bun.file(filePath).stat();
33672
+ return stat?.mtime?.getTime() || 0;
33673
+ } catch (error) {
33674
+ return 0;
33675
+ }
33676
+ }
33677
+ async function loadCache(cwd) {
33678
+ const cachePath = path6.join(cwd, CACHE_FILENAME);
33679
+ const cacheFile = Bun.file(cachePath);
33680
+ try {
33681
+ if (await cacheFile.exists()) {
33682
+ const content = await cacheFile.text();
33683
+ const cache = JSON.parse(content);
33684
+ if (cache.version !== CACHE_VERSION) {
33685
+ console.log(`Cache version mismatch (${cache.version} vs ${CACHE_VERSION}), rebuilding...`);
33686
+ return createEmptyCache();
33687
+ }
33688
+ return cache;
33689
+ }
33690
+ } catch (error) {
33691
+ console.warn("Error loading cache, rebuilding:", error);
33692
+ }
33693
+ return createEmptyCache();
33694
+ }
33695
+ async function saveCache(cwd, cache) {
33696
+ const cachePath = path6.join(cwd, CACHE_FILENAME);
33697
+ try {
33698
+ await Bun.write(cachePath, JSON.stringify(cache, null, 2));
33699
+ } catch (error) {
33700
+ console.warn("Error saving cache:", error);
33701
+ }
33702
+ }
33703
+ function createEmptyCache() {
33704
+ return {
33705
+ version: CACHE_VERSION,
33706
+ files: {}
33707
+ };
33708
+ }
33709
+ async function hasFileChanged(filePath, cache) {
33710
+ const cached = cache.files[filePath];
33711
+ if (!cached) {
33712
+ return true;
33713
+ }
33714
+ const currentMtime = await getFileMtime(filePath);
33715
+ if (currentMtime !== cached.mtime) {
33716
+ const currentHash = await hashFile(filePath);
33717
+ return currentHash !== cached.hash;
33718
+ }
33719
+ return false;
33720
+ }
33721
+ async function updateCacheEntry(filePath, cache, options2) {
33722
+ const currentHash = await hashFile(filePath);
33723
+ const currentMtime = await getFileMtime(filePath);
33724
+ cache.files[filePath] = {
33725
+ hash: currentHash,
33726
+ mtime: currentMtime,
33727
+ post: options2?.post,
33728
+ outputs: options2?.outputs
33729
+ };
33730
+ }
33731
+ async function hasConfigChanged(configPath, cache) {
33732
+ const currentHash = await hashFile(configPath);
33733
+ if (!cache.configHash) {
33734
+ cache.configHash = currentHash;
33735
+ return true;
33736
+ }
33737
+ if (currentHash !== cache.configHash) {
33738
+ cache.configHash = currentHash;
33739
+ return true;
33740
+ }
33741
+ return false;
33742
+ }
33743
+ function loadCachedPosts(cache, filePaths) {
33744
+ const posts = [];
33745
+ for (const filePath of filePaths) {
33746
+ const entry = cache.files[filePath];
33747
+ if (entry?.post) {
33748
+ posts.push(entry.post);
33749
+ }
33750
+ }
33751
+ return posts;
33752
+ }
33753
+
33754
+ // src/utils/change-detector.ts
33755
+ async function detectChanges(currentFiles, cache, options2 = {}) {
33756
+ const changes = {
33757
+ changedPosts: [],
33758
+ deletedPosts: [],
33759
+ stylesChanged: false,
33760
+ configChanged: false,
33761
+ templatesChanged: false,
33762
+ fullRebuild: false
33763
+ };
33764
+ if (options2.configPath) {
33765
+ const configChanged = await hasFileChanged(options2.configPath, cache);
33766
+ if (configChanged) {
33767
+ changes.configChanged = true;
33768
+ changes.fullRebuild = true;
33769
+ return changes;
33770
+ }
33771
+ }
33772
+ if (options2.templatePaths && options2.templatePaths.length > 0) {
33773
+ for (const templatePath of options2.templatePaths) {
33774
+ const changed = await hasFileChanged(templatePath, cache);
33775
+ if (changed) {
33776
+ changes.templatesChanged = true;
33777
+ changes.fullRebuild = true;
33778
+ return changes;
33779
+ }
33780
+ }
33781
+ }
33782
+ if (options2.stylesPaths && options2.stylesPaths.length > 0) {
33783
+ for (const stylePath of options2.stylesPaths) {
33784
+ const changed = await hasFileChanged(stylePath, cache);
33785
+ if (changed) {
33786
+ changes.stylesChanged = true;
33787
+ break;
33788
+ }
33789
+ }
33790
+ }
33791
+ for (const filePath of currentFiles) {
33792
+ const changed = await hasFileChanged(filePath, cache);
33793
+ if (changed) {
33794
+ changes.changedPosts.push(filePath);
33795
+ }
33796
+ }
33797
+ const cachedFiles = Object.keys(cache.files).filter((f) => f.endsWith(".md"));
33798
+ for (const cachedFile of cachedFiles) {
33799
+ if (!currentFiles.includes(cachedFile)) {
33800
+ changes.deletedPosts.push(cachedFile);
33801
+ }
33802
+ }
33803
+ if (changes.deletedPosts.length > 0) {
33804
+ changes.fullRebuild = true;
33805
+ }
33806
+ return changes;
33807
+ }
33808
+ function estimateTimeSaved(totalPosts, changedPosts) {
33809
+ const avgTimePerPost = 6;
33810
+ const skippedPosts = totalPosts - changedPosts;
33811
+ return skippedPosts * avgTimePerPost;
33812
+ }
33813
+
33641
33814
  // src/utils/xml-builder.ts
33642
33815
  function escapeXml(text) {
33643
33816
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
@@ -33870,14 +34043,14 @@ Sitemap: ${config.baseUrl}/sitemap.xml
33870
34043
 
33871
34044
  // src/generators/pages.ts
33872
34045
  var import_nunjucks = __toESM(require_nunjucks(), 1);
33873
- import path6 from "path";
34046
+ import path7 from "path";
33874
34047
  function getSortedTags(tags, limit) {
33875
34048
  const sorted = Object.values(tags).sort((a, b2) => b2.count - a.count);
33876
34049
  return limit ? sorted.slice(0, limit) : sorted;
33877
34050
  }
33878
34051
  async function writeHtmlFile(outputDir, relativePath, content) {
33879
- const fullPath = path6.join(outputDir, relativePath);
33880
- const dir = path6.dirname(fullPath);
34052
+ const fullPath = path7.join(outputDir, relativePath);
34053
+ const dir = path7.dirname(fullPath);
33881
34054
  await ensureDir(dir);
33882
34055
  await Bun.write(fullPath, content);
33883
34056
  }
@@ -34053,7 +34226,7 @@ async function generateMapPage(site, config, outputDir) {
34053
34226
 
34054
34227
  // src/generators/assets.ts
34055
34228
  var {Glob: Glob2 } = globalThis.Bun;
34056
- import path7 from "path";
34229
+ import path8 from "path";
34057
34230
  async function generateStylesheet(config, outputDir) {
34058
34231
  const cssConfig = config.css || getDefaultCSSConfig();
34059
34232
  if (!cssConfig.enabled) {
@@ -34074,15 +34247,15 @@ async function generateStylesheet(config, outputDir) {
34074
34247
  }
34075
34248
  }
34076
34249
  async function fallbackCSSGeneration(cssConfig, outputDir) {
34077
- const cssFilePath = path7.resolve(process.cwd(), cssConfig.input);
34250
+ const cssFilePath = path8.resolve(process.cwd(), cssConfig.input);
34078
34251
  const cssFile = Bun.file(cssFilePath);
34079
34252
  if (!await cssFile.exists()) {
34080
34253
  console.warn(`CSS input file not found: ${cssFilePath}`);
34081
34254
  return;
34082
34255
  }
34083
34256
  try {
34084
- const outputPath = path7.resolve(outputDir, cssConfig.output);
34085
- const outputDirPath = path7.dirname(outputPath);
34257
+ const outputPath = path8.resolve(outputDir, cssConfig.output);
34258
+ const outputDirPath = path8.dirname(outputPath);
34086
34259
  await ensureDir(outputDirPath);
34087
34260
  await Bun.write(outputPath, cssFile);
34088
34261
  console.log("\u2705 CSS file copied successfully (fallback mode)");
@@ -34091,19 +34264,19 @@ async function fallbackCSSGeneration(cssConfig, outputDir) {
34091
34264
  }
34092
34265
  }
34093
34266
  async function copyStaticAssets(templatesDir, outputDir) {
34094
- const assetsDir = path7.join(templatesDir, "assets");
34095
- const publicDir = path7.join(process.cwd(), "public");
34267
+ const assetsDir = path8.join(templatesDir, "assets");
34268
+ const publicDir = path8.join(process.cwd(), "public");
34096
34269
  if (await isDirectory(assetsDir)) {
34097
34270
  const assetGlob = new Glob2("**/*.*");
34098
- const assetsOutputDir = path7.join(outputDir, "assets");
34271
+ const assetsOutputDir = path8.join(outputDir, "assets");
34099
34272
  await ensureDir(assetsOutputDir);
34100
34273
  for await (const file of assetGlob.scan({
34101
34274
  cwd: assetsDir,
34102
34275
  absolute: true
34103
34276
  })) {
34104
- const relativePath = path7.relative(assetsDir, file);
34105
- const targetPath = path7.join(assetsOutputDir, relativePath);
34106
- const targetDir = path7.dirname(targetPath);
34277
+ const relativePath = path8.relative(assetsDir, file);
34278
+ const targetPath = path8.join(assetsOutputDir, relativePath);
34279
+ const targetDir = path8.dirname(targetPath);
34107
34280
  await ensureDir(targetDir);
34108
34281
  await copyFile(file, targetPath);
34109
34282
  }
@@ -34117,9 +34290,9 @@ async function copyStaticAssets(templatesDir, outputDir) {
34117
34290
  })) {
34118
34291
  if (await isDirectory(file))
34119
34292
  continue;
34120
- const relativePath = path7.relative(publicDir, file);
34121
- const destPath = path7.join(outputDir, relativePath);
34122
- const targetDir = path7.dirname(destPath);
34293
+ const relativePath = path8.relative(publicDir, file);
34294
+ const destPath = path8.join(outputDir, relativePath);
34295
+ const targetDir = path8.dirname(destPath);
34123
34296
  await ensureDir(targetDir);
34124
34297
  await copyFile(file, destPath);
34125
34298
  }
@@ -34201,6 +34374,8 @@ class SiteGenerator {
34201
34374
  options;
34202
34375
  site;
34203
34376
  metrics;
34377
+ cache = null;
34378
+ incrementalMode = false;
34204
34379
  constructor(options2) {
34205
34380
  this.options = options2;
34206
34381
  this.site = {
@@ -34212,8 +34387,7 @@ class SiteGenerator {
34212
34387
  this.metrics = new MetricsCollector;
34213
34388
  const env = import_nunjucks2.default.configure(this.options.templatesDir, {
34214
34389
  autoescape: true,
34215
- watch: false,
34216
- noCache: false
34390
+ watch: false
34217
34391
  });
34218
34392
  env.addFilter("date", (date, format) => {
34219
34393
  const d2 = toPacificTime(date);
@@ -34246,6 +34420,9 @@ class SiteGenerator {
34246
34420
  }
34247
34421
  });
34248
34422
  }
34423
+ enableIncrementalMode() {
34424
+ this.incrementalMode = true;
34425
+ }
34249
34426
  async initialize() {
34250
34427
  this.metrics.startStage("initialization");
34251
34428
  console.log("Initializing site generator...");
@@ -34254,7 +34431,7 @@ class SiteGenerator {
34254
34431
  setNoFollowExceptions(this.options.config.noFollowExceptions);
34255
34432
  }
34256
34433
  let tagDescriptions = {};
34257
- const tagsTomlPath = path8.join(process.cwd(), "src", "tags.toml");
34434
+ const tagsTomlPath = path9.join(process.cwd(), "src", "tags.toml");
34258
34435
  const tagsTomlFile = Bun.file(tagsTomlPath);
34259
34436
  if (await tagsTomlFile.exists()) {
34260
34437
  try {
@@ -34264,8 +34441,10 @@ class SiteGenerator {
34264
34441
  console.warn("Error loading tag descriptions:", error);
34265
34442
  }
34266
34443
  }
34267
- const strictMode = this.options.config.strictMode ?? false;
34268
- const posts = await parseMarkdownDirectory(this.options.contentDir, strictMode, this.options.config.cdn);
34444
+ if (this.incrementalMode) {
34445
+ this.cache = await loadCache(process.cwd());
34446
+ }
34447
+ const posts = await this.parseContent();
34269
34448
  const tags = {};
34270
34449
  posts.forEach((post) => {
34271
34450
  post.tagSlugs = {};
@@ -34303,7 +34482,21 @@ class SiteGenerator {
34303
34482
  console.log("Generating static site...");
34304
34483
  await ensureDir(this.options.outputDir);
34305
34484
  this.metrics.startStage("cssProcessing");
34306
- await generateStylesheet(this.options.config, this.options.outputDir);
34485
+ let cssChanged = true;
34486
+ if (this.cache && this.incrementalMode && this.options.config.css) {
34487
+ const cssInputPath = path9.resolve(process.cwd(), this.options.config.css.input);
34488
+ const cssOutputPath = path9.join(this.options.outputDir, this.options.config.css.output);
34489
+ const cssOutputExists = await Bun.file(cssOutputPath).exists();
34490
+ cssChanged = await hasFileChanged(cssInputPath, this.cache);
34491
+ if (!cssChanged && cssOutputExists) {
34492
+ console.log("\u23ED\uFE0F Skipping CSS (unchanged)");
34493
+ } else {
34494
+ await generateStylesheet(this.options.config, this.options.outputDir);
34495
+ await updateCacheEntry(cssInputPath, this.cache);
34496
+ }
34497
+ } else {
34498
+ await generateStylesheet(this.options.config, this.options.outputDir);
34499
+ }
34307
34500
  this.metrics.startStage("pageGeneration");
34308
34501
  await Promise.all([
34309
34502
  generateIndexPages(this.site, this.options.config, this.options.outputDir),
@@ -34320,25 +34513,75 @@ class SiteGenerator {
34320
34513
  const outputStats = await this.calculateOutputStats();
34321
34514
  const buildMetrics = this.metrics.getMetrics(outputStats);
34322
34515
  displayMetrics(buildMetrics);
34516
+ if (this.cache) {
34517
+ await saveCache(process.cwd(), this.cache);
34518
+ }
34323
34519
  }
34324
34520
  async generateFeeds() {
34325
34521
  const pageSize = 10;
34326
34522
  const rssContent = generateRSSFeed(this.site, this.options.config);
34327
- await Bun.write(path8.join(this.options.outputDir, "feed.xml"), rssContent);
34523
+ await Bun.write(path9.join(this.options.outputDir, "feed.xml"), rssContent);
34328
34524
  const sitemapContent = generateSitemap(this.site, this.options.config, pageSize);
34329
- await Bun.write(path8.join(this.options.outputDir, "sitemap.xml"), sitemapContent);
34525
+ await Bun.write(path9.join(this.options.outputDir, "sitemap.xml"), sitemapContent);
34330
34526
  console.log("Generated sitemap.xml");
34331
34527
  const urlCount = this.site.posts.length + Object.keys(this.site.tags).length + 10;
34332
34528
  const sitemapSize = sitemapContent.length;
34333
34529
  if (urlCount > 1000 || sitemapSize > 40000) {
34334
34530
  const sitemapIndexContent = generateSitemapIndex(this.options.config);
34335
- await Bun.write(path8.join(this.options.outputDir, "sitemap_index.xml"), sitemapIndexContent);
34531
+ await Bun.write(path9.join(this.options.outputDir, "sitemap_index.xml"), sitemapIndexContent);
34336
34532
  console.log("Generated sitemap_index.xml");
34337
34533
  }
34338
34534
  const robotsTxtContent = generateRobotsTxt(this.options.config);
34339
- await Bun.write(path8.join(this.options.outputDir, "robots.txt"), robotsTxtContent);
34535
+ await Bun.write(path9.join(this.options.outputDir, "robots.txt"), robotsTxtContent);
34340
34536
  console.log("Generated robots.txt");
34341
34537
  }
34538
+ async parseContent() {
34539
+ const strictMode = this.options.config.strictMode ?? false;
34540
+ if (!this.incrementalMode || !this.cache) {
34541
+ const posts = await parseMarkdownDirectory(this.options.contentDir, strictMode, this.options.config.cdn);
34542
+ if (this.cache) {
34543
+ const allFiles2 = await findFilesByPattern("**/*.md", this.options.contentDir, true);
34544
+ const postsByPath = new Map(posts.map((p) => [p.url, p]));
34545
+ for (let i = 0;i < allFiles2.length; i++) {
34546
+ const filePath = allFiles2[i];
34547
+ const post = posts[i];
34548
+ await updateCacheEntry(filePath, this.cache, { post });
34549
+ }
34550
+ }
34551
+ return posts;
34552
+ }
34553
+ const allFiles = await findFilesByPattern("**/*.md", this.options.contentDir, true);
34554
+ const configPath = path9.join(process.cwd(), "bunki.config.ts");
34555
+ const configChanged = await hasConfigChanged(configPath, this.cache);
34556
+ if (configChanged) {
34557
+ console.log("Config changed, full rebuild required");
34558
+ return this.parseContent();
34559
+ }
34560
+ const changes = await detectChanges(allFiles, this.cache);
34561
+ if (changes.fullRebuild) {
34562
+ console.log("Full rebuild required");
34563
+ this.incrementalMode = false;
34564
+ return this.parseContent();
34565
+ }
34566
+ if (changes.changedPosts.length === 0) {
34567
+ console.log("No content changes detected, using cached posts");
34568
+ const cachedPosts2 = loadCachedPosts(this.cache, allFiles);
34569
+ console.log(`\u2728 Loaded ${cachedPosts2.length} posts from cache (0ms parsing)`);
34570
+ return cachedPosts2;
34571
+ }
34572
+ const timeSaved = estimateTimeSaved(allFiles.length, changes.changedPosts.length);
34573
+ console.log(`\uD83D\uDCE6 Incremental build: ${changes.changedPosts.length}/${allFiles.length} files changed (~${timeSaved}ms saved)`);
34574
+ const changedPostsWithPaths = await parseMarkdownFiles(changes.changedPosts, this.options.config.cdn);
34575
+ const unchangedFiles = allFiles.filter((f) => !changes.changedPosts.includes(f));
34576
+ const cachedPosts = loadCachedPosts(this.cache, unchangedFiles);
34577
+ console.log(` Parsed: ${changedPostsWithPaths.length} new/changed, loaded: ${cachedPosts.length} from cache`);
34578
+ const changedPosts = changedPostsWithPaths.map((p) => p.post);
34579
+ const allPosts = [...changedPosts, ...cachedPosts].sort((a, b2) => new Date(b2.date).getTime() - new Date(a.date).getTime());
34580
+ for (const { post, filePath } of changedPostsWithPaths) {
34581
+ await updateCacheEntry(filePath, this.cache, { post });
34582
+ }
34583
+ return allPosts;
34584
+ }
34342
34585
  groupPostsByYear(posts) {
34343
34586
  const postsByYear = {};
34344
34587
  for (const post of posts) {
@@ -34387,15 +34630,18 @@ var defaultDeps2 = {
34387
34630
  };
34388
34631
  async function handleGenerateCommand(options2, deps = defaultDeps2) {
34389
34632
  try {
34390
- const configPath = path9.resolve(options2.config);
34391
- const contentDir = path9.resolve(options2.content);
34392
- const outputDir = path9.resolve(options2.output);
34393
- const templatesDir = path9.resolve(options2.templates);
34633
+ const configPath = path10.resolve(options2.config);
34634
+ const contentDir = path10.resolve(options2.content);
34635
+ const outputDir = path10.resolve(options2.output);
34636
+ const templatesDir = path10.resolve(options2.templates);
34394
34637
  deps.logger.log("Generating site with:");
34395
34638
  deps.logger.log(`- Config file: ${configPath}`);
34396
34639
  deps.logger.log(`- Content directory: ${contentDir}`);
34397
34640
  deps.logger.log(`- Output directory: ${outputDir}`);
34398
34641
  deps.logger.log(`- Templates directory: ${templatesDir}`);
34642
+ if (options2.incremental) {
34643
+ deps.logger.log(`- Incremental mode: enabled`);
34644
+ }
34399
34645
  const config = await deps.loadConfig(configPath);
34400
34646
  const generator = deps.createGenerator({
34401
34647
  contentDir,
@@ -34403,6 +34649,9 @@ async function handleGenerateCommand(options2, deps = defaultDeps2) {
34403
34649
  templatesDir,
34404
34650
  config
34405
34651
  });
34652
+ if (options2.incremental) {
34653
+ generator.enableIncrementalMode();
34654
+ }
34406
34655
  await generator.initialize();
34407
34656
  await generator.generate();
34408
34657
  deps.logger.log("Site generation completed successfully!");
@@ -34412,17 +34661,17 @@ async function handleGenerateCommand(options2, deps = defaultDeps2) {
34412
34661
  }
34413
34662
  }
34414
34663
  function registerGenerateCommand(program2) {
34415
- return program2.command("generate").description("Generate static site from markdown content").option("-c, --config <file>", "Config file path", "bunki.config.ts").option("-d, --content <dir>", "Content directory", DEFAULT_CONTENT_DIR).option("-o, --output <dir>", "Output directory", DEFAULT_OUTPUT_DIR).option("-t, --templates <dir>", "Templates directory", DEFAULT_TEMPLATES_DIR).action(async (options2) => {
34664
+ return program2.command("generate").description("Generate static site from markdown content").option("-c, --config <file>", "Config file path", "bunki.config.ts").option("-d, --content <dir>", "Content directory", DEFAULT_CONTENT_DIR).option("-o, --output <dir>", "Output directory", DEFAULT_OUTPUT_DIR).option("-t, --templates <dir>", "Templates directory", DEFAULT_TEMPLATES_DIR).option("-i, --incremental", "Enable incremental builds (only rebuild changed files)").action(async (options2) => {
34416
34665
  await handleGenerateCommand(options2);
34417
34666
  });
34418
34667
  }
34419
34668
 
34420
34669
  // src/utils/image-uploader.ts
34421
- import path11 from "path";
34670
+ import path12 from "path";
34422
34671
 
34423
34672
  // src/utils/s3-uploader.ts
34424
34673
  var {S3Client } = globalThis.Bun;
34425
- import path10 from "path";
34674
+ import path11 from "path";
34426
34675
 
34427
34676
  class S3Uploader {
34428
34677
  s3Config;
@@ -34545,8 +34794,8 @@ class S3Uploader {
34545
34794
  let failedCount = 0;
34546
34795
  const uploadTasks = imageFiles.map((imageFile) => async () => {
34547
34796
  try {
34548
- const imagePath = path10.join(imagesDir, imageFile);
34549
- const filename = path10.basename(imagePath);
34797
+ const imagePath = path11.join(imagesDir, imageFile);
34798
+ const filename = path11.basename(imagePath);
34550
34799
  const file = Bun.file(imagePath);
34551
34800
  const contentType = file.type;
34552
34801
  if (process.env.BUNKI_DRY_RUN === "true") {} else {
@@ -34580,10 +34829,10 @@ function createUploader(config) {
34580
34829
  }
34581
34830
 
34582
34831
  // src/utils/image-uploader.ts
34583
- var DEFAULT_IMAGES_DIR = path11.join(process.cwd(), "assets");
34832
+ var DEFAULT_IMAGES_DIR = path12.join(process.cwd(), "assets");
34584
34833
  async function uploadImages(options2 = {}) {
34585
34834
  try {
34586
- const imagesDir = path11.resolve(options2.images || DEFAULT_IMAGES_DIR);
34835
+ const imagesDir = path12.resolve(options2.images || DEFAULT_IMAGES_DIR);
34587
34836
  if (!await fileExists(imagesDir)) {
34588
34837
  console.log(`Creating images directory at ${imagesDir}...`);
34589
34838
  await ensureDir(imagesDir);
@@ -34620,7 +34869,7 @@ async function uploadImages(options2 = {}) {
34620
34869
  const uploader = createUploader(s3Config);
34621
34870
  const imageUrlMap = await uploader.uploadImages(imagesDir, options2.minYear);
34622
34871
  if (options2.outputJson) {
34623
- const outputFile = path11.resolve(options2.outputJson);
34872
+ const outputFile = path12.resolve(options2.outputJson);
34624
34873
  await Bun.write(outputFile, JSON.stringify(imageUrlMap, null, 2));
34625
34874
  console.log(`Image URL mapping saved to ${outputFile}`);
34626
34875
  }
@@ -34666,438 +34915,476 @@ function registerImagesPushCommand(program2) {
34666
34915
  }
34667
34916
 
34668
34917
  // src/cli/commands/init.ts
34669
- import path12 from "path";
34918
+ import path13 from "path";
34919
+ var defaultDependencies = {
34920
+ createDefaultConfig,
34921
+ ensureDir,
34922
+ writeFile: (filePath, data) => Bun.write(filePath, data),
34923
+ logger: console,
34924
+ exit: (code) => process.exit(code)
34925
+ };
34926
+ async function handleInitCommand(options2, deps = defaultDependencies) {
34927
+ try {
34928
+ const configPath = path13.resolve(options2.config);
34929
+ const configCreated = await deps.createDefaultConfig(configPath);
34930
+ if (!configCreated) {
34931
+ deps.logger.log(`
34932
+ Skipped initialization because the config file already exists`);
34933
+ return;
34934
+ }
34935
+ deps.logger.log("Creating directory structure...");
34936
+ const baseDir = process.cwd();
34937
+ const contentDir = path13.join(baseDir, "content");
34938
+ const templatesDir = path13.join(baseDir, "templates");
34939
+ const stylesDir = path13.join(templatesDir, "styles");
34940
+ const publicDir = path13.join(baseDir, "public");
34941
+ await deps.ensureDir(contentDir);
34942
+ await deps.ensureDir(templatesDir);
34943
+ await deps.ensureDir(stylesDir);
34944
+ await deps.ensureDir(publicDir);
34945
+ for (const [filename, content] of Object.entries(getDefaultTemplates())) {
34946
+ await deps.writeFile(path13.join(templatesDir, filename), content);
34947
+ }
34948
+ await deps.writeFile(path13.join(stylesDir, "main.css"), getDefaultCss());
34949
+ await deps.writeFile(path13.join(contentDir, "welcome.md"), getSamplePost());
34950
+ deps.logger.log(`
34951
+ Initialization complete! Here are the next steps:`);
34952
+ deps.logger.log("1. Edit bunki.config.ts to configure your site");
34953
+ deps.logger.log("2. Add markdown files to the content directory");
34954
+ deps.logger.log('3. Run "bunki generate" to build your site');
34955
+ deps.logger.log('4. Run "bunki serve" to preview your site locally');
34956
+ } catch (error) {
34957
+ deps.logger.error("Error initializing site:", error);
34958
+ deps.exit(1);
34959
+ }
34960
+ }
34961
+ function registerInitCommand(program2, deps = defaultDependencies) {
34962
+ return program2.command("init").description("Initialize a new site with default structure").option("-c, --config <file>", "Path to config file", "bunki.config.ts").action(async (options2) => {
34963
+ await handleInitCommand(options2, deps);
34964
+ });
34965
+ }
34966
+ function getDefaultTemplates() {
34967
+ return {
34968
+ "base.njk": String.raw`<!DOCTYPE html>
34969
+ <html lang="en">
34970
+ <head>
34971
+ <meta charset="UTF-8">
34972
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
34973
+ <title>{% block title %}{{ site.title }}{% endblock %}</title>
34974
+ <meta name="description" content="{% block description %}{{ site.description }}{% endblock %}">
34975
+ <link rel="stylesheet" href="/css/style.css">
34976
+ {% block head %}{% endblock %}
34977
+ </head>
34978
+ <body>
34979
+ <header>
34980
+ <div class="container">
34981
+ <h1><a href="/">{{ site.title }}</a></h1>
34982
+ <nav>
34983
+ <ul>
34984
+ <li><a href="/">Home</a></li>
34985
+ <li><a href="/tags/">Tags</a></li>
34986
+ </ul>
34987
+ </nav>
34988
+ </div>
34989
+ </header>
34670
34990
 
34671
- // src/cli/commands/templates/base-njk.ts
34672
- var baseNjk = String.raw`<!DOCTYPE html>
34673
- <html lang="en">
34674
- <head>
34675
- <meta charset="UTF-8">
34676
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
34677
- <title>{% block title %}{{ site.title }}{% endblock %}</title>
34678
- <meta name="description" content="{% block description %}{{ site.description }}{% endblock %}">
34679
- <link rel="stylesheet" href="/css/style.css">
34680
- {% block head %}{% endblock %}
34681
- </head>
34682
- <body>
34683
- <header>
34684
- <div class="container">
34685
- <h1><a href="/">{{ site.title }}</a></h1>
34686
- <nav>
34687
- <ul>
34688
- <li><a href="/">Home</a></li>
34689
- <li><a href="/tags/">Tags</a></li>
34690
- </ul>
34691
- </nav>
34692
- </div>
34693
- </header>
34694
-
34695
- <main class="container">
34696
- {% block content %}{% endblock %}
34697
- </main>
34698
-
34699
- <footer>
34700
- <div class="container">
34701
- <p>&copy; {{ "now" | date("YYYY") }} {{ site.title }}</p>
34702
- </div>
34703
- </footer>
34704
- </body>
34705
- </html>`;
34706
-
34707
- // src/cli/commands/templates/index-njk.ts
34708
- var indexNjk = String.raw`{% extends "base.njk" %}
34709
-
34710
- {% block content %}
34711
- <h1>Latest Posts</h1>
34712
-
34713
- {% if posts.length > 0 %}
34714
- <div class="posts">
34715
- {% for post in posts %}
34716
- <article class="post-card">
34717
- <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
34718
- <div class="post-meta">
34719
- <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
34720
- {% if post.tags.length > 0 %}
34721
- <span class="tags">
34722
- {% for tag in post.tags %}
34723
- <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
34724
- {% endfor %}
34725
- </span>
34726
- {% endif %}
34727
- </div>
34728
- <div class="post-excerpt">{{ post.excerpt }}</div>
34729
- <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
34730
- </article>
34731
- {% endfor %}
34732
- </div>
34733
-
34734
- {% if pagination.totalPages > 1 %}
34735
- <nav class="pagination">
34736
- {% if pagination.hasPrevPage %}
34737
- <a href="{{ pagination.pagePath }}{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
34738
- {% endif %}
34739
-
34740
- {% if pagination.hasNextPage %}
34741
- <a href="{{ pagination.pagePath }}page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
34742
- {% endif %}
34743
-
34744
- <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
34745
- </nav>
34991
+ <main class="container">
34992
+ {% block content %}{% endblock %}
34993
+ </main>
34994
+
34995
+ <footer>
34996
+ <div class="container">
34997
+ <p>&copy; {{ "now" | date("YYYY") }} {{ site.title }}</p>
34998
+ </div>
34999
+ </footer>
35000
+ </body>
35001
+ </html>`,
35002
+ "index.njk": String.raw`{% extends "base.njk" %}
35003
+
35004
+ {% block content %}
35005
+ <h1>Latest Posts</h1>
35006
+
35007
+ {% if posts.length > 0 %}
35008
+ <div class="posts">
35009
+ {% for post in posts %}
35010
+ <article class="post-card">
35011
+ <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35012
+ <div class="post-meta">
35013
+ <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35014
+ {% if post.tags.length > 0 %}
35015
+ <span class="tags">
35016
+ {% for tag in post.tags %}
35017
+ <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35018
+ {% endfor %}
35019
+ </span>
35020
+ {% endif %}
35021
+ </div>
35022
+ <div class="post-excerpt">{{ post.excerpt }}</div>
35023
+ <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35024
+ </article>
35025
+ {% endfor %}
35026
+ </div>
35027
+
35028
+ {% if pagination.totalPages > 1 %}
35029
+ <nav class="pagination">
35030
+ {% if pagination.hasPrevPage %}
35031
+ <a href="{{ pagination.pagePath }}{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35032
+ {% endif %}
35033
+
35034
+ {% if pagination.hasNextPage %}
35035
+ <a href="{{ pagination.pagePath }}page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
35036
+ {% endif %}
35037
+
35038
+ <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35039
+ </nav>
35040
+ {% endif %}
35041
+ {% else %}
35042
+ <p>No posts yet!</p>
34746
35043
  {% endif %}
34747
- {% else %}
34748
- <p>No posts yet!</p>
34749
- {% endif %}
34750
- {% endblock %}`;
34751
-
34752
- // src/cli/commands/templates/post-njk.ts
34753
- var postNjk = String.raw`{% extends "base.njk" %}
34754
-
34755
- {% block title %}{{ post.title }} | {{ site.title }}{% endblock %}
34756
- {% block description %}{{ post.excerpt }}{% endblock %}
34757
-
34758
- {% block content %}
34759
- <article class="post">
34760
- <header class="post-header">
34761
- <h1>{{ post.title }}</h1>
34762
- <div class="post-meta">
34763
- <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
34764
- {% if post.tags.length > 0 %}
34765
- <span class="tags">
34766
- {% for tag in post.tags %}
34767
- <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
34768
- {% endfor %}
34769
- </span>
34770
- {% endif %}
35044
+ {% endblock %}`,
35045
+ "post.njk": String.raw`{% extends "base.njk" %}
35046
+
35047
+ {% block title %}{{ post.title }} | {{ site.title }}{% endblock %}
35048
+ {% block description %}{{ post.excerpt }}{% endblock %}
35049
+
35050
+ {% block content %}
35051
+ <article class="post">
35052
+ <header class="post-header">
35053
+ <h1>{{ post.title }}</h1>
35054
+ <div class="post-meta">
35055
+ <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35056
+ {% if post.tags.length > 0 %}
35057
+ <span class="tags">
35058
+ {% for tag in post.tags %}
35059
+ <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35060
+ {% endfor %}
35061
+ </span>
35062
+ {% endif %}
35063
+ </div>
35064
+ </header>
35065
+
35066
+ <div class="post-content">
35067
+ {{ post.html | safe }}
34771
35068
  </div>
34772
- </header>
35069
+ </article>
35070
+ {% endblock %}`,
35071
+ "tag.njk": String.raw`{% extends "base.njk" %}
35072
+
35073
+ {% block title %}{{ tag.name }} | {{ site.title }}{% endblock %}
35074
+ {% block description %}Posts tagged with {{ tag.name }} on {{ site.title }}{% endblock %}
35075
+
35076
+ {% block content %}
35077
+ <h1>Posts tagged "{{ tag.name }}"</h1>
34773
35078
 
34774
- <div class="post-content">
34775
- {{ post.html | safe }}
34776
- </div>
34777
- </article>
34778
- {% endblock %}`;
34779
-
34780
- // src/cli/commands/templates/tag-njk.ts
34781
- var tagNjk = String.raw`{% extends "base.njk" %}
34782
-
34783
- {% block title %}{{ tag.name }} | {{ site.title }}{% endblock %}
34784
- {% block description %}Posts tagged with {{ tag.name }} on {{ site.title }}{% endblock %}
34785
-
34786
- {% block content %}
34787
- <h1>Posts tagged "{{ tag.name }}"</h1>
34788
-
34789
- {% if tag.description %}
34790
- <div class="tag-description">{{ tag.description }}</div>
34791
- {% endif %}
34792
-
34793
- {% if tag.posts.length > 0 %}
34794
- <div class="posts">
34795
- {% for post in tag.posts %}
34796
- <article class="post-card">
34797
- <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
34798
- <div class="post-meta">
34799
- <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
34800
- </div>
34801
- <div class="post-excerpt">{{ post.excerpt }}</div>
34802
- <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
34803
- </article>
34804
- {% endfor %}
34805
- </div>
34806
-
34807
- {% if pagination.totalPages > 1 %}
34808
- <nav class="pagination">
34809
- {% if pagination.hasPrevPage %}
34810
- <a href="{{ pagination.pagePath }}{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
34811
- {% endif %}
34812
-
34813
- {% if pagination.hasNextPage %}
34814
- <a href="{{ pagination.pagePath }}page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
34815
- {% endif %}
34816
-
34817
- <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
34818
- </nav>
35079
+ {% if tag.description %}
35080
+ <div class="tag-description">{{ tag.description }}</div>
34819
35081
  {% endif %}
34820
- {% else %}
34821
- <p>No posts with this tag yet!</p>
34822
- {% endif %}
34823
- {% endblock %}`;
34824
-
34825
- // src/cli/commands/templates/tags-njk.ts
34826
- var tagsNjk = String.raw`{% extends "base.njk" %}
34827
-
34828
- {% block title %}Tags | {{ site.title }}{% endblock %}
34829
- {% block description %}Browse all tags on {{ site.title }}{% endblock %}
34830
-
34831
- {% block content %}
34832
- <h1>All Tags</h1>
34833
-
34834
- {% if tags.length > 0 %}
34835
- <ul class="tags-list">
34836
- {% for tag in tags %}
34837
- <li>
34838
- <a href="/tags/{{ tag.slug }}/">{{ tag.name }}</a>
34839
- <span class="count">({{ tag.count }})</span>
34840
- {% if tag.description %}
34841
- <p class="description">{{ tag.description }}</p>
35082
+
35083
+ {% if tag.posts.length > 0 %}
35084
+ <div class="posts">
35085
+ {% for post in tag.posts %}
35086
+ <article class="post-card">
35087
+ <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35088
+ <div class="post-meta">
35089
+ <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35090
+ </div>
35091
+ <div class="post-excerpt">{{ post.excerpt }}</div>
35092
+ <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35093
+ </article>
35094
+ {% endfor %}
35095
+ </div>
35096
+
35097
+ {% if pagination.totalPages > 1 %}
35098
+ <nav class="pagination">
35099
+ {% if pagination.hasPrevPage %}
35100
+ <a href="{{ pagination.pagePath }}{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35101
+ {% endif %}
35102
+
35103
+ {% if pagination.hasNextPage %}
35104
+ <a href="{{ pagination.pagePath }}page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
34842
35105
  {% endif %}
34843
- </li>
34844
- {% endfor %}
34845
- </ul>
34846
- {% else %}
34847
- <p>No tags found!</p>
34848
- {% endif %}
34849
- {% endblock %}`;
34850
-
34851
- // src/cli/commands/templates/archive-njk.ts
34852
- var archiveNjk = String.raw`{% extends "base.njk" %}
34853
-
34854
- {% block title %}Archive {{ year }} | {{ site.title }}{% endblock %}
34855
- {% block description %}Posts from {{ year }} on {{ site.title }}{% endblock %}
34856
-
34857
- {% block content %}
34858
- <h1>Posts from {{ year }}</h1>
34859
-
34860
- {% if posts.length > 0 %}
34861
- <div class="posts">
34862
- {% for post in posts %}
34863
- <article class="post-card">
34864
- <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
34865
- <div class="post-meta">
34866
- <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
34867
- {% if post.tags.length > 0 %}
34868
- <span class="tags">
34869
- {% for tag in post.tags %}
34870
- <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
34871
- {% endfor %}
34872
- </span>
35106
+
35107
+ <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35108
+ </nav>
35109
+ {% endif %}
35110
+ {% else %}
35111
+ <p>No posts with this tag yet!</p>
35112
+ {% endif %}
35113
+ {% endblock %}`,
35114
+ "tags.njk": String.raw`{% extends "base.njk" %}
35115
+
35116
+ {% block title %}Tags | {{ site.title }}{% endblock %}
35117
+ {% block description %}Browse all tags on {{ site.title }}{% endblock %}
35118
+
35119
+ {% block content %}
35120
+ <h1>All Tags</h1>
35121
+
35122
+ {% if tags.length > 0 %}
35123
+ <ul class="tags-list">
35124
+ {% for tag in tags %}
35125
+ <li>
35126
+ <a href="/tags/{{ tag.slug }}/">{{ tag.name }}</a>
35127
+ <span class="count">({{ tag.count }})</span>
35128
+ {% if tag.description %}
35129
+ <p class="description">{{ tag.description }}</p>
34873
35130
  {% endif %}
34874
- </div>
34875
- <div class="post-excerpt">{{ post.excerpt }}</div>
34876
- <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
34877
- </article>
34878
- {% endfor %}
34879
- </div>
34880
-
34881
- {% if pagination.totalPages > 1 %}
34882
- <nav class="pagination">
34883
- {% if pagination.hasPrevPage %}
34884
- <a href="/{{ year }}/{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
34885
- {% endif %}
34886
-
34887
- {% if pagination.hasNextPage %}
34888
- <a href="/{{ year }}/page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
34889
- {% endif %}
34890
-
34891
- <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
34892
- </nav>
35131
+ </li>
35132
+ {% endfor %}
35133
+ </ul>
35134
+ {% else %}
35135
+ <p>No tags found!</p>
34893
35136
  {% endif %}
34894
- {% else %}
34895
- <p>No posts from {{ year }}!</p>
34896
- {% endif %}
34897
- {% endblock %}`;
34898
-
34899
- // src/cli/commands/templates/default-css.ts
34900
- var defaultCss = String.raw`/* Reset & base styles */
34901
- * {
34902
- margin: 0;
34903
- padding: 0;
34904
- box-sizing: border-box;
34905
- }
35137
+ {% endblock %}`,
35138
+ "archive.njk": String.raw`{% extends "base.njk" %}
35139
+
35140
+ {% block title %}Archive {{ year }} | {{ site.title }}{% endblock %}
35141
+ {% block description %}Posts from {{ year }} on {{ site.title }}{% endblock %}
35142
+
35143
+ {% block content %}
35144
+ <h1>Posts from {{ year }}</h1>
35145
+
35146
+ {% if posts.length > 0 %}
35147
+ <div class="posts">
35148
+ {% for post in posts %}
35149
+ <article class="post-card">
35150
+ <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35151
+ <div class="post-meta">
35152
+ <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35153
+ {% if post.tags.length > 0 %}
35154
+ <span class="tags">
35155
+ {% for tag in post.tags %}
35156
+ <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35157
+ {% endfor %}
35158
+ </span>
35159
+ {% endif %}
35160
+ </div>
35161
+ <div class="post-excerpt">{{ post.excerpt }}</div>
35162
+ <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35163
+ </article>
35164
+ {% endfor %}
35165
+ </div>
34906
35166
 
34907
- body {
34908
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
34909
- line-height: 1.6;
34910
- color: #333;
34911
- background-color: #f8f9fa;
34912
- padding-bottom: 2rem;
34913
- }
35167
+ {% if pagination.totalPages > 1 %}
35168
+ <nav class="pagination">
35169
+ {% if pagination.hasPrevPage %}
35170
+ <a href="/{{ year }}/{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35171
+ {% endif %}
34914
35172
 
34915
- a {
34916
- color: #0066cc;
34917
- text-decoration: none;
34918
- }
35173
+ {% if pagination.hasNextPage %}
35174
+ <a href="/{{ year }}/page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
35175
+ {% endif %}
34919
35176
 
34920
- a:hover {
34921
- text-decoration: underline;
35177
+ <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35178
+ </nav>
35179
+ {% endif %}
35180
+ {% else %}
35181
+ <p>No posts from {{ year }}!</p>
35182
+ {% endif %}
35183
+ {% endblock %}`
35184
+ };
34922
35185
  }
35186
+ function getDefaultCss() {
35187
+ return String.raw`/* Reset & base styles */
35188
+ * {
35189
+ margin: 0;
35190
+ padding: 0;
35191
+ box-sizing: border-box;
35192
+ }
34923
35193
 
34924
- .container {
34925
- max-width: 800px;
34926
- margin: 0 auto;
34927
- padding: 0 1.5rem;
34928
- }
35194
+ body {
35195
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
35196
+ line-height: 1.6;
35197
+ color: #333;
35198
+ background-color: #f8f9fa;
35199
+ padding-bottom: 2rem;
35200
+ }
34929
35201
 
34930
- /* Header */
34931
- header {
34932
- background-color: #fff;
34933
- padding: 1.5rem 0;
34934
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
34935
- margin-bottom: 2rem;
34936
- }
35202
+ a {
35203
+ color: #0066cc;
35204
+ text-decoration: none;
35205
+ }
34937
35206
 
34938
- header h1 {
34939
- font-size: 1.8rem;
34940
- margin: 0;
34941
- }
35207
+ a:hover {
35208
+ text-decoration: underline;
35209
+ }
34942
35210
 
34943
- header h1 a {
34944
- color: #333;
34945
- text-decoration: none;
34946
- }
35211
+ .container {
35212
+ max-width: 800px;
35213
+ margin: 0 auto;
35214
+ padding: 0 1.5rem;
35215
+ }
34947
35216
 
34948
- header nav {
34949
- margin-top: 0.5rem;
34950
- }
35217
+ /* Header */
35218
+ header {
35219
+ background-color: #fff;
35220
+ padding: 1.5rem 0;
35221
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
35222
+ margin-bottom: 2rem;
35223
+ }
34951
35224
 
34952
- header nav ul {
34953
- display: flex;
34954
- list-style: none;
34955
- gap: 1.5rem;
34956
- }
35225
+ header h1 {
35226
+ font-size: 1.8rem;
35227
+ margin: 0;
35228
+ }
34957
35229
 
34958
- /* Main content */
34959
- main {
34960
- background-color: #fff;
34961
- padding: 2rem;
34962
- border-radius: 5px;
34963
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
34964
- }
35230
+ header h1 a {
35231
+ color: #333;
35232
+ text-decoration: none;
35233
+ }
34965
35234
 
34966
- /* Posts */
34967
- .posts {
34968
- display: flex;
34969
- flex-direction: column;
34970
- gap: 2rem;
34971
- }
35235
+ header nav {
35236
+ margin-top: 0.5rem;
35237
+ }
34972
35238
 
34973
- .post-card {
34974
- border-bottom: 1px solid #eee;
34975
- padding-bottom: 1.5rem;
34976
- }
35239
+ header nav ul {
35240
+ display: flex;
35241
+ list-style: none;
35242
+ gap: 1.5rem;
35243
+ }
34977
35244
 
34978
- .post-card:last-child {
34979
- border-bottom: none;
34980
- }
35245
+ /* Main content */
35246
+ main {
35247
+ background-color: #fff;
35248
+ padding: 2rem;
35249
+ border-radius: 5px;
35250
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
35251
+ }
34981
35252
 
34982
- .post-card h2 {
34983
- margin-bottom: 0.5rem;
34984
- }
35253
+ /* Posts */
35254
+ .posts {
35255
+ display: flex;
35256
+ flex-direction: column;
35257
+ gap: 2rem;
35258
+ }
34985
35259
 
34986
- .post-meta {
34987
- font-size: 0.9rem;
34988
- color: #6c757d;
34989
- margin-bottom: 1rem;
34990
- }
35260
+ .post-card {
35261
+ border-bottom: 1px solid #eee;
35262
+ padding-bottom: 1.5rem;
35263
+ }
34991
35264
 
34992
- .post-excerpt {
34993
- margin-bottom: 1rem;
34994
- }
35265
+ .post-card:last-child {
35266
+ border-bottom: none;
35267
+ }
34995
35268
 
34996
- .read-more {
34997
- font-weight: 500;
34998
- }
35269
+ .post-card h2 {
35270
+ margin-bottom: 0.5rem;
35271
+ }
34999
35272
 
35000
- /* Single post */
35001
- .post-header {
35002
- margin-bottom: 2rem;
35003
- }
35273
+ .post-meta {
35274
+ font-size: 0.9rem;
35275
+ color: #6c757d;
35276
+ margin-bottom: 1rem;
35277
+ }
35004
35278
 
35005
- .post-content {
35006
- line-height: 1.8;
35007
- }
35279
+ .post-excerpt {
35280
+ margin-bottom: 1rem;
35281
+ }
35008
35282
 
35009
- .post-content p,
35010
- .post-content ul,
35011
- .post-content ol,
35012
- .post-content blockquote {
35013
- margin-bottom: 1.5rem;
35014
- }
35283
+ .read-more {
35284
+ font-weight: 500;
35285
+ }
35015
35286
 
35016
- .post-content h2,
35017
- .post-content h3,
35018
- .post-content h4 {
35019
- margin-top: 2rem;
35020
- margin-bottom: 1rem;
35021
- }
35287
+ /* Single post */
35288
+ .post-header {
35289
+ margin-bottom: 2rem;
35290
+ }
35022
35291
 
35023
- .post-content img {
35024
- max-width: 100%;
35025
- height: auto;
35026
- display: block;
35027
- margin: 2rem auto;
35028
- }
35292
+ .post-content {
35293
+ line-height: 1.8;
35294
+ }
35029
35295
 
35030
- .post-content pre {
35031
- background-color: #f5f5f5;
35032
- padding: 1rem;
35033
- border-radius: 4px;
35034
- overflow-x: auto;
35035
- margin-bottom: 1.5rem;
35036
- }
35296
+ .post-content p,
35297
+ .post-content ul,
35298
+ .post-content ol,
35299
+ .post-content blockquote {
35300
+ margin-bottom: 1.5rem;
35301
+ }
35037
35302
 
35038
- .post-content code {
35039
- font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
35040
- font-size: 0.9em;
35041
- background-color: #f5f5f5;
35042
- padding: 0.2em 0.4em;
35043
- border-radius: 3px;
35044
- }
35303
+ .post-content h2,
35304
+ .post-content h3,
35305
+ .post-content h4 {
35306
+ margin-top: 2rem;
35307
+ margin-bottom: 1rem;
35308
+ }
35045
35309
 
35046
- .post-content pre code {
35047
- padding: 0;
35048
- background-color: transparent;
35049
- }
35310
+ .post-content img {
35311
+ max-width: 100%;
35312
+ height: auto;
35313
+ display: block;
35314
+ margin: 2rem auto;
35315
+ }
35050
35316
 
35051
- /* Tags */
35052
- .tags a {
35053
- display: inline-block;
35054
- margin-left: 0.5rem;
35055
- }
35317
+ .post-content pre {
35318
+ background-color: #f5f5f5;
35319
+ padding: 1rem;
35320
+ border-radius: 4px;
35321
+ overflow-x: auto;
35322
+ margin-bottom: 1.5rem;
35323
+ }
35056
35324
 
35057
- .tags-list {
35058
- list-style: none;
35059
- }
35325
+ .post-content code {
35326
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
35327
+ font-size: 0.9em;
35328
+ background-color: #f5f5f5;
35329
+ padding: 0.2em 0.4em;
35330
+ border-radius: 3px;
35331
+ }
35060
35332
 
35061
- .tags-list li {
35062
- margin-bottom: 1rem;
35063
- }
35333
+ .post-content pre code {
35334
+ padding: 0;
35335
+ background-color: transparent;
35336
+ }
35064
35337
 
35065
- .tags-list .count {
35066
- color: #6c757d;
35067
- font-size: 0.9rem;
35068
- }
35338
+ /* Tags */
35339
+ .tags a {
35340
+ display: inline-block;
35341
+ margin-left: 0.5rem;
35342
+ }
35069
35343
 
35070
- .tags-list .description {
35071
- margin-top: 0.25rem;
35072
- font-size: 0.9rem;
35073
- color: #6c757d;
35074
- }
35344
+ .tags-list {
35345
+ list-style: none;
35346
+ }
35075
35347
 
35076
- /* Pagination */
35077
- .pagination {
35078
- display: flex;
35079
- justify-content: space-between;
35080
- align-items: center;
35081
- margin-top: 2rem;
35082
- padding-top: 1rem;
35083
- border-top: 1px solid #eee;
35084
- }
35348
+ .tags-list li {
35349
+ margin-bottom: 1rem;
35350
+ }
35085
35351
 
35086
- .pagination .page-info {
35087
- color: #6c757d;
35088
- font-size: 0.9rem;
35089
- }
35352
+ .tags-list .count {
35353
+ color: #6c757d;
35354
+ font-size: 0.9rem;
35355
+ }
35090
35356
 
35091
- /* Footer */
35092
- footer {
35093
- text-align: center;
35094
- padding: 2rem 0;
35095
- color: #6c757d;
35096
- font-size: 0.9rem;
35097
- }`;
35357
+ .tags-list .description {
35358
+ margin-top: 0.25rem;
35359
+ font-size: 0.9rem;
35360
+ color: #6c757d;
35361
+ }
35362
+
35363
+ /* Pagination */
35364
+ .pagination {
35365
+ display: flex;
35366
+ justify-content: space-between;
35367
+ align-items: center;
35368
+ margin-top: 2rem;
35369
+ padding-top: 1rem;
35370
+ border-top: 1px solid #eee;
35371
+ }
35098
35372
 
35099
- // src/cli/commands/templates/sample-post.ts
35100
- var samplePost = `---
35373
+ .pagination .page-info {
35374
+ color: #6c757d;
35375
+ font-size: 0.9rem;
35376
+ }
35377
+
35378
+ /* Footer */
35379
+ footer {
35380
+ text-align: center;
35381
+ padding: 2rem 0;
35382
+ color: #6c757d;
35383
+ font-size: 0.9rem;
35384
+ }`;
35385
+ }
35386
+ function getSamplePost() {
35387
+ return `---
35101
35388
  title: Welcome to Bunki
35102
35389
  date: 2025-01-15T12:00:00Z
35103
35390
  tags: [getting-started, bunki]
@@ -35148,68 +35435,10 @@ function hello() {
35148
35435
  4. Run \`bunki generate\` to build your site
35149
35436
  5. Run \`bunki serve\` to preview your site locally
35150
35437
  `;
35151
-
35152
- // src/cli/commands/templates/index.ts
35153
- var nunjucks3 = {
35154
- "base.njk": baseNjk,
35155
- "index.njk": indexNjk,
35156
- "post.njk": postNjk,
35157
- "tag.njk": tagNjk,
35158
- "tags.njk": tagsNjk,
35159
- "archive.njk": archiveNjk
35160
- };
35161
-
35162
- // src/cli/commands/init.ts
35163
- var defaultDependencies = {
35164
- createDefaultConfig,
35165
- ensureDir,
35166
- writeFile: (filePath, data) => Bun.write(filePath, data),
35167
- logger: console,
35168
- exit: (code) => process.exit(code)
35169
- };
35170
- async function handleInitCommand(options2, deps = defaultDependencies) {
35171
- try {
35172
- const configPath = path12.resolve(options2.config);
35173
- const configCreated = await deps.createDefaultConfig(configPath);
35174
- if (!configCreated) {
35175
- deps.logger.log(`
35176
- Skipped initialization because the config file already exists`);
35177
- return;
35178
- }
35179
- deps.logger.log("Creating directory structure...");
35180
- const baseDir = process.cwd();
35181
- const contentDir = path12.join(baseDir, "content");
35182
- const templatesDir = path12.join(baseDir, "templates");
35183
- const stylesDir = path12.join(templatesDir, "styles");
35184
- const publicDir = path12.join(baseDir, "public");
35185
- await deps.ensureDir(contentDir);
35186
- await deps.ensureDir(templatesDir);
35187
- await deps.ensureDir(stylesDir);
35188
- await deps.ensureDir(publicDir);
35189
- for (const [filename, content] of Object.entries(nunjucks3)) {
35190
- await deps.writeFile(path12.join(templatesDir, filename), content);
35191
- }
35192
- await deps.writeFile(path12.join(stylesDir, "main.css"), defaultCss);
35193
- await deps.writeFile(path12.join(contentDir, "welcome.md"), samplePost);
35194
- deps.logger.log(`
35195
- Initialization complete! Here are the next steps:`);
35196
- deps.logger.log("1. Edit bunki.config.ts to configure your site");
35197
- deps.logger.log("2. Add markdown files to the content directory");
35198
- deps.logger.log('3. Run "bunki generate" to build your site');
35199
- deps.logger.log('4. Run "bunki serve" to preview your site locally');
35200
- } catch (error) {
35201
- deps.logger.error("Error initializing site:", error);
35202
- deps.exit(1);
35203
- }
35204
- }
35205
- function registerInitCommand(program2, deps = defaultDependencies) {
35206
- return program2.command("init").description("Initialize a new site with default structure").option("-c, --config <file>", "Path to config file", "bunki.config.ts").action(async (options2) => {
35207
- await handleInitCommand(options2, deps);
35208
- });
35209
35438
  }
35210
35439
 
35211
35440
  // src/cli/commands/new-post.ts
35212
- import path13 from "path";
35441
+ import path14 from "path";
35213
35442
  var defaultDeps4 = {
35214
35443
  writeFile: (filePath, data) => Bun.write(filePath, data),
35215
35444
  now: () => new Date,
@@ -35233,7 +35462,7 @@ async function handleNewCommand(title, options2, deps = defaultDeps4) {
35233
35462
  ` + `# ${title}
35234
35463
 
35235
35464
  `;
35236
- const filePath = path13.join(DEFAULT_CONTENT_DIR, `${slug}.md`);
35465
+ const filePath = path14.join(DEFAULT_CONTENT_DIR, `${slug}.md`);
35237
35466
  await deps.writeFile(filePath, frontmatter);
35238
35467
  deps.logger.log(`Created new post: ${filePath}`);
35239
35468
  return filePath;
@@ -35250,11 +35479,11 @@ function registerNewCommand(program2) {
35250
35479
  }
35251
35480
 
35252
35481
  // src/cli/commands/serve.ts
35253
- import path15 from "path";
35482
+ import path16 from "path";
35254
35483
 
35255
35484
  // src/server.ts
35256
35485
  import fs2 from "fs";
35257
- import path14 from "path";
35486
+ import path15 from "path";
35258
35487
  async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
35259
35488
  try {
35260
35489
  const stats = await fs2.promises.stat(outputDir);
@@ -35289,18 +35518,18 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
35289
35518
  let filePath = "";
35290
35519
  if (homePaginationMatch) {
35291
35520
  const pageNumber = homePaginationMatch[1];
35292
- filePath = path14.join(outputDir, "page", pageNumber, "index.html");
35521
+ filePath = path15.join(outputDir, "page", pageNumber, "index.html");
35293
35522
  } else if (tagPaginationMatch) {
35294
35523
  const tagSlug = tagPaginationMatch[1];
35295
35524
  const pageNumber = tagPaginationMatch[2];
35296
- filePath = path14.join(outputDir, "tags", tagSlug, "page", pageNumber, "index.html");
35525
+ filePath = path15.join(outputDir, "tags", tagSlug, "page", pageNumber, "index.html");
35297
35526
  } else if (yearPaginationMatch) {
35298
35527
  const year = yearPaginationMatch[1];
35299
35528
  const pageNumber = yearPaginationMatch[2];
35300
- filePath = path14.join(outputDir, year, "page", pageNumber, "index.html");
35529
+ filePath = path15.join(outputDir, year, "page", pageNumber, "index.html");
35301
35530
  } else {
35302
- const directPath = path14.join(outputDir, pathname);
35303
- const withoutSlash = path14.join(outputDir, pathname + ".html");
35531
+ const directPath = path15.join(outputDir, pathname);
35532
+ const withoutSlash = path15.join(outputDir, pathname + ".html");
35304
35533
  const withHtml = pathname.endsWith(".html") ? directPath : withoutSlash;
35305
35534
  const bunFileDirect = Bun.file(directPath);
35306
35535
  const bunFileHtml = Bun.file(withHtml);
@@ -35309,7 +35538,7 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
35309
35538
  } else if (await bunFileHtml.exists()) {
35310
35539
  filePath = withHtml;
35311
35540
  } else {
35312
- const indexPath = path14.join(outputDir, pathname, "index.html");
35541
+ const indexPath = path15.join(outputDir, pathname, "index.html");
35313
35542
  const bunFileIndex = Bun.file(indexPath);
35314
35543
  if (await bunFileIndex.exists()) {
35315
35544
  filePath = indexPath;
@@ -35323,7 +35552,7 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
35323
35552
  }
35324
35553
  }
35325
35554
  console.log(`Serving file: ${filePath}`);
35326
- const extname = path14.extname(filePath);
35555
+ const extname = path15.extname(filePath);
35327
35556
  let contentType = "text/html";
35328
35557
  switch (extname) {
35329
35558
  case ".js":
@@ -35382,7 +35611,7 @@ var defaultDeps5 = {
35382
35611
  };
35383
35612
  async function handleServeCommand(options2, deps = defaultDeps5) {
35384
35613
  try {
35385
- const outputDir = path15.resolve(options2.output);
35614
+ const outputDir = path16.resolve(options2.output);
35386
35615
  const port = parseInt(options2.port, 10);
35387
35616
  await deps.startServer(outputDir, port);
35388
35617
  } catch (error) {