bunki 0.18.6 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -5,15 +5,29 @@ var __getProtoOf = Object.getPrototypeOf;
5
5
  var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ function __accessProp(key) {
9
+ return this[key];
10
+ }
11
+ var __toESMCache_node;
12
+ var __toESMCache_esm;
8
13
  var __toESM = (mod, isNodeMode, target) => {
14
+ var canCache = mod != null && typeof mod === "object";
15
+ if (canCache) {
16
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
+ var cached = cache.get(mod);
18
+ if (cached)
19
+ return cached;
20
+ }
9
21
  target = mod != null ? __create(__getProtoOf(mod)) : {};
10
22
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
23
  for (let key of __getOwnPropNames(mod))
12
24
  if (!__hasOwnProp.call(to, key))
13
25
  __defProp(to, key, {
14
- get: () => mod[key],
26
+ get: __accessProp.bind(mod, key),
15
27
  enumerable: true
16
28
  });
29
+ if (canCache)
30
+ cache.set(mod, to);
17
31
  return to;
18
32
  };
19
33
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
@@ -28555,11 +28569,11 @@ function registerCssCommand(program2) {
28555
28569
  }
28556
28570
 
28557
28571
  // src/cli/commands/generate.ts
28558
- import path10 from "path";
28572
+ import path11 from "path";
28559
28573
 
28560
28574
  // src/site-generator.ts
28561
28575
  var import_slugify = __toESM(require_slugify(), 1);
28562
- import path9 from "path";
28576
+ import path10 from "path";
28563
28577
 
28564
28578
  // src/parser.ts
28565
28579
  import path5 from "path";
@@ -33136,6 +33150,12 @@ function createMarked(cdnConfig) {
33136
33150
  return markdown2;
33137
33151
  },
33138
33152
  postprocess(html) {
33153
+ if (cdnConfig?.enabled && cdnConfig.postYear) {
33154
+ const year = cdnConfig.postYear;
33155
+ const base = cdnConfig.baseUrl;
33156
+ html = html.replace(/src=(["'])\.\/\_assets\/([^\s"']+)\1/g, (_m, q2, filename) => `src=${q2}${base}/${year}/${filename}${q2}`);
33157
+ html = html.replace(/!\[([^\]]*)\]\(\.\/\_assets\/([^\s)]+)\)/g, (_m, alt, filename) => `<img src="${base}/${year}/${filename}" alt="${alt}" loading="lazy">`);
33158
+ }
33139
33159
  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>');
33140
33160
  html = html.replace(/<img /g, '<img loading="lazy" ');
33141
33161
  return html.replace(EXTERNAL_LINK_REGEX, (match, protocol, rest) => {
@@ -33374,7 +33394,9 @@ async function parseMarkdownFile(filePath, cdnConfig) {
33374
33394
  const slug = getBaseFilename(filePath);
33375
33395
  const pacificDate = toPacificTime(data.date);
33376
33396
  const postYear = getPacificYear(data.date);
33377
- const cdnConfigWithYear = cdnConfig ? { ...cdnConfig, postYear: String(postYear) } : undefined;
33397
+ const yearFromPath = filePath.match(/\/(\d{4})\//)?.[1];
33398
+ const resolvedYear = String(postYear) !== "NaN" ? String(postYear) : yearFromPath;
33399
+ const cdnConfigWithYear = cdnConfig && resolvedYear ? { ...cdnConfig, postYear: resolvedYear } : undefined;
33378
33400
  const sanitizedHtml = convertMarkdownToHtml(content, cdnConfigWithYear);
33379
33401
  const post = {
33380
33402
  title: data.title,
@@ -34432,8 +34454,13 @@ function displayMetrics(metrics) {
34432
34454
 
34433
34455
  // src/utils/template-engine.ts
34434
34456
  var import_nunjucks2 = __toESM(require_nunjucks(), 1);
34457
+ import path9 from "path";
34458
+ import { existsSync } from "fs";
34459
+ var _distFragments = path9.join(import.meta.dir, "fragments");
34460
+ var _srcFragments = path9.join(import.meta.dir, "../fragments");
34461
+ var BUNKI_FRAGMENTS_DIR = existsSync(_distFragments) ? _distFragments : _srcFragments;
34435
34462
  function createTemplateEngine(templatesDir, watch = false) {
34436
- const env = import_nunjucks2.default.configure(templatesDir, {
34463
+ const env = import_nunjucks2.default.configure([templatesDir, BUNKI_FRAGMENTS_DIR], {
34437
34464
  autoescape: true,
34438
34465
  watch
34439
34466
  });
@@ -34484,12 +34511,24 @@ class SiteGenerator {
34484
34511
  async initialize() {
34485
34512
  this.metrics.startStage("initialization");
34486
34513
  console.log("Initializing site generator...");
34514
+ const flatAssetsDir = path10.join(process.cwd(), "content", "_assets");
34515
+ try {
34516
+ const stat = await import("fs/promises").then((m3) => m3.stat(flatAssetsDir));
34517
+ if (stat.isDirectory()) {
34518
+ throw new Error(`Build error: content/_assets/ must not exist.
34519
+ Images must be placed in content/{year}/_assets/ (e.g. content/2025/_assets/).
34520
+ Move any files from content/_assets/ into the correct year folder and retry.`);
34521
+ }
34522
+ } catch (err) {
34523
+ if (err.code !== "ENOENT")
34524
+ throw err;
34525
+ }
34487
34526
  await ensureDir(this.options.outputDir);
34488
34527
  if (this.options.config.noFollowExceptions) {
34489
34528
  setNoFollowExceptions(this.options.config.noFollowExceptions);
34490
34529
  }
34491
34530
  let tagDescriptions = {};
34492
- const tagsTomlPath = path9.join(process.cwd(), "src", "tags.toml");
34531
+ const tagsTomlPath = path10.join(process.cwd(), "src", "tags.toml");
34493
34532
  const tagsTomlFile = Bun.file(tagsTomlPath);
34494
34533
  if (await tagsTomlFile.exists()) {
34495
34534
  try {
@@ -34551,8 +34590,8 @@ class SiteGenerator {
34551
34590
  this.metrics.startStage("cssProcessing");
34552
34591
  let cssChanged = true;
34553
34592
  if (this.cache && this.incrementalMode && this.options.config.css) {
34554
- const cssInputPath = path9.resolve(process.cwd(), this.options.config.css.input);
34555
- const cssOutputPath = path9.join(this.options.outputDir, this.options.config.css.output);
34593
+ const cssInputPath = path10.resolve(process.cwd(), this.options.config.css.input);
34594
+ const cssOutputPath = path10.join(this.options.outputDir, this.options.config.css.output);
34556
34595
  const cssOutputExists = await Bun.file(cssOutputPath).exists();
34557
34596
  cssChanged = await hasFileChanged(cssInputPath, this.cache);
34558
34597
  if (!cssChanged && cssOutputExists) {
@@ -34586,19 +34625,19 @@ class SiteGenerator {
34586
34625
  }
34587
34626
  async generateFeeds() {
34588
34627
  const rssContent = generateRSSFeed(this.site, this.options.config);
34589
- await Bun.write(path9.join(this.options.outputDir, "feed.xml"), rssContent);
34628
+ await Bun.write(path10.join(this.options.outputDir, "feed.xml"), rssContent);
34590
34629
  const sitemapContent = generateSitemap(this.site, this.options.config, PAGINATION.DEFAULT_PAGE_SIZE);
34591
- await Bun.write(path9.join(this.options.outputDir, "sitemap.xml"), sitemapContent);
34630
+ await Bun.write(path10.join(this.options.outputDir, "sitemap.xml"), sitemapContent);
34592
34631
  console.log("Generated sitemap.xml");
34593
34632
  const urlCount = this.site.posts.length + Object.keys(this.site.tags).length + 10;
34594
34633
  const sitemapSize = sitemapContent.length;
34595
34634
  if (urlCount > FILES.MAX_SITEMAP_URLS || sitemapSize > FILES.MAX_SITEMAP_SIZE) {
34596
34635
  const sitemapIndexContent = generateSitemapIndex(this.options.config);
34597
- await Bun.write(path9.join(this.options.outputDir, "sitemap_index.xml"), sitemapIndexContent);
34636
+ await Bun.write(path10.join(this.options.outputDir, "sitemap_index.xml"), sitemapIndexContent);
34598
34637
  console.log("Generated sitemap_index.xml");
34599
34638
  }
34600
34639
  const robotsTxtContent = generateRobotsTxt(this.options.config);
34601
- await Bun.write(path9.join(this.options.outputDir, "robots.txt"), robotsTxtContent);
34640
+ await Bun.write(path10.join(this.options.outputDir, "robots.txt"), robotsTxtContent);
34602
34641
  console.log("Generated robots.txt");
34603
34642
  }
34604
34643
  async parseContent() {
@@ -34615,7 +34654,7 @@ class SiteGenerator {
34615
34654
  return posts;
34616
34655
  }
34617
34656
  const allFiles = await findFilesByPattern("**/*.md", this.options.contentDir, true);
34618
- const configPath = path9.join(process.cwd(), "bunki.config.ts");
34657
+ const configPath = path10.join(process.cwd(), "bunki.config.ts");
34619
34658
  const configChanged = await hasConfigChanged(configPath, this.cache);
34620
34659
  if (configChanged) {
34621
34660
  console.log("Config changed, full rebuild required");
@@ -34694,10 +34733,10 @@ var defaultDeps2 = {
34694
34733
  };
34695
34734
  async function handleGenerateCommand(options2, deps = defaultDeps2) {
34696
34735
  try {
34697
- const configPath = path10.resolve(options2.config);
34698
- const contentDir = path10.resolve(options2.content);
34699
- const outputDir = path10.resolve(options2.output);
34700
- const templatesDir = path10.resolve(options2.templates);
34736
+ const configPath = path11.resolve(options2.config);
34737
+ const contentDir = path11.resolve(options2.content);
34738
+ const outputDir = path11.resolve(options2.output);
34739
+ const templatesDir = path11.resolve(options2.templates);
34701
34740
  deps.logger.log("Generating site with:");
34702
34741
  deps.logger.log(`- Config file: ${configPath}`);
34703
34742
  deps.logger.log(`- Content directory: ${contentDir}`);
@@ -34731,11 +34770,11 @@ function registerGenerateCommand(program2) {
34731
34770
  }
34732
34771
 
34733
34772
  // src/utils/image-uploader.ts
34734
- import path12 from "path";
34773
+ import path13 from "path";
34735
34774
 
34736
34775
  // src/utils/s3-uploader.ts
34737
34776
  var {S3Client } = globalThis.Bun;
34738
- import path11 from "path";
34777
+ import path12 from "path";
34739
34778
 
34740
34779
  class S3Uploader {
34741
34780
  s3Config;
@@ -34858,7 +34897,7 @@ class S3Uploader {
34858
34897
  let failedCount = 0;
34859
34898
  const uploadTasks = imageFiles.map((imageFile) => async () => {
34860
34899
  try {
34861
- const imagePath = path11.join(imagesDir, imageFile);
34900
+ const imagePath = path12.join(imagesDir, imageFile);
34862
34901
  const s3Key = keyTransform ? keyTransform(imageFile) : imageFile;
34863
34902
  const file = Bun.file(imagePath);
34864
34903
  if (process.env.BUNKI_DRY_RUN === "true") {} else {
@@ -34892,13 +34931,13 @@ function createUploader(config) {
34892
34931
  }
34893
34932
 
34894
34933
  // src/utils/image-uploader.ts
34895
- var DEFAULT_IMAGES_DIR = path12.join(process.cwd(), "assets");
34896
- var DEFAULT_CONTENT_DIR2 = path12.join(process.cwd(), "content");
34934
+ var DEFAULT_IMAGES_DIR = path13.join(process.cwd(), "assets");
34935
+ var DEFAULT_CONTENT_DIR2 = path13.join(process.cwd(), "content");
34897
34936
  async function uploadImages(options2 = {}) {
34898
34937
  try {
34899
34938
  const contentAssetsMode = options2.contentAssets === true;
34900
34939
  const defaultDir = contentAssetsMode ? DEFAULT_CONTENT_DIR2 : DEFAULT_IMAGES_DIR;
34901
- const imagesDir = path12.resolve(options2.images || defaultDir);
34940
+ const imagesDir = path13.resolve(options2.images || defaultDir);
34902
34941
  if (!await fileExists(imagesDir)) {
34903
34942
  console.log(`Creating images directory at ${imagesDir}...`);
34904
34943
  await ensureDir(imagesDir);
@@ -34950,7 +34989,7 @@ async function uploadImages(options2 = {}) {
34950
34989
  const uploader = createUploader(s3Config);
34951
34990
  const imageUrlMap = await uploader.uploadImages(imagesDir, options2.minYear, keyTransform);
34952
34991
  if (options2.outputJson) {
34953
- const outputFile = path12.resolve(options2.outputJson);
34992
+ const outputFile = path13.resolve(options2.outputJson);
34954
34993
  await Bun.write(outputFile, JSON.stringify(imageUrlMap, null, 2));
34955
34994
  console.log(`Image URL mapping saved to ${outputFile}`);
34956
34995
  }
@@ -34998,7 +35037,7 @@ function registerImagesPushCommand(program2) {
34998
35037
  }
34999
35038
 
35000
35039
  // src/cli/commands/init.ts
35001
- import path13 from "path";
35040
+ import path14 from "path";
35002
35041
  var defaultDependencies = {
35003
35042
  createDefaultConfig,
35004
35043
  ensureDir,
@@ -35008,7 +35047,7 @@ var defaultDependencies = {
35008
35047
  };
35009
35048
  async function handleInitCommand(options2, deps = defaultDependencies) {
35010
35049
  try {
35011
- const configPath = path13.resolve(options2.config);
35050
+ const configPath = path14.resolve(options2.config);
35012
35051
  const configCreated = await deps.createDefaultConfig(configPath);
35013
35052
  if (!configCreated) {
35014
35053
  deps.logger.log(`
@@ -35017,19 +35056,19 @@ Skipped initialization because the config file already exists`);
35017
35056
  }
35018
35057
  deps.logger.log("Creating directory structure...");
35019
35058
  const baseDir = process.cwd();
35020
- const contentDir = path13.join(baseDir, "content");
35021
- const templatesDir = path13.join(baseDir, "templates");
35022
- const stylesDir = path13.join(templatesDir, "styles");
35023
- const publicDir = path13.join(baseDir, "public");
35059
+ const contentDir = path14.join(baseDir, "content");
35060
+ const templatesDir = path14.join(baseDir, "templates");
35061
+ const stylesDir = path14.join(templatesDir, "styles");
35062
+ const publicDir = path14.join(baseDir, "public");
35024
35063
  await deps.ensureDir(contentDir);
35025
35064
  await deps.ensureDir(templatesDir);
35026
35065
  await deps.ensureDir(stylesDir);
35027
35066
  await deps.ensureDir(publicDir);
35028
35067
  for (const [filename, content] of Object.entries(getDefaultTemplates())) {
35029
- await deps.writeFile(path13.join(templatesDir, filename), content);
35068
+ await deps.writeFile(path14.join(templatesDir, filename), content);
35030
35069
  }
35031
- await deps.writeFile(path13.join(stylesDir, "main.css"), getDefaultCss());
35032
- await deps.writeFile(path13.join(contentDir, "welcome.md"), getSamplePost());
35070
+ await deps.writeFile(path14.join(stylesDir, "main.css"), getDefaultCss());
35071
+ await deps.writeFile(path14.join(contentDir, "welcome.md"), getSamplePost());
35033
35072
  deps.logger.log(`
35034
35073
  Initialization complete! Here are the next steps:`);
35035
35074
  deps.logger.log("1. Edit bunki.config.ts to configure your site");
@@ -35049,221 +35088,302 @@ function registerInitCommand(program2, deps = defaultDependencies) {
35049
35088
  function getDefaultTemplates() {
35050
35089
  return {
35051
35090
  "base.njk": String.raw`<!DOCTYPE html>
35052
- <html lang="en">
35053
- <head>
35054
- <meta charset="UTF-8">
35055
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
35056
- <title>{% block title %}{{ site.title }}{% endblock %}</title>
35057
- <meta name="description" content="{% block description %}{{ site.description }}{% endblock %}">
35058
- <link rel="stylesheet" href="/css/style.css">
35059
- {% block head %}{% endblock %}
35060
- </head>
35061
- <body>
35062
- <header>
35063
- <div class="container">
35064
- <h1><a href="/">{{ site.title }}</a></h1>
35065
- <nav>
35066
- <ul>
35067
- <li><a href="/">Home</a></li>
35068
- <li><a href="/tags/">Tags</a></li>
35069
- </ul>
35070
- </nav>
35071
- </div>
35072
- </header>
35073
-
35074
- <main class="container">
35075
- {% block content %}{% endblock %}
35076
- </main>
35077
-
35078
- <footer>
35079
- <div class="container">
35080
- <p>&copy; {{ "now" | date("YYYY") }} {{ site.title }}</p>
35081
- </div>
35082
- </footer>
35083
- </body>
35084
- </html>`,
35091
+ <html lang="en">
35092
+ <head>
35093
+ <meta charset="UTF-8">
35094
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
35095
+ <title>{% block title %}{{ site.title }}{% endblock %}</title>
35096
+ <meta name="description" content="{% block description %}{{ site.description }}{% endblock %}">
35097
+
35098
+ {# Canonical URL #}
35099
+ <link rel="canonical" href="{% block canonical %}{{ site.baseUrl }}/{% endblock %}">
35100
+
35101
+ {# Open Graph meta tags #}
35102
+ <meta property="og:type" content="{% block og_type %}website{% endblock %}">
35103
+ <meta property="og:title" content="{% block og_title %}{{ site.title }}{% endblock %}">
35104
+ <meta property="og:description" content="{% block og_description %}{{ site.description }}{% endblock %}">
35105
+ <meta property="og:url" content="{% block og_url %}{{ site.baseUrl }}/{% endblock %}">
35106
+ <meta property="og:site_name" content="{{ site.title }}">
35107
+ {% block og_image %}{% endblock %}
35108
+
35109
+ {# Twitter Card meta tags #}
35110
+ <meta name="twitter:card" content="{% block twitter_card %}summary{% endblock %}">
35111
+ <meta name="twitter:title" content="{% block twitter_title %}{{ site.title }}{% endblock %}">
35112
+ <meta name="twitter:description" content="{% block twitter_description %}{{ site.description }}{% endblock %}">
35113
+ {% block twitter_image %}{% endblock %}
35114
+
35115
+ <link rel="stylesheet" href="/css/style.css">
35116
+ <link rel="alternate" type="application/rss+xml" title="{{ site.title }} RSS Feed" href="{{ site.baseUrl }}/feed.xml">
35117
+ {% block head %}{% endblock %}
35118
+ </head>
35119
+ <body>
35120
+ <header>
35121
+ <div class="container">
35122
+ <h1><a href="/">{{ site.title }}</a></h1>
35123
+ <nav>
35124
+ <ul>
35125
+ <li><a href="/">Home</a></li>
35126
+ <li><a href="/tags/">Tags</a></li>
35127
+ </ul>
35128
+ </nav>
35129
+ </div>
35130
+ </header>
35131
+
35132
+ <main class="container">
35133
+ {% block content %}{% endblock %}
35134
+ </main>
35135
+
35136
+ <footer>
35137
+ <div class="container">
35138
+ <p>&copy; {{ "now" | date("YYYY") }} {{ site.title }} - Powered by <a href="https://github.com/kahwee/bunki">Bunki</a></p>
35139
+ </div>
35140
+ </footer>
35141
+ </body>
35142
+ </html>`,
35085
35143
  "index.njk": String.raw`{% extends "base.njk" %}
35086
35144
 
35087
- {% block content %}
35088
- <h1>Latest Posts</h1>
35089
-
35090
- {% if posts.length > 0 %}
35091
- <div class="posts">
35092
- {% for post in posts %}
35093
- <article class="post-card">
35094
- <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35095
- <div class="post-meta">
35096
- <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35097
- {% if post.tags.length > 0 %}
35098
- <span class="tags">
35099
- {% for tag in post.tags %}
35100
- <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35101
- {% endfor %}
35102
- </span>
35103
- {% endif %}
35104
- </div>
35105
- <div class="post-excerpt">{{ post.excerpt }}</div>
35106
- <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35107
- </article>
35108
- {% endfor %}
35109
- </div>
35110
-
35111
- {% if pagination.totalPages > 1 %}
35112
- <nav class="pagination">
35113
- {% if pagination.hasPrevPage %}
35114
- <a href="{{ pagination.pagePath }}{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35115
- {% endif %}
35116
-
35117
- {% if pagination.hasNextPage %}
35118
- <a href="{{ pagination.pagePath }}page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
35119
- {% endif %}
35120
-
35121
- <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35122
- </nav>
35123
- {% endif %}
35124
- {% else %}
35125
- <p>No posts yet!</p>
35145
+ {% block canonical %}{{ site.baseUrl }}/{% if pagination.currentPage > 1 %}page/{{ pagination.currentPage }}/{% endif %}{% endblock %}
35146
+ {% block og_url %}{{ site.baseUrl }}/{% if pagination.currentPage > 1 %}page/{{ pagination.currentPage }}/{% endif %}{% endblock %}
35147
+
35148
+ {% block content %}
35149
+ <h1>Latest Posts</h1>
35150
+
35151
+ {% if posts.length > 0 %}
35152
+ <div class="posts">
35153
+ {% for post in posts %}
35154
+ <article class="post-card">
35155
+ <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35156
+ <div class="post-meta">
35157
+ <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35158
+ {% if post.tags.length > 0 %}
35159
+ <span class="tags">
35160
+ {% for tag in post.tags %}
35161
+ <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35162
+ {% endfor %}
35163
+ </span>
35164
+ {% endif %}
35165
+ </div>
35166
+ <div class="post-excerpt">{{ post.excerpt }}</div>
35167
+ <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35168
+ </article>
35169
+ {% endfor %}
35170
+ </div>
35171
+
35172
+ {% if pagination.totalPages > 1 %}
35173
+ <nav class="pagination">
35174
+ {% if pagination.hasPrevPage %}
35175
+ <a href="{{ pagination.pagePath }}{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35176
+ {% endif %}
35177
+ {% if pagination.hasNextPage %}
35178
+ <a href="{{ pagination.pagePath }}page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
35179
+ {% endif %}
35180
+ <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35181
+ </nav>
35126
35182
  {% endif %}
35127
- {% endblock %}`,
35183
+ {% else %}
35184
+ <p>No posts yet.</p>
35185
+ {% endif %}
35186
+ {% endblock %}`,
35128
35187
  "post.njk": String.raw`{% extends "base.njk" %}
35129
35188
 
35130
- {% block title %}{{ post.title }} | {{ site.title }}{% endblock %}
35131
- {% block description %}{{ post.excerpt }}{% endblock %}
35132
-
35133
- {% block content %}
35134
- <article class="post">
35135
- <header class="post-header">
35136
- <h1>{{ post.title }}</h1>
35137
- <div class="post-meta">
35138
- <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35139
- {% if post.tags.length > 0 %}
35140
- <span class="tags">
35141
- {% for tag in post.tags %}
35142
- <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35143
- {% endfor %}
35144
- </span>
35145
- {% endif %}
35146
- </div>
35147
- </header>
35148
-
35149
- <div class="post-content">
35150
- {{ post.html | safe }}
35189
+ {% from "og-image.njk" import og_image, twitter_image %}
35190
+ {% from "json-ld.njk" import blog_posting_schema %}
35191
+
35192
+ {% block title %}{{ post.title }} | {{ site.title }}{% endblock %}
35193
+ {% block description %}{{ post.excerpt }}{% endblock %}
35194
+
35195
+ {% block canonical %}{{ site.baseUrl }}{{ post.url }}{% endblock %}
35196
+
35197
+ {% block og_type %}article{% endblock %}
35198
+ {% block og_title %}{{ post.title }}{% endblock %}
35199
+ {% block og_description %}{{ post.excerpt }}{% endblock %}
35200
+ {% block og_url %}{{ site.baseUrl }}{{ post.url }}{% endblock %}
35201
+ {% block og_image %}{{ og_image(post, site) }}{% endblock %}
35202
+
35203
+ {% block twitter_card %}summary_large_image{% endblock %}
35204
+ {% block twitter_title %}{{ post.title }}{% endblock %}
35205
+ {% block twitter_description %}{{ post.excerpt }}{% endblock %}
35206
+ {% block twitter_image %}{{ twitter_image(post, site) }}{% endblock %}
35207
+
35208
+ {% block head %}
35209
+ {{ blog_posting_schema(post, site) }}
35210
+ {% endblock %}
35211
+
35212
+ {% block content %}
35213
+ <article class="post">
35214
+ <header class="post-header">
35215
+ <h1>{{ post.title }}</h1>
35216
+ <div class="post-meta">
35217
+ <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35218
+ {% if post.tags.length > 0 %}
35219
+ <span class="tags">
35220
+ {% for tag in post.tags %}
35221
+ <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35222
+ {% endfor %}
35223
+ </span>
35224
+ {% endif %}
35151
35225
  </div>
35152
- </article>
35153
- {% endblock %}`,
35154
- "tag.njk": String.raw`{% extends "base.njk" %}
35155
-
35156
- {% block title %}{{ tag.name }} | {{ site.title }}{% endblock %}
35157
- {% block description %}Posts tagged with {{ tag.name }} on {{ site.title }}{% endblock %}
35158
-
35159
- {% block content %}
35160
- <h1>Posts tagged "{{ tag.name }}"</h1>
35161
-
35162
- {% if tag.description %}
35163
- <div class="tag-description">{{ tag.description }}</div>
35164
- {% endif %}
35226
+ </header>
35165
35227
 
35166
- {% if tag.posts.length > 0 %}
35167
- <div class="posts">
35168
- {% for post in tag.posts %}
35169
- <article class="post-card">
35170
- <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35171
- <div class="post-meta">
35172
- <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35173
- </div>
35174
- <div class="post-excerpt">{{ post.excerpt }}</div>
35175
- <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35176
- </article>
35177
- {% endfor %}
35228
+ <div class="post-content">
35229
+ {{ post.html | safe }}
35230
+ </div>
35231
+
35232
+ <footer class="post-footer">
35233
+ <div class="share-buttons">
35234
+ <span class="share-label">Share:</span>
35235
+ <a href="https://twitter.com/intent/tweet?text={{ post.title | urlencode }}&url={{ site.baseUrl }}{{ post.url }}" target="_blank" rel="noopener noreferrer" class="share-button x" aria-label="Share on X">
35236
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
35237
+ </a>
35238
+ <a href="https://www.facebook.com/sharer/sharer.php?u={{ site.baseUrl }}{{ post.url }}" target="_blank" rel="noopener noreferrer" class="share-button facebook" aria-label="Share on Facebook">
35239
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 3.667h-3.533v7.98H9.101z"/></svg>
35240
+ </a>
35241
+ <a href="https://www.linkedin.com/sharing/share-offsite/?url={{ site.baseUrl }}{{ post.url }}" target="_blank" rel="noopener noreferrer" class="share-button linkedin" aria-label="Share on LinkedIn">
35242
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M6.5 21.5h-5v-13h5v13zM4 6.5C2.5 6.5 1.5 5.3 1.5 4s1-2.4 2.5-2.4c1.6 0 2.5 1 2.6 2.5 0 1.4-1 2.5-2.6 2.5zm11.5 6c-1 0-2 1-2 2v7h-5v-13h5V10s1.6-1.5 4-1.5c3 0 5 2.2 5 6.3v6.7h-5v-7c0-1-1-2-2-2z"/></svg>
35243
+ </a>
35244
+ <a href="mailto:?subject={{ post.title | urlencode }}&body=Check%20out%20this%20article%3A%20{{ site.baseUrl }}{{ post.url }}" class="share-button email" aria-label="Share via Email">
35245
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
35246
+ </a>
35178
35247
  </div>
35248
+ </footer>
35249
+ </article>
35250
+ {% endblock %}`,
35251
+ "tag.njk": String.raw`{% extends "base.njk" %}
35179
35252
 
35180
- {% if pagination.totalPages > 1 %}
35181
- <nav class="pagination">
35182
- {% if pagination.hasPrevPage %}
35183
- <a href="{{ pagination.pagePath }}{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35184
- {% endif %}
35185
-
35186
- {% if pagination.hasNextPage %}
35187
- <a href="{{ pagination.pagePath }}page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
35188
- {% endif %}
35189
-
35190
- <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35191
- </nav>
35192
- {% endif %}
35193
- {% else %}
35194
- <p>No posts with this tag yet!</p>
35253
+ {% block title %}{{ tag.name }} | {{ site.title }}{% endblock %}
35254
+ {% block description %}Posts tagged with {{ tag.name }} on {{ site.title }}{% endblock %}
35255
+
35256
+ {% block canonical %}{{ site.baseUrl }}/tags/{{ tag.slug }}/{% if pagination.currentPage > 1 %}page/{{ pagination.currentPage }}/{% endif %}{% endblock %}
35257
+
35258
+ {% block og_title %}{{ tag.name }} | {{ site.title }}{% endblock %}
35259
+ {% block og_description %}Posts tagged with {{ tag.name }} on {{ site.title }}{% endblock %}
35260
+ {% block og_url %}{{ site.baseUrl }}/tags/{{ tag.slug }}/{% if pagination.currentPage > 1 %}page/{{ pagination.currentPage }}/{% endif %}{% endblock %}
35261
+
35262
+ {% block twitter_title %}{{ tag.name }} | {{ site.title }}{% endblock %}
35263
+ {% block twitter_description %}Posts tagged with {{ tag.name }} on {{ site.title }}{% endblock %}
35264
+
35265
+ {% block content %}
35266
+ <h1>Posts tagged "{{ tag.name }}"</h1>
35267
+
35268
+ {% if tag.description %}
35269
+ <div class="tag-description">{{ tag.description }}</div>
35270
+ {% endif %}
35271
+
35272
+ {% if tag.posts.length > 0 %}
35273
+ <div class="posts">
35274
+ {% for post in tag.posts %}
35275
+ <article class="post-card">
35276
+ <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35277
+ <div class="post-meta">
35278
+ <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35279
+ </div>
35280
+ <div class="post-excerpt">{{ post.excerpt }}</div>
35281
+ <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35282
+ </article>
35283
+ {% endfor %}
35284
+ </div>
35285
+
35286
+ {% if pagination.totalPages > 1 %}
35287
+ <nav class="pagination">
35288
+ {% if pagination.hasPrevPage %}
35289
+ <a href="{{ pagination.pagePath }}{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35290
+ {% endif %}
35291
+ {% if pagination.hasNextPage %}
35292
+ <a href="{{ pagination.pagePath }}page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
35293
+ {% endif %}
35294
+ <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35295
+ </nav>
35195
35296
  {% endif %}
35196
- {% endblock %}`,
35297
+ {% else %}
35298
+ <p>No posts with this tag yet.</p>
35299
+ {% endif %}
35300
+ {% endblock %}`,
35197
35301
  "tags.njk": String.raw`{% extends "base.njk" %}
35198
35302
 
35199
- {% block title %}Tags | {{ site.title }}{% endblock %}
35200
- {% block description %}Browse all tags on {{ site.title }}{% endblock %}
35303
+ {% block title %}Tags | {{ site.title }}{% endblock %}
35304
+ {% block description %}Browse all tags on {{ site.title }}{% endblock %}
35201
35305
 
35202
- {% block content %}
35203
- <h1>All Tags</h1>
35306
+ {% block canonical %}{{ site.baseUrl }}/tags/{% endblock %}
35204
35307
 
35205
- {% if tags.length > 0 %}
35206
- <ul class="tags-list">
35207
- {% for tag in tags %}
35208
- <li>
35209
- <a href="/tags/{{ tag.slug }}/">{{ tag.name }}</a>
35210
- <span class="count">({{ tag.count }})</span>
35211
- {% if tag.description %}
35212
- <p class="description">{{ tag.description }}</p>
35213
- {% endif %}
35214
- </li>
35215
- {% endfor %}
35216
- </ul>
35217
- {% else %}
35218
- <p>No tags found!</p>
35219
- {% endif %}
35220
- {% endblock %}`,
35221
- "archive.njk": String.raw`{% extends "base.njk" %}
35308
+ {% block og_title %}Tags | {{ site.title }}{% endblock %}
35309
+ {% block og_description %}Browse all tags on {{ site.title }}{% endblock %}
35310
+ {% block og_url %}{{ site.baseUrl }}/tags/{% endblock %}
35222
35311
 
35223
- {% block title %}Archive {{ year }} | {{ site.title }}{% endblock %}
35224
- {% block description %}Posts from {{ year }} on {{ site.title }}{% endblock %}
35225
-
35226
- {% block content %}
35227
- <h1>Posts from {{ year }}</h1>
35228
-
35229
- {% if posts.length > 0 %}
35230
- <div class="posts">
35231
- {% for post in posts %}
35232
- <article class="post-card">
35233
- <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35234
- <div class="post-meta">
35235
- <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35236
- {% if post.tags.length > 0 %}
35237
- <span class="tags">
35238
- {% for tag in post.tags %}
35239
- <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35240
- {% endfor %}
35241
- </span>
35242
- {% endif %}
35243
- </div>
35244
- <div class="post-excerpt">{{ post.excerpt }}</div>
35245
- <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35246
- </article>
35247
- {% endfor %}
35248
- </div>
35312
+ {% block twitter_title %}Tags | {{ site.title }}{% endblock %}
35313
+ {% block twitter_description %}Browse all tags on {{ site.title }}{% endblock %}
35249
35314
 
35250
- {% if pagination.totalPages > 1 %}
35251
- <nav class="pagination">
35252
- {% if pagination.hasPrevPage %}
35253
- <a href="/{{ year }}/{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35254
- {% endif %}
35315
+ {% block content %}
35316
+ <h1>All Tags</h1>
35255
35317
 
35256
- {% if pagination.hasNextPage %}
35257
- <a href="/{{ year }}/page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
35318
+ {% if tags.length > 0 %}
35319
+ <ul class="tags-list">
35320
+ {% for tag in tags %}
35321
+ <li>
35322
+ <a href="/tags/{{ tag.slug }}/">{{ tag.name }}</a>
35323
+ <span class="count">({{ tag.count }})</span>
35324
+ {% if tag.description %}
35325
+ <p class="description">{{ tag.description }}</p>
35258
35326
  {% endif %}
35327
+ </li>
35328
+ {% endfor %}
35329
+ </ul>
35330
+ {% else %}
35331
+ <p>No tags yet.</p>
35332
+ {% endif %}
35333
+ {% endblock %}`,
35334
+ "archive.njk": String.raw`{% extends "base.njk" %}
35259
35335
 
35260
- <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35261
- </nav>
35262
- {% endif %}
35263
- {% else %}
35264
- <p>No posts from {{ year }}!</p>
35336
+ {% block title %}Archive {{ year }} | {{ site.title }}{% endblock %}
35337
+ {% block description %}Posts from {{ year }} on {{ site.title }}{% endblock %}
35338
+
35339
+ {% block canonical %}{{ site.baseUrl }}/{{ year }}/{% if pagination.currentPage > 1 %}page/{{ pagination.currentPage }}/{% endif %}{% endblock %}
35340
+
35341
+ {% block og_title %}Archive {{ year }} | {{ site.title }}{% endblock %}
35342
+ {% block og_description %}Posts from {{ year }} on {{ site.title }}{% endblock %}
35343
+ {% block og_url %}{{ site.baseUrl }}/{{ year }}/{% if pagination.currentPage > 1 %}page/{{ pagination.currentPage }}/{% endif %}{% endblock %}
35344
+
35345
+ {% block twitter_title %}Archive {{ year }} | {{ site.title }}{% endblock %}
35346
+ {% block twitter_description %}Posts from {{ year }} on {{ site.title }}{% endblock %}
35347
+
35348
+ {% block content %}
35349
+ <h1>Posts from {{ year }}</h1>
35350
+
35351
+ {% if posts.length > 0 %}
35352
+ <div class="posts">
35353
+ {% for post in posts %}
35354
+ <article class="post-card">
35355
+ <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35356
+ <div class="post-meta">
35357
+ <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35358
+ {% if post.tags.length > 0 %}
35359
+ <span class="tags">
35360
+ {% for tag in post.tags %}
35361
+ <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35362
+ {% endfor %}
35363
+ </span>
35364
+ {% endif %}
35365
+ </div>
35366
+ <div class="post-excerpt">{{ post.excerpt }}</div>
35367
+ <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35368
+ </article>
35369
+ {% endfor %}
35370
+ </div>
35371
+
35372
+ {% if pagination.totalPages > 1 %}
35373
+ <nav class="pagination">
35374
+ {% if pagination.hasPrevPage %}
35375
+ <a href="/{{ year }}/{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35376
+ {% endif %}
35377
+ {% if pagination.hasNextPage %}
35378
+ <a href="/{{ year }}/page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
35379
+ {% endif %}
35380
+ <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35381
+ </nav>
35265
35382
  {% endif %}
35266
- {% endblock %}`
35383
+ {% else %}
35384
+ <p>No posts from {{ year }} yet.</p>
35385
+ {% endif %}
35386
+ {% endblock %}`
35267
35387
  };
35268
35388
  }
35269
35389
  function getDefaultCss() {
@@ -35458,6 +35578,43 @@ function getDefaultCss() {
35458
35578
  font-size: 0.9rem;
35459
35579
  }
35460
35580
 
35581
+ /* Share buttons */
35582
+ .post-footer {
35583
+ margin-top: 2rem;
35584
+ padding-top: 1.5rem;
35585
+ border-top: 1px solid #eee;
35586
+ }
35587
+
35588
+ .share-buttons {
35589
+ display: flex;
35590
+ align-items: center;
35591
+ gap: 0.75rem;
35592
+ }
35593
+
35594
+ .share-label {
35595
+ font-size: 0.9rem;
35596
+ font-weight: 500;
35597
+ color: #6c757d;
35598
+ }
35599
+
35600
+ .share-button {
35601
+ display: inline-flex;
35602
+ align-items: center;
35603
+ justify-content: center;
35604
+ width: 2.25rem;
35605
+ height: 2.25rem;
35606
+ border-radius: 50%;
35607
+ background-color: #f5f5f5;
35608
+ color: #555;
35609
+ transition: background-color 0.2s, color 0.2s;
35610
+ }
35611
+
35612
+ .share-button:hover { text-decoration: none; }
35613
+ .share-button.x:hover { background-color: #000; color: #fff; }
35614
+ .share-button.facebook:hover { background-color: #1877f2; color: #fff; }
35615
+ .share-button.linkedin:hover { background-color: #0077b5; color: #fff; }
35616
+ .share-button.email:hover { background-color: #6c757d; color: #fff; }
35617
+
35461
35618
  /* Footer */
35462
35619
  footer {
35463
35620
  text-align: center;
@@ -35521,7 +35678,7 @@ function hello() {
35521
35678
  }
35522
35679
 
35523
35680
  // src/cli/commands/new-post.ts
35524
- import path14 from "path";
35681
+ import path15 from "path";
35525
35682
  var defaultDeps4 = {
35526
35683
  writeFile: (filePath, data) => Bun.write(filePath, data),
35527
35684
  now: () => new Date,
@@ -35545,7 +35702,7 @@ async function handleNewCommand(title, options2, deps = defaultDeps4) {
35545
35702
  ` + `# ${title}
35546
35703
 
35547
35704
  `;
35548
- const filePath = path14.join(DEFAULT_CONTENT_DIR, `${slug}.md`);
35705
+ const filePath = path15.join(DEFAULT_CONTENT_DIR, `${slug}.md`);
35549
35706
  await deps.writeFile(filePath, frontmatter);
35550
35707
  deps.logger.log(`Created new post: ${filePath}`);
35551
35708
  return filePath;
@@ -35562,11 +35719,11 @@ function registerNewCommand(program2) {
35562
35719
  }
35563
35720
 
35564
35721
  // src/cli/commands/serve.ts
35565
- import path16 from "path";
35722
+ import path17 from "path";
35566
35723
 
35567
35724
  // src/server.ts
35568
35725
  import fs2 from "fs";
35569
- import path15 from "path";
35726
+ import path16 from "path";
35570
35727
  async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
35571
35728
  try {
35572
35729
  const stats = await fs2.promises.stat(outputDir);
@@ -35601,18 +35758,18 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
35601
35758
  let filePath = "";
35602
35759
  if (homePaginationMatch) {
35603
35760
  const pageNumber = homePaginationMatch[1];
35604
- filePath = path15.join(outputDir, "page", pageNumber, "index.html");
35761
+ filePath = path16.join(outputDir, "page", pageNumber, "index.html");
35605
35762
  } else if (tagPaginationMatch) {
35606
35763
  const tagSlug = tagPaginationMatch[1];
35607
35764
  const pageNumber = tagPaginationMatch[2];
35608
- filePath = path15.join(outputDir, "tags", tagSlug, "page", pageNumber, "index.html");
35765
+ filePath = path16.join(outputDir, "tags", tagSlug, "page", pageNumber, "index.html");
35609
35766
  } else if (yearPaginationMatch) {
35610
35767
  const year = yearPaginationMatch[1];
35611
35768
  const pageNumber = yearPaginationMatch[2];
35612
- filePath = path15.join(outputDir, year, "page", pageNumber, "index.html");
35769
+ filePath = path16.join(outputDir, year, "page", pageNumber, "index.html");
35613
35770
  } else {
35614
- const directPath = path15.join(outputDir, pathname);
35615
- const withoutSlash = path15.join(outputDir, pathname + ".html");
35771
+ const directPath = path16.join(outputDir, pathname);
35772
+ const withoutSlash = path16.join(outputDir, pathname + ".html");
35616
35773
  const withHtml = pathname.endsWith(".html") ? directPath : withoutSlash;
35617
35774
  const bunFileDirect = Bun.file(directPath);
35618
35775
  const bunFileHtml = Bun.file(withHtml);
@@ -35621,7 +35778,7 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
35621
35778
  } else if (await bunFileHtml.exists()) {
35622
35779
  filePath = withHtml;
35623
35780
  } else {
35624
- const indexPath = path15.join(outputDir, pathname, "index.html");
35781
+ const indexPath = path16.join(outputDir, pathname, "index.html");
35625
35782
  const bunFileIndex = Bun.file(indexPath);
35626
35783
  if (await bunFileIndex.exists()) {
35627
35784
  filePath = indexPath;
@@ -35635,7 +35792,7 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
35635
35792
  }
35636
35793
  }
35637
35794
  console.log(`Serving file: ${filePath}`);
35638
- const extname = path15.extname(filePath);
35795
+ const extname = path16.extname(filePath);
35639
35796
  let contentType = "text/html";
35640
35797
  switch (extname) {
35641
35798
  case ".js":
@@ -35694,7 +35851,7 @@ var defaultDeps5 = {
35694
35851
  };
35695
35852
  async function handleServeCommand(options2, deps = defaultDeps5) {
35696
35853
  try {
35697
- const outputDir = path16.resolve(options2.output);
35854
+ const outputDir = path17.resolve(options2.output);
35698
35855
  const port = parseInt(options2.port, 10);
35699
35856
  await deps.startServer(outputDir, port);
35700
35857
  } catch (error) {
@@ -35737,7 +35894,7 @@ function registerValidateCommand(program2) {
35737
35894
  }
35738
35895
 
35739
35896
  // src/cli/commands/validate-media.ts
35740
- import { readdirSync, readFileSync, existsSync, statSync } from "fs";
35897
+ import { readdirSync, readFileSync, existsSync as existsSync2, statSync } from "fs";
35741
35898
  import { join, dirname, resolve, basename } from "path";
35742
35899
  var imageExtensions = [".jpg", ".jpeg", ".png", ".webp", ".gif"];
35743
35900
  var videoExtensions = [".mp4", ".webm", ".mov"];
@@ -35745,7 +35902,7 @@ var mediaExtensions = [...imageExtensions, ...videoExtensions];
35745
35902
  async function handleValidateMediaCommand(options2, deps = { logger: console, exit: (code) => process.exit(code) }) {
35746
35903
  const contentDir = options2.contentDir || join(process.cwd(), "content");
35747
35904
  const assetsDir = join(process.cwd(), "assets");
35748
- if (!existsSync(contentDir)) {
35905
+ if (!existsSync2(contentDir)) {
35749
35906
  deps.logger.error(`Content directory not found: ${contentDir}`);
35750
35907
  deps.exit(1);
35751
35908
  }
@@ -35867,7 +36024,7 @@ function validateMedia(contentDir, assetsDir) {
35867
36024
  }
35868
36025
  function getAllMediaFromContentAssets(contentDir) {
35869
36026
  const mediaFiles = [];
35870
- if (!existsSync(contentDir))
36027
+ if (!existsSync2(contentDir))
35871
36028
  return [];
35872
36029
  const years = readdirSync(contentDir).filter((f) => {
35873
36030
  const fullPath = join(contentDir, f);
@@ -35875,7 +36032,7 @@ function getAllMediaFromContentAssets(contentDir) {
35875
36032
  });
35876
36033
  for (const year of years) {
35877
36034
  const assetsDir = join(contentDir, year, "_assets");
35878
- if (!existsSync(assetsDir))
36035
+ if (!existsSync2(assetsDir))
35879
36036
  continue;
35880
36037
  const files = readdirSync(assetsDir);
35881
36038
  for (const file of files) {
@@ -35897,7 +36054,7 @@ function getAllMediaFromContentAssets(contentDir) {
35897
36054
  }
35898
36055
  function getAllMediaFromAssets(assetsDir) {
35899
36056
  const mediaFiles = [];
35900
- if (!existsSync(assetsDir))
36057
+ if (!existsSync2(assetsDir))
35901
36058
  return [];
35902
36059
  const years = readdirSync(assetsDir).filter((f) => {
35903
36060
  const fullPath = join(assetsDir, f);
@@ -35929,7 +36086,7 @@ function getAllMediaFromAssets(assetsDir) {
35929
36086
  function checkMediaReference(markdownFile, lineNumber, mediaPath, type, missingReferences) {
35930
36087
  const markdownDir = dirname(markdownFile);
35931
36088
  const resolvedPath = resolve(markdownDir, mediaPath);
35932
- if (!existsSync(resolvedPath)) {
36089
+ if (!existsSync2(resolvedPath)) {
35933
36090
  missingReferences.push({
35934
36091
  file: markdownFile.replace(process.cwd() + "/", ""),
35935
36092
  line: lineNumber,