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