bunki 0.16.2 → 0.17.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/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,37 +30601,19 @@ 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 || "";
30580
- let relativeMatch = token.href.match(/^(\.\.\/)+(\d{4})\/([a-zA-Z0-9_-]+?)(?:\.md)?(?:\/)?$/);
30613
+ const relativeMatch = token.href.match(RELATIVE_LINK_REGEX);
30581
30614
  if (relativeMatch) {
30582
- const [, , year, slug] = relativeMatch;
30583
- token.href = `/${year}/${slug}/`;
30615
+ const [, , year, slug, anchor = ""] = relativeMatch;
30616
+ token.href = `/${year}/${slug}/${anchor}`;
30584
30617
  }
30585
30618
  const isExternal = token.href && (token.href.startsWith("http://") || token.href.startsWith("https://") || token.href.startsWith("//"));
30586
30619
  if (isExternal) {
@@ -30605,9 +30638,9 @@ function createMarked(cdnConfig) {
30605
30638
  return markdown2;
30606
30639
  },
30607
30640
  postprocess(html) {
30608
- 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>');
30609
30642
  html = html.replace(/<img /g, '<img loading="lazy" ');
30610
- return html.replace(/<a href="(https?:\/\/|\/\/)([^"]+)"/g, (match, protocol, rest) => {
30643
+ return html.replace(EXTERNAL_LINK_REGEX, (match, protocol, rest) => {
30611
30644
  const fullUrl = protocol + rest;
30612
30645
  let relAttr = 'rel="noopener noreferrer';
30613
30646
  try {
@@ -30627,23 +30660,9 @@ function createMarked(cdnConfig) {
30627
30660
  });
30628
30661
  return marked;
30629
30662
  }
30630
- var marked = createMarked();
30631
- function setNoFollowExceptions(exceptions) {
30632
- noFollowExceptions = new Set(exceptions.map((domain) => domain.toLowerCase().replace(/^www\./, "")));
30633
- marked = createMarked();
30634
- }
30635
- function extractExcerpt(content, maxLength = 200) {
30636
- const plainText = content.replace(/^#.*$/gm, "").replace(/```[\s\S]*?```/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[*_]{1,2}([^*_]+)[*_]{1,2}/g, "$1").replace(/\n+/g, " ").trim();
30637
- if (plainText.length <= maxLength) {
30638
- return plainText;
30639
- }
30640
- const truncated = plainText.substring(0, maxLength);
30641
- const lastSpace = truncated.lastIndexOf(" ");
30642
- return truncated.substring(0, lastSpace) + "...";
30643
- }
30644
30663
  function convertMarkdownToHtml(markdownContent, cdnConfig) {
30645
- const markedInstance = cdnConfig ? createMarked(cdnConfig) : marked;
30646
- const html = markedInstance.parse(markdownContent, { async: false });
30664
+ const marked = createMarked(cdnConfig);
30665
+ const html = marked.parse(markdownContent, { async: false });
30647
30666
  let sanitized = import_sanitize_html.default(html, {
30648
30667
  allowedTags: import_sanitize_html.default.defaults.allowedTags.concat([
30649
30668
  "img",
@@ -30709,6 +30728,17 @@ function convertMarkdownToHtml(markdownContent, cdnConfig) {
30709
30728
  sanitized = sanitized.replace(/javascript:/gi, "").replace(/vbscript:/gi, "");
30710
30729
  return sanitized;
30711
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
30712
30742
  function validateBusinessLocation(business, filePath) {
30713
30743
  if (!business)
30714
30744
  return null;
@@ -30724,47 +30754,13 @@ function validateBusinessLocation(business, filePath) {
30724
30754
  suggestion: "Add 'type: Restaurant' (or Market, Park, Hotel, Museum, Cafe, Zoo, etc.) to frontmatter"
30725
30755
  };
30726
30756
  }
30727
- const validTypes = [
30728
- "Accommodation",
30729
- "Apartment",
30730
- "Attraction",
30731
- "Beach",
30732
- "BodyOfWater",
30733
- "Bridge",
30734
- "Building",
30735
- "BusStation",
30736
- "Cafe",
30737
- "Campground",
30738
- "CivicStructure",
30739
- "EventVenue",
30740
- "Ferry",
30741
- "Garden",
30742
- "HistoricalSite",
30743
- "Hotel",
30744
- "Hostel",
30745
- "Landmark",
30746
- "LodgingBusiness",
30747
- "Market",
30748
- "Monument",
30749
- "Museum",
30750
- "NaturalFeature",
30751
- "Park",
30752
- "Playground",
30753
- "Restaurant",
30754
- "ServiceCenter",
30755
- "ShoppingCenter",
30756
- "Store",
30757
- "TouristAttraction",
30758
- "TrainStation",
30759
- "Viewpoint",
30760
- "Zoo"
30761
- ];
30762
- 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);
30763
30759
  return {
30764
30760
  file: filePath,
30765
30761
  type: "validation",
30766
30762
  message: `Invalid business type '${loc.type}' in business${locIndex}`,
30767
- 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.`
30768
30764
  };
30769
30765
  }
30770
30766
  if (!loc.name) {
@@ -30795,6 +30791,33 @@ function validateBusinessLocation(business, filePath) {
30795
30791
  }
30796
30792
  return null;
30797
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
30798
30821
  async function parseMarkdownFile(filePath, cdnConfig) {
30799
30822
  try {
30800
30823
  const fileContent = await readFileAsText(filePath);
@@ -30825,15 +30848,11 @@ async function parseMarkdownFile(filePath, cdnConfig) {
30825
30848
  }
30826
30849
  };
30827
30850
  }
30828
- if (data.location) {
30851
+ const deprecatedFieldError = checkDeprecatedLocationField(data, filePath);
30852
+ if (deprecatedFieldError) {
30829
30853
  return {
30830
30854
  post: null,
30831
- error: {
30832
- file: filePath,
30833
- type: "validation",
30834
- message: "Use 'business:' instead of deprecated 'location:' field",
30835
- suggestion: "Replace 'location:' with 'business:' in frontmatter (business requires type, name, lat, lng)"
30836
- }
30855
+ error: deprecatedFieldError
30837
30856
  };
30838
30857
  }
30839
30858
  if (data.business) {
@@ -30846,20 +30865,15 @@ async function parseMarkdownFile(filePath, cdnConfig) {
30846
30865
  }
30847
30866
  }
30848
30867
  if (data.tags && Array.isArray(data.tags)) {
30849
- const tagsWithSpaces = data.tags.filter((tag) => tag.includes(" "));
30850
- if (tagsWithSpaces.length > 0) {
30868
+ const tagsError = validateTags(data.tags, filePath);
30869
+ if (tagsError) {
30851
30870
  return {
30852
30871
  post: null,
30853
- error: {
30854
- file: filePath,
30855
- type: "validation",
30856
- message: `Tags must not contain spaces. Found: ${tagsWithSpaces.map((t) => `"${t}"`).join(", ")}`,
30857
- suggestion: `Use hyphens instead of spaces. Example: "new-york-city" instead of "new york city"`
30858
- }
30872
+ error: tagsError
30859
30873
  };
30860
30874
  }
30861
30875
  }
30862
- let slug = getBaseFilename(filePath);
30876
+ const slug = getBaseFilename(filePath);
30863
30877
  const sanitizedHtml = convertMarkdownToHtml(content, cdnConfig);
30864
30878
  const pacificDate = toPacificTime(data.date);
30865
30879
  const postYear = getPacificYear(data.date);
@@ -30947,6 +30961,20 @@ function detectFileConflicts(files) {
30947
30961
  }
30948
30962
  return errors;
30949
30963
  }
30964
+ async function parseMarkdownFiles(filePaths, cdnConfig) {
30965
+ const resultsPromises = filePaths.map((filePath) => parseMarkdownFile(filePath, cdnConfig).then((result) => ({
30966
+ result,
30967
+ filePath
30968
+ })));
30969
+ const results = await Promise.all(resultsPromises);
30970
+ const postsWithPaths = [];
30971
+ for (const { result, filePath } of results) {
30972
+ if (result.post) {
30973
+ postsWithPaths.push({ post: result.post, filePath });
30974
+ }
30975
+ }
30976
+ return postsWithPaths;
30977
+ }
30950
30978
  async function parseMarkdownDirectory(contentDir, strictMode = false, cdnConfig) {
30951
30979
  try {
30952
30980
  const markdownFiles = await findFilesByPattern("**/*.md", contentDir, true);
@@ -31303,84 +31331,9 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
31303
31331
  return server;
31304
31332
  }
31305
31333
  // src/site-generator.ts
31306
- var import_nunjucks = __toESM(require_nunjucks(), 1);
31334
+ var import_nunjucks2 = __toESM(require_nunjucks(), 1);
31307
31335
  var import_slugify = __toESM(require_slugify(), 1);
31308
- var {Glob: Glob2 } = globalThis.Bun;
31309
- import fs3 from "fs";
31310
- import path6 from "path";
31311
-
31312
- // src/utils/css-processor.ts
31313
- import { spawn } from "child_process";
31314
- import fs2 from "fs";
31315
- import path5 from "path";
31316
- async function processCSS(options2) {
31317
- const { css, projectRoot, outputDir, verbose = false } = options2;
31318
- if (!css.enabled) {
31319
- if (verbose) {
31320
- console.log("CSS processing is disabled");
31321
- }
31322
- return;
31323
- }
31324
- const inputPath = path5.resolve(projectRoot, css.input);
31325
- const outputPath = path5.resolve(outputDir, css.output);
31326
- const postcssConfigPath = css.postcssConfig ? path5.resolve(projectRoot, css.postcssConfig) : path5.resolve(projectRoot, "postcss.config.js");
31327
- try {
31328
- await fs2.promises.access(inputPath);
31329
- } catch (error) {
31330
- throw new Error(`CSS input file not found: ${inputPath}`);
31331
- }
31332
- const outputDirPath = path5.dirname(outputPath);
31333
- await fs2.promises.mkdir(outputDirPath, { recursive: true });
31334
- if (verbose) {
31335
- console.log("\uD83C\uDFA8 Building CSS with PostCSS...");
31336
- console.log(`Input: ${inputPath}`);
31337
- console.log(`Output: ${outputPath}`);
31338
- console.log(`Config: ${postcssConfigPath}`);
31339
- }
31340
- await runPostCSS(inputPath, outputPath, postcssConfigPath, projectRoot, verbose);
31341
- }
31342
- function runPostCSS(inputPath, outputPath, configPath, projectRoot, verbose) {
31343
- return new Promise((resolve, reject) => {
31344
- const args = [
31345
- "postcss",
31346
- inputPath,
31347
- "-o",
31348
- outputPath,
31349
- "--config",
31350
- configPath
31351
- ];
31352
- const postcss = spawn("bunx", args, {
31353
- stdio: verbose ? "inherit" : ["ignore", "pipe", "pipe"],
31354
- cwd: projectRoot
31355
- });
31356
- let errorOutput = "";
31357
- if (!verbose) {
31358
- postcss.stderr?.on("data", (data) => {
31359
- errorOutput += data.toString();
31360
- });
31361
- }
31362
- postcss.on("close", (code) => {
31363
- if (code === 0) {
31364
- if (verbose)
31365
- console.log("\u2705 CSS build completed successfully!");
31366
- return resolve();
31367
- }
31368
- reject(new Error(`PostCSS failed with exit code ${code}: ${errorOutput.trim()}`));
31369
- });
31370
- postcss.on("error", (err) => {
31371
- reject(new Error(`Failed to start PostCSS: ${err.message}`));
31372
- });
31373
- });
31374
- }
31375
- function getDefaultCSSConfig() {
31376
- return {
31377
- input: "templates/styles/main.css",
31378
- output: "css/style.css",
31379
- postcssConfig: "postcss.config.js",
31380
- enabled: true,
31381
- watch: false
31382
- };
31383
- }
31336
+ import path9 from "path";
31384
31337
 
31385
31338
  // src/utils/json-ld.ts
31386
31339
  function generateOrganizationSchema(site) {
@@ -31546,706 +31499,1080 @@ function generateHomePageSchemas(options2) {
31546
31499
  return schemas;
31547
31500
  }
31548
31501
 
31549
- // src/site-generator.ts
31550
- class SiteGenerator {
31551
- options;
31552
- site;
31553
- formatRSSDate(date) {
31554
- return toPacificTime(date).toUTCString();
31502
+ // src/utils/build-cache.ts
31503
+ var {hash } = globalThis.Bun;
31504
+ import path5 from "path";
31505
+ var CACHE_VERSION = "2.0.0";
31506
+ var CACHE_FILENAME = ".bunki-cache.json";
31507
+ async function hashFile(filePath) {
31508
+ try {
31509
+ const file = Bun.file(filePath);
31510
+ const content = await file.arrayBuffer();
31511
+ return hash(content).toString(36);
31512
+ } catch (error) {
31513
+ return "";
31555
31514
  }
31556
- groupPostsByYear(posts) {
31557
- const postsByYear = {};
31558
- for (const post of posts) {
31559
- const year = getPacificYear(post.date).toString();
31560
- if (!postsByYear[year]) {
31561
- postsByYear[year] = [];
31515
+ }
31516
+ async function getFileMtime(filePath) {
31517
+ try {
31518
+ const stat = await Bun.file(filePath).stat();
31519
+ return stat?.mtime?.getTime() || 0;
31520
+ } catch (error) {
31521
+ return 0;
31522
+ }
31523
+ }
31524
+ async function loadCache(cwd) {
31525
+ const cachePath = path5.join(cwd, CACHE_FILENAME);
31526
+ const cacheFile = Bun.file(cachePath);
31527
+ try {
31528
+ if (await cacheFile.exists()) {
31529
+ const content = await cacheFile.text();
31530
+ const cache = JSON.parse(content);
31531
+ if (cache.version !== CACHE_VERSION) {
31532
+ console.log(`Cache version mismatch (${cache.version} vs ${CACHE_VERSION}), rebuilding...`);
31533
+ return createEmptyCache();
31562
31534
  }
31563
- postsByYear[year].push(post);
31535
+ return cache;
31564
31536
  }
31565
- return postsByYear;
31537
+ } catch (error) {
31538
+ console.warn("Error loading cache, rebuilding:", error);
31566
31539
  }
31567
- getSortedTags(limit) {
31568
- const sorted = Object.values(this.site.tags).sort((a, b2) => b2.count - a.count);
31569
- return limit ? sorted.slice(0, limit) : sorted;
31540
+ return createEmptyCache();
31541
+ }
31542
+ async function saveCache(cwd, cache) {
31543
+ const cachePath = path5.join(cwd, CACHE_FILENAME);
31544
+ try {
31545
+ await Bun.write(cachePath, JSON.stringify(cache, null, 2));
31546
+ } catch (error) {
31547
+ console.warn("Error saving cache:", error);
31570
31548
  }
31571
- createPagination(items, currentPage, pageSize, pagePath) {
31572
- const totalItems = items.length;
31573
- const totalPages = Math.ceil(totalItems / pageSize);
31574
- return {
31575
- currentPage,
31576
- totalPages,
31577
- hasNextPage: currentPage < totalPages,
31578
- hasPrevPage: currentPage > 1,
31579
- nextPage: currentPage < totalPages ? currentPage + 1 : null,
31580
- prevPage: currentPage > 1 ? currentPage - 1 : null,
31581
- pageSize,
31582
- totalItems,
31583
- pagePath
31584
- };
31549
+ }
31550
+ function createEmptyCache() {
31551
+ return {
31552
+ version: CACHE_VERSION,
31553
+ files: {}
31554
+ };
31555
+ }
31556
+ async function hasFileChanged(filePath, cache) {
31557
+ const cached = cache.files[filePath];
31558
+ if (!cached) {
31559
+ return true;
31585
31560
  }
31586
- constructor(options2) {
31587
- this.options = options2;
31588
- this.site = {
31589
- name: options2.config.domain,
31590
- posts: [],
31591
- tags: {},
31592
- postsByYear: {}
31593
- };
31594
- const env = import_nunjucks.default.configure(this.options.templatesDir, {
31595
- autoescape: true,
31596
- watch: false
31597
- });
31598
- env.addFilter("date", function(date, format) {
31599
- const d2 = toPacificTime(date);
31600
- const months = [
31601
- "January",
31602
- "February",
31603
- "March",
31604
- "April",
31605
- "May",
31606
- "June",
31607
- "July",
31608
- "August",
31609
- "September",
31610
- "October",
31611
- "November",
31612
- "December"
31613
- ];
31614
- if (format === "YYYY") {
31615
- return d2.getFullYear();
31616
- } else if (format === "MMMM D, YYYY") {
31617
- return `${months[d2.getMonth()]} ${d2.getDate()}, ${d2.getFullYear()}`;
31618
- } else if (format === "MMMM D, YYYY h:mm A") {
31619
- const hours = d2.getHours() % 12 || 12;
31620
- const ampm = d2.getHours() >= 12 ? "PM" : "AM";
31621
- return `${months[d2.getMonth()]} ${d2.getDate()}, ${d2.getFullYear()} @ ${hours} ${ampm}`;
31622
- } else {
31623
- return d2.toLocaleDateString("en-US", {
31624
- timeZone: "America/Los_Angeles"
31625
- });
31626
- }
31627
- });
31561
+ const currentMtime = await getFileMtime(filePath);
31562
+ if (currentMtime !== cached.mtime) {
31563
+ const currentHash = await hashFile(filePath);
31564
+ return currentHash !== cached.hash;
31628
31565
  }
31629
- async initialize() {
31630
- console.log("Initializing site generator...");
31631
- await ensureDir(this.options.outputDir);
31632
- if (this.options.config.noFollowExceptions) {
31633
- setNoFollowExceptions(this.options.config.noFollowExceptions);
31566
+ return false;
31567
+ }
31568
+ async function updateCacheEntry(filePath, cache, options2) {
31569
+ const currentHash = await hashFile(filePath);
31570
+ const currentMtime = await getFileMtime(filePath);
31571
+ cache.files[filePath] = {
31572
+ hash: currentHash,
31573
+ mtime: currentMtime,
31574
+ post: options2?.post,
31575
+ outputs: options2?.outputs
31576
+ };
31577
+ }
31578
+ async function hasConfigChanged(configPath, cache) {
31579
+ const currentHash = await hashFile(configPath);
31580
+ if (!cache.configHash) {
31581
+ cache.configHash = currentHash;
31582
+ return true;
31583
+ }
31584
+ if (currentHash !== cache.configHash) {
31585
+ cache.configHash = currentHash;
31586
+ return true;
31587
+ }
31588
+ return false;
31589
+ }
31590
+ function loadCachedPosts(cache, filePaths) {
31591
+ const posts = [];
31592
+ for (const filePath of filePaths) {
31593
+ const entry = cache.files[filePath];
31594
+ if (entry?.post) {
31595
+ posts.push(entry.post);
31634
31596
  }
31635
- let tagDescriptions = {};
31636
- const tagsTomlPath = path6.join(process.cwd(), "src", "tags.toml");
31637
- const tagsTomlFile = Bun.file(tagsTomlPath);
31638
- if (await tagsTomlFile.exists()) {
31639
- try {
31640
- tagDescriptions = __require(tagsTomlPath);
31641
- console.log("Loaded tag descriptions from tags.toml");
31642
- } catch (error) {
31643
- console.warn("Error loading tag descriptions:", error);
31597
+ }
31598
+ return posts;
31599
+ }
31600
+
31601
+ // src/utils/change-detector.ts
31602
+ async function detectChanges(currentFiles, cache, options2 = {}) {
31603
+ const changes = {
31604
+ changedPosts: [],
31605
+ deletedPosts: [],
31606
+ stylesChanged: false,
31607
+ configChanged: false,
31608
+ templatesChanged: false,
31609
+ fullRebuild: false
31610
+ };
31611
+ if (options2.configPath) {
31612
+ const configChanged = await hasFileChanged(options2.configPath, cache);
31613
+ if (configChanged) {
31614
+ changes.configChanged = true;
31615
+ changes.fullRebuild = true;
31616
+ return changes;
31617
+ }
31618
+ }
31619
+ if (options2.templatePaths && options2.templatePaths.length > 0) {
31620
+ for (const templatePath of options2.templatePaths) {
31621
+ const changed = await hasFileChanged(templatePath, cache);
31622
+ if (changed) {
31623
+ changes.templatesChanged = true;
31624
+ changes.fullRebuild = true;
31625
+ return changes;
31626
+ }
31627
+ }
31628
+ }
31629
+ if (options2.stylesPaths && options2.stylesPaths.length > 0) {
31630
+ for (const stylePath of options2.stylesPaths) {
31631
+ const changed = await hasFileChanged(stylePath, cache);
31632
+ if (changed) {
31633
+ changes.stylesChanged = true;
31634
+ break;
31644
31635
  }
31645
31636
  }
31646
- const strictMode = this.options.config.strictMode ?? false;
31647
- const posts = await parseMarkdownDirectory(this.options.contentDir, strictMode, this.options.config.cdn);
31648
- const tags = {};
31649
- posts.forEach((post) => {
31650
- post.tagSlugs = {};
31651
- const imageUrl = extractFirstImageUrl(post.html, this.options.config.baseUrl);
31652
- if (imageUrl) {
31653
- post.image = imageUrl;
31654
- }
31655
- post.tags.forEach((tagName) => {
31656
- const tagSlug = import_slugify.default(tagName, { lower: true, strict: true });
31657
- post.tagSlugs[tagName] = tagSlug;
31658
- if (!tags[tagName]) {
31659
- const tagData = {
31660
- name: tagName,
31661
- slug: tagSlug,
31662
- count: 0,
31663
- posts: []
31664
- };
31665
- if (tagDescriptions[tagName.toLowerCase()]) {
31666
- tagData.description = tagDescriptions[tagName.toLowerCase()];
31667
- }
31668
- tags[tagName] = tagData;
31669
- }
31670
- tags[tagName].count += 1;
31671
- tags[tagName].posts.push(post);
31672
- });
31673
- });
31674
- this.site = {
31675
- name: this.options.config.domain,
31676
- posts,
31677
- tags,
31678
- postsByYear: this.groupPostsByYear(posts)
31679
- };
31680
31637
  }
31681
- async generate() {
31682
- console.log("Generating static site...");
31683
- await ensureDir(this.options.outputDir);
31684
- await this.generateStylesheet();
31685
- await Promise.all([
31686
- this.generateIndexPage(),
31687
- this.generatePostPages(),
31688
- this.generateTagPages(),
31689
- this.generateMapPage(),
31690
- this.generateYearArchives(),
31691
- this.generateRSSFeed(),
31692
- this.generateSitemap(),
31693
- this.generateRobotsTxt(),
31694
- this.generate404Page(),
31695
- this.copyStaticAssets()
31696
- ]);
31697
- console.log("Site generation complete!");
31638
+ for (const filePath of currentFiles) {
31639
+ const changed = await hasFileChanged(filePath, cache);
31640
+ if (changed) {
31641
+ changes.changedPosts.push(filePath);
31642
+ }
31698
31643
  }
31699
- async generate404Page() {
31700
- try {
31701
- const notFoundHtml = import_nunjucks.default.render("404.njk", {
31702
- site: this.options.config
31703
- });
31704
- await Bun.write(path6.join(this.options.outputDir, "404.html"), notFoundHtml);
31705
- console.log("Generated 404.html");
31706
- } catch (error) {
31707
- if (error instanceof Error && error.message.includes("404.njk")) {
31708
- console.log("No 404.njk template found, skipping 404 page generation");
31709
- } else {
31710
- console.warn("Error generating 404 page:", error);
31711
- }
31712
- }
31713
- }
31714
- async generateYearArchives() {
31715
- for (const [year, yearPosts] of Object.entries(this.site.postsByYear)) {
31716
- const yearDir = path6.join(this.options.outputDir, year);
31717
- await ensureDir(yearDir);
31718
- const pageSize = 10;
31719
- const totalPages = Math.ceil(yearPosts.length / pageSize);
31720
- for (let page = 1;page <= totalPages; page++) {
31721
- const startIndex = (page - 1) * pageSize;
31722
- const endIndex = startIndex + pageSize;
31723
- const paginatedPosts = yearPosts.slice(startIndex, endIndex);
31724
- const pagination = this.createPagination(yearPosts, page, pageSize, `/${year}/`);
31725
- let jsonLd = "";
31726
- if (page === 1) {
31727
- const schemas = [];
31728
- schemas.push(generateCollectionPageSchema({
31729
- title: `Posts from ${year}`,
31730
- description: `Articles published in ${year}`,
31731
- url: `${this.options.config.baseUrl}/${year}/`,
31732
- posts: yearPosts,
31733
- site: this.options.config
31734
- }));
31735
- schemas.push(generateBreadcrumbListSchema({
31736
- site: this.options.config,
31737
- items: [
31738
- { name: "Home", url: `${this.options.config.baseUrl}/` },
31739
- { name: year, url: `${this.options.config.baseUrl}/${year}/` }
31740
- ]
31741
- }));
31742
- jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
31743
- `);
31744
- }
31745
- const yearPageHtml = import_nunjucks.default.render("archive.njk", {
31746
- site: this.options.config,
31747
- posts: paginatedPosts,
31748
- tags: this.getSortedTags(this.options.config.maxTagsOnHomepage),
31749
- year,
31750
- pagination,
31751
- noindex: page > 2,
31752
- jsonLd
31753
- });
31754
- if (page === 1) {
31755
- await Bun.write(path6.join(yearDir, "index.html"), yearPageHtml);
31756
- } else {
31757
- const pageDir = path6.join(yearDir, "page", page.toString());
31758
- await ensureDir(pageDir);
31759
- await Bun.write(path6.join(pageDir, "index.html"), yearPageHtml);
31760
- }
31761
- }
31644
+ const cachedFiles = Object.keys(cache.files).filter((f) => f.endsWith(".md"));
31645
+ for (const cachedFile of cachedFiles) {
31646
+ if (!currentFiles.includes(cachedFile)) {
31647
+ changes.deletedPosts.push(cachedFile);
31762
31648
  }
31763
31649
  }
31764
- async generateIndexPage() {
31765
- const pageSize = 10;
31766
- const totalPages = Math.ceil(this.site.posts.length / pageSize);
31767
- for (let page = 1;page <= totalPages; page++) {
31768
- const startIndex = (page - 1) * pageSize;
31769
- const endIndex = startIndex + pageSize;
31770
- const paginatedPosts = this.site.posts.slice(startIndex, endIndex);
31771
- const pagination = this.createPagination(this.site.posts, page, pageSize, "/");
31772
- let jsonLd = "";
31773
- if (page === 1) {
31774
- const schemas = generateHomePageSchemas({
31775
- site: this.options.config
31776
- });
31777
- jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
31650
+ if (changes.deletedPosts.length > 0) {
31651
+ changes.fullRebuild = true;
31652
+ }
31653
+ return changes;
31654
+ }
31655
+ function estimateTimeSaved(totalPosts, changedPosts) {
31656
+ const avgTimePerPost = 6;
31657
+ const skippedPosts = totalPosts - changedPosts;
31658
+ return skippedPosts * avgTimePerPost;
31659
+ }
31660
+
31661
+ // src/utils/xml-builder.ts
31662
+ function escapeXml(text) {
31663
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
31664
+ }
31665
+ function buildSitemapUrl(loc, lastmod, changefreq, priority) {
31666
+ return ` <url>
31667
+ <loc>${loc}</loc>
31668
+ <lastmod>${lastmod}</lastmod>
31669
+ <changefreq>${changefreq}</changefreq>
31670
+ <priority>${priority.toFixed(1)}</priority>
31671
+ </url>
31672
+ `;
31673
+ }
31674
+ function calculateFreshnessPriority(date, basePriority, now = Date.now()) {
31675
+ const ONE_DAY = 24 * 60 * 60 * 1000;
31676
+ const ONE_WEEK = 7 * ONE_DAY;
31677
+ const ONE_MONTH = 30 * ONE_DAY;
31678
+ const postTime = new Date(date).getTime();
31679
+ const age = now - postTime;
31680
+ if (age < ONE_WEEK) {
31681
+ return Math.min(1, basePriority + 0.2);
31682
+ } else if (age < ONE_MONTH) {
31683
+ return Math.min(1, basePriority + 0.1);
31684
+ }
31685
+ return basePriority;
31686
+ }
31687
+ function buildRSSItem(params) {
31688
+ const { title, link, pubDate, description, content, tags, author, image } = params;
31689
+ const categoryTags = tags?.map((tag) => ` <category>${escapeXml(tag)}</category>`).join(`
31690
+ `) || "";
31691
+ let itemXml = ` <item>
31692
+ <title><![CDATA[${title}]]></title>
31693
+ <link>${link}</link>
31694
+ <guid isPermaLink="true">${link}</guid>
31695
+ <pubDate>${pubDate}</pubDate>`;
31696
+ if (author) {
31697
+ itemXml += `
31698
+ <author>${author}</author>`;
31699
+ }
31700
+ let fullDescription = description;
31701
+ if (image) {
31702
+ fullDescription = `<img src="${escapeXml(image)}" alt="" style="max-width:100%; height:auto;" /><br/><br/>${description}`;
31703
+ }
31704
+ itemXml += `
31705
+ <description><![CDATA[${fullDescription}]]></description>`;
31706
+ if (categoryTags) {
31707
+ itemXml += `
31708
+ ${categoryTags}`;
31709
+ }
31710
+ itemXml += `
31711
+ <content:encoded><![CDATA[${content}]]></content:encoded>`;
31712
+ if (image) {
31713
+ itemXml += `
31714
+ <media:thumbnail url="${escapeXml(image)}" />`;
31715
+ itemXml += `
31716
+ <enclosure url="${escapeXml(image)}" type="image/jpeg" length="0" />`;
31717
+ }
31718
+ itemXml += `
31719
+ </item>`;
31720
+ return itemXml;
31721
+ }
31722
+
31723
+ // src/utils/pagination.ts
31724
+ function createPagination(items, currentPage, pageSize, pagePath) {
31725
+ const totalItems = items.length;
31726
+ const totalPages = Math.ceil(totalItems / pageSize);
31727
+ return {
31728
+ currentPage,
31729
+ totalPages,
31730
+ hasNextPage: currentPage < totalPages,
31731
+ hasPrevPage: currentPage > 1,
31732
+ nextPage: currentPage < totalPages ? currentPage + 1 : null,
31733
+ prevPage: currentPage > 1 ? currentPage - 1 : null,
31734
+ pageSize,
31735
+ totalItems,
31736
+ pagePath
31737
+ };
31738
+ }
31739
+ function getPaginatedItems(items, page, pageSize) {
31740
+ const startIndex = (page - 1) * pageSize;
31741
+ const endIndex = startIndex + pageSize;
31742
+ return items.slice(startIndex, endIndex);
31743
+ }
31744
+ function getTotalPages(totalItems, pageSize) {
31745
+ return Math.ceil(totalItems / pageSize);
31746
+ }
31747
+
31748
+ // src/generators/feeds.ts
31749
+ function extractFirstImageUrl2(html) {
31750
+ const imgRegex = /<img[^>]+src=["']([^"']+)["']/;
31751
+ const match = html.match(imgRegex);
31752
+ return match ? match[1] : null;
31753
+ }
31754
+ function makeAbsoluteUrl(imageUrl, baseUrl) {
31755
+ return imageUrl.startsWith("http") ? imageUrl : `${baseUrl}${imageUrl}`;
31756
+ }
31757
+ function formatRSSDate(date) {
31758
+ return toPacificTime(date).toUTCString();
31759
+ }
31760
+ function generateRSSFeed(site, config) {
31761
+ const posts = site.posts.slice(0, 15);
31762
+ const now = toPacificTime(new Date);
31763
+ const latestPostDate = posts.length > 0 ? posts[0].date : now.toISOString();
31764
+ const lastBuildDate = formatRSSDate(latestPostDate);
31765
+ const rssItems = posts.map((post) => {
31766
+ const postUrl = `${config.baseUrl}${post.url}`;
31767
+ const pubDate = formatRSSDate(post.date);
31768
+ const featuredImage = extractFirstImageUrl2(post.html);
31769
+ const absoluteImageUrl = featuredImage ? makeAbsoluteUrl(featuredImage, config.baseUrl) : null;
31770
+ const author = config.authorEmail && config.authorName ? `${config.authorEmail} (${config.authorName})` : config.authorEmail || undefined;
31771
+ return buildRSSItem({
31772
+ title: post.title,
31773
+ link: postUrl,
31774
+ pubDate,
31775
+ description: post.excerpt,
31776
+ content: post.html,
31777
+ tags: post.tags,
31778
+ author,
31779
+ image: absoluteImageUrl
31780
+ });
31781
+ }).join(`
31782
+ `);
31783
+ let channelXml = ` <channel>
31784
+ <title><![CDATA[${config.title}]]></title>
31785
+ <link>${config.baseUrl}/</link>
31786
+ <description><![CDATA[${config.description}]]></description>`;
31787
+ const language = config.rssLanguage || "en-US";
31788
+ channelXml += `
31789
+ <language>${language}</language>`;
31790
+ if (config.authorEmail && config.authorName) {
31791
+ channelXml += `
31792
+ <managingEditor>${config.authorEmail} (${config.authorName})</managingEditor>`;
31793
+ } else if (config.authorEmail) {
31794
+ channelXml += `
31795
+ <managingEditor>${config.authorEmail}</managingEditor>`;
31796
+ }
31797
+ if (config.webMaster) {
31798
+ channelXml += `
31799
+ <webMaster>${config.webMaster}</webMaster>`;
31800
+ }
31801
+ if (config.copyright) {
31802
+ channelXml += `
31803
+ <copyright><![CDATA[${config.copyright}]]></copyright>`;
31804
+ }
31805
+ channelXml += `
31806
+ <pubDate>${formatRSSDate(latestPostDate)}</pubDate>
31807
+ <lastBuildDate>${lastBuildDate}</lastBuildDate>
31808
+ <atom:link href="${config.baseUrl}/feed.xml" rel="self" type="application/rss+xml" />`;
31809
+ return `<?xml version="1.0" encoding="UTF-8"?>
31810
+ <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/">
31811
+ ${channelXml}
31812
+ ${rssItems}
31813
+ </channel>
31814
+ </rss>`;
31815
+ }
31816
+ function generateSitemap(site, config, pageSize = 10) {
31817
+ const currentDate = toPacificTime(new Date).toISOString();
31818
+ const now = toPacificTime(new Date).getTime();
31819
+ let sitemapContent = `<?xml version="1.0" encoding="UTF-8"?>
31820
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
31821
+ `;
31822
+ sitemapContent += buildSitemapUrl(`${config.baseUrl}/`, currentDate, "daily", 1);
31823
+ const totalHomePages = getTotalPages(site.posts.length, pageSize);
31824
+ for (let page = 2;page <= totalHomePages; page++) {
31825
+ sitemapContent += buildSitemapUrl(`${config.baseUrl}/page/${page}/`, currentDate, "daily", 0.8);
31826
+ }
31827
+ for (const post of site.posts) {
31828
+ const postUrl = `${config.baseUrl}${post.url}`;
31829
+ const postDate = new Date(post.date).toISOString();
31830
+ const priority = calculateFreshnessPriority(post.date, 0.7, now);
31831
+ const age = now - new Date(post.date).getTime();
31832
+ const ONE_MONTH = 30 * 24 * 60 * 60 * 1000;
31833
+ const changefreq = age < ONE_MONTH ? "weekly" : "monthly";
31834
+ sitemapContent += buildSitemapUrl(postUrl, postDate, changefreq, priority);
31835
+ }
31836
+ sitemapContent += buildSitemapUrl(`${config.baseUrl}/tags/`, currentDate, "weekly", 0.5);
31837
+ sitemapContent += buildSitemapUrl(`${config.baseUrl}/map/`, currentDate, "weekly", 0.6);
31838
+ for (const [, tagData] of Object.entries(site.tags)) {
31839
+ const tagUrl = `${config.baseUrl}/tags/${tagData.slug}/`;
31840
+ const mostRecentPost = tagData.posts[0];
31841
+ const tagPriority = mostRecentPost ? calculateFreshnessPriority(mostRecentPost.date, 0.4, now) : 0.4;
31842
+ sitemapContent += buildSitemapUrl(tagUrl, currentDate, "weekly", tagPriority);
31843
+ const totalTagPages = getTotalPages(tagData.posts.length, pageSize);
31844
+ for (let page = 2;page <= totalTagPages; page++) {
31845
+ sitemapContent += buildSitemapUrl(`${config.baseUrl}/tags/${tagData.slug}/page/${page}/`, currentDate, "weekly", Math.max(0.3, tagPriority - 0.1));
31846
+ }
31847
+ }
31848
+ for (const [year, yearPosts] of Object.entries(site.postsByYear)) {
31849
+ const currentYear = new Date().getFullYear();
31850
+ const isCurrentYear = parseInt(year) === currentYear;
31851
+ const yearPriority = isCurrentYear ? 0.7 : 0.5;
31852
+ sitemapContent += buildSitemapUrl(`${config.baseUrl}/${year}/`, currentDate, isCurrentYear ? "weekly" : "monthly", yearPriority);
31853
+ const totalYearPages = getTotalPages(yearPosts.length, pageSize);
31854
+ for (let page = 2;page <= totalYearPages; page++) {
31855
+ sitemapContent += buildSitemapUrl(`${config.baseUrl}/${year}/page/${page}/`, currentDate, isCurrentYear ? "weekly" : "monthly", yearPriority - 0.1);
31856
+ }
31857
+ }
31858
+ sitemapContent += `</urlset>`;
31859
+ return sitemapContent;
31860
+ }
31861
+ function generateSitemapIndex(config) {
31862
+ const currentDate = toPacificTime(new Date).toISOString();
31863
+ return `<?xml version="1.0" encoding="UTF-8"?>
31864
+ <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
31865
+ <sitemap>
31866
+ <loc>${config.baseUrl}/sitemap.xml</loc>
31867
+ <lastmod>${currentDate}</lastmod>
31868
+ </sitemap>
31869
+ </sitemapindex>`;
31870
+ }
31871
+ function generateRobotsTxt(config) {
31872
+ return `# Robots.txt for ${config.domain}
31873
+ # Generated by Bunki
31874
+
31875
+ User-agent: *
31876
+ Allow: /
31877
+
31878
+ # Sitemaps
31879
+ Sitemap: ${config.baseUrl}/sitemap.xml
31880
+
31881
+ # Crawl-delay (optional, adjust as needed)
31882
+ # Crawl-delay: 1
31883
+
31884
+ # Disallow specific paths (uncomment as needed)
31885
+ # Disallow: /private/
31886
+ # Disallow: /admin/
31887
+ # Disallow: /api/
31888
+ `;
31889
+ }
31890
+
31891
+ // src/generators/pages.ts
31892
+ var import_nunjucks = __toESM(require_nunjucks(), 1);
31893
+ import path6 from "path";
31894
+ function getSortedTags(tags, limit) {
31895
+ const sorted = Object.values(tags).sort((a, b2) => b2.count - a.count);
31896
+ return limit ? sorted.slice(0, limit) : sorted;
31897
+ }
31898
+ async function writeHtmlFile(outputDir, relativePath, content) {
31899
+ const fullPath = path6.join(outputDir, relativePath);
31900
+ const dir = path6.dirname(fullPath);
31901
+ await ensureDir(dir);
31902
+ await Bun.write(fullPath, content);
31903
+ }
31904
+ async function generateIndexPages(site, config, outputDir, pageSize = 10) {
31905
+ const totalPages = getTotalPages(site.posts.length, pageSize);
31906
+ for (let page = 1;page <= totalPages; page++) {
31907
+ const paginatedPosts = getPaginatedItems(site.posts, page, pageSize);
31908
+ const pagination = createPagination(site.posts, page, pageSize, "/");
31909
+ let jsonLd = "";
31910
+ if (page === 1) {
31911
+ const schemas = generateHomePageSchemas({ site: config });
31912
+ jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
31778
31913
  `);
31779
- }
31780
- const pageHtml = import_nunjucks.default.render("index.njk", {
31781
- site: this.options.config,
31782
- posts: paginatedPosts,
31783
- tags: this.getSortedTags(this.options.config.maxTagsOnHomepage),
31784
- pagination,
31785
- jsonLd,
31786
- noindex: page > 2
31787
- });
31788
- if (page === 1) {
31789
- await Bun.write(path6.join(this.options.outputDir, "index.html"), pageHtml);
31790
- } else {
31791
- const pageDir = path6.join(this.options.outputDir, "page", page.toString());
31792
- await ensureDir(pageDir);
31793
- await Bun.write(path6.join(pageDir, "index.html"), pageHtml);
31794
- }
31795
31914
  }
31915
+ const pageHtml = import_nunjucks.default.render("index.njk", {
31916
+ site: config,
31917
+ posts: paginatedPosts,
31918
+ tags: getSortedTags(site.tags, config.maxTagsOnHomepage),
31919
+ pagination,
31920
+ jsonLd,
31921
+ noindex: page > 2
31922
+ });
31923
+ const outputPath = page === 1 ? "index.html" : `page/${page}/index.html`;
31924
+ await writeHtmlFile(outputDir, outputPath, pageHtml);
31796
31925
  }
31797
- async generatePostPages() {
31798
- for (const post of this.site.posts) {
31926
+ }
31927
+ async function generatePostPages(site, config, outputDir) {
31928
+ const batchSize = 10;
31929
+ for (let i = 0;i < site.posts.length; i += batchSize) {
31930
+ const batch = site.posts.slice(i, i + batchSize);
31931
+ await Promise.all(batch.map(async (post) => {
31799
31932
  const postPath = post.url.substring(1);
31800
- const postDir = path6.join(this.options.outputDir, postPath);
31801
- await ensureDir(postDir);
31802
- const imageUrl = extractFirstImageUrl(post.html, this.options.config.baseUrl);
31933
+ const imageUrl = extractFirstImageUrl(post.html, config.baseUrl);
31803
31934
  const schemas = generatePostPageSchemas({
31804
31935
  post,
31805
- site: this.options.config,
31936
+ site: config,
31806
31937
  imageUrl
31807
31938
  });
31808
31939
  const jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
31809
31940
  `);
31810
31941
  const postHtml = import_nunjucks.default.render("post.njk", {
31811
- site: this.options.config,
31942
+ site: config,
31812
31943
  post,
31813
31944
  jsonLd
31814
31945
  });
31815
- await Bun.write(path6.join(postDir, "index.html"), postHtml);
31816
- }
31946
+ await writeHtmlFile(outputDir, `${postPath}index.html`, postHtml);
31947
+ }));
31817
31948
  }
31818
- async generateTagPages() {
31819
- const tagsDir = path6.join(this.options.outputDir, "tags");
31820
- await ensureDir(tagsDir);
31821
- const tagIndexHtml = import_nunjucks.default.render("tags.njk", {
31822
- site: this.options.config,
31823
- tags: this.getSortedTags()
31824
- });
31825
- await Bun.write(path6.join(tagsDir, "index.html"), tagIndexHtml);
31826
- for (const [tagName, tagData] of Object.entries(this.site.tags)) {
31827
- const tagDir = path6.join(tagsDir, tagData.slug);
31828
- await ensureDir(tagDir);
31829
- const pageSize = 10;
31830
- const totalPages = Math.ceil(tagData.posts.length / pageSize);
31831
- for (let page = 1;page <= totalPages; page++) {
31832
- const startIndex = (page - 1) * pageSize;
31833
- const endIndex = startIndex + pageSize;
31834
- const paginatedPosts = tagData.posts.slice(startIndex, endIndex);
31835
- const paginatedTagData = {
31836
- ...tagData,
31837
- posts: paginatedPosts
31838
- };
31839
- const pagination = this.createPagination(tagData.posts, page, pageSize, `/tags/${tagData.slug}/`);
31840
- let jsonLd = "";
31841
- if (page === 1) {
31842
- const schemas = [];
31843
- const description = tagData.description || `Articles tagged with ${tagName}`;
31844
- schemas.push(generateCollectionPageSchema({
31845
- title: `${tagName}`,
31846
- description,
31847
- url: `${this.options.config.baseUrl}/tags/${tagData.slug}/`,
31848
- posts: tagData.posts,
31849
- site: this.options.config
31850
- }));
31851
- schemas.push(generateBreadcrumbListSchema({
31852
- site: this.options.config,
31853
- items: [
31854
- { name: "Home", url: `${this.options.config.baseUrl}/` },
31855
- { name: tagName, url: `${this.options.config.baseUrl}/tags/${tagData.slug}/` }
31856
- ]
31857
- }));
31858
- jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
31949
+ }
31950
+ async function generateTagPages(site, config, outputDir, pageSize = 10) {
31951
+ const tagIndexHtml = import_nunjucks.default.render("tags.njk", {
31952
+ site: config,
31953
+ tags: getSortedTags(site.tags)
31954
+ });
31955
+ await writeHtmlFile(outputDir, "tags/index.html", tagIndexHtml);
31956
+ for (const [tagName, tagData] of Object.entries(site.tags)) {
31957
+ const totalPages = getTotalPages(tagData.posts.length, pageSize);
31958
+ for (let page = 1;page <= totalPages; page++) {
31959
+ const paginatedPosts = getPaginatedItems(tagData.posts, page, pageSize);
31960
+ const paginatedTagData = {
31961
+ ...tagData,
31962
+ posts: paginatedPosts
31963
+ };
31964
+ const pagination = createPagination(tagData.posts, page, pageSize, `/tags/${tagData.slug}/`);
31965
+ let jsonLd = "";
31966
+ if (page === 1) {
31967
+ const schemas = [];
31968
+ const description = tagData.description || `Articles tagged with ${tagName}`;
31969
+ schemas.push(generateCollectionPageSchema({
31970
+ title: `${tagName}`,
31971
+ description,
31972
+ url: `${config.baseUrl}/tags/${tagData.slug}/`,
31973
+ posts: tagData.posts,
31974
+ site: config
31975
+ }));
31976
+ schemas.push(generateBreadcrumbListSchema({
31977
+ site: config,
31978
+ items: [
31979
+ { name: "Home", url: `${config.baseUrl}/` },
31980
+ {
31981
+ name: tagName,
31982
+ url: `${config.baseUrl}/tags/${tagData.slug}/`
31983
+ }
31984
+ ]
31985
+ }));
31986
+ jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
31859
31987
  `);
31860
- }
31861
- const tagPageHtml = import_nunjucks.default.render("tag.njk", {
31862
- site: this.options.config,
31863
- tag: paginatedTagData,
31864
- tags: Object.values(this.site.tags),
31865
- pagination,
31866
- noindex: page > 2,
31867
- jsonLd
31868
- });
31869
- if (page === 1) {
31870
- await Bun.write(path6.join(tagDir, "index.html"), tagPageHtml);
31871
- } else {
31872
- const pageDir = path6.join(tagDir, "page", page.toString());
31873
- await ensureDir(pageDir);
31874
- await Bun.write(path6.join(pageDir, "index.html"), tagPageHtml);
31875
- }
31876
31988
  }
31989
+ const tagPageHtml = import_nunjucks.default.render("tag.njk", {
31990
+ site: config,
31991
+ tag: paginatedTagData,
31992
+ tags: Object.values(site.tags),
31993
+ pagination,
31994
+ noindex: page > 2,
31995
+ jsonLd
31996
+ });
31997
+ const outputPath = page === 1 ? `tags/${tagData.slug}/index.html` : `tags/${tagData.slug}/page/${page}/index.html`;
31998
+ await writeHtmlFile(outputDir, outputPath, tagPageHtml);
31877
31999
  }
31878
32000
  }
31879
- async generateMapPage() {
31880
- try {
31881
- const mapDir = path6.join(this.options.outputDir, "map");
31882
- await ensureDir(mapDir);
31883
- const mapHtml = import_nunjucks.default.render("map.njk", {
31884
- site: this.options.config,
31885
- posts: this.site.posts
31886
- });
31887
- await Bun.write(path6.join(mapDir, "index.html"), mapHtml);
31888
- console.log("Generated map page");
31889
- } catch (error) {
31890
- if (error instanceof Error && error.message.includes("map.njk")) {
31891
- console.log("No map.njk template found, skipping map page generation");
31892
- } else {
31893
- console.warn("Error generating map page:", error);
32001
+ }
32002
+ async function generateYearArchives(site, config, outputDir, pageSize = 10) {
32003
+ for (const [year, yearPosts] of Object.entries(site.postsByYear)) {
32004
+ const totalPages = getTotalPages(yearPosts.length, pageSize);
32005
+ for (let page = 1;page <= totalPages; page++) {
32006
+ const paginatedPosts = getPaginatedItems(yearPosts, page, pageSize);
32007
+ const pagination = createPagination(yearPosts, page, pageSize, `/${year}/`);
32008
+ let jsonLd = "";
32009
+ if (page === 1) {
32010
+ const schemas = [];
32011
+ schemas.push(generateCollectionPageSchema({
32012
+ title: `Posts from ${year}`,
32013
+ description: `Articles published in ${year}`,
32014
+ url: `${config.baseUrl}/${year}/`,
32015
+ posts: yearPosts,
32016
+ site: config
32017
+ }));
32018
+ schemas.push(generateBreadcrumbListSchema({
32019
+ site: config,
32020
+ items: [
32021
+ { name: "Home", url: `${config.baseUrl}/` },
32022
+ { name: year, url: `${config.baseUrl}/${year}/` }
32023
+ ]
32024
+ }));
32025
+ jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
32026
+ `);
31894
32027
  }
32028
+ const yearPageHtml = import_nunjucks.default.render("archive.njk", {
32029
+ site: config,
32030
+ posts: paginatedPosts,
32031
+ tags: getSortedTags(site.tags, config.maxTagsOnHomepage),
32032
+ year,
32033
+ pagination,
32034
+ noindex: page > 2,
32035
+ jsonLd
32036
+ });
32037
+ const outputPath = page === 1 ? `${year}/index.html` : `${year}/page/${page}/index.html`;
32038
+ await writeHtmlFile(outputDir, outputPath, yearPageHtml);
31895
32039
  }
31896
32040
  }
31897
- async generateStylesheet() {
31898
- const cssConfig = this.options.config.css || getDefaultCSSConfig();
31899
- if (!cssConfig.enabled) {
31900
- console.log("CSS processing is disabled, skipping stylesheet generation.");
31901
- return;
31902
- }
31903
- try {
31904
- await processCSS({
31905
- css: cssConfig,
31906
- projectRoot: process.cwd(),
31907
- outputDir: this.options.outputDir,
31908
- verbose: true
31909
- });
31910
- } catch (error) {
31911
- console.error("Error processing CSS:", error);
31912
- console.log("Falling back to simple CSS file copying...");
31913
- await this.fallbackCSSGeneration(cssConfig);
32041
+ }
32042
+ async function generate404Page(config, outputDir) {
32043
+ try {
32044
+ const notFoundHtml = import_nunjucks.default.render("404.njk", {
32045
+ site: config
32046
+ });
32047
+ await writeHtmlFile(outputDir, "404.html", notFoundHtml);
32048
+ console.log("Generated 404.html");
32049
+ } catch (error) {
32050
+ if (error instanceof Error && error.message.includes("404.njk")) {
32051
+ console.log("No 404.njk template found, skipping 404 page generation");
32052
+ } else {
32053
+ console.warn("Error generating 404 page:", error);
31914
32054
  }
31915
32055
  }
31916
- async fallbackCSSGeneration(cssConfig) {
31917
- const cssFilePath = path6.resolve(process.cwd(), cssConfig.input);
31918
- const cssFile = Bun.file(cssFilePath);
31919
- if (!await cssFile.exists()) {
31920
- console.warn(`CSS input file not found: ${cssFilePath}`);
31921
- return;
32056
+ }
32057
+ async function generateMapPage(site, config, outputDir) {
32058
+ try {
32059
+ const mapHtml = import_nunjucks.default.render("map.njk", {
32060
+ site: config,
32061
+ posts: site.posts
32062
+ });
32063
+ await writeHtmlFile(outputDir, "map/index.html", mapHtml);
32064
+ console.log("Generated map page");
32065
+ } catch (error) {
32066
+ if (error instanceof Error && error.message.includes("map.njk")) {
32067
+ console.log("No map.njk template found, skipping map page generation");
32068
+ } else {
32069
+ console.warn("Error generating map page:", error);
31922
32070
  }
31923
- try {
31924
- const cssContent = await cssFile.text();
31925
- const outputPath = path6.resolve(this.options.outputDir, cssConfig.output);
31926
- const outputDir = path6.dirname(outputPath);
31927
- await ensureDir(outputDir);
31928
- await Bun.write(outputPath, cssContent);
31929
- console.log("\u2705 CSS file copied successfully (fallback mode)");
31930
- } catch (error) {
31931
- console.error("Error in fallback CSS generation:", error);
32071
+ }
32072
+ }
32073
+
32074
+ // src/generators/assets.ts
32075
+ var {Glob: Glob2 } = globalThis.Bun;
32076
+ import path8 from "path";
32077
+
32078
+ // src/utils/css-processor.ts
32079
+ import { spawn } from "child_process";
32080
+ var {hash: hash2 } = globalThis.Bun;
32081
+ import path7 from "path";
32082
+ async function processCSS(options2) {
32083
+ const {
32084
+ css,
32085
+ projectRoot,
32086
+ outputDir,
32087
+ verbose = false,
32088
+ enableHashing = false
32089
+ } = options2;
32090
+ if (!css.enabled) {
32091
+ if (verbose) {
32092
+ console.log("CSS processing is disabled");
31932
32093
  }
32094
+ return { outputPath: "" };
31933
32095
  }
31934
- async copyStaticAssets() {
31935
- const assetsDir = path6.join(this.options.templatesDir, "assets");
31936
- const publicDir = path6.join(process.cwd(), "public");
31937
- async function dirExists(p) {
31938
- try {
31939
- const stat = await fs3.promises.stat(p);
31940
- return stat.isDirectory();
31941
- } catch {
31942
- return false;
31943
- }
32096
+ const inputPath = path7.resolve(projectRoot, css.input);
32097
+ const tempOutputPath = path7.resolve(outputDir, css.output);
32098
+ const postcssConfigPath = css.postcssConfig ? path7.resolve(projectRoot, css.postcssConfig) : path7.resolve(projectRoot, "postcss.config.js");
32099
+ const inputFile = Bun.file(inputPath);
32100
+ if (!await inputFile.exists()) {
32101
+ throw new Error(`CSS input file not found: ${inputPath}`);
32102
+ }
32103
+ const outputDirPath = path7.dirname(tempOutputPath);
32104
+ await ensureDir(outputDirPath);
32105
+ if (verbose) {
32106
+ console.log("\uD83C\uDFA8 Building CSS with PostCSS...");
32107
+ console.log(`Input: ${inputPath}`);
32108
+ console.log(`Output: ${tempOutputPath}`);
32109
+ console.log(`Config: ${postcssConfigPath}`);
32110
+ }
32111
+ await runPostCSS(inputPath, tempOutputPath, postcssConfigPath, projectRoot, verbose);
32112
+ if (enableHashing) {
32113
+ const cssFile = Bun.file(tempOutputPath);
32114
+ const cssContent = await cssFile.arrayBuffer();
32115
+ const contentHash = hash2(cssContent).toString(36).slice(0, 8);
32116
+ const ext = path7.extname(tempOutputPath);
32117
+ const basename = path7.basename(tempOutputPath, ext);
32118
+ const dir = path7.dirname(tempOutputPath);
32119
+ const hashedFilename = `${basename}.${contentHash}${ext}`;
32120
+ const hashedOutputPath = path7.join(dir, hashedFilename);
32121
+ await Bun.write(hashedOutputPath, cssFile);
32122
+ if (verbose) {
32123
+ console.log(`\u2705 CSS hashed: ${hashedFilename}`);
31944
32124
  }
31945
- const assetsDirFile = Bun.file(assetsDir);
31946
- if (await assetsDirFile.exists() && await dirExists(assetsDir)) {
31947
- const assetGlob = new Glob2("**/*.*");
31948
- const assetsOutputDir = path6.join(this.options.outputDir, "assets");
31949
- await ensureDir(assetsOutputDir);
31950
- for await (const file of assetGlob.scan({
31951
- cwd: assetsDir,
31952
- absolute: true
31953
- })) {
31954
- const relativePath = path6.relative(assetsDir, file);
31955
- const targetPath = path6.join(assetsOutputDir, relativePath);
31956
- const targetDir = path6.dirname(targetPath);
31957
- await ensureDir(targetDir);
31958
- await copyFile(file, targetPath);
31959
- }
32125
+ return {
32126
+ outputPath: hashedOutputPath,
32127
+ hash: contentHash
32128
+ };
32129
+ }
32130
+ return { outputPath: tempOutputPath };
32131
+ }
32132
+ function runPostCSS(inputPath, outputPath, configPath, projectRoot, verbose) {
32133
+ return new Promise((resolve, reject) => {
32134
+ const args = [
32135
+ "postcss",
32136
+ inputPath,
32137
+ "-o",
32138
+ outputPath,
32139
+ "--config",
32140
+ configPath
32141
+ ];
32142
+ const postcss = spawn("bunx", args, {
32143
+ stdio: verbose ? "inherit" : ["ignore", "pipe", "pipe"],
32144
+ cwd: projectRoot
32145
+ });
32146
+ let errorOutput = "";
32147
+ if (!verbose) {
32148
+ postcss.stderr?.on("data", (data) => {
32149
+ errorOutput += data.toString();
32150
+ });
31960
32151
  }
31961
- if (await dirExists(publicDir)) {
31962
- const copyRecursive = async (srcDir) => {
31963
- const entries = await fs3.promises.readdir(srcDir, {
31964
- withFileTypes: true
31965
- });
31966
- for (const entry of entries) {
31967
- const srcPath = path6.join(srcDir, entry.name);
31968
- const relativePath = path6.relative(publicDir, srcPath);
31969
- const destPath = path6.join(this.options.outputDir, relativePath);
31970
- if (!relativePath)
31971
- continue;
31972
- if (entry.isDirectory()) {
31973
- await ensureDir(destPath);
31974
- await copyRecursive(srcPath);
31975
- } else if (entry.isFile()) {
31976
- const targetFile = Bun.file(destPath);
31977
- if (!await targetFile.exists()) {
31978
- const targetDir = path6.dirname(destPath);
31979
- await ensureDir(targetDir);
31980
- await copyFile(srcPath, destPath);
31981
- }
31982
- }
31983
- }
31984
- };
31985
- await copyRecursive(publicDir);
31986
- console.log("Copied public files to site (including extensionless & dotfiles)");
31987
- }
31988
- }
31989
- extractFirstImageUrl(html) {
31990
- const imgRegex = /<img[^>]+src=["']([^"']+)["']/;
31991
- const match = html.match(imgRegex);
31992
- return match ? match[1] : null;
31993
- }
31994
- escapeXml(text) {
31995
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
31996
- }
31997
- async generateRSSFeed() {
31998
- const posts = this.site.posts.slice(0, 15);
31999
- const config = this.options.config;
32000
- const now = toPacificTime(new Date);
32001
- const latestPostDate = posts.length > 0 ? posts[0].date : now.toISOString();
32002
- const lastBuildDate = this.formatRSSDate(latestPostDate);
32003
- const rssItems = posts.map((post) => {
32004
- const postUrl = `${config.baseUrl}${post.url}`;
32005
- const pubDate = this.formatRSSDate(post.date);
32006
- const featuredImage = this.extractFirstImageUrl(post.html);
32007
- const categoryTags = post.tags.map((tag) => ` <category>${this.escapeXml(tag)}</category>`).join(`
32008
- `);
32009
- let itemXml = ` <item>
32010
- <title><![CDATA[${post.title}]]></title>
32011
- <link>${postUrl}</link>
32012
- <guid isPermaLink="true">${postUrl}</guid>
32013
- <pubDate>${pubDate}</pubDate>`;
32014
- if (config.authorEmail && config.authorName) {
32015
- itemXml += `
32016
- <author>${config.authorEmail} (${config.authorName})</author>`;
32017
- } else if (config.authorEmail) {
32018
- itemXml += `
32019
- <author>${config.authorEmail}</author>`;
32020
- }
32021
- let description = post.excerpt;
32022
- if (featuredImage) {
32023
- const absoluteImageUrl = featuredImage.startsWith("http") ? featuredImage : `${config.baseUrl}${featuredImage}`;
32024
- description = `<img src="${this.escapeXml(absoluteImageUrl)}" alt="" style="max-width:100%; height:auto;" /><br/><br/>${post.excerpt}`;
32025
- }
32026
- itemXml += `
32027
- <description><![CDATA[${description}]]></description>`;
32028
- if (post.tags.length > 0) {
32029
- itemXml += `
32030
- ${categoryTags}`;
32031
- }
32032
- itemXml += `
32033
- <content:encoded><![CDATA[${post.html}]]></content:encoded>`;
32034
- if (featuredImage) {
32035
- const absoluteImageUrl = featuredImage.startsWith("http") ? featuredImage : `${config.baseUrl}${featuredImage}`;
32036
- itemXml += `
32037
- <media:thumbnail url="${this.escapeXml(absoluteImageUrl)}" />`;
32038
- itemXml += `
32039
- <enclosure url="${this.escapeXml(absoluteImageUrl)}" type="image/jpeg" length="0" />`;
32152
+ postcss.on("close", (code) => {
32153
+ if (code === 0) {
32154
+ if (verbose)
32155
+ console.log("\u2705 CSS build completed successfully!");
32156
+ return resolve();
32040
32157
  }
32041
- itemXml += `
32042
- </item>`;
32043
- return itemXml;
32044
- }).join(`
32045
- `);
32046
- let channelXml = ` <channel>
32047
- <title><![CDATA[${config.title}]]></title>
32048
- <link>${config.baseUrl}/</link>
32049
- <description><![CDATA[${config.description}]]></description>`;
32050
- const language = config.rssLanguage || "en-US";
32051
- channelXml += `
32052
- <language>${language}</language>`;
32053
- if (config.authorEmail && config.authorName) {
32054
- channelXml += `
32055
- <managingEditor>${config.authorEmail} (${config.authorName})</managingEditor>`;
32056
- } else if (config.authorEmail) {
32057
- channelXml += `
32058
- <managingEditor>${config.authorEmail}</managingEditor>`;
32158
+ reject(new Error(`PostCSS failed with exit code ${code}: ${errorOutput.trim()}`));
32159
+ });
32160
+ postcss.on("error", (err) => {
32161
+ reject(new Error(`Failed to start PostCSS: ${err.message}`));
32162
+ });
32163
+ });
32164
+ }
32165
+ function getDefaultCSSConfig() {
32166
+ return {
32167
+ input: "templates/styles/main.css",
32168
+ output: "css/style.css",
32169
+ postcssConfig: "postcss.config.js",
32170
+ enabled: true,
32171
+ watch: false
32172
+ };
32173
+ }
32174
+
32175
+ // src/generators/assets.ts
32176
+ async function generateStylesheet(config, outputDir) {
32177
+ const cssConfig = config.css || getDefaultCSSConfig();
32178
+ if (!cssConfig.enabled) {
32179
+ console.log("CSS processing is disabled, skipping stylesheet generation.");
32180
+ return;
32181
+ }
32182
+ try {
32183
+ await processCSS({
32184
+ css: cssConfig,
32185
+ projectRoot: process.cwd(),
32186
+ outputDir,
32187
+ verbose: true
32188
+ });
32189
+ } catch (error) {
32190
+ console.error("Error processing CSS:", error);
32191
+ console.log("Falling back to simple CSS file copying...");
32192
+ await fallbackCSSGeneration(cssConfig, outputDir);
32193
+ }
32194
+ }
32195
+ async function fallbackCSSGeneration(cssConfig, outputDir) {
32196
+ const cssFilePath = path8.resolve(process.cwd(), cssConfig.input);
32197
+ const cssFile = Bun.file(cssFilePath);
32198
+ if (!await cssFile.exists()) {
32199
+ console.warn(`CSS input file not found: ${cssFilePath}`);
32200
+ return;
32201
+ }
32202
+ try {
32203
+ const outputPath = path8.resolve(outputDir, cssConfig.output);
32204
+ const outputDirPath = path8.dirname(outputPath);
32205
+ await ensureDir(outputDirPath);
32206
+ await Bun.write(outputPath, cssFile);
32207
+ console.log("\u2705 CSS file copied successfully (fallback mode)");
32208
+ } catch (error) {
32209
+ console.error("Error in fallback CSS generation:", error);
32210
+ }
32211
+ }
32212
+ async function copyStaticAssets(templatesDir, outputDir) {
32213
+ const assetsDir = path8.join(templatesDir, "assets");
32214
+ const publicDir = path8.join(process.cwd(), "public");
32215
+ if (await isDirectory(assetsDir)) {
32216
+ const assetGlob = new Glob2("**/*.*");
32217
+ const assetsOutputDir = path8.join(outputDir, "assets");
32218
+ await ensureDir(assetsOutputDir);
32219
+ for await (const file of assetGlob.scan({
32220
+ cwd: assetsDir,
32221
+ absolute: true
32222
+ })) {
32223
+ const relativePath = path8.relative(assetsDir, file);
32224
+ const targetPath = path8.join(assetsOutputDir, relativePath);
32225
+ const targetDir = path8.dirname(targetPath);
32226
+ await ensureDir(targetDir);
32227
+ await copyFile(file, targetPath);
32228
+ }
32229
+ }
32230
+ if (await isDirectory(publicDir)) {
32231
+ const publicGlob = new Glob2("**/*");
32232
+ for await (const file of publicGlob.scan({
32233
+ cwd: publicDir,
32234
+ absolute: true,
32235
+ dot: true
32236
+ })) {
32237
+ if (await isDirectory(file))
32238
+ continue;
32239
+ const relativePath = path8.relative(publicDir, file);
32240
+ const destPath = path8.join(outputDir, relativePath);
32241
+ const targetDir = path8.dirname(destPath);
32242
+ await ensureDir(targetDir);
32243
+ await copyFile(file, destPath);
32059
32244
  }
32060
- if (config.webMaster) {
32061
- channelXml += `
32062
- <webMaster>${config.webMaster}</webMaster>`;
32245
+ console.log("Copied public files to site (including extensionless & dotfiles)");
32246
+ }
32247
+ }
32248
+
32249
+ // src/utils/build-metrics.ts
32250
+ class MetricsCollector {
32251
+ startTime;
32252
+ stageTimings = new Map;
32253
+ currentStage = null;
32254
+ constructor() {
32255
+ this.startTime = performance.now();
32256
+ }
32257
+ startStage(name) {
32258
+ if (this.currentStage) {
32259
+ this.endStage();
32260
+ }
32261
+ this.currentStage = {
32262
+ name,
32263
+ startTime: performance.now()
32264
+ };
32265
+ }
32266
+ endStage() {
32267
+ if (!this.currentStage) {
32268
+ return;
32063
32269
  }
32064
- if (config.copyright) {
32065
- channelXml += `
32066
- <copyright><![CDATA[${config.copyright}]]></copyright>`;
32270
+ const duration = performance.now() - this.currentStage.startTime;
32271
+ this.stageTimings.set(this.currentStage.name, duration);
32272
+ this.currentStage = null;
32273
+ }
32274
+ getMetrics(outputs) {
32275
+ if (this.currentStage) {
32276
+ this.endStage();
32067
32277
  }
32068
- channelXml += `
32069
- <pubDate>${this.formatRSSDate(latestPostDate)}</pubDate>
32070
- <lastBuildDate>${lastBuildDate}</lastBuildDate>
32071
- <atom:link href="${config.baseUrl}/feed.xml" rel="self" type="application/rss+xml" />`;
32072
- const rssContent = `<?xml version="1.0" encoding="UTF-8"?>
32073
- <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/">
32074
- ${channelXml}
32075
- ${rssItems}
32076
- </channel>
32077
- </rss>`;
32078
- await Bun.write(path6.join(this.options.outputDir, "feed.xml"), rssContent);
32278
+ const totalTime = performance.now() - this.startTime;
32279
+ return {
32280
+ totalTime,
32281
+ stages: {
32282
+ initialization: this.stageTimings.get("initialization") || 0,
32283
+ cssProcessing: this.stageTimings.get("cssProcessing") || 0,
32284
+ pageGeneration: this.stageTimings.get("pageGeneration") || 0,
32285
+ feedGeneration: this.stageTimings.get("feedGeneration") || 0,
32286
+ assetCopying: this.stageTimings.get("assetCopying") || 0
32287
+ },
32288
+ outputs
32289
+ };
32079
32290
  }
32080
- async generateSitemap() {
32081
- const currentDate = toPacificTime(new Date).toISOString();
32082
- const pageSize = 10;
32083
- const config = this.options.config;
32084
- const now = toPacificTime(new Date).getTime();
32085
- const ONE_DAY = 24 * 60 * 60 * 1000;
32086
- const ONE_WEEK = 7 * ONE_DAY;
32087
- const ONE_MONTH = 30 * ONE_DAY;
32088
- const calculatePriority = (date, basePriority) => {
32089
- const postTime = new Date(date).getTime();
32090
- const age = now - postTime;
32091
- if (age < ONE_WEEK) {
32092
- return Math.min(1, basePriority + 0.2);
32093
- } else if (age < ONE_MONTH) {
32094
- return Math.min(1, basePriority + 0.1);
32095
- }
32096
- return basePriority;
32097
- };
32098
- let sitemapContent = `<?xml version="1.0" encoding="UTF-8"?>
32099
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
32100
- `;
32101
- sitemapContent += ` <url>
32102
- <loc>${config.baseUrl}/</loc>
32103
- <lastmod>${currentDate}</lastmod>
32104
- <changefreq>daily</changefreq>
32105
- <priority>1.0</priority>
32106
- </url>
32107
- `;
32108
- const totalHomePages = Math.ceil(this.site.posts.length / pageSize);
32109
- if (totalHomePages > 1) {
32110
- for (let page = 2;page <= totalHomePages; page++) {
32111
- sitemapContent += ` <url>
32112
- <loc>${config.baseUrl}/page/${page}/</loc>
32113
- <lastmod>${currentDate}</lastmod>
32114
- <changefreq>daily</changefreq>
32115
- <priority>0.8</priority>
32116
- </url>
32117
- `;
32291
+ }
32292
+ function formatBytes(bytes) {
32293
+ if (bytes === 0)
32294
+ return "0 B";
32295
+ const k2 = 1024;
32296
+ const sizes = ["B", "KB", "MB", "GB"];
32297
+ const i = Math.floor(Math.log(bytes) / Math.log(k2));
32298
+ return `${(bytes / Math.pow(k2, i)).toFixed(2)} ${sizes[i]}`;
32299
+ }
32300
+ function displayMetrics(metrics) {
32301
+ console.log(`
32302
+ \uD83D\uDCCA Build Complete in ${metrics.totalTime.toFixed(0)}ms
32303
+ `);
32304
+ console.log("\u23F1\uFE0F Timing Breakdown:");
32305
+ console.log(` Initialization: ${metrics.stages.initialization.toFixed(0)}ms`);
32306
+ console.log(` CSS Processing: ${metrics.stages.cssProcessing.toFixed(0)}ms`);
32307
+ console.log(` Page Generation: ${metrics.stages.pageGeneration.toFixed(0)}ms`);
32308
+ console.log(` Feed Generation: ${metrics.stages.feedGeneration.toFixed(0)}ms`);
32309
+ console.log(` Asset Copying: ${metrics.stages.assetCopying.toFixed(0)}ms`);
32310
+ console.log(`
32311
+ \uD83D\uDCE6 Output:`);
32312
+ console.log(` Posts: ${metrics.outputs.posts}`);
32313
+ console.log(` Pages: ${metrics.outputs.pages}`);
32314
+ console.log(` Total: ${formatBytes(metrics.outputs.totalSize)}
32315
+ `);
32316
+ }
32317
+
32318
+ // src/site-generator.ts
32319
+ class SiteGenerator {
32320
+ options;
32321
+ site;
32322
+ metrics;
32323
+ cache = null;
32324
+ incrementalMode = false;
32325
+ constructor(options2) {
32326
+ this.options = options2;
32327
+ this.site = {
32328
+ name: options2.config.domain,
32329
+ posts: [],
32330
+ tags: {},
32331
+ postsByYear: {}
32332
+ };
32333
+ this.metrics = new MetricsCollector;
32334
+ const env = import_nunjucks2.default.configure(this.options.templatesDir, {
32335
+ autoescape: true,
32336
+ watch: false,
32337
+ noCache: false
32338
+ });
32339
+ env.addFilter("date", (date, format) => {
32340
+ const d2 = toPacificTime(date);
32341
+ const months = [
32342
+ "January",
32343
+ "February",
32344
+ "March",
32345
+ "April",
32346
+ "May",
32347
+ "June",
32348
+ "July",
32349
+ "August",
32350
+ "September",
32351
+ "October",
32352
+ "November",
32353
+ "December"
32354
+ ];
32355
+ if (format === "YYYY") {
32356
+ return d2.getFullYear();
32357
+ } else if (format === "MMMM D, YYYY") {
32358
+ return `${months[d2.getMonth()]} ${d2.getDate()}, ${d2.getFullYear()}`;
32359
+ } else if (format === "MMMM D, YYYY h:mm A") {
32360
+ const hours = d2.getHours() % 12 || 12;
32361
+ const ampm = d2.getHours() >= 12 ? "PM" : "AM";
32362
+ return `${months[d2.getMonth()]} ${d2.getDate()}, ${d2.getFullYear()} @ ${hours} ${ampm}`;
32363
+ } else {
32364
+ return d2.toLocaleDateString("en-US", {
32365
+ timeZone: "America/Los_Angeles"
32366
+ });
32118
32367
  }
32368
+ });
32369
+ }
32370
+ enableIncrementalMode() {
32371
+ this.incrementalMode = true;
32372
+ }
32373
+ async initialize() {
32374
+ this.metrics.startStage("initialization");
32375
+ console.log("Initializing site generator...");
32376
+ await ensureDir(this.options.outputDir);
32377
+ if (this.options.config.noFollowExceptions) {
32378
+ setNoFollowExceptions(this.options.config.noFollowExceptions);
32119
32379
  }
32120
- for (const post of this.site.posts) {
32121
- const postUrl = `${config.baseUrl}${post.url}`;
32122
- const postDate = new Date(post.date).toISOString();
32123
- const priority = calculatePriority(post.date, 0.7);
32124
- const age = now - new Date(post.date).getTime();
32125
- const changefreq = age < ONE_MONTH ? "weekly" : "monthly";
32126
- sitemapContent += ` <url>
32127
- <loc>${postUrl}</loc>
32128
- <lastmod>${postDate}</lastmod>
32129
- <changefreq>${changefreq}</changefreq>
32130
- <priority>${priority.toFixed(1)}</priority>
32131
- </url>
32132
- `;
32133
- }
32134
- sitemapContent += ` <url>
32135
- <loc>${config.baseUrl}/tags/</loc>
32136
- <lastmod>${currentDate}</lastmod>
32137
- <changefreq>weekly</changefreq>
32138
- <priority>0.5</priority>
32139
- </url>
32140
- `;
32141
- sitemapContent += ` <url>
32142
- <loc>${config.baseUrl}/map/</loc>
32143
- <lastmod>${currentDate}</lastmod>
32144
- <changefreq>weekly</changefreq>
32145
- <priority>0.6</priority>
32146
- </url>
32147
- `;
32148
- for (const [, tagData] of Object.entries(this.site.tags)) {
32149
- const tagUrl = `${config.baseUrl}/tags/${tagData.slug}/`;
32150
- const mostRecentPost = tagData.posts[0];
32151
- const tagPriority = mostRecentPost ? calculatePriority(mostRecentPost.date, 0.4) : 0.4;
32152
- sitemapContent += ` <url>
32153
- <loc>${tagUrl}</loc>
32154
- <lastmod>${currentDate}</lastmod>
32155
- <changefreq>weekly</changefreq>
32156
- <priority>${tagPriority.toFixed(1)}</priority>
32157
- </url>
32158
- `;
32159
- const totalTagPages = Math.ceil(tagData.posts.length / pageSize);
32160
- if (totalTagPages > 1) {
32161
- for (let page = 2;page <= totalTagPages; page++) {
32162
- sitemapContent += ` <url>
32163
- <loc>${config.baseUrl}/tags/${tagData.slug}/page/${page}/</loc>
32164
- <lastmod>${currentDate}</lastmod>
32165
- <changefreq>weekly</changefreq>
32166
- <priority>${Math.max(0.3, tagPriority - 0.1).toFixed(1)}</priority>
32167
- </url>
32168
- `;
32169
- }
32380
+ let tagDescriptions = {};
32381
+ const tagsTomlPath = path9.join(process.cwd(), "src", "tags.toml");
32382
+ const tagsTomlFile = Bun.file(tagsTomlPath);
32383
+ if (await tagsTomlFile.exists()) {
32384
+ try {
32385
+ tagDescriptions = __require(tagsTomlPath);
32386
+ console.log("Loaded tag descriptions from tags.toml");
32387
+ } catch (error) {
32388
+ console.warn("Error loading tag descriptions:", error);
32170
32389
  }
32171
32390
  }
32172
- for (const [year, yearPosts] of Object.entries(this.site.postsByYear)) {
32173
- const currentYear = new Date().getFullYear();
32174
- const isCurrentYear = parseInt(year) === currentYear;
32175
- const yearPriority = isCurrentYear ? 0.7 : 0.5;
32176
- sitemapContent += ` <url>
32177
- <loc>${config.baseUrl}/${year}/</loc>
32178
- <lastmod>${currentDate}</lastmod>
32179
- <changefreq>${isCurrentYear ? "weekly" : "monthly"}</changefreq>
32180
- <priority>${yearPriority.toFixed(1)}</priority>
32181
- </url>
32182
- `;
32183
- const totalYearPages = Math.ceil(yearPosts.length / pageSize);
32184
- if (totalYearPages > 1) {
32185
- for (let page = 2;page <= totalYearPages; page++) {
32186
- sitemapContent += ` <url>
32187
- <loc>${config.baseUrl}/${year}/page/${page}/</loc>
32188
- <lastmod>${currentDate}</lastmod>
32189
- <changefreq>${isCurrentYear ? "weekly" : "monthly"}</changefreq>
32190
- <priority>${(yearPriority - 0.1).toFixed(1)}</priority>
32191
- </url>
32192
- `;
32391
+ if (this.incrementalMode) {
32392
+ this.cache = await loadCache(process.cwd());
32393
+ }
32394
+ const posts = await this.parseContent();
32395
+ const tags = {};
32396
+ posts.forEach((post) => {
32397
+ post.tagSlugs = {};
32398
+ const imageUrl = extractFirstImageUrl(post.html, this.options.config.baseUrl);
32399
+ if (imageUrl) {
32400
+ post.image = imageUrl;
32401
+ }
32402
+ post.tags.forEach((tagName) => {
32403
+ const tagSlug = import_slugify.default(tagName, { lower: true, strict: true });
32404
+ post.tagSlugs[tagName] = tagSlug;
32405
+ if (!tags[tagName]) {
32406
+ const tagData = {
32407
+ name: tagName,
32408
+ slug: tagSlug,
32409
+ count: 0,
32410
+ posts: []
32411
+ };
32412
+ if (tagDescriptions[tagName.toLowerCase()]) {
32413
+ tagData.description = tagDescriptions[tagName.toLowerCase()];
32414
+ }
32415
+ tags[tagName] = tagData;
32193
32416
  }
32417
+ tags[tagName].count += 1;
32418
+ tags[tagName].posts.push(post);
32419
+ });
32420
+ });
32421
+ this.site = {
32422
+ name: this.options.config.domain,
32423
+ posts,
32424
+ tags,
32425
+ postsByYear: this.groupPostsByYear(posts)
32426
+ };
32427
+ }
32428
+ async generate() {
32429
+ console.log("Generating static site...");
32430
+ await ensureDir(this.options.outputDir);
32431
+ this.metrics.startStage("cssProcessing");
32432
+ let cssChanged = true;
32433
+ if (this.cache && this.incrementalMode && this.options.config.css) {
32434
+ const cssInputPath = path9.resolve(process.cwd(), this.options.config.css.input);
32435
+ const cssOutputPath = path9.join(this.options.outputDir, this.options.config.css.output);
32436
+ const cssOutputExists = await Bun.file(cssOutputPath).exists();
32437
+ cssChanged = await hasFileChanged(cssInputPath, this.cache);
32438
+ if (!cssChanged && cssOutputExists) {
32439
+ console.log("\u23ED\uFE0F Skipping CSS (unchanged)");
32440
+ } else {
32441
+ await generateStylesheet(this.options.config, this.options.outputDir);
32442
+ await updateCacheEntry(cssInputPath, this.cache);
32194
32443
  }
32444
+ } else {
32445
+ await generateStylesheet(this.options.config, this.options.outputDir);
32195
32446
  }
32196
- sitemapContent += `</urlset>`;
32197
- await Bun.write(path6.join(this.options.outputDir, "sitemap.xml"), sitemapContent);
32447
+ this.metrics.startStage("pageGeneration");
32448
+ await Promise.all([
32449
+ generateIndexPages(this.site, this.options.config, this.options.outputDir),
32450
+ generatePostPages(this.site, this.options.config, this.options.outputDir),
32451
+ generateTagPages(this.site, this.options.config, this.options.outputDir),
32452
+ generateYearArchives(this.site, this.options.config, this.options.outputDir),
32453
+ generateMapPage(this.site, this.options.config, this.options.outputDir),
32454
+ generate404Page(this.options.config, this.options.outputDir)
32455
+ ]);
32456
+ this.metrics.startStage("assetCopying");
32457
+ await copyStaticAssets(this.options.templatesDir, this.options.outputDir);
32458
+ this.metrics.startStage("feedGeneration");
32459
+ await this.generateFeeds();
32460
+ const outputStats = await this.calculateOutputStats();
32461
+ const buildMetrics = this.metrics.getMetrics(outputStats);
32462
+ displayMetrics(buildMetrics);
32463
+ if (this.cache) {
32464
+ await saveCache(process.cwd(), this.cache);
32465
+ }
32466
+ }
32467
+ async generateFeeds() {
32468
+ const pageSize = 10;
32469
+ const rssContent = generateRSSFeed(this.site, this.options.config);
32470
+ await Bun.write(path9.join(this.options.outputDir, "feed.xml"), rssContent);
32471
+ const sitemapContent = generateSitemap(this.site, this.options.config, pageSize);
32472
+ await Bun.write(path9.join(this.options.outputDir, "sitemap.xml"), sitemapContent);
32198
32473
  console.log("Generated sitemap.xml");
32199
32474
  const urlCount = this.site.posts.length + Object.keys(this.site.tags).length + 10;
32200
32475
  const sitemapSize = sitemapContent.length;
32201
32476
  if (urlCount > 1000 || sitemapSize > 40000) {
32202
- await this.generateSitemapIndex();
32477
+ const sitemapIndexContent = generateSitemapIndex(this.options.config);
32478
+ await Bun.write(path9.join(this.options.outputDir, "sitemap_index.xml"), sitemapIndexContent);
32479
+ console.log("Generated sitemap_index.xml");
32203
32480
  }
32481
+ const robotsTxtContent = generateRobotsTxt(this.options.config);
32482
+ await Bun.write(path9.join(this.options.outputDir, "robots.txt"), robotsTxtContent);
32483
+ console.log("Generated robots.txt");
32204
32484
  }
32205
- async generateSitemapIndex() {
32206
- const currentDate = toPacificTime(new Date).toISOString();
32207
- const config = this.options.config;
32208
- let sitemapIndexContent = `<?xml version="1.0" encoding="UTF-8"?>
32209
- <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
32210
- `;
32211
- sitemapIndexContent += ` <sitemap>
32212
- <loc>${config.baseUrl}/sitemap.xml</loc>
32213
- <lastmod>${currentDate}</lastmod>
32214
- </sitemap>
32215
- `;
32216
- sitemapIndexContent += `</sitemapindex>`;
32217
- await Bun.write(path6.join(this.options.outputDir, "sitemap_index.xml"), sitemapIndexContent);
32218
- console.log("Generated sitemap_index.xml");
32485
+ async parseContent() {
32486
+ const strictMode = this.options.config.strictMode ?? false;
32487
+ if (!this.incrementalMode || !this.cache) {
32488
+ const posts = await parseMarkdownDirectory(this.options.contentDir, strictMode, this.options.config.cdn);
32489
+ if (this.cache) {
32490
+ const allFiles2 = await findFilesByPattern("**/*.md", this.options.contentDir, true);
32491
+ const postsByPath = new Map(posts.map((p) => [p.url, p]));
32492
+ for (let i = 0;i < allFiles2.length; i++) {
32493
+ const filePath = allFiles2[i];
32494
+ const post = posts[i];
32495
+ await updateCacheEntry(filePath, this.cache, { post });
32496
+ }
32497
+ }
32498
+ return posts;
32499
+ }
32500
+ const allFiles = await findFilesByPattern("**/*.md", this.options.contentDir, true);
32501
+ const configPath = path9.join(process.cwd(), "bunki.config.ts");
32502
+ const configChanged = await hasConfigChanged(configPath, this.cache);
32503
+ if (configChanged) {
32504
+ console.log("Config changed, full rebuild required");
32505
+ return this.parseContent();
32506
+ }
32507
+ const changes = await detectChanges(allFiles, this.cache);
32508
+ if (changes.fullRebuild) {
32509
+ console.log("Full rebuild required");
32510
+ this.incrementalMode = false;
32511
+ return this.parseContent();
32512
+ }
32513
+ if (changes.changedPosts.length === 0) {
32514
+ console.log("No content changes detected, using cached posts");
32515
+ const cachedPosts2 = loadCachedPosts(this.cache, allFiles);
32516
+ console.log(`\u2728 Loaded ${cachedPosts2.length} posts from cache (0ms parsing)`);
32517
+ return cachedPosts2;
32518
+ }
32519
+ const timeSaved = estimateTimeSaved(allFiles.length, changes.changedPosts.length);
32520
+ console.log(`\uD83D\uDCE6 Incremental build: ${changes.changedPosts.length}/${allFiles.length} files changed (~${timeSaved}ms saved)`);
32521
+ const changedPostsWithPaths = await parseMarkdownFiles(changes.changedPosts, this.options.config.cdn);
32522
+ const unchangedFiles = allFiles.filter((f) => !changes.changedPosts.includes(f));
32523
+ const cachedPosts = loadCachedPosts(this.cache, unchangedFiles);
32524
+ console.log(` Parsed: ${changedPostsWithPaths.length} new/changed, loaded: ${cachedPosts.length} from cache`);
32525
+ const changedPosts = changedPostsWithPaths.map((p) => p.post);
32526
+ const allPosts = [...changedPosts, ...cachedPosts].sort((a, b2) => new Date(b2.date).getTime() - new Date(a.date).getTime());
32527
+ for (const { post, filePath } of changedPostsWithPaths) {
32528
+ await updateCacheEntry(filePath, this.cache, { post });
32529
+ }
32530
+ return allPosts;
32219
32531
  }
32220
- async generateRobotsTxt() {
32221
- const config = this.options.config;
32222
- const robotsTxtContent = `# Robots.txt for ${config.domain}
32223
- # Generated by Bunki
32224
-
32225
- User-agent: *
32226
- Allow: /
32227
-
32228
- # Sitemaps
32229
- Sitemap: ${config.baseUrl}/sitemap.xml
32230
-
32231
- # Crawl-delay (optional, adjust as needed)
32232
- # Crawl-delay: 1
32233
-
32234
- # Disallow specific paths (uncomment as needed)
32235
- # Disallow: /private/
32236
- # Disallow: /admin/
32237
- # Disallow: /api/
32238
- `;
32239
- await Bun.write(path6.join(this.options.outputDir, "robots.txt"), robotsTxtContent);
32240
- console.log("Generated robots.txt");
32532
+ groupPostsByYear(posts) {
32533
+ const postsByYear = {};
32534
+ for (const post of posts) {
32535
+ const year = getPacificYear(post.date).toString();
32536
+ if (!postsByYear[year]) {
32537
+ postsByYear[year] = [];
32538
+ }
32539
+ postsByYear[year].push(post);
32540
+ }
32541
+ return postsByYear;
32542
+ }
32543
+ async calculateOutputStats() {
32544
+ const outputDir = this.options.outputDir;
32545
+ let totalSize = 0;
32546
+ let pageCount = 0;
32547
+ try {
32548
+ const { Glob: Glob3 } = await Promise.resolve(globalThis.Bun);
32549
+ const glob = new Glob3("**/*.html");
32550
+ for await (const filePath of glob.scan({
32551
+ cwd: outputDir,
32552
+ absolute: true
32553
+ })) {
32554
+ pageCount++;
32555
+ const stat = await Bun.file(filePath).stat();
32556
+ if (stat) {
32557
+ totalSize += stat.size;
32558
+ }
32559
+ }
32560
+ } catch (error) {
32561
+ console.warn("Could not calculate output stats:", error);
32562
+ }
32563
+ return {
32564
+ posts: this.site.posts.length,
32565
+ pages: pageCount,
32566
+ totalSize
32567
+ };
32241
32568
  }
32242
32569
  }
32243
32570
  // src/utils/image-uploader.ts
32244
- import path8 from "path";
32571
+ import path11 from "path";
32245
32572
 
32246
32573
  // src/utils/s3-uploader.ts
32247
32574
  var {S3Client } = globalThis.Bun;
32248
- import path7 from "path";
32575
+ import path10 from "path";
32249
32576
 
32250
32577
  class S3Uploader {
32251
32578
  s3Config;
@@ -32368,8 +32695,8 @@ class S3Uploader {
32368
32695
  let failedCount = 0;
32369
32696
  const uploadTasks = imageFiles.map((imageFile) => async () => {
32370
32697
  try {
32371
- const imagePath = path7.join(imagesDir, imageFile);
32372
- const filename = path7.basename(imagePath);
32698
+ const imagePath = path10.join(imagesDir, imageFile);
32699
+ const filename = path10.basename(imagePath);
32373
32700
  const file = Bun.file(imagePath);
32374
32701
  const contentType = file.type;
32375
32702
  if (process.env.BUNKI_DRY_RUN === "true") {} else {
@@ -32403,10 +32730,10 @@ function createUploader(config) {
32403
32730
  }
32404
32731
 
32405
32732
  // src/utils/image-uploader.ts
32406
- var DEFAULT_IMAGES_DIR = path8.join(process.cwd(), "assets");
32733
+ var DEFAULT_IMAGES_DIR = path11.join(process.cwd(), "assets");
32407
32734
  async function uploadImages(options2 = {}) {
32408
32735
  try {
32409
- const imagesDir = path8.resolve(options2.images || DEFAULT_IMAGES_DIR);
32736
+ const imagesDir = path11.resolve(options2.images || DEFAULT_IMAGES_DIR);
32410
32737
  if (!await fileExists(imagesDir)) {
32411
32738
  console.log(`Creating images directory at ${imagesDir}...`);
32412
32739
  await ensureDir(imagesDir);
@@ -32443,7 +32770,7 @@ async function uploadImages(options2 = {}) {
32443
32770
  const uploader = createUploader(s3Config);
32444
32771
  const imageUrlMap = await uploader.uploadImages(imagesDir, options2.minYear);
32445
32772
  if (options2.outputJson) {
32446
- const outputFile = path8.resolve(options2.outputJson);
32773
+ const outputFile = path11.resolve(options2.outputJson);
32447
32774
  await Bun.write(outputFile, JSON.stringify(imageUrlMap, null, 2));
32448
32775
  console.log(`Image URL mapping saved to ${outputFile}`);
32449
32776
  }