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/README.md +141 -15
- package/dist/cli/commands/generate.d.ts +1 -0
- package/dist/cli/commands/templates/archive-njk.d.ts +4 -0
- package/dist/cli/commands/templates/base-njk.d.ts +4 -0
- package/dist/cli/commands/templates/default-css.d.ts +4 -0
- package/dist/cli/commands/templates/index-njk.d.ts +4 -0
- package/dist/cli/commands/templates/index.d.ts +14 -0
- package/dist/cli/commands/templates/post-njk.d.ts +4 -0
- package/dist/cli/commands/templates/sample-post.d.ts +4 -0
- package/dist/cli/commands/templates/tag-njk.d.ts +4 -0
- package/dist/cli/commands/templates/tags-njk.d.ts +4 -0
- package/dist/cli/commands/validate.d.ts +0 -3
- package/dist/cli.js +1880 -1524
- package/dist/generators/assets.d.ts +16 -0
- package/dist/generators/feeds.d.ts +32 -0
- package/dist/generators/pages.d.ts +48 -0
- package/dist/index.js +1149 -822
- package/dist/parser.d.ts +8 -0
- package/dist/site-generator.d.ts +32 -22
- package/dist/utils/build-cache.d.ts +76 -0
- package/dist/utils/build-metrics.d.ts +52 -0
- package/dist/utils/change-detector.d.ts +40 -0
- package/dist/utils/css-processor.d.ts +10 -1
- package/dist/utils/markdown/constants.d.ts +16 -0
- package/dist/utils/markdown/parser.d.ts +32 -0
- package/dist/utils/markdown/validators.d.ts +31 -0
- package/dist/utils/markdown-utils.d.ts +16 -11
- package/dist/utils/pagination.d.ts +38 -0
- package/dist/utils/xml-builder.d.ts +42 -0
- package/package.json +1 -1
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
|
|
30518
|
+
// src/utils/markdown/parser.ts
|
|
30509
30519
|
var import_sanitize_html = __toESM(require_sanitize_html(), 1);
|
|
30510
30520
|
|
|
30511
|
-
// src/utils/
|
|
30512
|
-
|
|
30513
|
-
|
|
30514
|
-
|
|
30515
|
-
|
|
30516
|
-
|
|
30517
|
-
|
|
30518
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
30556
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
30646
|
-
const html =
|
|
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
|
-
|
|
30728
|
-
|
|
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: ${
|
|
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
|
-
|
|
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
|
|
30850
|
-
if (
|
|
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
|
-
|
|
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
|
|
31334
|
+
var import_nunjucks2 = __toESM(require_nunjucks(), 1);
|
|
31307
31335
|
var import_slugify = __toESM(require_slugify(), 1);
|
|
31308
|
-
|
|
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/
|
|
31550
|
-
|
|
31551
|
-
|
|
31552
|
-
|
|
31553
|
-
|
|
31554
|
-
|
|
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
|
-
|
|
31557
|
-
|
|
31558
|
-
|
|
31559
|
-
|
|
31560
|
-
|
|
31561
|
-
|
|
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
|
-
|
|
31535
|
+
return cache;
|
|
31564
31536
|
}
|
|
31565
|
-
|
|
31537
|
+
} catch (error) {
|
|
31538
|
+
console.warn("Error loading cache, rebuilding:", error);
|
|
31566
31539
|
}
|
|
31567
|
-
|
|
31568
|
-
|
|
31569
|
-
|
|
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
|
-
|
|
31572
|
-
|
|
31573
|
-
|
|
31574
|
-
|
|
31575
|
-
|
|
31576
|
-
|
|
31577
|
-
|
|
31578
|
-
|
|
31579
|
-
|
|
31580
|
-
|
|
31581
|
-
|
|
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
|
-
|
|
31587
|
-
|
|
31588
|
-
|
|
31589
|
-
|
|
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
|
-
|
|
31630
|
-
|
|
31631
|
-
|
|
31632
|
-
|
|
31633
|
-
|
|
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
|
-
|
|
31636
|
-
|
|
31637
|
-
|
|
31638
|
-
|
|
31639
|
-
|
|
31640
|
-
|
|
31641
|
-
|
|
31642
|
-
|
|
31643
|
-
|
|
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
|
-
|
|
31682
|
-
|
|
31683
|
-
|
|
31684
|
-
|
|
31685
|
-
|
|
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
|
-
|
|
31700
|
-
|
|
31701
|
-
|
|
31702
|
-
|
|
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
|
-
|
|
31765
|
-
|
|
31766
|
-
|
|
31767
|
-
|
|
31768
|
-
|
|
31769
|
-
|
|
31770
|
-
|
|
31771
|
-
|
|
31772
|
-
|
|
31773
|
-
|
|
31774
|
-
|
|
31775
|
-
|
|
31776
|
-
|
|
31777
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
-
|
|
31798
|
-
|
|
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
|
|
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:
|
|
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:
|
|
31942
|
+
site: config,
|
|
31812
31943
|
post,
|
|
31813
31944
|
jsonLd
|
|
31814
31945
|
});
|
|
31815
|
-
await
|
|
31816
|
-
}
|
|
31946
|
+
await writeHtmlFile(outputDir, `${postPath}index.html`, postHtml);
|
|
31947
|
+
}));
|
|
31817
31948
|
}
|
|
31818
|
-
|
|
31819
|
-
|
|
31820
|
-
|
|
31821
|
-
|
|
31822
|
-
|
|
31823
|
-
|
|
31824
|
-
|
|
31825
|
-
|
|
31826
|
-
|
|
31827
|
-
|
|
31828
|
-
|
|
31829
|
-
const
|
|
31830
|
-
|
|
31831
|
-
|
|
31832
|
-
|
|
31833
|
-
|
|
31834
|
-
|
|
31835
|
-
|
|
31836
|
-
|
|
31837
|
-
|
|
31838
|
-
|
|
31839
|
-
|
|
31840
|
-
|
|
31841
|
-
|
|
31842
|
-
|
|
31843
|
-
|
|
31844
|
-
|
|
31845
|
-
|
|
31846
|
-
|
|
31847
|
-
|
|
31848
|
-
|
|
31849
|
-
|
|
31850
|
-
|
|
31851
|
-
|
|
31852
|
-
|
|
31853
|
-
|
|
31854
|
-
|
|
31855
|
-
|
|
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
|
-
|
|
31880
|
-
|
|
31881
|
-
|
|
31882
|
-
|
|
31883
|
-
|
|
31884
|
-
|
|
31885
|
-
|
|
31886
|
-
|
|
31887
|
-
|
|
31888
|
-
|
|
31889
|
-
|
|
31890
|
-
|
|
31891
|
-
|
|
31892
|
-
|
|
31893
|
-
|
|
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
|
-
|
|
31898
|
-
|
|
31899
|
-
|
|
31900
|
-
|
|
31901
|
-
|
|
31902
|
-
}
|
|
31903
|
-
|
|
31904
|
-
|
|
31905
|
-
|
|
31906
|
-
|
|
31907
|
-
|
|
31908
|
-
|
|
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
|
-
|
|
31917
|
-
|
|
31918
|
-
|
|
31919
|
-
|
|
31920
|
-
|
|
31921
|
-
|
|
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
|
-
|
|
31924
|
-
|
|
31925
|
-
|
|
31926
|
-
|
|
31927
|
-
|
|
31928
|
-
|
|
31929
|
-
|
|
31930
|
-
|
|
31931
|
-
|
|
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
|
-
|
|
31935
|
-
|
|
31936
|
-
|
|
31937
|
-
|
|
31938
|
-
|
|
31939
|
-
|
|
31940
|
-
|
|
31941
|
-
|
|
31942
|
-
|
|
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
|
-
|
|
31946
|
-
|
|
31947
|
-
|
|
31948
|
-
|
|
31949
|
-
|
|
31950
|
-
|
|
31951
|
-
|
|
31952
|
-
|
|
31953
|
-
|
|
31954
|
-
|
|
31955
|
-
|
|
31956
|
-
|
|
31957
|
-
|
|
31958
|
-
|
|
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
|
-
|
|
31962
|
-
|
|
31963
|
-
|
|
31964
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
-
|
|
32042
|
-
|
|
32043
|
-
|
|
32044
|
-
|
|
32045
|
-
|
|
32046
|
-
|
|
32047
|
-
|
|
32048
|
-
|
|
32049
|
-
|
|
32050
|
-
|
|
32051
|
-
|
|
32052
|
-
|
|
32053
|
-
|
|
32054
|
-
|
|
32055
|
-
|
|
32056
|
-
|
|
32057
|
-
|
|
32058
|
-
|
|
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
|
-
|
|
32061
|
-
|
|
32062
|
-
|
|
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
|
-
|
|
32065
|
-
|
|
32066
|
-
|
|
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
|
-
|
|
32069
|
-
|
|
32070
|
-
|
|
32071
|
-
|
|
32072
|
-
|
|
32073
|
-
|
|
32074
|
-
|
|
32075
|
-
|
|
32076
|
-
|
|
32077
|
-
|
|
32078
|
-
|
|
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
|
-
|
|
32081
|
-
|
|
32082
|
-
|
|
32083
|
-
|
|
32084
|
-
|
|
32085
|
-
|
|
32086
|
-
|
|
32087
|
-
|
|
32088
|
-
|
|
32089
|
-
|
|
32090
|
-
|
|
32091
|
-
|
|
32092
|
-
|
|
32093
|
-
|
|
32094
|
-
|
|
32095
|
-
|
|
32096
|
-
|
|
32097
|
-
|
|
32098
|
-
|
|
32099
|
-
|
|
32100
|
-
|
|
32101
|
-
|
|
32102
|
-
|
|
32103
|
-
|
|
32104
|
-
|
|
32105
|
-
|
|
32106
|
-
|
|
32107
|
-
|
|
32108
|
-
|
|
32109
|
-
|
|
32110
|
-
|
|
32111
|
-
|
|
32112
|
-
|
|
32113
|
-
|
|
32114
|
-
|
|
32115
|
-
|
|
32116
|
-
|
|
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
|
-
|
|
32121
|
-
|
|
32122
|
-
|
|
32123
|
-
|
|
32124
|
-
|
|
32125
|
-
|
|
32126
|
-
|
|
32127
|
-
|
|
32128
|
-
|
|
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
|
-
|
|
32173
|
-
|
|
32174
|
-
|
|
32175
|
-
|
|
32176
|
-
|
|
32177
|
-
|
|
32178
|
-
|
|
32179
|
-
|
|
32180
|
-
|
|
32181
|
-
|
|
32182
|
-
|
|
32183
|
-
|
|
32184
|
-
|
|
32185
|
-
|
|
32186
|
-
|
|
32187
|
-
|
|
32188
|
-
|
|
32189
|
-
|
|
32190
|
-
|
|
32191
|
-
|
|
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
|
-
|
|
32197
|
-
await
|
|
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
|
-
|
|
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
|
|
32206
|
-
const
|
|
32207
|
-
|
|
32208
|
-
|
|
32209
|
-
|
|
32210
|
-
|
|
32211
|
-
|
|
32212
|
-
|
|
32213
|
-
|
|
32214
|
-
|
|
32215
|
-
|
|
32216
|
-
|
|
32217
|
-
|
|
32218
|
-
|
|
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
|
-
|
|
32221
|
-
const
|
|
32222
|
-
const
|
|
32223
|
-
|
|
32224
|
-
|
|
32225
|
-
|
|
32226
|
-
|
|
32227
|
-
|
|
32228
|
-
|
|
32229
|
-
|
|
32230
|
-
|
|
32231
|
-
|
|
32232
|
-
|
|
32233
|
-
|
|
32234
|
-
|
|
32235
|
-
|
|
32236
|
-
|
|
32237
|
-
|
|
32238
|
-
|
|
32239
|
-
|
|
32240
|
-
|
|
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
|
|
32571
|
+
import path11 from "path";
|
|
32245
32572
|
|
|
32246
32573
|
// src/utils/s3-uploader.ts
|
|
32247
32574
|
var {S3Client } = globalThis.Bun;
|
|
32248
|
-
import
|
|
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 =
|
|
32372
|
-
const filename =
|
|
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 =
|
|
32733
|
+
var DEFAULT_IMAGES_DIR = path11.join(process.cwd(), "assets");
|
|
32407
32734
|
async function uploadImages(options2 = {}) {
|
|
32408
32735
|
try {
|
|
32409
|
-
const imagesDir =
|
|
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 =
|
|
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
|
}
|