bunki 0.9.0 → 0.10.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.
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Validate markdown files for parsing errors
3
+ */
4
+ import { Command } from "commander";
5
+ export declare function registerValidateCommand(program: Command): void;
package/dist/cli.js CHANGED
@@ -32764,6 +32764,18 @@ function escape(html, encode) {
32764
32764
 
32765
32765
  // src/utils/markdown-utils.ts
32766
32766
  var import_sanitize_html = __toESM(require_sanitize_html(), 1);
32767
+
32768
+ // src/utils/date-utils.ts
32769
+ function toPacificTime(date) {
32770
+ return new Date(new Date(date).toLocaleString("en-US", {
32771
+ timeZone: "America/Los_Angeles"
32772
+ }));
32773
+ }
32774
+ function getPacificYear(date) {
32775
+ return toPacificTime(date).getFullYear();
32776
+ }
32777
+
32778
+ // src/utils/markdown-utils.ts
32767
32779
  core_default.registerLanguage("javascript", javascript);
32768
32780
  core_default.registerLanguage("typescript", typescript);
32769
32781
  core_default.registerLanguage("markdown", markdown);
@@ -32890,20 +32902,36 @@ async function parseMarkdownFile(filePath) {
32890
32902
  try {
32891
32903
  const fileContent = await readFileAsText(filePath);
32892
32904
  if (fileContent === null) {
32893
- console.warn(`File not found or couldn't be read: ${filePath}`);
32894
- return null;
32905
+ return {
32906
+ post: null,
32907
+ error: {
32908
+ file: filePath,
32909
+ type: "file_not_found",
32910
+ message: "File not found or couldn't be read"
32911
+ }
32912
+ };
32895
32913
  }
32896
32914
  const { data, content } = import_gray_matter.default(fileContent);
32897
32915
  if (!data.title || !data.date) {
32898
- console.warn(`Skipping ${filePath}: missing required frontmatter (title or date)`);
32899
- return null;
32916
+ const missingFields = [];
32917
+ if (!data.title)
32918
+ missingFields.push("title");
32919
+ if (!data.date)
32920
+ missingFields.push("date");
32921
+ return {
32922
+ post: null,
32923
+ error: {
32924
+ file: filePath,
32925
+ type: "missing_field",
32926
+ message: `Missing required fields: ${missingFields.join(", ")}`,
32927
+ suggestion: "Add required frontmatter fields (title and date)"
32928
+ }
32929
+ };
32900
32930
  }
32901
32931
  let slug = data.slug || getBaseFilename(filePath);
32902
32932
  const sanitizedHtml = convertMarkdownToHtml(content);
32903
- const pacificDate = new Date(new Date(data.date).toLocaleString("en-US", {
32904
- timeZone: "America/Los_Angeles"
32905
- }));
32906
- const postYear = pacificDate.getFullYear();
32933
+ const pacificDate = toPacificTime(data.date);
32934
+ const postYear = getPacificYear(data.date);
32907
32935
  const post = {
32908
32936
  title: data.title,
32909
32937
  date: pacificDate.toISOString(),
@@ -32915,24 +32943,98 @@ async function parseMarkdownFile(filePath) {
32915
32943
  excerpt: data.excerpt || extractExcerpt(content),
32916
32944
  html: sanitizedHtml
32917
32945
  };
32918
- return post;
32946
+ return { post, error: null };
32919
32947
  } catch (error) {
32920
- console.error(`Error parsing markdown file ${filePath}:`, error);
32921
- return null;
32948
+ const isYamlError = error?.name === "YAMLException" || error?.message?.includes("YAML") || error?.message?.includes("mapping pair");
32949
+ let suggestion;
32950
+ if (isYamlError) {
32951
+ if (error?.message?.includes("mapping pair") || error?.message?.includes("colon")) {
32952
+ suggestion = 'Quote titles/descriptions containing colons (e.g., title: "My Post: A Guide")';
32953
+ } else if (error?.message?.includes("multiline key")) {
32954
+ suggestion = "Remove nested quotes or use single quotes inside double quotes";
32955
+ }
32956
+ }
32957
+ return {
32958
+ post: null,
32959
+ error: {
32960
+ file: filePath,
32961
+ type: isYamlError ? "yaml" : "unknown",
32962
+ message: error?.message || String(error),
32963
+ suggestion
32964
+ }
32965
+ };
32922
32966
  }
32923
32967
  }
32924
32968
 
32925
32969
  // src/parser.ts
32926
- async function parseMarkdownDirectory(contentDir) {
32970
+ async function parseMarkdownDirectory(contentDir, strictMode = false) {
32927
32971
  try {
32928
32972
  const markdownFiles = await findFilesByPattern("**/*.md", contentDir, true);
32929
32973
  console.log(`Found ${markdownFiles.length} markdown files`);
32930
- const postsPromises = markdownFiles.map((filePath) => parseMarkdownFile(filePath));
32931
- const posts = await Promise.all(postsPromises);
32932
- return posts.filter((post) => post !== null).sort((a, b2) => new Date(b2.date).getTime() - new Date(a.date).getTime());
32974
+ const resultsPromises = markdownFiles.map((filePath) => parseMarkdownFile(filePath));
32975
+ const results = await Promise.all(resultsPromises);
32976
+ const posts = [];
32977
+ const errors = [];
32978
+ for (const result of results) {
32979
+ if (result.post) {
32980
+ posts.push(result.post);
32981
+ } else if (result.error) {
32982
+ errors.push(result.error);
32983
+ }
32984
+ }
32985
+ if (errors.length > 0) {
32986
+ console.error(`
32987
+ \u26A0\uFE0F Found ${errors.length} parsing error(s):
32988
+ `);
32989
+ const yamlErrors = errors.filter((e) => e.type === "yaml");
32990
+ const missingFieldErrors = errors.filter((e) => e.type === "missing_field");
32991
+ const otherErrors = errors.filter((e) => e.type !== "yaml" && e.type !== "missing_field");
32992
+ if (yamlErrors.length > 0) {
32993
+ console.error(` YAML Parsing Errors (${yamlErrors.length}):`);
32994
+ yamlErrors.slice(0, 5).forEach((e) => {
32995
+ console.error(` \u274C ${e.file}`);
32996
+ if (e.suggestion) {
32997
+ console.error(` \uD83D\uDCA1 ${e.suggestion}`);
32998
+ }
32999
+ });
33000
+ if (yamlErrors.length > 5) {
33001
+ console.error(` ... and ${yamlErrors.length - 5} more`);
33002
+ }
33003
+ console.error("");
33004
+ }
33005
+ if (missingFieldErrors.length > 0) {
33006
+ console.error(` Missing Required Fields (${missingFieldErrors.length}):`);
33007
+ missingFieldErrors.slice(0, 5).forEach((e) => {
33008
+ console.error(` \u26A0\uFE0F ${e.file}: ${e.message}`);
33009
+ });
33010
+ if (missingFieldErrors.length > 5) {
33011
+ console.error(` ... and ${missingFieldErrors.length - 5} more`);
33012
+ }
33013
+ console.error("");
33014
+ }
33015
+ if (otherErrors.length > 0) {
33016
+ console.error(` Other Errors (${otherErrors.length}):`);
33017
+ otherErrors.slice(0, 3).forEach((e) => {
33018
+ console.error(` \u274C ${e.file}: ${e.message}`);
33019
+ });
33020
+ if (otherErrors.length > 3) {
33021
+ console.error(` ... and ${otherErrors.length - 3} more`);
33022
+ }
33023
+ console.error("");
33024
+ }
33025
+ console.error(`\uD83D\uDCDD Tip: Fix YAML errors by quoting titles/descriptions with colons`);
33026
+ console.error(` Example: title: "My Post: A Guide" (quotes required for colons)
33027
+ `);
33028
+ if (strictMode) {
33029
+ throw new Error(`Build failed: ${errors.length} parsing error(s) found (strictMode enabled)`);
33030
+ }
33031
+ }
33032
+ const sortedPosts = posts.sort((a, b2) => new Date(b2.date).getTime() - new Date(a.date).getTime());
33033
+ console.log(`Parsed ${sortedPosts.length} posts`);
33034
+ return sortedPosts;
32933
33035
  } catch (error) {
32934
33036
  console.error(`Error parsing markdown directory:`, error);
32935
- return [];
33037
+ throw error;
32936
33038
  }
32937
33039
  }
32938
33040
 
@@ -33087,15 +33189,18 @@ class SiteGenerator {
33087
33189
  options;
33088
33190
  site;
33089
33191
  formatRSSDate(date) {
33090
- const pacificDate = new Date(new Date(date).toLocaleString("en-US", {
33091
- timeZone: "America/Los_Angeles"
33092
- }));
33093
- return pacificDate.toUTCString();
33192
+ return toPacificTime(date).toUTCString();
33094
33193
  }
33095
- getPacificDate(date) {
33096
- return new Date(new Date(date).toLocaleString("en-US", {
33097
- timeZone: "America/Los_Angeles"
33098
- }));
33194
+ groupPostsByYear(posts) {
33195
+ const postsByYear = {};
33196
+ for (const post of posts) {
33197
+ const year = getPacificYear(post.date).toString();
33198
+ if (!postsByYear[year]) {
33199
+ postsByYear[year] = [];
33200
+ }
33201
+ postsByYear[year].push(post);
33202
+ }
33203
+ return postsByYear;
33099
33204
  }
33100
33205
  getSortedTags(limit) {
33101
33206
  const sorted = Object.values(this.site.tags).sort((a, b2) => b2.count - a.count);
@@ -33121,17 +33226,15 @@ class SiteGenerator {
33121
33226
  this.site = {
33122
33227
  name: options2.config.domain,
33123
33228
  posts: [],
33124
- tags: {}
33229
+ tags: {},
33230
+ postsByYear: {}
33125
33231
  };
33126
33232
  const env = import_nunjucks.default.configure(this.options.templatesDir, {
33127
33233
  autoescape: true,
33128
33234
  watch: false
33129
33235
  });
33130
33236
  env.addFilter("date", function(date, format) {
33131
- const pstDate = new Date(date).toLocaleString("en-US", {
33132
- timeZone: "America/Los_Angeles"
33133
- });
33134
- const d2 = new Date(pstDate);
33237
+ const d2 = toPacificTime(date);
33135
33238
  const months = [
33136
33239
  "January",
33137
33240
  "February",
@@ -33178,8 +33281,8 @@ class SiteGenerator {
33178
33281
  console.warn("Error loading tag descriptions:", error);
33179
33282
  }
33180
33283
  }
33181
- const posts = await parseMarkdownDirectory(this.options.contentDir);
33182
- console.log(`Parsed ${posts.length} posts`);
33284
+ const strictMode = this.options.config.strictMode ?? false;
33285
+ const posts = await parseMarkdownDirectory(this.options.contentDir, strictMode);
33183
33286
  const tags = {};
33184
33287
  posts.forEach((post) => {
33185
33288
  post.tagSlugs = {};
@@ -33205,34 +33308,28 @@ class SiteGenerator {
33205
33308
  this.site = {
33206
33309
  name: this.options.config.domain,
33207
33310
  posts,
33208
- tags
33311
+ tags,
33312
+ postsByYear: this.groupPostsByYear(posts)
33209
33313
  };
33210
33314
  }
33211
33315
  async generate() {
33212
33316
  console.log("Generating static site...");
33213
33317
  await ensureDir(this.options.outputDir);
33214
33318
  await this.generateStylesheet();
33215
- await this.generateIndexPage();
33216
- await this.generatePostPages();
33217
- await this.generateTagPages();
33218
- await this.generateYearArchives();
33219
- await this.generateRSSFeed();
33220
- await this.generateSitemap();
33221
- await this.generateRobotsTxt();
33222
- await this.copyStaticAssets();
33319
+ await Promise.all([
33320
+ this.generateIndexPage(),
33321
+ this.generatePostPages(),
33322
+ this.generateTagPages(),
33323
+ this.generateYearArchives(),
33324
+ this.generateRSSFeed(),
33325
+ this.generateSitemap(),
33326
+ this.generateRobotsTxt(),
33327
+ this.copyStaticAssets()
33328
+ ]);
33223
33329
  console.log("Site generation complete!");
33224
33330
  }
33225
33331
  async generateYearArchives() {
33226
- const postsByYear = {};
33227
- for (const post of this.site.posts) {
33228
- const postDate = new Date(post.date);
33229
- const year = postDate.getFullYear().toString();
33230
- if (!postsByYear[year]) {
33231
- postsByYear[year] = [];
33232
- }
33233
- postsByYear[year].push(post);
33234
- }
33235
- for (const [year, yearPosts] of Object.entries(postsByYear)) {
33332
+ for (const [year, yearPosts] of Object.entries(this.site.postsByYear)) {
33236
33333
  const yearDir = path5.join(this.options.outputDir, year);
33237
33334
  await ensureDir(yearDir);
33238
33335
  const pageSize = 10;
@@ -33453,7 +33550,7 @@ class SiteGenerator {
33453
33550
  async generateRSSFeed() {
33454
33551
  const posts = this.site.posts.slice(0, 15);
33455
33552
  const config = this.options.config;
33456
- const now = this.getPacificDate(new Date);
33553
+ const now = toPacificTime(new Date);
33457
33554
  const latestPostDate = posts.length > 0 ? posts[0].date : now.toISOString();
33458
33555
  const lastBuildDate = this.formatRSSDate(latestPostDate);
33459
33556
  const rssItems = posts.map((post) => {
@@ -33527,10 +33624,10 @@ ${rssItems}
33527
33624
  await Bun.write(path5.join(this.options.outputDir, "feed.xml"), rssContent);
33528
33625
  }
33529
33626
  async generateSitemap() {
33530
- const currentDate = this.getPacificDate(new Date).toISOString();
33627
+ const currentDate = toPacificTime(new Date).toISOString();
33531
33628
  const pageSize = 10;
33532
33629
  const config = this.options.config;
33533
- const now = this.getPacificDate(new Date).getTime();
33630
+ const now = toPacificTime(new Date).getTime();
33534
33631
  const ONE_DAY = 24 * 60 * 60 * 1000;
33535
33632
  const ONE_WEEK = 7 * ONE_DAY;
33536
33633
  const ONE_MONTH = 30 * ONE_DAY;
@@ -33611,16 +33708,7 @@ ${rssItems}
33611
33708
  }
33612
33709
  }
33613
33710
  }
33614
- const postsByYear = {};
33615
- for (const post of this.site.posts) {
33616
- const postDate = new Date(post.date);
33617
- const year = postDate.getFullYear().toString();
33618
- if (!postsByYear[year]) {
33619
- postsByYear[year] = [];
33620
- }
33621
- postsByYear[year].push(post);
33622
- }
33623
- for (const [year, yearPosts] of Object.entries(postsByYear)) {
33711
+ for (const [year, yearPosts] of Object.entries(this.site.postsByYear)) {
33624
33712
  const currentYear = new Date().getFullYear();
33625
33713
  const isCurrentYear = parseInt(year) === currentYear;
33626
33714
  const yearPriority = isCurrentYear ? 0.7 : 0.5;
@@ -33654,7 +33742,7 @@ ${rssItems}
33654
33742
  }
33655
33743
  }
33656
33744
  async generateSitemapIndex() {
33657
- const currentDate = this.getPacificDate(new Date).toISOString();
33745
+ const currentDate = toPacificTime(new Date).toISOString();
33658
33746
  const config = this.options.config;
33659
33747
  let sitemapIndexContent = `<?xml version="1.0" encoding="UTF-8"?>
33660
33748
  <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
@@ -34690,6 +34778,32 @@ function registerServeCommand(program2) {
34690
34778
  });
34691
34779
  }
34692
34780
 
34781
+ // src/cli/commands/validate.ts
34782
+ function registerValidateCommand(program2) {
34783
+ program2.command("validate").description("Validate markdown files for parsing errors").option("-c, --config <path>", "Path to config file", "bunki.config.ts").action(async (options2) => {
34784
+ try {
34785
+ const config = await loadConfig(options2.config);
34786
+ console.log(`\uD83D\uDD0D Validating markdown files...
34787
+ `);
34788
+ const contentDir = "./content";
34789
+ try {
34790
+ await parseMarkdownDirectory(contentDir, true);
34791
+ console.log(`
34792
+ \u2705 All markdown files are valid!`);
34793
+ process.exit(0);
34794
+ } catch (error) {
34795
+ console.error(`
34796
+ \u274C Validation failed
34797
+ `);
34798
+ process.exit(1);
34799
+ }
34800
+ } catch (error) {
34801
+ console.error("Error during validation:", error.message);
34802
+ process.exit(1);
34803
+ }
34804
+ });
34805
+ }
34806
+
34693
34807
  // src/cli.ts
34694
34808
  var program2 = new Command;
34695
34809
  registerInitCommand(program2);
@@ -34698,7 +34812,8 @@ registerGenerateCommand(program2);
34698
34812
  registerServeCommand(program2);
34699
34813
  registerCssCommand(program2);
34700
34814
  registerImagesPushCommand(program2);
34701
- program2.name("bunki").description("An opinionated static site generator built with Bun").version("0.5.3");
34815
+ registerValidateCommand(program2);
34816
+ program2.name("bunki").description("An opinionated static site generator built with Bun").version("0.10.0");
34702
34817
  var currentFile = import.meta.url.replace("file://", "");
34703
34818
  var mainFile = Bun.main;
34704
34819
  if (currentFile === mainFile || currentFile.endsWith(mainFile)) {
package/dist/index.js CHANGED
@@ -30382,6 +30382,18 @@ function escape(html, encode) {
30382
30382
 
30383
30383
  // src/utils/markdown-utils.ts
30384
30384
  var import_sanitize_html = __toESM(require_sanitize_html(), 1);
30385
+
30386
+ // src/utils/date-utils.ts
30387
+ function toPacificTime(date) {
30388
+ return new Date(new Date(date).toLocaleString("en-US", {
30389
+ timeZone: "America/Los_Angeles"
30390
+ }));
30391
+ }
30392
+ function getPacificYear(date) {
30393
+ return toPacificTime(date).getFullYear();
30394
+ }
30395
+
30396
+ // src/utils/markdown-utils.ts
30385
30397
  core_default.registerLanguage("javascript", javascript);
30386
30398
  core_default.registerLanguage("typescript", typescript);
30387
30399
  core_default.registerLanguage("markdown", markdown);
@@ -30508,20 +30520,36 @@ async function parseMarkdownFile(filePath) {
30508
30520
  try {
30509
30521
  const fileContent = await readFileAsText(filePath);
30510
30522
  if (fileContent === null) {
30511
- console.warn(`File not found or couldn't be read: ${filePath}`);
30512
- return null;
30523
+ return {
30524
+ post: null,
30525
+ error: {
30526
+ file: filePath,
30527
+ type: "file_not_found",
30528
+ message: "File not found or couldn't be read"
30529
+ }
30530
+ };
30513
30531
  }
30514
30532
  const { data, content } = import_gray_matter.default(fileContent);
30515
30533
  if (!data.title || !data.date) {
30516
- console.warn(`Skipping ${filePath}: missing required frontmatter (title or date)`);
30517
- return null;
30534
+ const missingFields = [];
30535
+ if (!data.title)
30536
+ missingFields.push("title");
30537
+ if (!data.date)
30538
+ missingFields.push("date");
30539
+ return {
30540
+ post: null,
30541
+ error: {
30542
+ file: filePath,
30543
+ type: "missing_field",
30544
+ message: `Missing required fields: ${missingFields.join(", ")}`,
30545
+ suggestion: "Add required frontmatter fields (title and date)"
30546
+ }
30547
+ };
30518
30548
  }
30519
30549
  let slug = data.slug || getBaseFilename(filePath);
30520
30550
  const sanitizedHtml = convertMarkdownToHtml(content);
30521
- const pacificDate = new Date(new Date(data.date).toLocaleString("en-US", {
30522
- timeZone: "America/Los_Angeles"
30523
- }));
30524
- const postYear = pacificDate.getFullYear();
30551
+ const pacificDate = toPacificTime(data.date);
30552
+ const postYear = getPacificYear(data.date);
30525
30553
  const post = {
30526
30554
  title: data.title,
30527
30555
  date: pacificDate.toISOString(),
@@ -30533,24 +30561,98 @@ async function parseMarkdownFile(filePath) {
30533
30561
  excerpt: data.excerpt || extractExcerpt(content),
30534
30562
  html: sanitizedHtml
30535
30563
  };
30536
- return post;
30564
+ return { post, error: null };
30537
30565
  } catch (error) {
30538
- console.error(`Error parsing markdown file ${filePath}:`, error);
30539
- return null;
30566
+ const isYamlError = error?.name === "YAMLException" || error?.message?.includes("YAML") || error?.message?.includes("mapping pair");
30567
+ let suggestion;
30568
+ if (isYamlError) {
30569
+ if (error?.message?.includes("mapping pair") || error?.message?.includes("colon")) {
30570
+ suggestion = 'Quote titles/descriptions containing colons (e.g., title: "My Post: A Guide")';
30571
+ } else if (error?.message?.includes("multiline key")) {
30572
+ suggestion = "Remove nested quotes or use single quotes inside double quotes";
30573
+ }
30574
+ }
30575
+ return {
30576
+ post: null,
30577
+ error: {
30578
+ file: filePath,
30579
+ type: isYamlError ? "yaml" : "unknown",
30580
+ message: error?.message || String(error),
30581
+ suggestion
30582
+ }
30583
+ };
30540
30584
  }
30541
30585
  }
30542
30586
 
30543
30587
  // src/parser.ts
30544
- async function parseMarkdownDirectory(contentDir) {
30588
+ async function parseMarkdownDirectory(contentDir, strictMode = false) {
30545
30589
  try {
30546
30590
  const markdownFiles = await findFilesByPattern("**/*.md", contentDir, true);
30547
30591
  console.log(`Found ${markdownFiles.length} markdown files`);
30548
- const postsPromises = markdownFiles.map((filePath) => parseMarkdownFile(filePath));
30549
- const posts = await Promise.all(postsPromises);
30550
- return posts.filter((post) => post !== null).sort((a, b2) => new Date(b2.date).getTime() - new Date(a.date).getTime());
30592
+ const resultsPromises = markdownFiles.map((filePath) => parseMarkdownFile(filePath));
30593
+ const results = await Promise.all(resultsPromises);
30594
+ const posts = [];
30595
+ const errors = [];
30596
+ for (const result of results) {
30597
+ if (result.post) {
30598
+ posts.push(result.post);
30599
+ } else if (result.error) {
30600
+ errors.push(result.error);
30601
+ }
30602
+ }
30603
+ if (errors.length > 0) {
30604
+ console.error(`
30605
+ \u26A0\uFE0F Found ${errors.length} parsing error(s):
30606
+ `);
30607
+ const yamlErrors = errors.filter((e) => e.type === "yaml");
30608
+ const missingFieldErrors = errors.filter((e) => e.type === "missing_field");
30609
+ const otherErrors = errors.filter((e) => e.type !== "yaml" && e.type !== "missing_field");
30610
+ if (yamlErrors.length > 0) {
30611
+ console.error(` YAML Parsing Errors (${yamlErrors.length}):`);
30612
+ yamlErrors.slice(0, 5).forEach((e) => {
30613
+ console.error(` \u274C ${e.file}`);
30614
+ if (e.suggestion) {
30615
+ console.error(` \uD83D\uDCA1 ${e.suggestion}`);
30616
+ }
30617
+ });
30618
+ if (yamlErrors.length > 5) {
30619
+ console.error(` ... and ${yamlErrors.length - 5} more`);
30620
+ }
30621
+ console.error("");
30622
+ }
30623
+ if (missingFieldErrors.length > 0) {
30624
+ console.error(` Missing Required Fields (${missingFieldErrors.length}):`);
30625
+ missingFieldErrors.slice(0, 5).forEach((e) => {
30626
+ console.error(` \u26A0\uFE0F ${e.file}: ${e.message}`);
30627
+ });
30628
+ if (missingFieldErrors.length > 5) {
30629
+ console.error(` ... and ${missingFieldErrors.length - 5} more`);
30630
+ }
30631
+ console.error("");
30632
+ }
30633
+ if (otherErrors.length > 0) {
30634
+ console.error(` Other Errors (${otherErrors.length}):`);
30635
+ otherErrors.slice(0, 3).forEach((e) => {
30636
+ console.error(` \u274C ${e.file}: ${e.message}`);
30637
+ });
30638
+ if (otherErrors.length > 3) {
30639
+ console.error(` ... and ${otherErrors.length - 3} more`);
30640
+ }
30641
+ console.error("");
30642
+ }
30643
+ console.error(`\uD83D\uDCDD Tip: Fix YAML errors by quoting titles/descriptions with colons`);
30644
+ console.error(` Example: title: "My Post: A Guide" (quotes required for colons)
30645
+ `);
30646
+ if (strictMode) {
30647
+ throw new Error(`Build failed: ${errors.length} parsing error(s) found (strictMode enabled)`);
30648
+ }
30649
+ }
30650
+ const sortedPosts = posts.sort((a, b2) => new Date(b2.date).getTime() - new Date(a.date).getTime());
30651
+ console.log(`Parsed ${sortedPosts.length} posts`);
30652
+ return sortedPosts;
30551
30653
  } catch (error) {
30552
30654
  console.error(`Error parsing markdown directory:`, error);
30553
- return [];
30655
+ throw error;
30554
30656
  }
30555
30657
  }
30556
30658
  // src/server.ts
@@ -31034,15 +31136,18 @@ class SiteGenerator {
31034
31136
  options;
31035
31137
  site;
31036
31138
  formatRSSDate(date) {
31037
- const pacificDate = new Date(new Date(date).toLocaleString("en-US", {
31038
- timeZone: "America/Los_Angeles"
31039
- }));
31040
- return pacificDate.toUTCString();
31139
+ return toPacificTime(date).toUTCString();
31041
31140
  }
31042
- getPacificDate(date) {
31043
- return new Date(new Date(date).toLocaleString("en-US", {
31044
- timeZone: "America/Los_Angeles"
31045
- }));
31141
+ groupPostsByYear(posts) {
31142
+ const postsByYear = {};
31143
+ for (const post of posts) {
31144
+ const year = getPacificYear(post.date).toString();
31145
+ if (!postsByYear[year]) {
31146
+ postsByYear[year] = [];
31147
+ }
31148
+ postsByYear[year].push(post);
31149
+ }
31150
+ return postsByYear;
31046
31151
  }
31047
31152
  getSortedTags(limit) {
31048
31153
  const sorted = Object.values(this.site.tags).sort((a, b2) => b2.count - a.count);
@@ -31068,17 +31173,15 @@ class SiteGenerator {
31068
31173
  this.site = {
31069
31174
  name: options2.config.domain,
31070
31175
  posts: [],
31071
- tags: {}
31176
+ tags: {},
31177
+ postsByYear: {}
31072
31178
  };
31073
31179
  const env = import_nunjucks.default.configure(this.options.templatesDir, {
31074
31180
  autoescape: true,
31075
31181
  watch: false
31076
31182
  });
31077
31183
  env.addFilter("date", function(date, format) {
31078
- const pstDate = new Date(date).toLocaleString("en-US", {
31079
- timeZone: "America/Los_Angeles"
31080
- });
31081
- const d2 = new Date(pstDate);
31184
+ const d2 = toPacificTime(date);
31082
31185
  const months = [
31083
31186
  "January",
31084
31187
  "February",
@@ -31125,8 +31228,8 @@ class SiteGenerator {
31125
31228
  console.warn("Error loading tag descriptions:", error);
31126
31229
  }
31127
31230
  }
31128
- const posts = await parseMarkdownDirectory(this.options.contentDir);
31129
- console.log(`Parsed ${posts.length} posts`);
31231
+ const strictMode = this.options.config.strictMode ?? false;
31232
+ const posts = await parseMarkdownDirectory(this.options.contentDir, strictMode);
31130
31233
  const tags = {};
31131
31234
  posts.forEach((post) => {
31132
31235
  post.tagSlugs = {};
@@ -31152,34 +31255,28 @@ class SiteGenerator {
31152
31255
  this.site = {
31153
31256
  name: this.options.config.domain,
31154
31257
  posts,
31155
- tags
31258
+ tags,
31259
+ postsByYear: this.groupPostsByYear(posts)
31156
31260
  };
31157
31261
  }
31158
31262
  async generate() {
31159
31263
  console.log("Generating static site...");
31160
31264
  await ensureDir(this.options.outputDir);
31161
31265
  await this.generateStylesheet();
31162
- await this.generateIndexPage();
31163
- await this.generatePostPages();
31164
- await this.generateTagPages();
31165
- await this.generateYearArchives();
31166
- await this.generateRSSFeed();
31167
- await this.generateSitemap();
31168
- await this.generateRobotsTxt();
31169
- await this.copyStaticAssets();
31266
+ await Promise.all([
31267
+ this.generateIndexPage(),
31268
+ this.generatePostPages(),
31269
+ this.generateTagPages(),
31270
+ this.generateYearArchives(),
31271
+ this.generateRSSFeed(),
31272
+ this.generateSitemap(),
31273
+ this.generateRobotsTxt(),
31274
+ this.copyStaticAssets()
31275
+ ]);
31170
31276
  console.log("Site generation complete!");
31171
31277
  }
31172
31278
  async generateYearArchives() {
31173
- const postsByYear = {};
31174
- for (const post of this.site.posts) {
31175
- const postDate = new Date(post.date);
31176
- const year = postDate.getFullYear().toString();
31177
- if (!postsByYear[year]) {
31178
- postsByYear[year] = [];
31179
- }
31180
- postsByYear[year].push(post);
31181
- }
31182
- for (const [year, yearPosts] of Object.entries(postsByYear)) {
31279
+ for (const [year, yearPosts] of Object.entries(this.site.postsByYear)) {
31183
31280
  const yearDir = path5.join(this.options.outputDir, year);
31184
31281
  await ensureDir(yearDir);
31185
31282
  const pageSize = 10;
@@ -31400,7 +31497,7 @@ class SiteGenerator {
31400
31497
  async generateRSSFeed() {
31401
31498
  const posts = this.site.posts.slice(0, 15);
31402
31499
  const config = this.options.config;
31403
- const now = this.getPacificDate(new Date);
31500
+ const now = toPacificTime(new Date);
31404
31501
  const latestPostDate = posts.length > 0 ? posts[0].date : now.toISOString();
31405
31502
  const lastBuildDate = this.formatRSSDate(latestPostDate);
31406
31503
  const rssItems = posts.map((post) => {
@@ -31474,10 +31571,10 @@ ${rssItems}
31474
31571
  await Bun.write(path5.join(this.options.outputDir, "feed.xml"), rssContent);
31475
31572
  }
31476
31573
  async generateSitemap() {
31477
- const currentDate = this.getPacificDate(new Date).toISOString();
31574
+ const currentDate = toPacificTime(new Date).toISOString();
31478
31575
  const pageSize = 10;
31479
31576
  const config = this.options.config;
31480
- const now = this.getPacificDate(new Date).getTime();
31577
+ const now = toPacificTime(new Date).getTime();
31481
31578
  const ONE_DAY = 24 * 60 * 60 * 1000;
31482
31579
  const ONE_WEEK = 7 * ONE_DAY;
31483
31580
  const ONE_MONTH = 30 * ONE_DAY;
@@ -31558,16 +31655,7 @@ ${rssItems}
31558
31655
  }
31559
31656
  }
31560
31657
  }
31561
- const postsByYear = {};
31562
- for (const post of this.site.posts) {
31563
- const postDate = new Date(post.date);
31564
- const year = postDate.getFullYear().toString();
31565
- if (!postsByYear[year]) {
31566
- postsByYear[year] = [];
31567
- }
31568
- postsByYear[year].push(post);
31569
- }
31570
- for (const [year, yearPosts] of Object.entries(postsByYear)) {
31658
+ for (const [year, yearPosts] of Object.entries(this.site.postsByYear)) {
31571
31659
  const currentYear = new Date().getFullYear();
31572
31660
  const isCurrentYear = parseInt(year) === currentYear;
31573
31661
  const yearPriority = isCurrentYear ? 0.7 : 0.5;
@@ -31601,7 +31689,7 @@ ${rssItems}
31601
31689
  }
31602
31690
  }
31603
31691
  async generateSitemapIndex() {
31604
- const currentDate = this.getPacificDate(new Date).toISOString();
31692
+ const currentDate = toPacificTime(new Date).toISOString();
31605
31693
  const config = this.options.config;
31606
31694
  let sitemapIndexContent = `<?xml version="1.0" encoding="UTF-8"?>
31607
31695
  <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
package/dist/parser.d.ts CHANGED
@@ -1,2 +1,7 @@
1
1
  import { Post } from "./types";
2
- export declare function parseMarkdownDirectory(contentDir: string): Promise<Post[]>;
2
+ import { type ParseError } from "./utils/markdown-utils";
3
+ export interface ParseResult {
4
+ posts: Post[];
5
+ errors: ParseError[];
6
+ }
7
+ export declare function parseMarkdownDirectory(contentDir: string, strictMode?: boolean): Promise<Post[]>;
@@ -3,7 +3,7 @@ export declare class SiteGenerator {
3
3
  private options;
4
4
  private site;
5
5
  private formatRSSDate;
6
- private getPacificDate;
6
+ private groupPostsByYear;
7
7
  private getSortedTags;
8
8
  private createPagination;
9
9
  constructor(options: GeneratorOptions);
package/dist/types.d.ts CHANGED
@@ -68,6 +68,8 @@ export interface SiteConfig {
68
68
  webMaster?: string;
69
69
  /** Copyright statement for RSS feed (e.g., "Copyright © 2025 Your Site Name") */
70
70
  copyright?: string;
71
+ /** Strict mode: fail build on parsing errors (default: false) */
72
+ strictMode?: boolean;
71
73
  /** Additional custom configuration options */
72
74
  [key: string]: any;
73
75
  }
@@ -132,6 +134,8 @@ export interface Site {
132
134
  posts: Post[];
133
135
  /** Map of tag names to tag data */
134
136
  tags: Record<string, TagData>;
137
+ /** Posts grouped by year for efficient year archive generation */
138
+ postsByYear: Record<string, Post[]>;
135
139
  }
136
140
  /**
137
141
  * Interface for uploaders (different services can implement this)
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Date utility functions for bunki
3
+ */
4
+ /**
5
+ * Converts a date to Pacific Time (America/Los_Angeles timezone)
6
+ * This is used consistently across the codebase for date handling
7
+ *
8
+ * @param date - Date string or Date object to convert
9
+ * @returns Date object in Pacific timezone
10
+ */
11
+ export declare function toPacificTime(date: string | Date): Date;
12
+ /**
13
+ * Gets the year from a date in Pacific timezone
14
+ *
15
+ * @param date - Date string or Date object
16
+ * @returns Year as number
17
+ */
18
+ export declare function getPacificYear(date: string | Date): number;
@@ -2,4 +2,14 @@ import { Post } from "../types";
2
2
  export declare function setNoFollowExceptions(exceptions: string[]): void;
3
3
  export declare function extractExcerpt(content: string, maxLength?: number): string;
4
4
  export declare function convertMarkdownToHtml(markdownContent: string): string;
5
- export declare function parseMarkdownFile(filePath: string): Promise<Post | null>;
5
+ export interface ParseError {
6
+ file: string;
7
+ type: "yaml" | "missing_field" | "file_not_found" | "unknown";
8
+ message: string;
9
+ suggestion?: string;
10
+ }
11
+ export interface ParseMarkdownResult {
12
+ post: Post | null;
13
+ error: ParseError | null;
14
+ }
15
+ export declare function parseMarkdownFile(filePath: string): Promise<ParseMarkdownResult>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunki",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "An opinionated static site generator built with Bun featuring PostCSS integration and modern web development workflows",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",