bunki 0.16.1 → 0.17.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/index.js CHANGED
@@ -26084,6 +26084,16 @@ async function copyFile(sourcePath, targetPath) {
26084
26084
  // src/utils/markdown-utils.ts
26085
26085
  var import_gray_matter = __toESM(require_gray_matter(), 1);
26086
26086
 
26087
+ // src/utils/date-utils.ts
26088
+ function toPacificTime(date) {
26089
+ return new Date(new Date(date).toLocaleString("en-US", {
26090
+ timeZone: "America/Los_Angeles"
26091
+ }));
26092
+ }
26093
+ function getPacificYear(date) {
26094
+ return toPacificTime(date).getFullYear();
26095
+ }
26096
+
26087
26097
  // node_modules/highlight.js/es/core.js
26088
26098
  var import_core = __toESM(require_core2(), 1);
26089
26099
  var core_default = import_core.default;
@@ -30505,20 +30515,58 @@ function escape(html, encode) {
30505
30515
  return html;
30506
30516
  }
30507
30517
 
30508
- // src/utils/markdown-utils.ts
30518
+ // src/utils/markdown/parser.ts
30509
30519
  var import_sanitize_html = __toESM(require_sanitize_html(), 1);
30510
30520
 
30511
- // src/utils/date-utils.ts
30512
- function toPacificTime(date) {
30513
- return new Date(new Date(date).toLocaleString("en-US", {
30514
- timeZone: "America/Los_Angeles"
30515
- }));
30516
- }
30517
- function getPacificYear(date) {
30518
- return toPacificTime(date).getFullYear();
30519
- }
30521
+ // src/utils/markdown/constants.ts
30522
+ var RELATIVE_LINK_REGEX = /^(\.\.\/)+(\d{4})\/([a-zA-Z0-9_-]+?)(?:\.md)?(?:\/)?(#[^#]*)?$/;
30523
+ var IMAGE_PATH_REGEX = /^\.\.\/\.\.\/assets\/(\d{4})\/([^/]+)\/(.+)$/;
30524
+ var YOUTUBE_EMBED_REGEX = /<a href="(https?:\/\/(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)[^"]*)"[^>]*>(.*?)<\/a>/g;
30525
+ var EXTERNAL_LINK_REGEX = /<a href="(https?:\/\/|\/\/)([^"]+)"/g;
30526
+ var SCHEMA_ORG_PLACE_TYPES = new Set([
30527
+ "Accommodation",
30528
+ "Apartment",
30529
+ "Attraction",
30530
+ "Beach",
30531
+ "BodyOfWater",
30532
+ "Bridge",
30533
+ "Building",
30534
+ "BusStation",
30535
+ "Cafe",
30536
+ "Campground",
30537
+ "CivicStructure",
30538
+ "EventVenue",
30539
+ "Ferry",
30540
+ "Garden",
30541
+ "HistoricalSite",
30542
+ "Hotel",
30543
+ "Hostel",
30544
+ "Landmark",
30545
+ "LodgingBusiness",
30546
+ "Market",
30547
+ "Monument",
30548
+ "Museum",
30549
+ "NaturalFeature",
30550
+ "Park",
30551
+ "Playground",
30552
+ "Restaurant",
30553
+ "ServiceCenter",
30554
+ "ShoppingCenter",
30555
+ "Store",
30556
+ "TouristAttraction",
30557
+ "TrainStation",
30558
+ "Viewpoint",
30559
+ "Zoo"
30560
+ ]);
30561
+ var ALERT_ICONS = {
30562
+ note: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-7-4a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM9 9a.75.75 0 0 0 0 1.5h.253a.25.25 0 0 1 .244.304l-.459 2.066A1.75 1.75 0 0 0 10.747 15H11a.75.75 0 0 0 0-1.5h-.253a.25.25 0 0 1-.244-.304l.459-2.066A1.75 1.75 0 0 0 9.253 9H9Z" clip-rule="evenodd" /></svg>',
30563
+ tip: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1a6 6 0 0 0-3.815 10.631C7.237 12.5 8 13.443 8 14.456v.644a.75.75 0 0 0 .75.75h2.5a.75.75 0 0 0 .75-.75v-.644c0-1.013.762-1.957 1.815-2.825A6 6 0 0 0 10 1ZM8.863 17.414a.75.75 0 0 0-.226 1.483 9.066 9.066 0 0 0 2.726 0 .75.75 0 0 0-.226-1.483 7.553 7.553 0 0 1-2.274 0Z" /></svg>',
30564
+ important: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-8-5a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-1.5 0v-4.5A.75.75 0 0 1 10 5Zm0 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd" /></svg>',
30565
+ warning: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 5Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd" /></svg>',
30566
+ caution: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0ZM8.28 7.22a.75.75 0 0 0-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 1 0 1.06 1.06L10 11.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L11.06 10l1.72-1.72a.75.75 0 0 0-1.06-1.06L10 8.94 8.28 7.22Z" clip-rule="evenodd" /></svg>'
30567
+ };
30520
30568
 
30521
- // src/utils/markdown-utils.ts
30569
+ // src/utils/markdown/parser.ts
30522
30570
  core_default.registerLanguage("javascript", javascript);
30523
30571
  core_default.registerLanguage("typescript", typescript);
30524
30572
  core_default.registerLanguage("markdown", markdown);
@@ -30528,8 +30576,11 @@ core_default.registerLanguage("python", python);
30528
30576
  core_default.registerLanguage("json", json);
30529
30577
  core_default.registerLanguage("swift", swift);
30530
30578
  var noFollowExceptions = new Set;
30579
+ function setNoFollowExceptions(exceptions) {
30580
+ noFollowExceptions = new Set(exceptions.map((domain) => domain.toLowerCase().replace(/^www\./, "")));
30581
+ }
30531
30582
  function transformImagePath(relativePath, config) {
30532
- const match = relativePath.match(/^\.\.\/\.\.\/assets\/(\d{4})\/([^/]+)\/(.+)$/);
30583
+ const match = relativePath.match(IMAGE_PATH_REGEX);
30533
30584
  if (!match)
30534
30585
  return null;
30535
30586
  const [, year, slug, filename] = match;
@@ -30550,33 +30601,20 @@ function createMarked(cdnConfig) {
30550
30601
  breaks: true
30551
30602
  });
30552
30603
  marked.use(A({
30553
- variants: [
30554
- {
30555
- type: "note",
30556
- icon: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-7-4a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM9 9a.75.75 0 0 0 0 1.5h.253a.25.25 0 0 1 .244.304l-.459 2.066A1.75 1.75 0 0 0 10.747 15H11a.75.75 0 0 0 0-1.5h-.253a.25.25 0 0 1-.244-.304l.459-2.066A1.75 1.75 0 0 0 9.253 9H9Z" clip-rule="evenodd" /></svg>'
30557
- },
30558
- {
30559
- type: "tip",
30560
- icon: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1a6 6 0 0 0-3.815 10.631C7.237 12.5 8 13.443 8 14.456v.644a.75.75 0 0 0 .75.75h2.5a.75.75 0 0 0 .75-.75v-.644c0-1.013.762-1.957 1.815-2.825A6 6 0 0 0 10 1ZM8.863 17.414a.75.75 0 0 0-.226 1.483 9.066 9.066 0 0 0 2.726 0 .75.75 0 0 0-.226-1.483 7.553 7.553 0 0 1-2.274 0Z" /></svg>'
30561
- },
30562
- {
30563
- type: "important",
30564
- icon: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-8-5a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-1.5 0v-4.5A.75.75 0 0 1 10 5Zm0 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd" /></svg>'
30565
- },
30566
- {
30567
- type: "warning",
30568
- icon: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 5Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd" /></svg>'
30569
- },
30570
- {
30571
- type: "caution",
30572
- icon: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0ZM8.28 7.22a.75.75 0 0 0-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 1 0 1.06 1.06L10 11.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L11.06 10l1.72-1.72a.75.75 0 0 0-1.06-1.06L10 8.94 8.28 7.22Z" clip-rule="evenodd" /></svg>'
30573
- }
30574
- ]
30604
+ variants: Object.entries(ALERT_ICONS).map(([type, icon]) => ({
30605
+ type,
30606
+ icon
30607
+ }))
30575
30608
  }));
30576
30609
  marked.use({
30577
30610
  walkTokens(token) {
30578
30611
  if (token.type === "link") {
30579
30612
  token.href = token.href || "";
30613
+ const relativeMatch = token.href.match(RELATIVE_LINK_REGEX);
30614
+ if (relativeMatch) {
30615
+ const [, , year, slug, anchor = ""] = relativeMatch;
30616
+ token.href = `/${year}/${slug}/${anchor}`;
30617
+ }
30580
30618
  const isExternal = token.href && (token.href.startsWith("http://") || token.href.startsWith("https://") || token.href.startsWith("//"));
30581
30619
  if (isExternal) {
30582
30620
  token.isExternalLink = true;
@@ -30600,9 +30638,9 @@ function createMarked(cdnConfig) {
30600
30638
  return markdown2;
30601
30639
  },
30602
30640
  postprocess(html) {
30603
- html = html.replace(/<a href="(https?:\/\/(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)[^"]*)"[^>]*>(.*?)<\/a>/g, '<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>');
30641
+ 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>');
30604
30642
  html = html.replace(/<img /g, '<img loading="lazy" ');
30605
- return html.replace(/<a href="(https?:\/\/|\/\/)([^"]+)"/g, (match, protocol, rest) => {
30643
+ return html.replace(EXTERNAL_LINK_REGEX, (match, protocol, rest) => {
30606
30644
  const fullUrl = protocol + rest;
30607
30645
  let relAttr = 'rel="noopener noreferrer';
30608
30646
  try {
@@ -30622,23 +30660,9 @@ function createMarked(cdnConfig) {
30622
30660
  });
30623
30661
  return marked;
30624
30662
  }
30625
- var marked = createMarked();
30626
- function setNoFollowExceptions(exceptions) {
30627
- noFollowExceptions = new Set(exceptions.map((domain) => domain.toLowerCase().replace(/^www\./, "")));
30628
- marked = createMarked();
30629
- }
30630
- function extractExcerpt(content, maxLength = 200) {
30631
- const plainText = content.replace(/^#.*$/gm, "").replace(/```[\s\S]*?```/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[*_]{1,2}([^*_]+)[*_]{1,2}/g, "$1").replace(/\n+/g, " ").trim();
30632
- if (plainText.length <= maxLength) {
30633
- return plainText;
30634
- }
30635
- const truncated = plainText.substring(0, maxLength);
30636
- const lastSpace = truncated.lastIndexOf(" ");
30637
- return truncated.substring(0, lastSpace) + "...";
30638
- }
30639
30663
  function convertMarkdownToHtml(markdownContent, cdnConfig) {
30640
- const markedInstance = cdnConfig ? createMarked(cdnConfig) : marked;
30641
- const html = markedInstance.parse(markdownContent, { async: false });
30664
+ const marked = createMarked(cdnConfig);
30665
+ const html = marked.parse(markdownContent, { async: false });
30642
30666
  let sanitized = import_sanitize_html.default(html, {
30643
30667
  allowedTags: import_sanitize_html.default.defaults.allowedTags.concat([
30644
30668
  "img",
@@ -30704,6 +30728,17 @@ function convertMarkdownToHtml(markdownContent, cdnConfig) {
30704
30728
  sanitized = sanitized.replace(/javascript:/gi, "").replace(/vbscript:/gi, "");
30705
30729
  return sanitized;
30706
30730
  }
30731
+ function extractExcerpt(content, maxLength = 200) {
30732
+ const plainText = content.replace(/^#.*$/gm, "").replace(/```[\s\S]*?```/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[*_]{1,2}([^*_]+)[*_]{1,2}/g, "$1").replace(/\n+/g, " ").trim();
30733
+ if (plainText.length <= maxLength) {
30734
+ return plainText;
30735
+ }
30736
+ const truncated = plainText.substring(0, maxLength);
30737
+ const lastSpace = truncated.lastIndexOf(" ");
30738
+ return truncated.substring(0, lastSpace) + "...";
30739
+ }
30740
+
30741
+ // src/utils/markdown/validators.ts
30707
30742
  function validateBusinessLocation(business, filePath) {
30708
30743
  if (!business)
30709
30744
  return null;
@@ -30719,47 +30754,13 @@ function validateBusinessLocation(business, filePath) {
30719
30754
  suggestion: "Add 'type: Restaurant' (or Market, Park, Hotel, Museum, Cafe, Zoo, etc.) to frontmatter"
30720
30755
  };
30721
30756
  }
30722
- const validTypes = [
30723
- "Accommodation",
30724
- "Apartment",
30725
- "Attraction",
30726
- "Beach",
30727
- "BodyOfWater",
30728
- "Bridge",
30729
- "Building",
30730
- "BusStation",
30731
- "Cafe",
30732
- "Campground",
30733
- "CivicStructure",
30734
- "EventVenue",
30735
- "Ferry",
30736
- "Garden",
30737
- "HistoricalSite",
30738
- "Hotel",
30739
- "Hostel",
30740
- "Landmark",
30741
- "LodgingBusiness",
30742
- "Market",
30743
- "Monument",
30744
- "Museum",
30745
- "NaturalFeature",
30746
- "Park",
30747
- "Playground",
30748
- "Restaurant",
30749
- "ServiceCenter",
30750
- "ShoppingCenter",
30751
- "Store",
30752
- "TouristAttraction",
30753
- "TrainStation",
30754
- "Viewpoint",
30755
- "Zoo"
30756
- ];
30757
- if (!validTypes.includes(loc.type)) {
30757
+ if (!SCHEMA_ORG_PLACE_TYPES.has(loc.type)) {
30758
+ const exampleTypes = Array.from(SCHEMA_ORG_PLACE_TYPES).slice(0, 10);
30758
30759
  return {
30759
30760
  file: filePath,
30760
30761
  type: "validation",
30761
30762
  message: `Invalid business type '${loc.type}' in business${locIndex}`,
30762
- suggestion: `Use a valid Schema.org Place type: ${validTypes.slice(0, 10).join(", ")}, etc.`
30763
+ suggestion: `Use a valid Schema.org Place type: ${exampleTypes.join(", ")}, etc.`
30763
30764
  };
30764
30765
  }
30765
30766
  if (!loc.name) {
@@ -30790,6 +30791,33 @@ function validateBusinessLocation(business, filePath) {
30790
30791
  }
30791
30792
  return null;
30792
30793
  }
30794
+ function validateTags(tags, filePath) {
30795
+ if (!tags || !Array.isArray(tags))
30796
+ return null;
30797
+ const tagsWithSpaces = tags.filter((tag) => tag.includes(" "));
30798
+ if (tagsWithSpaces.length > 0) {
30799
+ return {
30800
+ file: filePath,
30801
+ type: "validation",
30802
+ message: `Tags must not contain spaces. Found: ${tagsWithSpaces.map((t) => `"${t}"`).join(", ")}`,
30803
+ suggestion: `Use hyphens instead of spaces. Example: "new-york-city" instead of "new york city"`
30804
+ };
30805
+ }
30806
+ return null;
30807
+ }
30808
+ function checkDeprecatedLocationField(data, filePath) {
30809
+ if (data && data.location) {
30810
+ return {
30811
+ file: filePath,
30812
+ type: "validation",
30813
+ message: "Use 'business:' instead of deprecated 'location:' field",
30814
+ suggestion: "Replace 'location:' with 'business:' in frontmatter (business requires type, name, lat, lng)"
30815
+ };
30816
+ }
30817
+ return null;
30818
+ }
30819
+
30820
+ // src/utils/markdown-utils.ts
30793
30821
  async function parseMarkdownFile(filePath, cdnConfig) {
30794
30822
  try {
30795
30823
  const fileContent = await readFileAsText(filePath);
@@ -30820,15 +30848,11 @@ async function parseMarkdownFile(filePath, cdnConfig) {
30820
30848
  }
30821
30849
  };
30822
30850
  }
30823
- if (data.location) {
30851
+ const deprecatedFieldError = checkDeprecatedLocationField(data, filePath);
30852
+ if (deprecatedFieldError) {
30824
30853
  return {
30825
30854
  post: null,
30826
- error: {
30827
- file: filePath,
30828
- type: "validation",
30829
- message: "Use 'business:' instead of deprecated 'location:' field",
30830
- suggestion: "Replace 'location:' with 'business:' in frontmatter (business requires type, name, lat, lng)"
30831
- }
30855
+ error: deprecatedFieldError
30832
30856
  };
30833
30857
  }
30834
30858
  if (data.business) {
@@ -30841,20 +30865,15 @@ async function parseMarkdownFile(filePath, cdnConfig) {
30841
30865
  }
30842
30866
  }
30843
30867
  if (data.tags && Array.isArray(data.tags)) {
30844
- const tagsWithSpaces = data.tags.filter((tag) => tag.includes(" "));
30845
- if (tagsWithSpaces.length > 0) {
30868
+ const tagsError = validateTags(data.tags, filePath);
30869
+ if (tagsError) {
30846
30870
  return {
30847
30871
  post: null,
30848
- error: {
30849
- file: filePath,
30850
- type: "validation",
30851
- message: `Tags must not contain spaces. Found: ${tagsWithSpaces.map((t) => `"${t}"`).join(", ")}`,
30852
- suggestion: `Use hyphens instead of spaces. Example: "new-york-city" instead of "new york city"`
30853
- }
30872
+ error: tagsError
30854
30873
  };
30855
30874
  }
30856
30875
  }
30857
- let slug = getBaseFilename(filePath);
30876
+ const slug = getBaseFilename(filePath);
30858
30877
  const sanitizedHtml = convertMarkdownToHtml(content, cdnConfig);
30859
30878
  const pacificDate = toPacificTime(data.date);
30860
30879
  const postYear = getPacificYear(data.date);
@@ -31298,84 +31317,9 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
31298
31317
  return server;
31299
31318
  }
31300
31319
  // src/site-generator.ts
31301
- var import_nunjucks = __toESM(require_nunjucks(), 1);
31320
+ var import_nunjucks2 = __toESM(require_nunjucks(), 1);
31302
31321
  var import_slugify = __toESM(require_slugify(), 1);
31303
- var {Glob: Glob2 } = globalThis.Bun;
31304
- import fs3 from "fs";
31305
- import path6 from "path";
31306
-
31307
- // src/utils/css-processor.ts
31308
- import { spawn } from "child_process";
31309
- import fs2 from "fs";
31310
- import path5 from "path";
31311
- async function processCSS(options2) {
31312
- const { css, projectRoot, outputDir, verbose = false } = options2;
31313
- if (!css.enabled) {
31314
- if (verbose) {
31315
- console.log("CSS processing is disabled");
31316
- }
31317
- return;
31318
- }
31319
- const inputPath = path5.resolve(projectRoot, css.input);
31320
- const outputPath = path5.resolve(outputDir, css.output);
31321
- const postcssConfigPath = css.postcssConfig ? path5.resolve(projectRoot, css.postcssConfig) : path5.resolve(projectRoot, "postcss.config.js");
31322
- try {
31323
- await fs2.promises.access(inputPath);
31324
- } catch (error) {
31325
- throw new Error(`CSS input file not found: ${inputPath}`);
31326
- }
31327
- const outputDirPath = path5.dirname(outputPath);
31328
- await fs2.promises.mkdir(outputDirPath, { recursive: true });
31329
- if (verbose) {
31330
- console.log("\uD83C\uDFA8 Building CSS with PostCSS...");
31331
- console.log(`Input: ${inputPath}`);
31332
- console.log(`Output: ${outputPath}`);
31333
- console.log(`Config: ${postcssConfigPath}`);
31334
- }
31335
- await runPostCSS(inputPath, outputPath, postcssConfigPath, projectRoot, verbose);
31336
- }
31337
- function runPostCSS(inputPath, outputPath, configPath, projectRoot, verbose) {
31338
- return new Promise((resolve, reject) => {
31339
- const args = [
31340
- "postcss",
31341
- inputPath,
31342
- "-o",
31343
- outputPath,
31344
- "--config",
31345
- configPath
31346
- ];
31347
- const postcss = spawn("bunx", args, {
31348
- stdio: verbose ? "inherit" : ["ignore", "pipe", "pipe"],
31349
- cwd: projectRoot
31350
- });
31351
- let errorOutput = "";
31352
- if (!verbose) {
31353
- postcss.stderr?.on("data", (data) => {
31354
- errorOutput += data.toString();
31355
- });
31356
- }
31357
- postcss.on("close", (code) => {
31358
- if (code === 0) {
31359
- if (verbose)
31360
- console.log("\u2705 CSS build completed successfully!");
31361
- return resolve();
31362
- }
31363
- reject(new Error(`PostCSS failed with exit code ${code}: ${errorOutput.trim()}`));
31364
- });
31365
- postcss.on("error", (err) => {
31366
- reject(new Error(`Failed to start PostCSS: ${err.message}`));
31367
- });
31368
- });
31369
- }
31370
- function getDefaultCSSConfig() {
31371
- return {
31372
- input: "templates/styles/main.css",
31373
- output: "css/style.css",
31374
- postcssConfig: "postcss.config.js",
31375
- enabled: true,
31376
- watch: false
31377
- };
31378
- }
31322
+ import path8 from "path";
31379
31323
 
31380
31324
  // src/utils/json-ld.ts
31381
31325
  function generateOrganizationSchema(site) {
@@ -31541,105 +31485,733 @@ function generateHomePageSchemas(options2) {
31541
31485
  return schemas;
31542
31486
  }
31543
31487
 
31544
- // src/site-generator.ts
31545
- class SiteGenerator {
31546
- options;
31547
- site;
31548
- formatRSSDate(date) {
31549
- return toPacificTime(date).toUTCString();
31488
+ // src/utils/xml-builder.ts
31489
+ function escapeXml(text) {
31490
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
31491
+ }
31492
+ function buildSitemapUrl(loc, lastmod, changefreq, priority) {
31493
+ return ` <url>
31494
+ <loc>${loc}</loc>
31495
+ <lastmod>${lastmod}</lastmod>
31496
+ <changefreq>${changefreq}</changefreq>
31497
+ <priority>${priority.toFixed(1)}</priority>
31498
+ </url>
31499
+ `;
31500
+ }
31501
+ function calculateFreshnessPriority(date, basePriority, now = Date.now()) {
31502
+ const ONE_DAY = 24 * 60 * 60 * 1000;
31503
+ const ONE_WEEK = 7 * ONE_DAY;
31504
+ const ONE_MONTH = 30 * ONE_DAY;
31505
+ const postTime = new Date(date).getTime();
31506
+ const age = now - postTime;
31507
+ if (age < ONE_WEEK) {
31508
+ return Math.min(1, basePriority + 0.2);
31509
+ } else if (age < ONE_MONTH) {
31510
+ return Math.min(1, basePriority + 0.1);
31511
+ }
31512
+ return basePriority;
31513
+ }
31514
+ function buildRSSItem(params) {
31515
+ const { title, link, pubDate, description, content, tags, author, image } = params;
31516
+ const categoryTags = tags?.map((tag) => ` <category>${escapeXml(tag)}</category>`).join(`
31517
+ `) || "";
31518
+ let itemXml = ` <item>
31519
+ <title><![CDATA[${title}]]></title>
31520
+ <link>${link}</link>
31521
+ <guid isPermaLink="true">${link}</guid>
31522
+ <pubDate>${pubDate}</pubDate>`;
31523
+ if (author) {
31524
+ itemXml += `
31525
+ <author>${author}</author>`;
31526
+ }
31527
+ let fullDescription = description;
31528
+ if (image) {
31529
+ fullDescription = `<img src="${escapeXml(image)}" alt="" style="max-width:100%; height:auto;" /><br/><br/>${description}`;
31530
+ }
31531
+ itemXml += `
31532
+ <description><![CDATA[${fullDescription}]]></description>`;
31533
+ if (categoryTags) {
31534
+ itemXml += `
31535
+ ${categoryTags}`;
31550
31536
  }
31551
- groupPostsByYear(posts) {
31552
- const postsByYear = {};
31553
- for (const post of posts) {
31554
- const year = getPacificYear(post.date).toString();
31555
- if (!postsByYear[year]) {
31556
- postsByYear[year] = [];
31557
- }
31558
- postsByYear[year].push(post);
31559
- }
31560
- return postsByYear;
31537
+ itemXml += `
31538
+ <content:encoded><![CDATA[${content}]]></content:encoded>`;
31539
+ if (image) {
31540
+ itemXml += `
31541
+ <media:thumbnail url="${escapeXml(image)}" />`;
31542
+ itemXml += `
31543
+ <enclosure url="${escapeXml(image)}" type="image/jpeg" length="0" />`;
31561
31544
  }
31562
- getSortedTags(limit) {
31563
- const sorted = Object.values(this.site.tags).sort((a, b2) => b2.count - a.count);
31564
- return limit ? sorted.slice(0, limit) : sorted;
31545
+ itemXml += `
31546
+ </item>`;
31547
+ return itemXml;
31548
+ }
31549
+
31550
+ // src/utils/pagination.ts
31551
+ function createPagination(items, currentPage, pageSize, pagePath) {
31552
+ const totalItems = items.length;
31553
+ const totalPages = Math.ceil(totalItems / pageSize);
31554
+ return {
31555
+ currentPage,
31556
+ totalPages,
31557
+ hasNextPage: currentPage < totalPages,
31558
+ hasPrevPage: currentPage > 1,
31559
+ nextPage: currentPage < totalPages ? currentPage + 1 : null,
31560
+ prevPage: currentPage > 1 ? currentPage - 1 : null,
31561
+ pageSize,
31562
+ totalItems,
31563
+ pagePath
31564
+ };
31565
+ }
31566
+ function getPaginatedItems(items, page, pageSize) {
31567
+ const startIndex = (page - 1) * pageSize;
31568
+ const endIndex = startIndex + pageSize;
31569
+ return items.slice(startIndex, endIndex);
31570
+ }
31571
+ function getTotalPages(totalItems, pageSize) {
31572
+ return Math.ceil(totalItems / pageSize);
31573
+ }
31574
+
31575
+ // src/generators/feeds.ts
31576
+ function extractFirstImageUrl2(html) {
31577
+ const imgRegex = /<img[^>]+src=["']([^"']+)["']/;
31578
+ const match = html.match(imgRegex);
31579
+ return match ? match[1] : null;
31580
+ }
31581
+ function makeAbsoluteUrl(imageUrl, baseUrl) {
31582
+ return imageUrl.startsWith("http") ? imageUrl : `${baseUrl}${imageUrl}`;
31583
+ }
31584
+ function formatRSSDate(date) {
31585
+ return toPacificTime(date).toUTCString();
31586
+ }
31587
+ function generateRSSFeed(site, config) {
31588
+ const posts = site.posts.slice(0, 15);
31589
+ const now = toPacificTime(new Date);
31590
+ const latestPostDate = posts.length > 0 ? posts[0].date : now.toISOString();
31591
+ const lastBuildDate = formatRSSDate(latestPostDate);
31592
+ const rssItems = posts.map((post) => {
31593
+ const postUrl = `${config.baseUrl}${post.url}`;
31594
+ const pubDate = formatRSSDate(post.date);
31595
+ const featuredImage = extractFirstImageUrl2(post.html);
31596
+ const absoluteImageUrl = featuredImage ? makeAbsoluteUrl(featuredImage, config.baseUrl) : null;
31597
+ const author = config.authorEmail && config.authorName ? `${config.authorEmail} (${config.authorName})` : config.authorEmail || undefined;
31598
+ return buildRSSItem({
31599
+ title: post.title,
31600
+ link: postUrl,
31601
+ pubDate,
31602
+ description: post.excerpt,
31603
+ content: post.html,
31604
+ tags: post.tags,
31605
+ author,
31606
+ image: absoluteImageUrl
31607
+ });
31608
+ }).join(`
31609
+ `);
31610
+ let channelXml = ` <channel>
31611
+ <title><![CDATA[${config.title}]]></title>
31612
+ <link>${config.baseUrl}/</link>
31613
+ <description><![CDATA[${config.description}]]></description>`;
31614
+ const language = config.rssLanguage || "en-US";
31615
+ channelXml += `
31616
+ <language>${language}</language>`;
31617
+ if (config.authorEmail && config.authorName) {
31618
+ channelXml += `
31619
+ <managingEditor>${config.authorEmail} (${config.authorName})</managingEditor>`;
31620
+ } else if (config.authorEmail) {
31621
+ channelXml += `
31622
+ <managingEditor>${config.authorEmail}</managingEditor>`;
31565
31623
  }
31566
- createPagination(items, currentPage, pageSize, pagePath) {
31567
- const totalItems = items.length;
31568
- const totalPages = Math.ceil(totalItems / pageSize);
31569
- return {
31570
- currentPage,
31571
- totalPages,
31572
- hasNextPage: currentPage < totalPages,
31573
- hasPrevPage: currentPage > 1,
31574
- nextPage: currentPage < totalPages ? currentPage + 1 : null,
31575
- prevPage: currentPage > 1 ? currentPage - 1 : null,
31576
- pageSize,
31577
- totalItems,
31578
- pagePath
31579
- };
31624
+ if (config.webMaster) {
31625
+ channelXml += `
31626
+ <webMaster>${config.webMaster}</webMaster>`;
31580
31627
  }
31581
- constructor(options2) {
31582
- this.options = options2;
31583
- this.site = {
31584
- name: options2.config.domain,
31585
- posts: [],
31586
- tags: {},
31587
- postsByYear: {}
31588
- };
31589
- const env = import_nunjucks.default.configure(this.options.templatesDir, {
31590
- autoescape: true,
31591
- watch: false
31592
- });
31593
- env.addFilter("date", function(date, format) {
31594
- const d2 = toPacificTime(date);
31595
- const months = [
31596
- "January",
31597
- "February",
31598
- "March",
31599
- "April",
31600
- "May",
31601
- "June",
31602
- "July",
31603
- "August",
31604
- "September",
31605
- "October",
31606
- "November",
31607
- "December"
31608
- ];
31609
- if (format === "YYYY") {
31610
- return d2.getFullYear();
31611
- } else if (format === "MMMM D, YYYY") {
31612
- return `${months[d2.getMonth()]} ${d2.getDate()}, ${d2.getFullYear()}`;
31613
- } else if (format === "MMMM D, YYYY h:mm A") {
31614
- const hours = d2.getHours() % 12 || 12;
31615
- const ampm = d2.getHours() >= 12 ? "PM" : "AM";
31616
- return `${months[d2.getMonth()]} ${d2.getDate()}, ${d2.getFullYear()} @ ${hours} ${ampm}`;
31617
- } else {
31618
- return d2.toLocaleDateString("en-US", {
31619
- timeZone: "America/Los_Angeles"
31620
- });
31621
- }
31622
- });
31628
+ if (config.copyright) {
31629
+ channelXml += `
31630
+ <copyright><![CDATA[${config.copyright}]]></copyright>`;
31623
31631
  }
31624
- async initialize() {
31625
- console.log("Initializing site generator...");
31626
- await ensureDir(this.options.outputDir);
31627
- if (this.options.config.noFollowExceptions) {
31628
- setNoFollowExceptions(this.options.config.noFollowExceptions);
31629
- }
31630
- let tagDescriptions = {};
31631
- const tagsTomlPath = path6.join(process.cwd(), "src", "tags.toml");
31632
- const tagsTomlFile = Bun.file(tagsTomlPath);
31633
- if (await tagsTomlFile.exists()) {
31634
- try {
31635
- tagDescriptions = __require(tagsTomlPath);
31636
- console.log("Loaded tag descriptions from tags.toml");
31637
- } catch (error) {
31638
- console.warn("Error loading tag descriptions:", error);
31639
- }
31640
- }
31641
- const strictMode = this.options.config.strictMode ?? false;
31642
- const posts = await parseMarkdownDirectory(this.options.contentDir, strictMode, this.options.config.cdn);
31632
+ channelXml += `
31633
+ <pubDate>${formatRSSDate(latestPostDate)}</pubDate>
31634
+ <lastBuildDate>${lastBuildDate}</lastBuildDate>
31635
+ <atom:link href="${config.baseUrl}/feed.xml" rel="self" type="application/rss+xml" />`;
31636
+ return `<?xml version="1.0" encoding="UTF-8"?>
31637
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/">
31638
+ ${channelXml}
31639
+ ${rssItems}
31640
+ </channel>
31641
+ </rss>`;
31642
+ }
31643
+ function generateSitemap(site, config, pageSize = 10) {
31644
+ const currentDate = toPacificTime(new Date).toISOString();
31645
+ const now = toPacificTime(new Date).getTime();
31646
+ let sitemapContent = `<?xml version="1.0" encoding="UTF-8"?>
31647
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
31648
+ `;
31649
+ sitemapContent += buildSitemapUrl(`${config.baseUrl}/`, currentDate, "daily", 1);
31650
+ const totalHomePages = getTotalPages(site.posts.length, pageSize);
31651
+ for (let page = 2;page <= totalHomePages; page++) {
31652
+ sitemapContent += buildSitemapUrl(`${config.baseUrl}/page/${page}/`, currentDate, "daily", 0.8);
31653
+ }
31654
+ for (const post of site.posts) {
31655
+ const postUrl = `${config.baseUrl}${post.url}`;
31656
+ const postDate = new Date(post.date).toISOString();
31657
+ const priority = calculateFreshnessPriority(post.date, 0.7, now);
31658
+ const age = now - new Date(post.date).getTime();
31659
+ const ONE_MONTH = 30 * 24 * 60 * 60 * 1000;
31660
+ const changefreq = age < ONE_MONTH ? "weekly" : "monthly";
31661
+ sitemapContent += buildSitemapUrl(postUrl, postDate, changefreq, priority);
31662
+ }
31663
+ sitemapContent += buildSitemapUrl(`${config.baseUrl}/tags/`, currentDate, "weekly", 0.5);
31664
+ sitemapContent += buildSitemapUrl(`${config.baseUrl}/map/`, currentDate, "weekly", 0.6);
31665
+ for (const [, tagData] of Object.entries(site.tags)) {
31666
+ const tagUrl = `${config.baseUrl}/tags/${tagData.slug}/`;
31667
+ const mostRecentPost = tagData.posts[0];
31668
+ const tagPriority = mostRecentPost ? calculateFreshnessPriority(mostRecentPost.date, 0.4, now) : 0.4;
31669
+ sitemapContent += buildSitemapUrl(tagUrl, currentDate, "weekly", tagPriority);
31670
+ const totalTagPages = getTotalPages(tagData.posts.length, pageSize);
31671
+ for (let page = 2;page <= totalTagPages; page++) {
31672
+ sitemapContent += buildSitemapUrl(`${config.baseUrl}/tags/${tagData.slug}/page/${page}/`, currentDate, "weekly", Math.max(0.3, tagPriority - 0.1));
31673
+ }
31674
+ }
31675
+ for (const [year, yearPosts] of Object.entries(site.postsByYear)) {
31676
+ const currentYear = new Date().getFullYear();
31677
+ const isCurrentYear = parseInt(year) === currentYear;
31678
+ const yearPriority = isCurrentYear ? 0.7 : 0.5;
31679
+ sitemapContent += buildSitemapUrl(`${config.baseUrl}/${year}/`, currentDate, isCurrentYear ? "weekly" : "monthly", yearPriority);
31680
+ const totalYearPages = getTotalPages(yearPosts.length, pageSize);
31681
+ for (let page = 2;page <= totalYearPages; page++) {
31682
+ sitemapContent += buildSitemapUrl(`${config.baseUrl}/${year}/page/${page}/`, currentDate, isCurrentYear ? "weekly" : "monthly", yearPriority - 0.1);
31683
+ }
31684
+ }
31685
+ sitemapContent += `</urlset>`;
31686
+ return sitemapContent;
31687
+ }
31688
+ function generateSitemapIndex(config) {
31689
+ const currentDate = toPacificTime(new Date).toISOString();
31690
+ return `<?xml version="1.0" encoding="UTF-8"?>
31691
+ <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
31692
+ <sitemap>
31693
+ <loc>${config.baseUrl}/sitemap.xml</loc>
31694
+ <lastmod>${currentDate}</lastmod>
31695
+ </sitemap>
31696
+ </sitemapindex>`;
31697
+ }
31698
+ function generateRobotsTxt(config) {
31699
+ return `# Robots.txt for ${config.domain}
31700
+ # Generated by Bunki
31701
+
31702
+ User-agent: *
31703
+ Allow: /
31704
+
31705
+ # Sitemaps
31706
+ Sitemap: ${config.baseUrl}/sitemap.xml
31707
+
31708
+ # Crawl-delay (optional, adjust as needed)
31709
+ # Crawl-delay: 1
31710
+
31711
+ # Disallow specific paths (uncomment as needed)
31712
+ # Disallow: /private/
31713
+ # Disallow: /admin/
31714
+ # Disallow: /api/
31715
+ `;
31716
+ }
31717
+
31718
+ // src/generators/pages.ts
31719
+ var import_nunjucks = __toESM(require_nunjucks(), 1);
31720
+ import path5 from "path";
31721
+ function getSortedTags(tags, limit) {
31722
+ const sorted = Object.values(tags).sort((a, b2) => b2.count - a.count);
31723
+ return limit ? sorted.slice(0, limit) : sorted;
31724
+ }
31725
+ async function writeHtmlFile(outputDir, relativePath, content) {
31726
+ const fullPath = path5.join(outputDir, relativePath);
31727
+ const dir = path5.dirname(fullPath);
31728
+ await ensureDir(dir);
31729
+ await Bun.write(fullPath, content);
31730
+ }
31731
+ async function generateIndexPages(site, config, outputDir, pageSize = 10) {
31732
+ const totalPages = getTotalPages(site.posts.length, pageSize);
31733
+ for (let page = 1;page <= totalPages; page++) {
31734
+ const paginatedPosts = getPaginatedItems(site.posts, page, pageSize);
31735
+ const pagination = createPagination(site.posts, page, pageSize, "/");
31736
+ let jsonLd = "";
31737
+ if (page === 1) {
31738
+ const schemas = generateHomePageSchemas({ site: config });
31739
+ jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
31740
+ `);
31741
+ }
31742
+ const pageHtml = import_nunjucks.default.render("index.njk", {
31743
+ site: config,
31744
+ posts: paginatedPosts,
31745
+ tags: getSortedTags(site.tags, config.maxTagsOnHomepage),
31746
+ pagination,
31747
+ jsonLd,
31748
+ noindex: page > 2
31749
+ });
31750
+ const outputPath = page === 1 ? "index.html" : `page/${page}/index.html`;
31751
+ await writeHtmlFile(outputDir, outputPath, pageHtml);
31752
+ }
31753
+ }
31754
+ async function generatePostPages(site, config, outputDir) {
31755
+ const batchSize = 10;
31756
+ for (let i = 0;i < site.posts.length; i += batchSize) {
31757
+ const batch = site.posts.slice(i, i + batchSize);
31758
+ await Promise.all(batch.map(async (post) => {
31759
+ const postPath = post.url.substring(1);
31760
+ const imageUrl = extractFirstImageUrl(post.html, config.baseUrl);
31761
+ const schemas = generatePostPageSchemas({
31762
+ post,
31763
+ site: config,
31764
+ imageUrl
31765
+ });
31766
+ const jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
31767
+ `);
31768
+ const postHtml = import_nunjucks.default.render("post.njk", {
31769
+ site: config,
31770
+ post,
31771
+ jsonLd
31772
+ });
31773
+ await writeHtmlFile(outputDir, `${postPath}index.html`, postHtml);
31774
+ }));
31775
+ }
31776
+ }
31777
+ async function generateTagPages(site, config, outputDir, pageSize = 10) {
31778
+ const tagIndexHtml = import_nunjucks.default.render("tags.njk", {
31779
+ site: config,
31780
+ tags: getSortedTags(site.tags)
31781
+ });
31782
+ await writeHtmlFile(outputDir, "tags/index.html", tagIndexHtml);
31783
+ for (const [tagName, tagData] of Object.entries(site.tags)) {
31784
+ const totalPages = getTotalPages(tagData.posts.length, pageSize);
31785
+ for (let page = 1;page <= totalPages; page++) {
31786
+ const paginatedPosts = getPaginatedItems(tagData.posts, page, pageSize);
31787
+ const paginatedTagData = {
31788
+ ...tagData,
31789
+ posts: paginatedPosts
31790
+ };
31791
+ const pagination = createPagination(tagData.posts, page, pageSize, `/tags/${tagData.slug}/`);
31792
+ let jsonLd = "";
31793
+ if (page === 1) {
31794
+ const schemas = [];
31795
+ const description = tagData.description || `Articles tagged with ${tagName}`;
31796
+ schemas.push(generateCollectionPageSchema({
31797
+ title: `${tagName}`,
31798
+ description,
31799
+ url: `${config.baseUrl}/tags/${tagData.slug}/`,
31800
+ posts: tagData.posts,
31801
+ site: config
31802
+ }));
31803
+ schemas.push(generateBreadcrumbListSchema({
31804
+ site: config,
31805
+ items: [
31806
+ { name: "Home", url: `${config.baseUrl}/` },
31807
+ {
31808
+ name: tagName,
31809
+ url: `${config.baseUrl}/tags/${tagData.slug}/`
31810
+ }
31811
+ ]
31812
+ }));
31813
+ jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
31814
+ `);
31815
+ }
31816
+ const tagPageHtml = import_nunjucks.default.render("tag.njk", {
31817
+ site: config,
31818
+ tag: paginatedTagData,
31819
+ tags: Object.values(site.tags),
31820
+ pagination,
31821
+ noindex: page > 2,
31822
+ jsonLd
31823
+ });
31824
+ const outputPath = page === 1 ? `tags/${tagData.slug}/index.html` : `tags/${tagData.slug}/page/${page}/index.html`;
31825
+ await writeHtmlFile(outputDir, outputPath, tagPageHtml);
31826
+ }
31827
+ }
31828
+ }
31829
+ async function generateYearArchives(site, config, outputDir, pageSize = 10) {
31830
+ for (const [year, yearPosts] of Object.entries(site.postsByYear)) {
31831
+ const totalPages = getTotalPages(yearPosts.length, pageSize);
31832
+ for (let page = 1;page <= totalPages; page++) {
31833
+ const paginatedPosts = getPaginatedItems(yearPosts, page, pageSize);
31834
+ const pagination = createPagination(yearPosts, page, pageSize, `/${year}/`);
31835
+ let jsonLd = "";
31836
+ if (page === 1) {
31837
+ const schemas = [];
31838
+ schemas.push(generateCollectionPageSchema({
31839
+ title: `Posts from ${year}`,
31840
+ description: `Articles published in ${year}`,
31841
+ url: `${config.baseUrl}/${year}/`,
31842
+ posts: yearPosts,
31843
+ site: config
31844
+ }));
31845
+ schemas.push(generateBreadcrumbListSchema({
31846
+ site: config,
31847
+ items: [
31848
+ { name: "Home", url: `${config.baseUrl}/` },
31849
+ { name: year, url: `${config.baseUrl}/${year}/` }
31850
+ ]
31851
+ }));
31852
+ jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
31853
+ `);
31854
+ }
31855
+ const yearPageHtml = import_nunjucks.default.render("archive.njk", {
31856
+ site: config,
31857
+ posts: paginatedPosts,
31858
+ tags: getSortedTags(site.tags, config.maxTagsOnHomepage),
31859
+ year,
31860
+ pagination,
31861
+ noindex: page > 2,
31862
+ jsonLd
31863
+ });
31864
+ const outputPath = page === 1 ? `${year}/index.html` : `${year}/page/${page}/index.html`;
31865
+ await writeHtmlFile(outputDir, outputPath, yearPageHtml);
31866
+ }
31867
+ }
31868
+ }
31869
+ async function generate404Page(config, outputDir) {
31870
+ try {
31871
+ const notFoundHtml = import_nunjucks.default.render("404.njk", {
31872
+ site: config
31873
+ });
31874
+ await writeHtmlFile(outputDir, "404.html", notFoundHtml);
31875
+ console.log("Generated 404.html");
31876
+ } catch (error) {
31877
+ if (error instanceof Error && error.message.includes("404.njk")) {
31878
+ console.log("No 404.njk template found, skipping 404 page generation");
31879
+ } else {
31880
+ console.warn("Error generating 404 page:", error);
31881
+ }
31882
+ }
31883
+ }
31884
+ async function generateMapPage(site, config, outputDir) {
31885
+ try {
31886
+ const mapHtml = import_nunjucks.default.render("map.njk", {
31887
+ site: config,
31888
+ posts: site.posts
31889
+ });
31890
+ await writeHtmlFile(outputDir, "map/index.html", mapHtml);
31891
+ console.log("Generated map page");
31892
+ } catch (error) {
31893
+ if (error instanceof Error && error.message.includes("map.njk")) {
31894
+ console.log("No map.njk template found, skipping map page generation");
31895
+ } else {
31896
+ console.warn("Error generating map page:", error);
31897
+ }
31898
+ }
31899
+ }
31900
+
31901
+ // src/generators/assets.ts
31902
+ var {Glob: Glob2 } = globalThis.Bun;
31903
+ import path7 from "path";
31904
+
31905
+ // src/utils/css-processor.ts
31906
+ import { spawn } from "child_process";
31907
+ var {hash } = globalThis.Bun;
31908
+ import path6 from "path";
31909
+ async function processCSS(options2) {
31910
+ const {
31911
+ css,
31912
+ projectRoot,
31913
+ outputDir,
31914
+ verbose = false,
31915
+ enableHashing = false
31916
+ } = options2;
31917
+ if (!css.enabled) {
31918
+ if (verbose) {
31919
+ console.log("CSS processing is disabled");
31920
+ }
31921
+ return { outputPath: "" };
31922
+ }
31923
+ const inputPath = path6.resolve(projectRoot, css.input);
31924
+ const tempOutputPath = path6.resolve(outputDir, css.output);
31925
+ const postcssConfigPath = css.postcssConfig ? path6.resolve(projectRoot, css.postcssConfig) : path6.resolve(projectRoot, "postcss.config.js");
31926
+ const inputFile = Bun.file(inputPath);
31927
+ if (!await inputFile.exists()) {
31928
+ throw new Error(`CSS input file not found: ${inputPath}`);
31929
+ }
31930
+ const outputDirPath = path6.dirname(tempOutputPath);
31931
+ await ensureDir(outputDirPath);
31932
+ if (verbose) {
31933
+ console.log("\uD83C\uDFA8 Building CSS with PostCSS...");
31934
+ console.log(`Input: ${inputPath}`);
31935
+ console.log(`Output: ${tempOutputPath}`);
31936
+ console.log(`Config: ${postcssConfigPath}`);
31937
+ }
31938
+ await runPostCSS(inputPath, tempOutputPath, postcssConfigPath, projectRoot, verbose);
31939
+ if (enableHashing) {
31940
+ const cssFile = Bun.file(tempOutputPath);
31941
+ const cssContent = await cssFile.arrayBuffer();
31942
+ const contentHash = hash(cssContent).toString(36).slice(0, 8);
31943
+ const ext = path6.extname(tempOutputPath);
31944
+ const basename = path6.basename(tempOutputPath, ext);
31945
+ const dir = path6.dirname(tempOutputPath);
31946
+ const hashedFilename = `${basename}.${contentHash}${ext}`;
31947
+ const hashedOutputPath = path6.join(dir, hashedFilename);
31948
+ await Bun.write(hashedOutputPath, cssFile);
31949
+ if (verbose) {
31950
+ console.log(`\u2705 CSS hashed: ${hashedFilename}`);
31951
+ }
31952
+ return {
31953
+ outputPath: hashedOutputPath,
31954
+ hash: contentHash
31955
+ };
31956
+ }
31957
+ return { outputPath: tempOutputPath };
31958
+ }
31959
+ function runPostCSS(inputPath, outputPath, configPath, projectRoot, verbose) {
31960
+ return new Promise((resolve, reject) => {
31961
+ const args = [
31962
+ "postcss",
31963
+ inputPath,
31964
+ "-o",
31965
+ outputPath,
31966
+ "--config",
31967
+ configPath
31968
+ ];
31969
+ const postcss = spawn("bunx", args, {
31970
+ stdio: verbose ? "inherit" : ["ignore", "pipe", "pipe"],
31971
+ cwd: projectRoot
31972
+ });
31973
+ let errorOutput = "";
31974
+ if (!verbose) {
31975
+ postcss.stderr?.on("data", (data) => {
31976
+ errorOutput += data.toString();
31977
+ });
31978
+ }
31979
+ postcss.on("close", (code) => {
31980
+ if (code === 0) {
31981
+ if (verbose)
31982
+ console.log("\u2705 CSS build completed successfully!");
31983
+ return resolve();
31984
+ }
31985
+ reject(new Error(`PostCSS failed with exit code ${code}: ${errorOutput.trim()}`));
31986
+ });
31987
+ postcss.on("error", (err) => {
31988
+ reject(new Error(`Failed to start PostCSS: ${err.message}`));
31989
+ });
31990
+ });
31991
+ }
31992
+ function getDefaultCSSConfig() {
31993
+ return {
31994
+ input: "templates/styles/main.css",
31995
+ output: "css/style.css",
31996
+ postcssConfig: "postcss.config.js",
31997
+ enabled: true,
31998
+ watch: false
31999
+ };
32000
+ }
32001
+
32002
+ // src/generators/assets.ts
32003
+ async function generateStylesheet(config, outputDir) {
32004
+ const cssConfig = config.css || getDefaultCSSConfig();
32005
+ if (!cssConfig.enabled) {
32006
+ console.log("CSS processing is disabled, skipping stylesheet generation.");
32007
+ return;
32008
+ }
32009
+ try {
32010
+ await processCSS({
32011
+ css: cssConfig,
32012
+ projectRoot: process.cwd(),
32013
+ outputDir,
32014
+ verbose: true
32015
+ });
32016
+ } catch (error) {
32017
+ console.error("Error processing CSS:", error);
32018
+ console.log("Falling back to simple CSS file copying...");
32019
+ await fallbackCSSGeneration(cssConfig, outputDir);
32020
+ }
32021
+ }
32022
+ async function fallbackCSSGeneration(cssConfig, outputDir) {
32023
+ const cssFilePath = path7.resolve(process.cwd(), cssConfig.input);
32024
+ const cssFile = Bun.file(cssFilePath);
32025
+ if (!await cssFile.exists()) {
32026
+ console.warn(`CSS input file not found: ${cssFilePath}`);
32027
+ return;
32028
+ }
32029
+ try {
32030
+ const outputPath = path7.resolve(outputDir, cssConfig.output);
32031
+ const outputDirPath = path7.dirname(outputPath);
32032
+ await ensureDir(outputDirPath);
32033
+ await Bun.write(outputPath, cssFile);
32034
+ console.log("\u2705 CSS file copied successfully (fallback mode)");
32035
+ } catch (error) {
32036
+ console.error("Error in fallback CSS generation:", error);
32037
+ }
32038
+ }
32039
+ async function copyStaticAssets(templatesDir, outputDir) {
32040
+ const assetsDir = path7.join(templatesDir, "assets");
32041
+ const publicDir = path7.join(process.cwd(), "public");
32042
+ if (await isDirectory(assetsDir)) {
32043
+ const assetGlob = new Glob2("**/*.*");
32044
+ const assetsOutputDir = path7.join(outputDir, "assets");
32045
+ await ensureDir(assetsOutputDir);
32046
+ for await (const file of assetGlob.scan({
32047
+ cwd: assetsDir,
32048
+ absolute: true
32049
+ })) {
32050
+ const relativePath = path7.relative(assetsDir, file);
32051
+ const targetPath = path7.join(assetsOutputDir, relativePath);
32052
+ const targetDir = path7.dirname(targetPath);
32053
+ await ensureDir(targetDir);
32054
+ await copyFile(file, targetPath);
32055
+ }
32056
+ }
32057
+ if (await isDirectory(publicDir)) {
32058
+ const publicGlob = new Glob2("**/*");
32059
+ for await (const file of publicGlob.scan({
32060
+ cwd: publicDir,
32061
+ absolute: true,
32062
+ dot: true
32063
+ })) {
32064
+ if (await isDirectory(file))
32065
+ continue;
32066
+ const relativePath = path7.relative(publicDir, file);
32067
+ const destPath = path7.join(outputDir, relativePath);
32068
+ const targetDir = path7.dirname(destPath);
32069
+ await ensureDir(targetDir);
32070
+ await copyFile(file, destPath);
32071
+ }
32072
+ console.log("Copied public files to site (including extensionless & dotfiles)");
32073
+ }
32074
+ }
32075
+
32076
+ // src/utils/build-metrics.ts
32077
+ class MetricsCollector {
32078
+ startTime;
32079
+ stageTimings = new Map;
32080
+ currentStage = null;
32081
+ constructor() {
32082
+ this.startTime = performance.now();
32083
+ }
32084
+ startStage(name) {
32085
+ if (this.currentStage) {
32086
+ this.endStage();
32087
+ }
32088
+ this.currentStage = {
32089
+ name,
32090
+ startTime: performance.now()
32091
+ };
32092
+ }
32093
+ endStage() {
32094
+ if (!this.currentStage) {
32095
+ return;
32096
+ }
32097
+ const duration = performance.now() - this.currentStage.startTime;
32098
+ this.stageTimings.set(this.currentStage.name, duration);
32099
+ this.currentStage = null;
32100
+ }
32101
+ getMetrics(outputs) {
32102
+ if (this.currentStage) {
32103
+ this.endStage();
32104
+ }
32105
+ const totalTime = performance.now() - this.startTime;
32106
+ return {
32107
+ totalTime,
32108
+ stages: {
32109
+ initialization: this.stageTimings.get("initialization") || 0,
32110
+ cssProcessing: this.stageTimings.get("cssProcessing") || 0,
32111
+ pageGeneration: this.stageTimings.get("pageGeneration") || 0,
32112
+ feedGeneration: this.stageTimings.get("feedGeneration") || 0,
32113
+ assetCopying: this.stageTimings.get("assetCopying") || 0
32114
+ },
32115
+ outputs
32116
+ };
32117
+ }
32118
+ }
32119
+ function formatBytes(bytes) {
32120
+ if (bytes === 0)
32121
+ return "0 B";
32122
+ const k2 = 1024;
32123
+ const sizes = ["B", "KB", "MB", "GB"];
32124
+ const i = Math.floor(Math.log(bytes) / Math.log(k2));
32125
+ return `${(bytes / Math.pow(k2, i)).toFixed(2)} ${sizes[i]}`;
32126
+ }
32127
+ function displayMetrics(metrics) {
32128
+ console.log(`
32129
+ \uD83D\uDCCA Build Complete in ${metrics.totalTime.toFixed(0)}ms
32130
+ `);
32131
+ console.log("\u23F1\uFE0F Timing Breakdown:");
32132
+ console.log(` Initialization: ${metrics.stages.initialization.toFixed(0)}ms`);
32133
+ console.log(` CSS Processing: ${metrics.stages.cssProcessing.toFixed(0)}ms`);
32134
+ console.log(` Page Generation: ${metrics.stages.pageGeneration.toFixed(0)}ms`);
32135
+ console.log(` Feed Generation: ${metrics.stages.feedGeneration.toFixed(0)}ms`);
32136
+ console.log(` Asset Copying: ${metrics.stages.assetCopying.toFixed(0)}ms`);
32137
+ console.log(`
32138
+ \uD83D\uDCE6 Output:`);
32139
+ console.log(` Posts: ${metrics.outputs.posts}`);
32140
+ console.log(` Pages: ${metrics.outputs.pages}`);
32141
+ console.log(` Total: ${formatBytes(metrics.outputs.totalSize)}
32142
+ `);
32143
+ }
32144
+
32145
+ // src/site-generator.ts
32146
+ class SiteGenerator {
32147
+ options;
32148
+ site;
32149
+ metrics;
32150
+ constructor(options2) {
32151
+ this.options = options2;
32152
+ this.site = {
32153
+ name: options2.config.domain,
32154
+ posts: [],
32155
+ tags: {},
32156
+ postsByYear: {}
32157
+ };
32158
+ this.metrics = new MetricsCollector;
32159
+ const env = import_nunjucks2.default.configure(this.options.templatesDir, {
32160
+ autoescape: true,
32161
+ watch: false,
32162
+ noCache: false
32163
+ });
32164
+ env.addFilter("date", (date, format) => {
32165
+ const d2 = toPacificTime(date);
32166
+ const months = [
32167
+ "January",
32168
+ "February",
32169
+ "March",
32170
+ "April",
32171
+ "May",
32172
+ "June",
32173
+ "July",
32174
+ "August",
32175
+ "September",
32176
+ "October",
32177
+ "November",
32178
+ "December"
32179
+ ];
32180
+ if (format === "YYYY") {
32181
+ return d2.getFullYear();
32182
+ } else if (format === "MMMM D, YYYY") {
32183
+ return `${months[d2.getMonth()]} ${d2.getDate()}, ${d2.getFullYear()}`;
32184
+ } else if (format === "MMMM D, YYYY h:mm A") {
32185
+ const hours = d2.getHours() % 12 || 12;
32186
+ const ampm = d2.getHours() >= 12 ? "PM" : "AM";
32187
+ return `${months[d2.getMonth()]} ${d2.getDate()}, ${d2.getFullYear()} @ ${hours} ${ampm}`;
32188
+ } else {
32189
+ return d2.toLocaleDateString("en-US", {
32190
+ timeZone: "America/Los_Angeles"
32191
+ });
32192
+ }
32193
+ });
32194
+ }
32195
+ async initialize() {
32196
+ this.metrics.startStage("initialization");
32197
+ console.log("Initializing site generator...");
32198
+ await ensureDir(this.options.outputDir);
32199
+ if (this.options.config.noFollowExceptions) {
32200
+ setNoFollowExceptions(this.options.config.noFollowExceptions);
32201
+ }
32202
+ let tagDescriptions = {};
32203
+ const tagsTomlPath = path8.join(process.cwd(), "src", "tags.toml");
32204
+ const tagsTomlFile = Bun.file(tagsTomlPath);
32205
+ if (await tagsTomlFile.exists()) {
32206
+ try {
32207
+ tagDescriptions = __require(tagsTomlPath);
32208
+ console.log("Loaded tag descriptions from tags.toml");
32209
+ } catch (error) {
32210
+ console.warn("Error loading tag descriptions:", error);
32211
+ }
32212
+ }
32213
+ const strictMode = this.options.config.strictMode ?? false;
32214
+ const posts = await parseMarkdownDirectory(this.options.contentDir, strictMode, this.options.config.cdn);
31643
32215
  const tags = {};
31644
32216
  posts.forEach((post) => {
31645
32217
  post.tagSlugs = {};
@@ -31676,571 +32248,87 @@ class SiteGenerator {
31676
32248
  async generate() {
31677
32249
  console.log("Generating static site...");
31678
32250
  await ensureDir(this.options.outputDir);
31679
- await this.generateStylesheet();
32251
+ this.metrics.startStage("cssProcessing");
32252
+ await generateStylesheet(this.options.config, this.options.outputDir);
32253
+ this.metrics.startStage("pageGeneration");
31680
32254
  await Promise.all([
31681
- this.generateIndexPage(),
31682
- this.generatePostPages(),
31683
- this.generateTagPages(),
31684
- this.generateMapPage(),
31685
- this.generateYearArchives(),
31686
- this.generateRSSFeed(),
31687
- this.generateSitemap(),
31688
- this.generateRobotsTxt(),
31689
- this.generate404Page(),
31690
- this.copyStaticAssets()
32255
+ generateIndexPages(this.site, this.options.config, this.options.outputDir),
32256
+ generatePostPages(this.site, this.options.config, this.options.outputDir),
32257
+ generateTagPages(this.site, this.options.config, this.options.outputDir),
32258
+ generateYearArchives(this.site, this.options.config, this.options.outputDir),
32259
+ generateMapPage(this.site, this.options.config, this.options.outputDir),
32260
+ generate404Page(this.options.config, this.options.outputDir)
31691
32261
  ]);
31692
- console.log("Site generation complete!");
31693
- }
31694
- async generate404Page() {
31695
- try {
31696
- const notFoundHtml = import_nunjucks.default.render("404.njk", {
31697
- site: this.options.config
31698
- });
31699
- await Bun.write(path6.join(this.options.outputDir, "404.html"), notFoundHtml);
31700
- console.log("Generated 404.html");
31701
- } catch (error) {
31702
- if (error instanceof Error && error.message.includes("404.njk")) {
31703
- console.log("No 404.njk template found, skipping 404 page generation");
31704
- } else {
31705
- console.warn("Error generating 404 page:", error);
31706
- }
31707
- }
31708
- }
31709
- async generateYearArchives() {
31710
- for (const [year, yearPosts] of Object.entries(this.site.postsByYear)) {
31711
- const yearDir = path6.join(this.options.outputDir, year);
31712
- await ensureDir(yearDir);
31713
- const pageSize = 10;
31714
- const totalPages = Math.ceil(yearPosts.length / pageSize);
31715
- for (let page = 1;page <= totalPages; page++) {
31716
- const startIndex = (page - 1) * pageSize;
31717
- const endIndex = startIndex + pageSize;
31718
- const paginatedPosts = yearPosts.slice(startIndex, endIndex);
31719
- const pagination = this.createPagination(yearPosts, page, pageSize, `/${year}/`);
31720
- let jsonLd = "";
31721
- if (page === 1) {
31722
- const schemas = [];
31723
- schemas.push(generateCollectionPageSchema({
31724
- title: `Posts from ${year}`,
31725
- description: `Articles published in ${year}`,
31726
- url: `${this.options.config.baseUrl}/${year}/`,
31727
- posts: yearPosts,
31728
- site: this.options.config
31729
- }));
31730
- schemas.push(generateBreadcrumbListSchema({
31731
- site: this.options.config,
31732
- items: [
31733
- { name: "Home", url: `${this.options.config.baseUrl}/` },
31734
- { name: year, url: `${this.options.config.baseUrl}/${year}/` }
31735
- ]
31736
- }));
31737
- jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
31738
- `);
31739
- }
31740
- const yearPageHtml = import_nunjucks.default.render("archive.njk", {
31741
- site: this.options.config,
31742
- posts: paginatedPosts,
31743
- tags: this.getSortedTags(this.options.config.maxTagsOnHomepage),
31744
- year,
31745
- pagination,
31746
- noindex: page > 2,
31747
- jsonLd
31748
- });
31749
- if (page === 1) {
31750
- await Bun.write(path6.join(yearDir, "index.html"), yearPageHtml);
31751
- } else {
31752
- const pageDir = path6.join(yearDir, "page", page.toString());
31753
- await ensureDir(pageDir);
31754
- await Bun.write(path6.join(pageDir, "index.html"), yearPageHtml);
31755
- }
31756
- }
31757
- }
31758
- }
31759
- async generateIndexPage() {
32262
+ this.metrics.startStage("assetCopying");
32263
+ await copyStaticAssets(this.options.templatesDir, this.options.outputDir);
32264
+ this.metrics.startStage("feedGeneration");
32265
+ await this.generateFeeds();
32266
+ const outputStats = await this.calculateOutputStats();
32267
+ const buildMetrics = this.metrics.getMetrics(outputStats);
32268
+ displayMetrics(buildMetrics);
32269
+ }
32270
+ async generateFeeds() {
31760
32271
  const pageSize = 10;
31761
- const totalPages = Math.ceil(this.site.posts.length / pageSize);
31762
- for (let page = 1;page <= totalPages; page++) {
31763
- const startIndex = (page - 1) * pageSize;
31764
- const endIndex = startIndex + pageSize;
31765
- const paginatedPosts = this.site.posts.slice(startIndex, endIndex);
31766
- const pagination = this.createPagination(this.site.posts, page, pageSize, "/");
31767
- let jsonLd = "";
31768
- if (page === 1) {
31769
- const schemas = generateHomePageSchemas({
31770
- site: this.options.config
31771
- });
31772
- jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
31773
- `);
31774
- }
31775
- const pageHtml = import_nunjucks.default.render("index.njk", {
31776
- site: this.options.config,
31777
- posts: paginatedPosts,
31778
- tags: this.getSortedTags(this.options.config.maxTagsOnHomepage),
31779
- pagination,
31780
- jsonLd,
31781
- noindex: page > 2
31782
- });
31783
- if (page === 1) {
31784
- await Bun.write(path6.join(this.options.outputDir, "index.html"), pageHtml);
31785
- } else {
31786
- const pageDir = path6.join(this.options.outputDir, "page", page.toString());
31787
- await ensureDir(pageDir);
31788
- await Bun.write(path6.join(pageDir, "index.html"), pageHtml);
31789
- }
31790
- }
31791
- }
31792
- async generatePostPages() {
31793
- for (const post of this.site.posts) {
31794
- const postPath = post.url.substring(1);
31795
- const postDir = path6.join(this.options.outputDir, postPath);
31796
- await ensureDir(postDir);
31797
- const imageUrl = extractFirstImageUrl(post.html, this.options.config.baseUrl);
31798
- const schemas = generatePostPageSchemas({
31799
- post,
31800
- site: this.options.config,
31801
- imageUrl
31802
- });
31803
- const jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
31804
- `);
31805
- const postHtml = import_nunjucks.default.render("post.njk", {
31806
- site: this.options.config,
31807
- post,
31808
- jsonLd
31809
- });
31810
- await Bun.write(path6.join(postDir, "index.html"), postHtml);
31811
- }
31812
- }
31813
- async generateTagPages() {
31814
- const tagsDir = path6.join(this.options.outputDir, "tags");
31815
- await ensureDir(tagsDir);
31816
- const tagIndexHtml = import_nunjucks.default.render("tags.njk", {
31817
- site: this.options.config,
31818
- tags: this.getSortedTags()
31819
- });
31820
- await Bun.write(path6.join(tagsDir, "index.html"), tagIndexHtml);
31821
- for (const [tagName, tagData] of Object.entries(this.site.tags)) {
31822
- const tagDir = path6.join(tagsDir, tagData.slug);
31823
- await ensureDir(tagDir);
31824
- const pageSize = 10;
31825
- const totalPages = Math.ceil(tagData.posts.length / pageSize);
31826
- for (let page = 1;page <= totalPages; page++) {
31827
- const startIndex = (page - 1) * pageSize;
31828
- const endIndex = startIndex + pageSize;
31829
- const paginatedPosts = tagData.posts.slice(startIndex, endIndex);
31830
- const paginatedTagData = {
31831
- ...tagData,
31832
- posts: paginatedPosts
31833
- };
31834
- const pagination = this.createPagination(tagData.posts, page, pageSize, `/tags/${tagData.slug}/`);
31835
- let jsonLd = "";
31836
- if (page === 1) {
31837
- const schemas = [];
31838
- const description = tagData.description || `Articles tagged with ${tagName}`;
31839
- schemas.push(generateCollectionPageSchema({
31840
- title: `${tagName}`,
31841
- description,
31842
- url: `${this.options.config.baseUrl}/tags/${tagData.slug}/`,
31843
- posts: tagData.posts,
31844
- site: this.options.config
31845
- }));
31846
- schemas.push(generateBreadcrumbListSchema({
31847
- site: this.options.config,
31848
- items: [
31849
- { name: "Home", url: `${this.options.config.baseUrl}/` },
31850
- { name: tagName, url: `${this.options.config.baseUrl}/tags/${tagData.slug}/` }
31851
- ]
31852
- }));
31853
- jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
31854
- `);
31855
- }
31856
- const tagPageHtml = import_nunjucks.default.render("tag.njk", {
31857
- site: this.options.config,
31858
- tag: paginatedTagData,
31859
- tags: Object.values(this.site.tags),
31860
- pagination,
31861
- noindex: page > 2,
31862
- jsonLd
31863
- });
31864
- if (page === 1) {
31865
- await Bun.write(path6.join(tagDir, "index.html"), tagPageHtml);
31866
- } else {
31867
- const pageDir = path6.join(tagDir, "page", page.toString());
31868
- await ensureDir(pageDir);
31869
- await Bun.write(path6.join(pageDir, "index.html"), tagPageHtml);
31870
- }
31871
- }
32272
+ const rssContent = generateRSSFeed(this.site, this.options.config);
32273
+ await Bun.write(path8.join(this.options.outputDir, "feed.xml"), rssContent);
32274
+ const sitemapContent = generateSitemap(this.site, this.options.config, pageSize);
32275
+ await Bun.write(path8.join(this.options.outputDir, "sitemap.xml"), sitemapContent);
32276
+ console.log("Generated sitemap.xml");
32277
+ const urlCount = this.site.posts.length + Object.keys(this.site.tags).length + 10;
32278
+ const sitemapSize = sitemapContent.length;
32279
+ if (urlCount > 1000 || sitemapSize > 40000) {
32280
+ const sitemapIndexContent = generateSitemapIndex(this.options.config);
32281
+ await Bun.write(path8.join(this.options.outputDir, "sitemap_index.xml"), sitemapIndexContent);
32282
+ console.log("Generated sitemap_index.xml");
31872
32283
  }
32284
+ const robotsTxtContent = generateRobotsTxt(this.options.config);
32285
+ await Bun.write(path8.join(this.options.outputDir, "robots.txt"), robotsTxtContent);
32286
+ console.log("Generated robots.txt");
31873
32287
  }
31874
- async generateMapPage() {
31875
- try {
31876
- const mapDir = path6.join(this.options.outputDir, "map");
31877
- await ensureDir(mapDir);
31878
- const mapHtml = import_nunjucks.default.render("map.njk", {
31879
- site: this.options.config,
31880
- posts: this.site.posts
31881
- });
31882
- await Bun.write(path6.join(mapDir, "index.html"), mapHtml);
31883
- console.log("Generated map page");
31884
- } catch (error) {
31885
- if (error instanceof Error && error.message.includes("map.njk")) {
31886
- console.log("No map.njk template found, skipping map page generation");
31887
- } else {
31888
- console.warn("Error generating map page:", error);
32288
+ groupPostsByYear(posts) {
32289
+ const postsByYear = {};
32290
+ for (const post of posts) {
32291
+ const year = getPacificYear(post.date).toString();
32292
+ if (!postsByYear[year]) {
32293
+ postsByYear[year] = [];
31889
32294
  }
32295
+ postsByYear[year].push(post);
31890
32296
  }
32297
+ return postsByYear;
31891
32298
  }
31892
- async generateStylesheet() {
31893
- const cssConfig = this.options.config.css || getDefaultCSSConfig();
31894
- if (!cssConfig.enabled) {
31895
- console.log("CSS processing is disabled, skipping stylesheet generation.");
31896
- return;
31897
- }
31898
- try {
31899
- await processCSS({
31900
- css: cssConfig,
31901
- projectRoot: process.cwd(),
31902
- outputDir: this.options.outputDir,
31903
- verbose: true
31904
- });
31905
- } catch (error) {
31906
- console.error("Error processing CSS:", error);
31907
- console.log("Falling back to simple CSS file copying...");
31908
- await this.fallbackCSSGeneration(cssConfig);
31909
- }
31910
- }
31911
- async fallbackCSSGeneration(cssConfig) {
31912
- const cssFilePath = path6.resolve(process.cwd(), cssConfig.input);
31913
- const cssFile = Bun.file(cssFilePath);
31914
- if (!await cssFile.exists()) {
31915
- console.warn(`CSS input file not found: ${cssFilePath}`);
31916
- return;
31917
- }
32299
+ async calculateOutputStats() {
32300
+ const outputDir = this.options.outputDir;
32301
+ let totalSize = 0;
32302
+ let pageCount = 0;
31918
32303
  try {
31919
- const cssContent = await cssFile.text();
31920
- const outputPath = path6.resolve(this.options.outputDir, cssConfig.output);
31921
- const outputDir = path6.dirname(outputPath);
31922
- await ensureDir(outputDir);
31923
- await Bun.write(outputPath, cssContent);
31924
- console.log("\u2705 CSS file copied successfully (fallback mode)");
31925
- } catch (error) {
31926
- console.error("Error in fallback CSS generation:", error);
31927
- }
31928
- }
31929
- async copyStaticAssets() {
31930
- const assetsDir = path6.join(this.options.templatesDir, "assets");
31931
- const publicDir = path6.join(process.cwd(), "public");
31932
- async function dirExists(p) {
31933
- try {
31934
- const stat = await fs3.promises.stat(p);
31935
- return stat.isDirectory();
31936
- } catch {
31937
- return false;
31938
- }
31939
- }
31940
- const assetsDirFile = Bun.file(assetsDir);
31941
- if (await assetsDirFile.exists() && await dirExists(assetsDir)) {
31942
- const assetGlob = new Glob2("**/*.*");
31943
- const assetsOutputDir = path6.join(this.options.outputDir, "assets");
31944
- await ensureDir(assetsOutputDir);
31945
- for await (const file of assetGlob.scan({
31946
- cwd: assetsDir,
32304
+ const { Glob: Glob3 } = await Promise.resolve(globalThis.Bun);
32305
+ const glob = new Glob3("**/*.html");
32306
+ for await (const filePath of glob.scan({
32307
+ cwd: outputDir,
31947
32308
  absolute: true
31948
32309
  })) {
31949
- const relativePath = path6.relative(assetsDir, file);
31950
- const targetPath = path6.join(assetsOutputDir, relativePath);
31951
- const targetDir = path6.dirname(targetPath);
31952
- await ensureDir(targetDir);
31953
- await copyFile(file, targetPath);
31954
- }
31955
- }
31956
- if (await dirExists(publicDir)) {
31957
- const copyRecursive = async (srcDir) => {
31958
- const entries = await fs3.promises.readdir(srcDir, {
31959
- withFileTypes: true
31960
- });
31961
- for (const entry of entries) {
31962
- const srcPath = path6.join(srcDir, entry.name);
31963
- const relativePath = path6.relative(publicDir, srcPath);
31964
- const destPath = path6.join(this.options.outputDir, relativePath);
31965
- if (!relativePath)
31966
- continue;
31967
- if (entry.isDirectory()) {
31968
- await ensureDir(destPath);
31969
- await copyRecursive(srcPath);
31970
- } else if (entry.isFile()) {
31971
- const targetFile = Bun.file(destPath);
31972
- if (!await targetFile.exists()) {
31973
- const targetDir = path6.dirname(destPath);
31974
- await ensureDir(targetDir);
31975
- await copyFile(srcPath, destPath);
31976
- }
31977
- }
31978
- }
31979
- };
31980
- await copyRecursive(publicDir);
31981
- console.log("Copied public files to site (including extensionless & dotfiles)");
31982
- }
31983
- }
31984
- extractFirstImageUrl(html) {
31985
- const imgRegex = /<img[^>]+src=["']([^"']+)["']/;
31986
- const match = html.match(imgRegex);
31987
- return match ? match[1] : null;
31988
- }
31989
- escapeXml(text) {
31990
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
31991
- }
31992
- async generateRSSFeed() {
31993
- const posts = this.site.posts.slice(0, 15);
31994
- const config = this.options.config;
31995
- const now = toPacificTime(new Date);
31996
- const latestPostDate = posts.length > 0 ? posts[0].date : now.toISOString();
31997
- const lastBuildDate = this.formatRSSDate(latestPostDate);
31998
- const rssItems = posts.map((post) => {
31999
- const postUrl = `${config.baseUrl}${post.url}`;
32000
- const pubDate = this.formatRSSDate(post.date);
32001
- const featuredImage = this.extractFirstImageUrl(post.html);
32002
- const categoryTags = post.tags.map((tag) => ` <category>${this.escapeXml(tag)}</category>`).join(`
32003
- `);
32004
- let itemXml = ` <item>
32005
- <title><![CDATA[${post.title}]]></title>
32006
- <link>${postUrl}</link>
32007
- <guid isPermaLink="true">${postUrl}</guid>
32008
- <pubDate>${pubDate}</pubDate>`;
32009
- if (config.authorEmail && config.authorName) {
32010
- itemXml += `
32011
- <author>${config.authorEmail} (${config.authorName})</author>`;
32012
- } else if (config.authorEmail) {
32013
- itemXml += `
32014
- <author>${config.authorEmail}</author>`;
32015
- }
32016
- let description = post.excerpt;
32017
- if (featuredImage) {
32018
- const absoluteImageUrl = featuredImage.startsWith("http") ? featuredImage : `${config.baseUrl}${featuredImage}`;
32019
- description = `<img src="${this.escapeXml(absoluteImageUrl)}" alt="" style="max-width:100%; height:auto;" /><br/><br/>${post.excerpt}`;
32020
- }
32021
- itemXml += `
32022
- <description><![CDATA[${description}]]></description>`;
32023
- if (post.tags.length > 0) {
32024
- itemXml += `
32025
- ${categoryTags}`;
32026
- }
32027
- itemXml += `
32028
- <content:encoded><![CDATA[${post.html}]]></content:encoded>`;
32029
- if (featuredImage) {
32030
- const absoluteImageUrl = featuredImage.startsWith("http") ? featuredImage : `${config.baseUrl}${featuredImage}`;
32031
- itemXml += `
32032
- <media:thumbnail url="${this.escapeXml(absoluteImageUrl)}" />`;
32033
- itemXml += `
32034
- <enclosure url="${this.escapeXml(absoluteImageUrl)}" type="image/jpeg" length="0" />`;
32035
- }
32036
- itemXml += `
32037
- </item>`;
32038
- return itemXml;
32039
- }).join(`
32040
- `);
32041
- let channelXml = ` <channel>
32042
- <title><![CDATA[${config.title}]]></title>
32043
- <link>${config.baseUrl}/</link>
32044
- <description><![CDATA[${config.description}]]></description>`;
32045
- const language = config.rssLanguage || "en-US";
32046
- channelXml += `
32047
- <language>${language}</language>`;
32048
- if (config.authorEmail && config.authorName) {
32049
- channelXml += `
32050
- <managingEditor>${config.authorEmail} (${config.authorName})</managingEditor>`;
32051
- } else if (config.authorEmail) {
32052
- channelXml += `
32053
- <managingEditor>${config.authorEmail}</managingEditor>`;
32054
- }
32055
- if (config.webMaster) {
32056
- channelXml += `
32057
- <webMaster>${config.webMaster}</webMaster>`;
32058
- }
32059
- if (config.copyright) {
32060
- channelXml += `
32061
- <copyright><![CDATA[${config.copyright}]]></copyright>`;
32062
- }
32063
- channelXml += `
32064
- <pubDate>${this.formatRSSDate(latestPostDate)}</pubDate>
32065
- <lastBuildDate>${lastBuildDate}</lastBuildDate>
32066
- <atom:link href="${config.baseUrl}/feed.xml" rel="self" type="application/rss+xml" />`;
32067
- const rssContent = `<?xml version="1.0" encoding="UTF-8"?>
32068
- <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/">
32069
- ${channelXml}
32070
- ${rssItems}
32071
- </channel>
32072
- </rss>`;
32073
- await Bun.write(path6.join(this.options.outputDir, "feed.xml"), rssContent);
32074
- }
32075
- async generateSitemap() {
32076
- const currentDate = toPacificTime(new Date).toISOString();
32077
- const pageSize = 10;
32078
- const config = this.options.config;
32079
- const now = toPacificTime(new Date).getTime();
32080
- const ONE_DAY = 24 * 60 * 60 * 1000;
32081
- const ONE_WEEK = 7 * ONE_DAY;
32082
- const ONE_MONTH = 30 * ONE_DAY;
32083
- const calculatePriority = (date, basePriority) => {
32084
- const postTime = new Date(date).getTime();
32085
- const age = now - postTime;
32086
- if (age < ONE_WEEK) {
32087
- return Math.min(1, basePriority + 0.2);
32088
- } else if (age < ONE_MONTH) {
32089
- return Math.min(1, basePriority + 0.1);
32090
- }
32091
- return basePriority;
32092
- };
32093
- let sitemapContent = `<?xml version="1.0" encoding="UTF-8"?>
32094
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
32095
- `;
32096
- sitemapContent += ` <url>
32097
- <loc>${config.baseUrl}/</loc>
32098
- <lastmod>${currentDate}</lastmod>
32099
- <changefreq>daily</changefreq>
32100
- <priority>1.0</priority>
32101
- </url>
32102
- `;
32103
- const totalHomePages = Math.ceil(this.site.posts.length / pageSize);
32104
- if (totalHomePages > 1) {
32105
- for (let page = 2;page <= totalHomePages; page++) {
32106
- sitemapContent += ` <url>
32107
- <loc>${config.baseUrl}/page/${page}/</loc>
32108
- <lastmod>${currentDate}</lastmod>
32109
- <changefreq>daily</changefreq>
32110
- <priority>0.8</priority>
32111
- </url>
32112
- `;
32113
- }
32114
- }
32115
- for (const post of this.site.posts) {
32116
- const postUrl = `${config.baseUrl}${post.url}`;
32117
- const postDate = new Date(post.date).toISOString();
32118
- const priority = calculatePriority(post.date, 0.7);
32119
- const age = now - new Date(post.date).getTime();
32120
- const changefreq = age < ONE_MONTH ? "weekly" : "monthly";
32121
- sitemapContent += ` <url>
32122
- <loc>${postUrl}</loc>
32123
- <lastmod>${postDate}</lastmod>
32124
- <changefreq>${changefreq}</changefreq>
32125
- <priority>${priority.toFixed(1)}</priority>
32126
- </url>
32127
- `;
32128
- }
32129
- sitemapContent += ` <url>
32130
- <loc>${config.baseUrl}/tags/</loc>
32131
- <lastmod>${currentDate}</lastmod>
32132
- <changefreq>weekly</changefreq>
32133
- <priority>0.5</priority>
32134
- </url>
32135
- `;
32136
- sitemapContent += ` <url>
32137
- <loc>${config.baseUrl}/map/</loc>
32138
- <lastmod>${currentDate}</lastmod>
32139
- <changefreq>weekly</changefreq>
32140
- <priority>0.6</priority>
32141
- </url>
32142
- `;
32143
- for (const [, tagData] of Object.entries(this.site.tags)) {
32144
- const tagUrl = `${config.baseUrl}/tags/${tagData.slug}/`;
32145
- const mostRecentPost = tagData.posts[0];
32146
- const tagPriority = mostRecentPost ? calculatePriority(mostRecentPost.date, 0.4) : 0.4;
32147
- sitemapContent += ` <url>
32148
- <loc>${tagUrl}</loc>
32149
- <lastmod>${currentDate}</lastmod>
32150
- <changefreq>weekly</changefreq>
32151
- <priority>${tagPriority.toFixed(1)}</priority>
32152
- </url>
32153
- `;
32154
- const totalTagPages = Math.ceil(tagData.posts.length / pageSize);
32155
- if (totalTagPages > 1) {
32156
- for (let page = 2;page <= totalTagPages; page++) {
32157
- sitemapContent += ` <url>
32158
- <loc>${config.baseUrl}/tags/${tagData.slug}/page/${page}/</loc>
32159
- <lastmod>${currentDate}</lastmod>
32160
- <changefreq>weekly</changefreq>
32161
- <priority>${Math.max(0.3, tagPriority - 0.1).toFixed(1)}</priority>
32162
- </url>
32163
- `;
32310
+ pageCount++;
32311
+ const stat = await Bun.file(filePath).stat();
32312
+ if (stat) {
32313
+ totalSize += stat.size;
32164
32314
  }
32165
32315
  }
32316
+ } catch (error) {
32317
+ console.warn("Could not calculate output stats:", error);
32166
32318
  }
32167
- for (const [year, yearPosts] of Object.entries(this.site.postsByYear)) {
32168
- const currentYear = new Date().getFullYear();
32169
- const isCurrentYear = parseInt(year) === currentYear;
32170
- const yearPriority = isCurrentYear ? 0.7 : 0.5;
32171
- sitemapContent += ` <url>
32172
- <loc>${config.baseUrl}/${year}/</loc>
32173
- <lastmod>${currentDate}</lastmod>
32174
- <changefreq>${isCurrentYear ? "weekly" : "monthly"}</changefreq>
32175
- <priority>${yearPriority.toFixed(1)}</priority>
32176
- </url>
32177
- `;
32178
- const totalYearPages = Math.ceil(yearPosts.length / pageSize);
32179
- if (totalYearPages > 1) {
32180
- for (let page = 2;page <= totalYearPages; page++) {
32181
- sitemapContent += ` <url>
32182
- <loc>${config.baseUrl}/${year}/page/${page}/</loc>
32183
- <lastmod>${currentDate}</lastmod>
32184
- <changefreq>${isCurrentYear ? "weekly" : "monthly"}</changefreq>
32185
- <priority>${(yearPriority - 0.1).toFixed(1)}</priority>
32186
- </url>
32187
- `;
32188
- }
32189
- }
32190
- }
32191
- sitemapContent += `</urlset>`;
32192
- await Bun.write(path6.join(this.options.outputDir, "sitemap.xml"), sitemapContent);
32193
- console.log("Generated sitemap.xml");
32194
- const urlCount = this.site.posts.length + Object.keys(this.site.tags).length + 10;
32195
- const sitemapSize = sitemapContent.length;
32196
- if (urlCount > 1000 || sitemapSize > 40000) {
32197
- await this.generateSitemapIndex();
32198
- }
32199
- }
32200
- async generateSitemapIndex() {
32201
- const currentDate = toPacificTime(new Date).toISOString();
32202
- const config = this.options.config;
32203
- let sitemapIndexContent = `<?xml version="1.0" encoding="UTF-8"?>
32204
- <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
32205
- `;
32206
- sitemapIndexContent += ` <sitemap>
32207
- <loc>${config.baseUrl}/sitemap.xml</loc>
32208
- <lastmod>${currentDate}</lastmod>
32209
- </sitemap>
32210
- `;
32211
- sitemapIndexContent += `</sitemapindex>`;
32212
- await Bun.write(path6.join(this.options.outputDir, "sitemap_index.xml"), sitemapIndexContent);
32213
- console.log("Generated sitemap_index.xml");
32214
- }
32215
- async generateRobotsTxt() {
32216
- const config = this.options.config;
32217
- const robotsTxtContent = `# Robots.txt for ${config.domain}
32218
- # Generated by Bunki
32219
-
32220
- User-agent: *
32221
- Allow: /
32222
-
32223
- # Sitemaps
32224
- Sitemap: ${config.baseUrl}/sitemap.xml
32225
-
32226
- # Crawl-delay (optional, adjust as needed)
32227
- # Crawl-delay: 1
32228
-
32229
- # Disallow specific paths (uncomment as needed)
32230
- # Disallow: /private/
32231
- # Disallow: /admin/
32232
- # Disallow: /api/
32233
- `;
32234
- await Bun.write(path6.join(this.options.outputDir, "robots.txt"), robotsTxtContent);
32235
- console.log("Generated robots.txt");
32319
+ return {
32320
+ posts: this.site.posts.length,
32321
+ pages: pageCount,
32322
+ totalSize
32323
+ };
32236
32324
  }
32237
32325
  }
32238
32326
  // src/utils/image-uploader.ts
32239
- import path8 from "path";
32327
+ import path10 from "path";
32240
32328
 
32241
32329
  // src/utils/s3-uploader.ts
32242
32330
  var {S3Client } = globalThis.Bun;
32243
- import path7 from "path";
32331
+ import path9 from "path";
32244
32332
 
32245
32333
  class S3Uploader {
32246
32334
  s3Config;
@@ -32363,8 +32451,8 @@ class S3Uploader {
32363
32451
  let failedCount = 0;
32364
32452
  const uploadTasks = imageFiles.map((imageFile) => async () => {
32365
32453
  try {
32366
- const imagePath = path7.join(imagesDir, imageFile);
32367
- const filename = path7.basename(imagePath);
32454
+ const imagePath = path9.join(imagesDir, imageFile);
32455
+ const filename = path9.basename(imagePath);
32368
32456
  const file = Bun.file(imagePath);
32369
32457
  const contentType = file.type;
32370
32458
  if (process.env.BUNKI_DRY_RUN === "true") {} else {
@@ -32398,10 +32486,10 @@ function createUploader(config) {
32398
32486
  }
32399
32487
 
32400
32488
  // src/utils/image-uploader.ts
32401
- var DEFAULT_IMAGES_DIR = path8.join(process.cwd(), "assets");
32489
+ var DEFAULT_IMAGES_DIR = path10.join(process.cwd(), "assets");
32402
32490
  async function uploadImages(options2 = {}) {
32403
32491
  try {
32404
- const imagesDir = path8.resolve(options2.images || DEFAULT_IMAGES_DIR);
32492
+ const imagesDir = path10.resolve(options2.images || DEFAULT_IMAGES_DIR);
32405
32493
  if (!await fileExists(imagesDir)) {
32406
32494
  console.log(`Creating images directory at ${imagesDir}...`);
32407
32495
  await ensureDir(imagesDir);
@@ -32438,7 +32526,7 @@ async function uploadImages(options2 = {}) {
32438
32526
  const uploader = createUploader(s3Config);
32439
32527
  const imageUrlMap = await uploader.uploadImages(imagesDir, options2.minYear);
32440
32528
  if (options2.outputJson) {
32441
- const outputFile = path8.resolve(options2.outputJson);
32529
+ const outputFile = path10.resolve(options2.outputJson);
32442
32530
  await Bun.write(outputFile, JSON.stringify(imageUrlMap, null, 2));
32443
32531
  console.log(`Image URL mapping saved to ${outputFile}`);
32444
32532
  }