bunki 0.18.6 → 0.19.1

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
@@ -5,15 +5,29 @@ var __getProtoOf = Object.getPrototypeOf;
5
5
  var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ function __accessProp(key) {
9
+ return this[key];
10
+ }
11
+ var __toESMCache_node;
12
+ var __toESMCache_esm;
8
13
  var __toESM = (mod, isNodeMode, target) => {
14
+ var canCache = mod != null && typeof mod === "object";
15
+ if (canCache) {
16
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
+ var cached = cache.get(mod);
18
+ if (cached)
19
+ return cached;
20
+ }
9
21
  target = mod != null ? __create(__getProtoOf(mod)) : {};
10
22
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
23
  for (let key of __getOwnPropNames(mod))
12
24
  if (!__hasOwnProp.call(to, key))
13
25
  __defProp(to, key, {
14
- get: () => mod[key],
26
+ get: __accessProp.bind(mod, key),
15
27
  enumerable: true
16
28
  });
29
+ if (canCache)
30
+ cache.set(mod, to);
17
31
  return to;
18
32
  };
19
33
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
@@ -28555,11 +28569,11 @@ function registerCssCommand(program2) {
28555
28569
  }
28556
28570
 
28557
28571
  // src/cli/commands/generate.ts
28558
- import path10 from "path";
28572
+ import path11 from "path";
28559
28573
 
28560
28574
  // src/site-generator.ts
28561
28575
  var import_slugify = __toESM(require_slugify(), 1);
28562
- import path9 from "path";
28576
+ import path10 from "path";
28563
28577
 
28564
28578
  // src/parser.ts
28565
28579
  import path5 from "path";
@@ -33003,6 +33017,7 @@ var import_sanitize_html = __toESM(require_sanitize_html(), 1);
33003
33017
 
33004
33018
  // src/utils/markdown/constants.ts
33005
33019
  var RELATIVE_LINK_REGEX = /^(\.\.\/)+(\d{4})\/([a-zA-Z0-9_-]+?)(?:\.md)?(?:\/)?(#[^#]*)?$/;
33020
+ var SAME_DIR_LINK_REGEX = /^\.\/([a-zA-Z0-9_-]+?)(?:\.md)?(?:\/)?(#[^#]*)?$/;
33006
33021
  var IMAGE_PATH_REGEX = /^\.\.\/\.\.\/assets\/(\d{4})\/([^/]+)\/(.+)$/;
33007
33022
  var IMAGE_PATH_ASSETS_DIR = /^\.\.\/\_assets\/(.+)$/;
33008
33023
  var IMAGE_PATH_ASSETS_SAME_DIR = /^\.\/\_assets\/(.+)$/;
@@ -33113,6 +33128,11 @@ function createMarked(cdnConfig) {
33113
33128
  const [, , year, slug, anchor = ""] = relativeMatch;
33114
33129
  token.href = `/${year}/${slug}/${anchor}`;
33115
33130
  }
33131
+ const sameDirMatch = token.href.match(SAME_DIR_LINK_REGEX);
33132
+ if (sameDirMatch && cdnConfig?.postYear) {
33133
+ const [, slug, anchor = ""] = sameDirMatch;
33134
+ token.href = `/${cdnConfig.postYear}/${slug}/${anchor}`;
33135
+ }
33116
33136
  const isExternal = token.href && (token.href.startsWith("http://") || token.href.startsWith("https://") || token.href.startsWith("//"));
33117
33137
  if (isExternal) {
33118
33138
  token.isExternalLink = true;
@@ -33136,6 +33156,12 @@ function createMarked(cdnConfig) {
33136
33156
  return markdown2;
33137
33157
  },
33138
33158
  postprocess(html) {
33159
+ if (cdnConfig?.enabled && cdnConfig.postYear) {
33160
+ const year = cdnConfig.postYear;
33161
+ const base = cdnConfig.baseUrl;
33162
+ html = html.replace(/src=(["'])\.\/\_assets\/([^\s"']+)\1/g, (_m, q2, filename) => `src=${q2}${base}/${year}/${filename}${q2}`);
33163
+ html = html.replace(/!\[([^\]]*)\]\(\.\/\_assets\/([^\s)]+)\)/g, (_m, alt, filename) => `<img src="${base}/${year}/${filename}" alt="${alt}" loading="lazy">`);
33164
+ }
33139
33165
  html = html.replace(YOUTUBE_EMBED_REGEX, '<div class="video-container"><iframe src="https://www.youtube.com/embed/$4" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen loading="lazy"></iframe></div>');
33140
33166
  html = html.replace(/<img /g, '<img loading="lazy" ');
33141
33167
  return html.replace(EXTERNAL_LINK_REGEX, (match, protocol, rest) => {
@@ -33252,12 +33278,13 @@ function validateBusinessLocation(business, filePath) {
33252
33278
  suggestion: "Add 'type: Restaurant' (or Market, Park, Hotel, Museum, Cafe, Zoo, etc.) to frontmatter"
33253
33279
  };
33254
33280
  }
33255
- if (!SCHEMA_ORG_PLACE_TYPES.has(loc.type)) {
33281
+ const locType = String(loc.type);
33282
+ if (!SCHEMA_ORG_PLACE_TYPES.has(locType)) {
33256
33283
  const exampleTypes = Array.from(SCHEMA_ORG_PLACE_TYPES).slice(0, 10);
33257
33284
  return {
33258
33285
  file: filePath,
33259
33286
  type: "validation",
33260
- message: `Invalid business type '${loc.type}' in business${locIndex}`,
33287
+ message: `Invalid business type '${locType}' in business${locIndex}`,
33261
33288
  suggestion: `Use a valid Schema.org Place type: ${exampleTypes.join(", ")}, etc.`
33262
33289
  };
33263
33290
  }
@@ -33374,7 +33401,9 @@ async function parseMarkdownFile(filePath, cdnConfig) {
33374
33401
  const slug = getBaseFilename(filePath);
33375
33402
  const pacificDate = toPacificTime(data.date);
33376
33403
  const postYear = getPacificYear(data.date);
33377
- const cdnConfigWithYear = cdnConfig ? { ...cdnConfig, postYear: String(postYear) } : undefined;
33404
+ const yearFromPath = filePath.match(/\/(\d{4})\//)?.[1];
33405
+ const resolvedYear = String(postYear) !== "NaN" ? String(postYear) : yearFromPath;
33406
+ const cdnConfigWithYear = cdnConfig && resolvedYear ? { ...cdnConfig, postYear: resolvedYear } : undefined;
33378
33407
  const sanitizedHtml = convertMarkdownToHtml(content, cdnConfigWithYear);
33379
33408
  const post = {
33380
33409
  title: data.title,
@@ -33413,12 +33442,14 @@ async function parseMarkdownFile(filePath, cdnConfig) {
33413
33442
  };
33414
33443
  return { post, error: null };
33415
33444
  } catch (error) {
33416
- const isYamlError = error?.name === "YAMLException" || error?.message?.includes("YAML") || error?.message?.includes("mapping pair");
33445
+ const msg = error instanceof Error ? error.message : String(error);
33446
+ const name = error instanceof Error ? error.name : "";
33447
+ const isYamlError = name === "YAMLException" || msg.includes("YAML") || msg.includes("mapping pair");
33417
33448
  let suggestion;
33418
33449
  if (isYamlError) {
33419
- if (error?.message?.includes("mapping pair") || error?.message?.includes("colon")) {
33450
+ if (msg.includes("mapping pair") || msg.includes("colon")) {
33420
33451
  suggestion = 'Quote titles/descriptions containing colons (e.g., title: "My Post: A Guide")';
33421
- } else if (error?.message?.includes("multiline key")) {
33452
+ } else if (msg.includes("multiline key")) {
33422
33453
  suggestion = "Remove nested quotes or use single quotes inside double quotes";
33423
33454
  }
33424
33455
  }
@@ -33427,7 +33458,7 @@ async function parseMarkdownFile(filePath, cdnConfig) {
33427
33458
  error: {
33428
33459
  file: filePath,
33429
33460
  type: isYamlError ? "yaml" : "unknown",
33430
- message: error?.message || String(error),
33461
+ message: msg,
33431
33462
  suggestion
33432
33463
  }
33433
33464
  };
@@ -33435,6 +33466,22 @@ async function parseMarkdownFile(filePath, cdnConfig) {
33435
33466
  }
33436
33467
 
33437
33468
  // src/parser.ts
33469
+ function logErrorGroup(label, errors, opts) {
33470
+ if (errors.length === 0)
33471
+ return;
33472
+ console.error(` ${label} (${errors.length}):`);
33473
+ errors.slice(0, opts.limit).forEach((e) => {
33474
+ const msg = opts.showMessage ? `: ${e.message}` : "";
33475
+ console.error(` ${opts.icon} ${e.file}${msg}`);
33476
+ if (opts.showSuggestion && e.suggestion) {
33477
+ console.error(` \uD83D\uDCA1 ${e.suggestion}`);
33478
+ }
33479
+ });
33480
+ if (errors.length > opts.limit) {
33481
+ console.error(` ... and ${errors.length - opts.limit} more`);
33482
+ }
33483
+ console.error("");
33484
+ }
33438
33485
  function detectFileConflicts(files) {
33439
33486
  const errors = [];
33440
33487
  const slugMap = new Map;
@@ -33513,52 +33560,30 @@ async function parseMarkdownDirectory(contentDir, strictMode = false, cdnConfig)
33513
33560
  const missingFieldErrors = errors.filter((e) => e.type === "missing_field");
33514
33561
  const validationErrors = errors.filter((e) => e.type === "validation");
33515
33562
  const otherErrors = errors.filter((e) => e.type !== "yaml" && e.type !== "missing_field" && e.type !== "validation");
33516
- if (yamlErrors.length > 0) {
33517
- console.error(` YAML Parsing Errors (${yamlErrors.length}):`);
33518
- yamlErrors.slice(0, 5).forEach((e) => {
33519
- console.error(` \u274C ${e.file}`);
33520
- if (e.suggestion) {
33521
- console.error(` \uD83D\uDCA1 ${e.suggestion}`);
33522
- }
33523
- });
33524
- if (yamlErrors.length > 5) {
33525
- console.error(` ... and ${yamlErrors.length - 5} more`);
33526
- }
33527
- console.error("");
33528
- }
33529
- if (missingFieldErrors.length > 0) {
33530
- console.error(` Missing Required Fields (${missingFieldErrors.length}):`);
33531
- missingFieldErrors.slice(0, 5).forEach((e) => {
33532
- console.error(` \u26A0\uFE0F ${e.file}: ${e.message}`);
33533
- });
33534
- if (missingFieldErrors.length > 5) {
33535
- console.error(` ... and ${missingFieldErrors.length - 5} more`);
33536
- }
33537
- console.error("");
33538
- }
33539
- if (validationErrors.length > 0) {
33540
- console.error(` Validation Errors (${validationErrors.length}):`);
33541
- validationErrors.slice(0, 5).forEach((e) => {
33542
- console.error(` \u26A0\uFE0F ${e.file}: ${e.message}`);
33543
- if (e.suggestion) {
33544
- console.error(` \uD83D\uDCA1 ${e.suggestion}`);
33545
- }
33546
- });
33547
- if (validationErrors.length > 5) {
33548
- console.error(` ... and ${validationErrors.length - 5} more`);
33549
- }
33550
- console.error("");
33551
- }
33552
- if (otherErrors.length > 0) {
33553
- console.error(` Other Errors (${otherErrors.length}):`);
33554
- otherErrors.slice(0, 3).forEach((e) => {
33555
- console.error(` \u274C ${e.file}: ${e.message}`);
33556
- });
33557
- if (otherErrors.length > 3) {
33558
- console.error(` ... and ${otherErrors.length - 3} more`);
33559
- }
33560
- console.error("");
33561
- }
33563
+ logErrorGroup("YAML Parsing Errors", yamlErrors, {
33564
+ icon: "\u274C",
33565
+ showMessage: false,
33566
+ showSuggestion: true,
33567
+ limit: 5
33568
+ });
33569
+ logErrorGroup("Missing Required Fields", missingFieldErrors, {
33570
+ icon: "\u26A0\uFE0F ",
33571
+ showMessage: true,
33572
+ showSuggestion: false,
33573
+ limit: 5
33574
+ });
33575
+ logErrorGroup("Validation Errors", validationErrors, {
33576
+ icon: "\u26A0\uFE0F ",
33577
+ showMessage: true,
33578
+ showSuggestion: true,
33579
+ limit: 5
33580
+ });
33581
+ logErrorGroup("Other Errors", otherErrors, {
33582
+ icon: "\u274C",
33583
+ showMessage: true,
33584
+ showSuggestion: false,
33585
+ limit: 3
33586
+ });
33562
33587
  console.error(`\uD83D\uDCDD Tip: Fix YAML errors by quoting titles/descriptions with colons`);
33563
33588
  console.error(` Example: title: "My Post: A Guide" (quotes required for colons)
33564
33589
  `);
@@ -34146,6 +34171,19 @@ async function writeHtmlFile(outputDir, relativePath, content) {
34146
34171
  await ensureDir(dir);
34147
34172
  await Bun.write(fullPath, content);
34148
34173
  }
34174
+ async function generateOptionalPage(templateName, context, outputDir, outputPath, label) {
34175
+ try {
34176
+ const html = import_nunjucks.default.render(templateName, context);
34177
+ await writeHtmlFile(outputDir, outputPath, html);
34178
+ console.log(`Generated ${label}`);
34179
+ } catch (error) {
34180
+ if (error instanceof Error && error.message.includes(templateName)) {
34181
+ console.log(`No ${templateName} template found, skipping ${label}`);
34182
+ } else {
34183
+ console.warn(`Error generating ${label}:`, error);
34184
+ }
34185
+ }
34186
+ }
34149
34187
  async function generateIndexPages(site, config, outputDir, pageSize = PAGINATION.DEFAULT_PAGE_SIZE) {
34150
34188
  const totalPages = getTotalPages(site.posts.length, pageSize);
34151
34189
  for (let page = 1;page <= totalPages; page++) {
@@ -34254,35 +34292,10 @@ async function generateYearArchives(site, config, outputDir, pageSize = PAGINATI
34254
34292
  }
34255
34293
  }
34256
34294
  async function generate404Page(config, outputDir) {
34257
- try {
34258
- const notFoundHtml = import_nunjucks.default.render("404.njk", {
34259
- site: config
34260
- });
34261
- await writeHtmlFile(outputDir, "404.html", notFoundHtml);
34262
- console.log("Generated 404.html");
34263
- } catch (error) {
34264
- if (error instanceof Error && error.message.includes("404.njk")) {
34265
- console.log("No 404.njk template found, skipping 404 page generation");
34266
- } else {
34267
- console.warn("Error generating 404 page:", error);
34268
- }
34269
- }
34295
+ await generateOptionalPage("404.njk", { site: config }, outputDir, "404.html", "404.html");
34270
34296
  }
34271
34297
  async function generateMapPage(site, config, outputDir) {
34272
- try {
34273
- const mapHtml = import_nunjucks.default.render("map.njk", {
34274
- site: config,
34275
- posts: site.posts
34276
- });
34277
- await writeHtmlFile(outputDir, "map/index.html", mapHtml);
34278
- console.log("Generated map page");
34279
- } catch (error) {
34280
- if (error instanceof Error && error.message.includes("map.njk")) {
34281
- console.log("No map.njk template found, skipping map page generation");
34282
- } else {
34283
- console.warn("Error generating map page:", error);
34284
- }
34285
- }
34298
+ await generateOptionalPage("map.njk", { site: config, posts: site.posts }, outputDir, "map/index.html", "map page");
34286
34299
  }
34287
34300
 
34288
34301
  // src/generators/assets.ts
@@ -34432,8 +34445,13 @@ function displayMetrics(metrics) {
34432
34445
 
34433
34446
  // src/utils/template-engine.ts
34434
34447
  var import_nunjucks2 = __toESM(require_nunjucks(), 1);
34448
+ import path9 from "path";
34449
+ import { existsSync } from "fs";
34450
+ var _distFragments = path9.join(import.meta.dir, "fragments");
34451
+ var _srcFragments = path9.join(import.meta.dir, "../fragments");
34452
+ var BUNKI_FRAGMENTS_DIR = existsSync(_distFragments) ? _distFragments : _srcFragments;
34435
34453
  function createTemplateEngine(templatesDir, watch = false) {
34436
- const env = import_nunjucks2.default.configure(templatesDir, {
34454
+ const env = import_nunjucks2.default.configure([templatesDir, BUNKI_FRAGMENTS_DIR], {
34437
34455
  autoescape: true,
34438
34456
  watch
34439
34457
  });
@@ -34484,12 +34502,24 @@ class SiteGenerator {
34484
34502
  async initialize() {
34485
34503
  this.metrics.startStage("initialization");
34486
34504
  console.log("Initializing site generator...");
34505
+ const flatAssetsDir = path10.join(process.cwd(), "content", "_assets");
34506
+ try {
34507
+ const stat = await import("fs/promises").then((m3) => m3.stat(flatAssetsDir));
34508
+ if (stat.isDirectory()) {
34509
+ throw new Error(`Build error: content/_assets/ must not exist.
34510
+ Images must be placed in content/{year}/_assets/ (e.g. content/2025/_assets/).
34511
+ Move any files from content/_assets/ into the correct year folder and retry.`);
34512
+ }
34513
+ } catch (err) {
34514
+ if (err.code !== "ENOENT")
34515
+ throw err;
34516
+ }
34487
34517
  await ensureDir(this.options.outputDir);
34488
34518
  if (this.options.config.noFollowExceptions) {
34489
34519
  setNoFollowExceptions(this.options.config.noFollowExceptions);
34490
34520
  }
34491
34521
  let tagDescriptions = {};
34492
- const tagsTomlPath = path9.join(process.cwd(), "src", "tags.toml");
34522
+ const tagsTomlPath = path10.join(process.cwd(), "src", "tags.toml");
34493
34523
  const tagsTomlFile = Bun.file(tagsTomlPath);
34494
34524
  if (await tagsTomlFile.exists()) {
34495
34525
  try {
@@ -34551,8 +34581,8 @@ class SiteGenerator {
34551
34581
  this.metrics.startStage("cssProcessing");
34552
34582
  let cssChanged = true;
34553
34583
  if (this.cache && this.incrementalMode && this.options.config.css) {
34554
- const cssInputPath = path9.resolve(process.cwd(), this.options.config.css.input);
34555
- const cssOutputPath = path9.join(this.options.outputDir, this.options.config.css.output);
34584
+ const cssInputPath = path10.resolve(process.cwd(), this.options.config.css.input);
34585
+ const cssOutputPath = path10.join(this.options.outputDir, this.options.config.css.output);
34556
34586
  const cssOutputExists = await Bun.file(cssOutputPath).exists();
34557
34587
  cssChanged = await hasFileChanged(cssInputPath, this.cache);
34558
34588
  if (!cssChanged && cssOutputExists) {
@@ -34586,19 +34616,19 @@ class SiteGenerator {
34586
34616
  }
34587
34617
  async generateFeeds() {
34588
34618
  const rssContent = generateRSSFeed(this.site, this.options.config);
34589
- await Bun.write(path9.join(this.options.outputDir, "feed.xml"), rssContent);
34619
+ await Bun.write(path10.join(this.options.outputDir, "feed.xml"), rssContent);
34590
34620
  const sitemapContent = generateSitemap(this.site, this.options.config, PAGINATION.DEFAULT_PAGE_SIZE);
34591
- await Bun.write(path9.join(this.options.outputDir, "sitemap.xml"), sitemapContent);
34621
+ await Bun.write(path10.join(this.options.outputDir, "sitemap.xml"), sitemapContent);
34592
34622
  console.log("Generated sitemap.xml");
34593
34623
  const urlCount = this.site.posts.length + Object.keys(this.site.tags).length + 10;
34594
34624
  const sitemapSize = sitemapContent.length;
34595
34625
  if (urlCount > FILES.MAX_SITEMAP_URLS || sitemapSize > FILES.MAX_SITEMAP_SIZE) {
34596
34626
  const sitemapIndexContent = generateSitemapIndex(this.options.config);
34597
- await Bun.write(path9.join(this.options.outputDir, "sitemap_index.xml"), sitemapIndexContent);
34627
+ await Bun.write(path10.join(this.options.outputDir, "sitemap_index.xml"), sitemapIndexContent);
34598
34628
  console.log("Generated sitemap_index.xml");
34599
34629
  }
34600
34630
  const robotsTxtContent = generateRobotsTxt(this.options.config);
34601
- await Bun.write(path9.join(this.options.outputDir, "robots.txt"), robotsTxtContent);
34631
+ await Bun.write(path10.join(this.options.outputDir, "robots.txt"), robotsTxtContent);
34602
34632
  console.log("Generated robots.txt");
34603
34633
  }
34604
34634
  async parseContent() {
@@ -34615,7 +34645,7 @@ class SiteGenerator {
34615
34645
  return posts;
34616
34646
  }
34617
34647
  const allFiles = await findFilesByPattern("**/*.md", this.options.contentDir, true);
34618
- const configPath = path9.join(process.cwd(), "bunki.config.ts");
34648
+ const configPath = path10.join(process.cwd(), "bunki.config.ts");
34619
34649
  const configChanged = await hasConfigChanged(configPath, this.cache);
34620
34650
  if (configChanged) {
34621
34651
  console.log("Config changed, full rebuild required");
@@ -34694,10 +34724,10 @@ var defaultDeps2 = {
34694
34724
  };
34695
34725
  async function handleGenerateCommand(options2, deps = defaultDeps2) {
34696
34726
  try {
34697
- const configPath = path10.resolve(options2.config);
34698
- const contentDir = path10.resolve(options2.content);
34699
- const outputDir = path10.resolve(options2.output);
34700
- const templatesDir = path10.resolve(options2.templates);
34727
+ const configPath = path11.resolve(options2.config);
34728
+ const contentDir = path11.resolve(options2.content);
34729
+ const outputDir = path11.resolve(options2.output);
34730
+ const templatesDir = path11.resolve(options2.templates);
34701
34731
  deps.logger.log("Generating site with:");
34702
34732
  deps.logger.log(`- Config file: ${configPath}`);
34703
34733
  deps.logger.log(`- Content directory: ${contentDir}`);
@@ -34731,11 +34761,11 @@ function registerGenerateCommand(program2) {
34731
34761
  }
34732
34762
 
34733
34763
  // src/utils/image-uploader.ts
34734
- import path12 from "path";
34764
+ import path13 from "path";
34735
34765
 
34736
34766
  // src/utils/s3-uploader.ts
34737
34767
  var {S3Client } = globalThis.Bun;
34738
- import path11 from "path";
34768
+ import path12 from "path";
34739
34769
 
34740
34770
  class S3Uploader {
34741
34771
  s3Config;
@@ -34858,7 +34888,7 @@ class S3Uploader {
34858
34888
  let failedCount = 0;
34859
34889
  const uploadTasks = imageFiles.map((imageFile) => async () => {
34860
34890
  try {
34861
- const imagePath = path11.join(imagesDir, imageFile);
34891
+ const imagePath = path12.join(imagesDir, imageFile);
34862
34892
  const s3Key = keyTransform ? keyTransform(imageFile) : imageFile;
34863
34893
  const file = Bun.file(imagePath);
34864
34894
  if (process.env.BUNKI_DRY_RUN === "true") {} else {
@@ -34892,13 +34922,13 @@ function createUploader(config) {
34892
34922
  }
34893
34923
 
34894
34924
  // src/utils/image-uploader.ts
34895
- var DEFAULT_IMAGES_DIR = path12.join(process.cwd(), "assets");
34896
- var DEFAULT_CONTENT_DIR2 = path12.join(process.cwd(), "content");
34925
+ var DEFAULT_IMAGES_DIR = path13.join(process.cwd(), "assets");
34926
+ var DEFAULT_CONTENT_DIR2 = path13.join(process.cwd(), "content");
34897
34927
  async function uploadImages(options2 = {}) {
34898
34928
  try {
34899
34929
  const contentAssetsMode = options2.contentAssets === true;
34900
34930
  const defaultDir = contentAssetsMode ? DEFAULT_CONTENT_DIR2 : DEFAULT_IMAGES_DIR;
34901
- const imagesDir = path12.resolve(options2.images || defaultDir);
34931
+ const imagesDir = path13.resolve(options2.images || defaultDir);
34902
34932
  if (!await fileExists(imagesDir)) {
34903
34933
  console.log(`Creating images directory at ${imagesDir}...`);
34904
34934
  await ensureDir(imagesDir);
@@ -34950,7 +34980,7 @@ async function uploadImages(options2 = {}) {
34950
34980
  const uploader = createUploader(s3Config);
34951
34981
  const imageUrlMap = await uploader.uploadImages(imagesDir, options2.minYear, keyTransform);
34952
34982
  if (options2.outputJson) {
34953
- const outputFile = path12.resolve(options2.outputJson);
34983
+ const outputFile = path13.resolve(options2.outputJson);
34954
34984
  await Bun.write(outputFile, JSON.stringify(imageUrlMap, null, 2));
34955
34985
  console.log(`Image URL mapping saved to ${outputFile}`);
34956
34986
  }
@@ -34998,7 +35028,7 @@ function registerImagesPushCommand(program2) {
34998
35028
  }
34999
35029
 
35000
35030
  // src/cli/commands/init.ts
35001
- import path13 from "path";
35031
+ import path14 from "path";
35002
35032
  var defaultDependencies = {
35003
35033
  createDefaultConfig,
35004
35034
  ensureDir,
@@ -35008,7 +35038,7 @@ var defaultDependencies = {
35008
35038
  };
35009
35039
  async function handleInitCommand(options2, deps = defaultDependencies) {
35010
35040
  try {
35011
- const configPath = path13.resolve(options2.config);
35041
+ const configPath = path14.resolve(options2.config);
35012
35042
  const configCreated = await deps.createDefaultConfig(configPath);
35013
35043
  if (!configCreated) {
35014
35044
  deps.logger.log(`
@@ -35017,19 +35047,19 @@ Skipped initialization because the config file already exists`);
35017
35047
  }
35018
35048
  deps.logger.log("Creating directory structure...");
35019
35049
  const baseDir = process.cwd();
35020
- const contentDir = path13.join(baseDir, "content");
35021
- const templatesDir = path13.join(baseDir, "templates");
35022
- const stylesDir = path13.join(templatesDir, "styles");
35023
- const publicDir = path13.join(baseDir, "public");
35050
+ const contentDir = path14.join(baseDir, "content");
35051
+ const templatesDir = path14.join(baseDir, "templates");
35052
+ const stylesDir = path14.join(templatesDir, "styles");
35053
+ const publicDir = path14.join(baseDir, "public");
35024
35054
  await deps.ensureDir(contentDir);
35025
35055
  await deps.ensureDir(templatesDir);
35026
35056
  await deps.ensureDir(stylesDir);
35027
35057
  await deps.ensureDir(publicDir);
35028
35058
  for (const [filename, content] of Object.entries(getDefaultTemplates())) {
35029
- await deps.writeFile(path13.join(templatesDir, filename), content);
35059
+ await deps.writeFile(path14.join(templatesDir, filename), content);
35030
35060
  }
35031
- await deps.writeFile(path13.join(stylesDir, "main.css"), getDefaultCss());
35032
- await deps.writeFile(path13.join(contentDir, "welcome.md"), getSamplePost());
35061
+ await deps.writeFile(path14.join(stylesDir, "main.css"), getDefaultCss());
35062
+ await deps.writeFile(path14.join(contentDir, "welcome.md"), getSamplePost());
35033
35063
  deps.logger.log(`
35034
35064
  Initialization complete! Here are the next steps:`);
35035
35065
  deps.logger.log("1. Edit bunki.config.ts to configure your site");
@@ -35049,221 +35079,302 @@ function registerInitCommand(program2, deps = defaultDependencies) {
35049
35079
  function getDefaultTemplates() {
35050
35080
  return {
35051
35081
  "base.njk": String.raw`<!DOCTYPE html>
35052
- <html lang="en">
35053
- <head>
35054
- <meta charset="UTF-8">
35055
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
35056
- <title>{% block title %}{{ site.title }}{% endblock %}</title>
35057
- <meta name="description" content="{% block description %}{{ site.description }}{% endblock %}">
35058
- <link rel="stylesheet" href="/css/style.css">
35059
- {% block head %}{% endblock %}
35060
- </head>
35061
- <body>
35062
- <header>
35063
- <div class="container">
35064
- <h1><a href="/">{{ site.title }}</a></h1>
35065
- <nav>
35066
- <ul>
35067
- <li><a href="/">Home</a></li>
35068
- <li><a href="/tags/">Tags</a></li>
35069
- </ul>
35070
- </nav>
35071
- </div>
35072
- </header>
35073
-
35074
- <main class="container">
35075
- {% block content %}{% endblock %}
35076
- </main>
35077
-
35078
- <footer>
35079
- <div class="container">
35080
- <p>&copy; {{ "now" | date("YYYY") }} {{ site.title }}</p>
35081
- </div>
35082
- </footer>
35083
- </body>
35084
- </html>`,
35082
+ <html lang="en">
35083
+ <head>
35084
+ <meta charset="UTF-8">
35085
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
35086
+ <title>{% block title %}{{ site.title }}{% endblock %}</title>
35087
+ <meta name="description" content="{% block description %}{{ site.description }}{% endblock %}">
35088
+
35089
+ {# Canonical URL #}
35090
+ <link rel="canonical" href="{% block canonical %}{{ site.baseUrl }}/{% endblock %}">
35091
+
35092
+ {# Open Graph meta tags #}
35093
+ <meta property="og:type" content="{% block og_type %}website{% endblock %}">
35094
+ <meta property="og:title" content="{% block og_title %}{{ site.title }}{% endblock %}">
35095
+ <meta property="og:description" content="{% block og_description %}{{ site.description }}{% endblock %}">
35096
+ <meta property="og:url" content="{% block og_url %}{{ site.baseUrl }}/{% endblock %}">
35097
+ <meta property="og:site_name" content="{{ site.title }}">
35098
+ {% block og_image %}{% endblock %}
35099
+
35100
+ {# Twitter Card meta tags #}
35101
+ <meta name="twitter:card" content="{% block twitter_card %}summary{% endblock %}">
35102
+ <meta name="twitter:title" content="{% block twitter_title %}{{ site.title }}{% endblock %}">
35103
+ <meta name="twitter:description" content="{% block twitter_description %}{{ site.description }}{% endblock %}">
35104
+ {% block twitter_image %}{% endblock %}
35105
+
35106
+ <link rel="stylesheet" href="/css/style.css">
35107
+ <link rel="alternate" type="application/rss+xml" title="{{ site.title }} RSS Feed" href="{{ site.baseUrl }}/feed.xml">
35108
+ {% block head %}{% endblock %}
35109
+ </head>
35110
+ <body>
35111
+ <header>
35112
+ <div class="container">
35113
+ <h1><a href="/">{{ site.title }}</a></h1>
35114
+ <nav>
35115
+ <ul>
35116
+ <li><a href="/">Home</a></li>
35117
+ <li><a href="/tags/">Tags</a></li>
35118
+ </ul>
35119
+ </nav>
35120
+ </div>
35121
+ </header>
35122
+
35123
+ <main class="container">
35124
+ {% block content %}{% endblock %}
35125
+ </main>
35126
+
35127
+ <footer>
35128
+ <div class="container">
35129
+ <p>&copy; {{ "now" | date("YYYY") }} {{ site.title }} - Powered by <a href="https://github.com/kahwee/bunki">Bunki</a></p>
35130
+ </div>
35131
+ </footer>
35132
+ </body>
35133
+ </html>`,
35085
35134
  "index.njk": String.raw`{% extends "base.njk" %}
35086
35135
 
35087
- {% block content %}
35088
- <h1>Latest Posts</h1>
35089
-
35090
- {% if posts.length > 0 %}
35091
- <div class="posts">
35092
- {% for post in posts %}
35093
- <article class="post-card">
35094
- <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35095
- <div class="post-meta">
35096
- <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35097
- {% if post.tags.length > 0 %}
35098
- <span class="tags">
35099
- {% for tag in post.tags %}
35100
- <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35101
- {% endfor %}
35102
- </span>
35103
- {% endif %}
35104
- </div>
35105
- <div class="post-excerpt">{{ post.excerpt }}</div>
35106
- <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35107
- </article>
35108
- {% endfor %}
35109
- </div>
35110
-
35111
- {% if pagination.totalPages > 1 %}
35112
- <nav class="pagination">
35113
- {% if pagination.hasPrevPage %}
35114
- <a href="{{ pagination.pagePath }}{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35115
- {% endif %}
35116
-
35117
- {% if pagination.hasNextPage %}
35118
- <a href="{{ pagination.pagePath }}page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
35119
- {% endif %}
35120
-
35121
- <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35122
- </nav>
35123
- {% endif %}
35124
- {% else %}
35125
- <p>No posts yet!</p>
35136
+ {% block canonical %}{{ site.baseUrl }}/{% if pagination.currentPage > 1 %}page/{{ pagination.currentPage }}/{% endif %}{% endblock %}
35137
+ {% block og_url %}{{ site.baseUrl }}/{% if pagination.currentPage > 1 %}page/{{ pagination.currentPage }}/{% endif %}{% endblock %}
35138
+
35139
+ {% block content %}
35140
+ <h1>Latest Posts</h1>
35141
+
35142
+ {% if posts.length > 0 %}
35143
+ <div class="posts">
35144
+ {% for post in posts %}
35145
+ <article class="post-card">
35146
+ <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35147
+ <div class="post-meta">
35148
+ <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35149
+ {% if post.tags.length > 0 %}
35150
+ <span class="tags">
35151
+ {% for tag in post.tags %}
35152
+ <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35153
+ {% endfor %}
35154
+ </span>
35155
+ {% endif %}
35156
+ </div>
35157
+ <div class="post-excerpt">{{ post.excerpt }}</div>
35158
+ <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35159
+ </article>
35160
+ {% endfor %}
35161
+ </div>
35162
+
35163
+ {% if pagination.totalPages > 1 %}
35164
+ <nav class="pagination">
35165
+ {% if pagination.hasPrevPage %}
35166
+ <a href="{{ pagination.pagePath }}{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35167
+ {% endif %}
35168
+ {% if pagination.hasNextPage %}
35169
+ <a href="{{ pagination.pagePath }}page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
35170
+ {% endif %}
35171
+ <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35172
+ </nav>
35126
35173
  {% endif %}
35127
- {% endblock %}`,
35174
+ {% else %}
35175
+ <p>No posts yet.</p>
35176
+ {% endif %}
35177
+ {% endblock %}`,
35128
35178
  "post.njk": String.raw`{% extends "base.njk" %}
35129
35179
 
35130
- {% block title %}{{ post.title }} | {{ site.title }}{% endblock %}
35131
- {% block description %}{{ post.excerpt }}{% endblock %}
35132
-
35133
- {% block content %}
35134
- <article class="post">
35135
- <header class="post-header">
35136
- <h1>{{ post.title }}</h1>
35137
- <div class="post-meta">
35138
- <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35139
- {% if post.tags.length > 0 %}
35140
- <span class="tags">
35141
- {% for tag in post.tags %}
35142
- <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35143
- {% endfor %}
35144
- </span>
35145
- {% endif %}
35146
- </div>
35147
- </header>
35148
-
35149
- <div class="post-content">
35150
- {{ post.html | safe }}
35180
+ {% from "og-image.njk" import og_image, twitter_image %}
35181
+ {% from "json-ld.njk" import blog_posting_schema %}
35182
+
35183
+ {% block title %}{{ post.title }} | {{ site.title }}{% endblock %}
35184
+ {% block description %}{{ post.excerpt }}{% endblock %}
35185
+
35186
+ {% block canonical %}{{ site.baseUrl }}{{ post.url }}{% endblock %}
35187
+
35188
+ {% block og_type %}article{% endblock %}
35189
+ {% block og_title %}{{ post.title }}{% endblock %}
35190
+ {% block og_description %}{{ post.excerpt }}{% endblock %}
35191
+ {% block og_url %}{{ site.baseUrl }}{{ post.url }}{% endblock %}
35192
+ {% block og_image %}{{ og_image(post, site) }}{% endblock %}
35193
+
35194
+ {% block twitter_card %}summary_large_image{% endblock %}
35195
+ {% block twitter_title %}{{ post.title }}{% endblock %}
35196
+ {% block twitter_description %}{{ post.excerpt }}{% endblock %}
35197
+ {% block twitter_image %}{{ twitter_image(post, site) }}{% endblock %}
35198
+
35199
+ {% block head %}
35200
+ {{ blog_posting_schema(post, site) }}
35201
+ {% endblock %}
35202
+
35203
+ {% block content %}
35204
+ <article class="post">
35205
+ <header class="post-header">
35206
+ <h1>{{ post.title }}</h1>
35207
+ <div class="post-meta">
35208
+ <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35209
+ {% if post.tags.length > 0 %}
35210
+ <span class="tags">
35211
+ {% for tag in post.tags %}
35212
+ <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35213
+ {% endfor %}
35214
+ </span>
35215
+ {% endif %}
35151
35216
  </div>
35152
- </article>
35153
- {% endblock %}`,
35154
- "tag.njk": String.raw`{% extends "base.njk" %}
35155
-
35156
- {% block title %}{{ tag.name }} | {{ site.title }}{% endblock %}
35157
- {% block description %}Posts tagged with {{ tag.name }} on {{ site.title }}{% endblock %}
35158
-
35159
- {% block content %}
35160
- <h1>Posts tagged "{{ tag.name }}"</h1>
35161
-
35162
- {% if tag.description %}
35163
- <div class="tag-description">{{ tag.description }}</div>
35164
- {% endif %}
35217
+ </header>
35165
35218
 
35166
- {% if tag.posts.length > 0 %}
35167
- <div class="posts">
35168
- {% for post in tag.posts %}
35169
- <article class="post-card">
35170
- <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35171
- <div class="post-meta">
35172
- <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35173
- </div>
35174
- <div class="post-excerpt">{{ post.excerpt }}</div>
35175
- <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35176
- </article>
35177
- {% endfor %}
35219
+ <div class="post-content">
35220
+ {{ post.html | safe }}
35221
+ </div>
35222
+
35223
+ <footer class="post-footer">
35224
+ <div class="share-buttons">
35225
+ <span class="share-label">Share:</span>
35226
+ <a href="https://twitter.com/intent/tweet?text={{ post.title | urlencode }}&url={{ site.baseUrl }}{{ post.url }}" target="_blank" rel="noopener noreferrer" class="share-button x" aria-label="Share on X">
35227
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
35228
+ </a>
35229
+ <a href="https://www.facebook.com/sharer/sharer.php?u={{ site.baseUrl }}{{ post.url }}" target="_blank" rel="noopener noreferrer" class="share-button facebook" aria-label="Share on Facebook">
35230
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 3.667h-3.533v7.98H9.101z"/></svg>
35231
+ </a>
35232
+ <a href="https://www.linkedin.com/sharing/share-offsite/?url={{ site.baseUrl }}{{ post.url }}" target="_blank" rel="noopener noreferrer" class="share-button linkedin" aria-label="Share on LinkedIn">
35233
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M6.5 21.5h-5v-13h5v13zM4 6.5C2.5 6.5 1.5 5.3 1.5 4s1-2.4 2.5-2.4c1.6 0 2.5 1 2.6 2.5 0 1.4-1 2.5-2.6 2.5zm11.5 6c-1 0-2 1-2 2v7h-5v-13h5V10s1.6-1.5 4-1.5c3 0 5 2.2 5 6.3v6.7h-5v-7c0-1-1-2-2-2z"/></svg>
35234
+ </a>
35235
+ <a href="mailto:?subject={{ post.title | urlencode }}&body=Check%20out%20this%20article%3A%20{{ site.baseUrl }}{{ post.url }}" class="share-button email" aria-label="Share via Email">
35236
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
35237
+ </a>
35178
35238
  </div>
35239
+ </footer>
35240
+ </article>
35241
+ {% endblock %}`,
35242
+ "tag.njk": String.raw`{% extends "base.njk" %}
35179
35243
 
35180
- {% if pagination.totalPages > 1 %}
35181
- <nav class="pagination">
35182
- {% if pagination.hasPrevPage %}
35183
- <a href="{{ pagination.pagePath }}{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35184
- {% endif %}
35185
-
35186
- {% if pagination.hasNextPage %}
35187
- <a href="{{ pagination.pagePath }}page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
35188
- {% endif %}
35189
-
35190
- <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35191
- </nav>
35192
- {% endif %}
35193
- {% else %}
35194
- <p>No posts with this tag yet!</p>
35244
+ {% block title %}{{ tag.name }} | {{ site.title }}{% endblock %}
35245
+ {% block description %}Posts tagged with {{ tag.name }} on {{ site.title }}{% endblock %}
35246
+
35247
+ {% block canonical %}{{ site.baseUrl }}/tags/{{ tag.slug }}/{% if pagination.currentPage > 1 %}page/{{ pagination.currentPage }}/{% endif %}{% endblock %}
35248
+
35249
+ {% block og_title %}{{ tag.name }} | {{ site.title }}{% endblock %}
35250
+ {% block og_description %}Posts tagged with {{ tag.name }} on {{ site.title }}{% endblock %}
35251
+ {% block og_url %}{{ site.baseUrl }}/tags/{{ tag.slug }}/{% if pagination.currentPage > 1 %}page/{{ pagination.currentPage }}/{% endif %}{% endblock %}
35252
+
35253
+ {% block twitter_title %}{{ tag.name }} | {{ site.title }}{% endblock %}
35254
+ {% block twitter_description %}Posts tagged with {{ tag.name }} on {{ site.title }}{% endblock %}
35255
+
35256
+ {% block content %}
35257
+ <h1>Posts tagged "{{ tag.name }}"</h1>
35258
+
35259
+ {% if tag.description %}
35260
+ <div class="tag-description">{{ tag.description }}</div>
35261
+ {% endif %}
35262
+
35263
+ {% if tag.posts.length > 0 %}
35264
+ <div class="posts">
35265
+ {% for post in tag.posts %}
35266
+ <article class="post-card">
35267
+ <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35268
+ <div class="post-meta">
35269
+ <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35270
+ </div>
35271
+ <div class="post-excerpt">{{ post.excerpt }}</div>
35272
+ <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35273
+ </article>
35274
+ {% endfor %}
35275
+ </div>
35276
+
35277
+ {% if pagination.totalPages > 1 %}
35278
+ <nav class="pagination">
35279
+ {% if pagination.hasPrevPage %}
35280
+ <a href="{{ pagination.pagePath }}{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35281
+ {% endif %}
35282
+ {% if pagination.hasNextPage %}
35283
+ <a href="{{ pagination.pagePath }}page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
35284
+ {% endif %}
35285
+ <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35286
+ </nav>
35195
35287
  {% endif %}
35196
- {% endblock %}`,
35288
+ {% else %}
35289
+ <p>No posts with this tag yet.</p>
35290
+ {% endif %}
35291
+ {% endblock %}`,
35197
35292
  "tags.njk": String.raw`{% extends "base.njk" %}
35198
35293
 
35199
- {% block title %}Tags | {{ site.title }}{% endblock %}
35200
- {% block description %}Browse all tags on {{ site.title }}{% endblock %}
35294
+ {% block title %}Tags | {{ site.title }}{% endblock %}
35295
+ {% block description %}Browse all tags on {{ site.title }}{% endblock %}
35201
35296
 
35202
- {% block content %}
35203
- <h1>All Tags</h1>
35297
+ {% block canonical %}{{ site.baseUrl }}/tags/{% endblock %}
35204
35298
 
35205
- {% if tags.length > 0 %}
35206
- <ul class="tags-list">
35207
- {% for tag in tags %}
35208
- <li>
35209
- <a href="/tags/{{ tag.slug }}/">{{ tag.name }}</a>
35210
- <span class="count">({{ tag.count }})</span>
35211
- {% if tag.description %}
35212
- <p class="description">{{ tag.description }}</p>
35213
- {% endif %}
35214
- </li>
35215
- {% endfor %}
35216
- </ul>
35217
- {% else %}
35218
- <p>No tags found!</p>
35219
- {% endif %}
35220
- {% endblock %}`,
35221
- "archive.njk": String.raw`{% extends "base.njk" %}
35299
+ {% block og_title %}Tags | {{ site.title }}{% endblock %}
35300
+ {% block og_description %}Browse all tags on {{ site.title }}{% endblock %}
35301
+ {% block og_url %}{{ site.baseUrl }}/tags/{% endblock %}
35222
35302
 
35223
- {% block title %}Archive {{ year }} | {{ site.title }}{% endblock %}
35224
- {% block description %}Posts from {{ year }} on {{ site.title }}{% endblock %}
35225
-
35226
- {% block content %}
35227
- <h1>Posts from {{ year }}</h1>
35228
-
35229
- {% if posts.length > 0 %}
35230
- <div class="posts">
35231
- {% for post in posts %}
35232
- <article class="post-card">
35233
- <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35234
- <div class="post-meta">
35235
- <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35236
- {% if post.tags.length > 0 %}
35237
- <span class="tags">
35238
- {% for tag in post.tags %}
35239
- <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35240
- {% endfor %}
35241
- </span>
35242
- {% endif %}
35243
- </div>
35244
- <div class="post-excerpt">{{ post.excerpt }}</div>
35245
- <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35246
- </article>
35247
- {% endfor %}
35248
- </div>
35303
+ {% block twitter_title %}Tags | {{ site.title }}{% endblock %}
35304
+ {% block twitter_description %}Browse all tags on {{ site.title }}{% endblock %}
35249
35305
 
35250
- {% if pagination.totalPages > 1 %}
35251
- <nav class="pagination">
35252
- {% if pagination.hasPrevPage %}
35253
- <a href="/{{ year }}/{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35254
- {% endif %}
35306
+ {% block content %}
35307
+ <h1>All Tags</h1>
35255
35308
 
35256
- {% if pagination.hasNextPage %}
35257
- <a href="/{{ year }}/page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
35309
+ {% if tags.length > 0 %}
35310
+ <ul class="tags-list">
35311
+ {% for tag in tags %}
35312
+ <li>
35313
+ <a href="/tags/{{ tag.slug }}/">{{ tag.name }}</a>
35314
+ <span class="count">({{ tag.count }})</span>
35315
+ {% if tag.description %}
35316
+ <p class="description">{{ tag.description }}</p>
35258
35317
  {% endif %}
35318
+ </li>
35319
+ {% endfor %}
35320
+ </ul>
35321
+ {% else %}
35322
+ <p>No tags yet.</p>
35323
+ {% endif %}
35324
+ {% endblock %}`,
35325
+ "archive.njk": String.raw`{% extends "base.njk" %}
35259
35326
 
35260
- <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35261
- </nav>
35262
- {% endif %}
35263
- {% else %}
35264
- <p>No posts from {{ year }}!</p>
35327
+ {% block title %}Archive {{ year }} | {{ site.title }}{% endblock %}
35328
+ {% block description %}Posts from {{ year }} on {{ site.title }}{% endblock %}
35329
+
35330
+ {% block canonical %}{{ site.baseUrl }}/{{ year }}/{% if pagination.currentPage > 1 %}page/{{ pagination.currentPage }}/{% endif %}{% endblock %}
35331
+
35332
+ {% block og_title %}Archive {{ year }} | {{ site.title }}{% endblock %}
35333
+ {% block og_description %}Posts from {{ year }} on {{ site.title }}{% endblock %}
35334
+ {% block og_url %}{{ site.baseUrl }}/{{ year }}/{% if pagination.currentPage > 1 %}page/{{ pagination.currentPage }}/{% endif %}{% endblock %}
35335
+
35336
+ {% block twitter_title %}Archive {{ year }} | {{ site.title }}{% endblock %}
35337
+ {% block twitter_description %}Posts from {{ year }} on {{ site.title }}{% endblock %}
35338
+
35339
+ {% block content %}
35340
+ <h1>Posts from {{ year }}</h1>
35341
+
35342
+ {% if posts.length > 0 %}
35343
+ <div class="posts">
35344
+ {% for post in posts %}
35345
+ <article class="post-card">
35346
+ <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35347
+ <div class="post-meta">
35348
+ <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35349
+ {% if post.tags.length > 0 %}
35350
+ <span class="tags">
35351
+ {% for tag in post.tags %}
35352
+ <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35353
+ {% endfor %}
35354
+ </span>
35355
+ {% endif %}
35356
+ </div>
35357
+ <div class="post-excerpt">{{ post.excerpt }}</div>
35358
+ <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35359
+ </article>
35360
+ {% endfor %}
35361
+ </div>
35362
+
35363
+ {% if pagination.totalPages > 1 %}
35364
+ <nav class="pagination">
35365
+ {% if pagination.hasPrevPage %}
35366
+ <a href="/{{ year }}/{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35367
+ {% endif %}
35368
+ {% if pagination.hasNextPage %}
35369
+ <a href="/{{ year }}/page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
35370
+ {% endif %}
35371
+ <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35372
+ </nav>
35265
35373
  {% endif %}
35266
- {% endblock %}`
35374
+ {% else %}
35375
+ <p>No posts from {{ year }} yet.</p>
35376
+ {% endif %}
35377
+ {% endblock %}`
35267
35378
  };
35268
35379
  }
35269
35380
  function getDefaultCss() {
@@ -35458,6 +35569,43 @@ function getDefaultCss() {
35458
35569
  font-size: 0.9rem;
35459
35570
  }
35460
35571
 
35572
+ /* Share buttons */
35573
+ .post-footer {
35574
+ margin-top: 2rem;
35575
+ padding-top: 1.5rem;
35576
+ border-top: 1px solid #eee;
35577
+ }
35578
+
35579
+ .share-buttons {
35580
+ display: flex;
35581
+ align-items: center;
35582
+ gap: 0.75rem;
35583
+ }
35584
+
35585
+ .share-label {
35586
+ font-size: 0.9rem;
35587
+ font-weight: 500;
35588
+ color: #6c757d;
35589
+ }
35590
+
35591
+ .share-button {
35592
+ display: inline-flex;
35593
+ align-items: center;
35594
+ justify-content: center;
35595
+ width: 2.25rem;
35596
+ height: 2.25rem;
35597
+ border-radius: 50%;
35598
+ background-color: #f5f5f5;
35599
+ color: #555;
35600
+ transition: background-color 0.2s, color 0.2s;
35601
+ }
35602
+
35603
+ .share-button:hover { text-decoration: none; }
35604
+ .share-button.x:hover { background-color: #000; color: #fff; }
35605
+ .share-button.facebook:hover { background-color: #1877f2; color: #fff; }
35606
+ .share-button.linkedin:hover { background-color: #0077b5; color: #fff; }
35607
+ .share-button.email:hover { background-color: #6c757d; color: #fff; }
35608
+
35461
35609
  /* Footer */
35462
35610
  footer {
35463
35611
  text-align: center;
@@ -35521,7 +35669,7 @@ function hello() {
35521
35669
  }
35522
35670
 
35523
35671
  // src/cli/commands/new-post.ts
35524
- import path14 from "path";
35672
+ import path15 from "path";
35525
35673
  var defaultDeps4 = {
35526
35674
  writeFile: (filePath, data) => Bun.write(filePath, data),
35527
35675
  now: () => new Date,
@@ -35545,7 +35693,7 @@ async function handleNewCommand(title, options2, deps = defaultDeps4) {
35545
35693
  ` + `# ${title}
35546
35694
 
35547
35695
  `;
35548
- const filePath = path14.join(DEFAULT_CONTENT_DIR, `${slug}.md`);
35696
+ const filePath = path15.join(DEFAULT_CONTENT_DIR, `${slug}.md`);
35549
35697
  await deps.writeFile(filePath, frontmatter);
35550
35698
  deps.logger.log(`Created new post: ${filePath}`);
35551
35699
  return filePath;
@@ -35562,21 +35710,12 @@ function registerNewCommand(program2) {
35562
35710
  }
35563
35711
 
35564
35712
  // src/cli/commands/serve.ts
35565
- import path16 from "path";
35713
+ import path17 from "path";
35566
35714
 
35567
35715
  // src/server.ts
35568
- import fs2 from "fs";
35569
- import path15 from "path";
35716
+ import path16 from "path";
35570
35717
  async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
35571
- try {
35572
- const stats = await fs2.promises.stat(outputDir);
35573
- if (!stats.isDirectory()) {
35574
- const msg = `Error: Output directory ${outputDir} does not exist or is not accessible.`;
35575
- console.error(msg);
35576
- console.log('Try running "bunki generate" first to build your site.');
35577
- throw new Error(msg);
35578
- }
35579
- } catch (error) {
35718
+ if (!await isDirectory(outputDir)) {
35580
35719
  const msg = `Error: Output directory ${outputDir} does not exist or is not accessible.`;
35581
35720
  console.error(msg);
35582
35721
  console.log('Try running "bunki generate" first to build your site.');
@@ -35601,18 +35740,18 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
35601
35740
  let filePath = "";
35602
35741
  if (homePaginationMatch) {
35603
35742
  const pageNumber = homePaginationMatch[1];
35604
- filePath = path15.join(outputDir, "page", pageNumber, "index.html");
35743
+ filePath = path16.join(outputDir, "page", pageNumber, "index.html");
35605
35744
  } else if (tagPaginationMatch) {
35606
35745
  const tagSlug = tagPaginationMatch[1];
35607
35746
  const pageNumber = tagPaginationMatch[2];
35608
- filePath = path15.join(outputDir, "tags", tagSlug, "page", pageNumber, "index.html");
35747
+ filePath = path16.join(outputDir, "tags", tagSlug, "page", pageNumber, "index.html");
35609
35748
  } else if (yearPaginationMatch) {
35610
35749
  const year = yearPaginationMatch[1];
35611
35750
  const pageNumber = yearPaginationMatch[2];
35612
- filePath = path15.join(outputDir, year, "page", pageNumber, "index.html");
35751
+ filePath = path16.join(outputDir, year, "page", pageNumber, "index.html");
35613
35752
  } else {
35614
- const directPath = path15.join(outputDir, pathname);
35615
- const withoutSlash = path15.join(outputDir, pathname + ".html");
35753
+ const directPath = path16.join(outputDir, pathname);
35754
+ const withoutSlash = path16.join(outputDir, pathname + ".html");
35616
35755
  const withHtml = pathname.endsWith(".html") ? directPath : withoutSlash;
35617
35756
  const bunFileDirect = Bun.file(directPath);
35618
35757
  const bunFileHtml = Bun.file(withHtml);
@@ -35621,7 +35760,7 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
35621
35760
  } else if (await bunFileHtml.exists()) {
35622
35761
  filePath = withHtml;
35623
35762
  } else {
35624
- const indexPath = path15.join(outputDir, pathname, "index.html");
35763
+ const indexPath = path16.join(outputDir, pathname, "index.html");
35625
35764
  const bunFileIndex = Bun.file(indexPath);
35626
35765
  if (await bunFileIndex.exists()) {
35627
35766
  filePath = indexPath;
@@ -35635,7 +35774,7 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
35635
35774
  }
35636
35775
  }
35637
35776
  console.log(`Serving file: ${filePath}`);
35638
- const extname = path15.extname(filePath);
35777
+ const extname = path16.extname(filePath);
35639
35778
  let contentType = "text/html";
35640
35779
  switch (extname) {
35641
35780
  case ".js":
@@ -35694,7 +35833,7 @@ var defaultDeps5 = {
35694
35833
  };
35695
35834
  async function handleServeCommand(options2, deps = defaultDeps5) {
35696
35835
  try {
35697
- const outputDir = path16.resolve(options2.output);
35836
+ const outputDir = path17.resolve(options2.output);
35698
35837
  const port = parseInt(options2.port, 10);
35699
35838
  await deps.startServer(outputDir, port);
35700
35839
  } catch (error) {
@@ -35737,7 +35876,7 @@ function registerValidateCommand(program2) {
35737
35876
  }
35738
35877
 
35739
35878
  // src/cli/commands/validate-media.ts
35740
- import { readdirSync, readFileSync, existsSync, statSync } from "fs";
35879
+ import { readdirSync, readFileSync, existsSync as existsSync2, statSync } from "fs";
35741
35880
  import { join, dirname, resolve, basename } from "path";
35742
35881
  var imageExtensions = [".jpg", ".jpeg", ".png", ".webp", ".gif"];
35743
35882
  var videoExtensions = [".mp4", ".webm", ".mov"];
@@ -35745,7 +35884,7 @@ var mediaExtensions = [...imageExtensions, ...videoExtensions];
35745
35884
  async function handleValidateMediaCommand(options2, deps = { logger: console, exit: (code) => process.exit(code) }) {
35746
35885
  const contentDir = options2.contentDir || join(process.cwd(), "content");
35747
35886
  const assetsDir = join(process.cwd(), "assets");
35748
- if (!existsSync(contentDir)) {
35887
+ if (!existsSync2(contentDir)) {
35749
35888
  deps.logger.error(`Content directory not found: ${contentDir}`);
35750
35889
  deps.exit(1);
35751
35890
  }
@@ -35867,7 +36006,7 @@ function validateMedia(contentDir, assetsDir) {
35867
36006
  }
35868
36007
  function getAllMediaFromContentAssets(contentDir) {
35869
36008
  const mediaFiles = [];
35870
- if (!existsSync(contentDir))
36009
+ if (!existsSync2(contentDir))
35871
36010
  return [];
35872
36011
  const years = readdirSync(contentDir).filter((f) => {
35873
36012
  const fullPath = join(contentDir, f);
@@ -35875,7 +36014,7 @@ function getAllMediaFromContentAssets(contentDir) {
35875
36014
  });
35876
36015
  for (const year of years) {
35877
36016
  const assetsDir = join(contentDir, year, "_assets");
35878
- if (!existsSync(assetsDir))
36017
+ if (!existsSync2(assetsDir))
35879
36018
  continue;
35880
36019
  const files = readdirSync(assetsDir);
35881
36020
  for (const file of files) {
@@ -35897,7 +36036,7 @@ function getAllMediaFromContentAssets(contentDir) {
35897
36036
  }
35898
36037
  function getAllMediaFromAssets(assetsDir) {
35899
36038
  const mediaFiles = [];
35900
- if (!existsSync(assetsDir))
36039
+ if (!existsSync2(assetsDir))
35901
36040
  return [];
35902
36041
  const years = readdirSync(assetsDir).filter((f) => {
35903
36042
  const fullPath = join(assetsDir, f);
@@ -35929,7 +36068,7 @@ function getAllMediaFromAssets(assetsDir) {
35929
36068
  function checkMediaReference(markdownFile, lineNumber, mediaPath, type, missingReferences) {
35930
36069
  const markdownDir = dirname(markdownFile);
35931
36070
  const resolvedPath = resolve(markdownDir, mediaPath);
35932
- if (!existsSync(resolvedPath)) {
36071
+ if (!existsSync2(resolvedPath)) {
35933
36072
  missingReferences.push({
35934
36073
  file: markdownFile.replace(process.cwd() + "/", ""),
35935
36074
  line: lineNumber,