bunki 0.18.5 → 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,
@@ -33964,31 +33986,6 @@ ${categoryTags}`;
33964
33986
  return itemXml;
33965
33987
  }
33966
33988
 
33967
- // src/utils/pagination.ts
33968
- function createPagination(items, currentPage, pageSize, pagePath) {
33969
- const totalItems = items.length;
33970
- const totalPages = Math.ceil(totalItems / pageSize);
33971
- return {
33972
- currentPage,
33973
- totalPages,
33974
- hasNextPage: currentPage < totalPages,
33975
- hasPrevPage: currentPage > 1,
33976
- nextPage: currentPage < totalPages ? currentPage + 1 : null,
33977
- prevPage: currentPage > 1 ? currentPage - 1 : null,
33978
- pageSize,
33979
- totalItems,
33980
- pagePath
33981
- };
33982
- }
33983
- function getPaginatedItems(items, page, pageSize) {
33984
- const startIndex = (page - 1) * pageSize;
33985
- const endIndex = startIndex + pageSize;
33986
- return items.slice(startIndex, endIndex);
33987
- }
33988
- function getTotalPages(totalItems, pageSize) {
33989
- return Math.ceil(totalItems / pageSize);
33990
- }
33991
-
33992
33989
  // src/generators/feeds.ts
33993
33990
  function makeAbsoluteUrl(imageUrl, baseUrl) {
33994
33991
  return imageUrl.startsWith("http") ? imageUrl : `${baseUrl}${imageUrl}`;
@@ -34058,10 +34055,6 @@ function generateSitemap(site, config, pageSize = 10) {
34058
34055
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
34059
34056
  `;
34060
34057
  sitemapContent += buildSitemapUrl(`${config.baseUrl}/`, currentDate, "daily", 1);
34061
- const totalHomePages = getTotalPages(site.posts.length, pageSize);
34062
- for (let page = 2;page <= totalHomePages; page++) {
34063
- sitemapContent += buildSitemapUrl(`${config.baseUrl}/page/${page}/`, currentDate, "daily", 0.8);
34064
- }
34065
34058
  for (const post of site.posts) {
34066
34059
  const postUrl = `${config.baseUrl}${post.url}`;
34067
34060
  const postDate = new Date(post.date).toISOString();
@@ -34077,20 +34070,12 @@ function generateSitemap(site, config, pageSize = 10) {
34077
34070
  const mostRecentPost = tagData.posts[0];
34078
34071
  const tagPriority = mostRecentPost ? calculateFreshnessPriority(mostRecentPost.date, 0.4, now) : 0.4;
34079
34072
  sitemapContent += buildSitemapUrl(tagUrl, currentDate, "weekly", tagPriority);
34080
- const totalTagPages = getTotalPages(tagData.posts.length, pageSize);
34081
- for (let page = 2;page <= totalTagPages; page++) {
34082
- sitemapContent += buildSitemapUrl(`${config.baseUrl}/tags/${tagData.slug}/page/${page}/`, currentDate, "weekly", Math.max(0.3, tagPriority - 0.1));
34083
- }
34084
34073
  }
34085
34074
  for (const [year, yearPosts] of Object.entries(site.postsByYear)) {
34086
34075
  const currentYear = new Date().getFullYear();
34087
34076
  const isCurrentYear = parseInt(year) === currentYear;
34088
34077
  const yearPriority = isCurrentYear ? 0.7 : 0.5;
34089
34078
  sitemapContent += buildSitemapUrl(`${config.baseUrl}/${year}/`, currentDate, isCurrentYear ? "weekly" : "monthly", yearPriority);
34090
- const totalYearPages = getTotalPages(yearPosts.length, pageSize);
34091
- for (let page = 2;page <= totalYearPages; page++) {
34092
- sitemapContent += buildSitemapUrl(`${config.baseUrl}/${year}/page/${page}/`, currentDate, isCurrentYear ? "weekly" : "monthly", yearPriority - 0.1);
34093
- }
34094
34079
  }
34095
34080
  sitemapContent += `</urlset>`;
34096
34081
  return sitemapContent;
@@ -34129,6 +34114,31 @@ Sitemap: ${config.baseUrl}/sitemap.xml
34129
34114
  var import_nunjucks = __toESM(require_nunjucks(), 1);
34130
34115
  import path7 from "path";
34131
34116
 
34117
+ // src/utils/pagination.ts
34118
+ function createPagination(items, currentPage, pageSize, pagePath) {
34119
+ const totalItems = items.length;
34120
+ const totalPages = Math.ceil(totalItems / pageSize);
34121
+ return {
34122
+ currentPage,
34123
+ totalPages,
34124
+ hasNextPage: currentPage < totalPages,
34125
+ hasPrevPage: currentPage > 1,
34126
+ nextPage: currentPage < totalPages ? currentPage + 1 : null,
34127
+ prevPage: currentPage > 1 ? currentPage - 1 : null,
34128
+ pageSize,
34129
+ totalItems,
34130
+ pagePath
34131
+ };
34132
+ }
34133
+ function getPaginatedItems(items, page, pageSize) {
34134
+ const startIndex = (page - 1) * pageSize;
34135
+ const endIndex = startIndex + pageSize;
34136
+ return items.slice(startIndex, endIndex);
34137
+ }
34138
+ function getTotalPages(totalItems, pageSize) {
34139
+ return Math.ceil(totalItems / pageSize);
34140
+ }
34141
+
34132
34142
  // src/utils/schema-factory.ts
34133
34143
  function generateCollectionSchemas(config, options2) {
34134
34144
  const schemas = [
@@ -34444,8 +34454,13 @@ function displayMetrics(metrics) {
34444
34454
 
34445
34455
  // src/utils/template-engine.ts
34446
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;
34447
34462
  function createTemplateEngine(templatesDir, watch = false) {
34448
- const env = import_nunjucks2.default.configure(templatesDir, {
34463
+ const env = import_nunjucks2.default.configure([templatesDir, BUNKI_FRAGMENTS_DIR], {
34449
34464
  autoescape: true,
34450
34465
  watch
34451
34466
  });
@@ -34496,12 +34511,24 @@ class SiteGenerator {
34496
34511
  async initialize() {
34497
34512
  this.metrics.startStage("initialization");
34498
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
+ }
34499
34526
  await ensureDir(this.options.outputDir);
34500
34527
  if (this.options.config.noFollowExceptions) {
34501
34528
  setNoFollowExceptions(this.options.config.noFollowExceptions);
34502
34529
  }
34503
34530
  let tagDescriptions = {};
34504
- const tagsTomlPath = path9.join(process.cwd(), "src", "tags.toml");
34531
+ const tagsTomlPath = path10.join(process.cwd(), "src", "tags.toml");
34505
34532
  const tagsTomlFile = Bun.file(tagsTomlPath);
34506
34533
  if (await tagsTomlFile.exists()) {
34507
34534
  try {
@@ -34563,8 +34590,8 @@ class SiteGenerator {
34563
34590
  this.metrics.startStage("cssProcessing");
34564
34591
  let cssChanged = true;
34565
34592
  if (this.cache && this.incrementalMode && this.options.config.css) {
34566
- const cssInputPath = path9.resolve(process.cwd(), this.options.config.css.input);
34567
- 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);
34568
34595
  const cssOutputExists = await Bun.file(cssOutputPath).exists();
34569
34596
  cssChanged = await hasFileChanged(cssInputPath, this.cache);
34570
34597
  if (!cssChanged && cssOutputExists) {
@@ -34598,19 +34625,19 @@ class SiteGenerator {
34598
34625
  }
34599
34626
  async generateFeeds() {
34600
34627
  const rssContent = generateRSSFeed(this.site, this.options.config);
34601
- await Bun.write(path9.join(this.options.outputDir, "feed.xml"), rssContent);
34628
+ await Bun.write(path10.join(this.options.outputDir, "feed.xml"), rssContent);
34602
34629
  const sitemapContent = generateSitemap(this.site, this.options.config, PAGINATION.DEFAULT_PAGE_SIZE);
34603
- await Bun.write(path9.join(this.options.outputDir, "sitemap.xml"), sitemapContent);
34630
+ await Bun.write(path10.join(this.options.outputDir, "sitemap.xml"), sitemapContent);
34604
34631
  console.log("Generated sitemap.xml");
34605
34632
  const urlCount = this.site.posts.length + Object.keys(this.site.tags).length + 10;
34606
34633
  const sitemapSize = sitemapContent.length;
34607
34634
  if (urlCount > FILES.MAX_SITEMAP_URLS || sitemapSize > FILES.MAX_SITEMAP_SIZE) {
34608
34635
  const sitemapIndexContent = generateSitemapIndex(this.options.config);
34609
- 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);
34610
34637
  console.log("Generated sitemap_index.xml");
34611
34638
  }
34612
34639
  const robotsTxtContent = generateRobotsTxt(this.options.config);
34613
- await Bun.write(path9.join(this.options.outputDir, "robots.txt"), robotsTxtContent);
34640
+ await Bun.write(path10.join(this.options.outputDir, "robots.txt"), robotsTxtContent);
34614
34641
  console.log("Generated robots.txt");
34615
34642
  }
34616
34643
  async parseContent() {
@@ -34627,7 +34654,7 @@ class SiteGenerator {
34627
34654
  return posts;
34628
34655
  }
34629
34656
  const allFiles = await findFilesByPattern("**/*.md", this.options.contentDir, true);
34630
- const configPath = path9.join(process.cwd(), "bunki.config.ts");
34657
+ const configPath = path10.join(process.cwd(), "bunki.config.ts");
34631
34658
  const configChanged = await hasConfigChanged(configPath, this.cache);
34632
34659
  if (configChanged) {
34633
34660
  console.log("Config changed, full rebuild required");
@@ -34706,10 +34733,10 @@ var defaultDeps2 = {
34706
34733
  };
34707
34734
  async function handleGenerateCommand(options2, deps = defaultDeps2) {
34708
34735
  try {
34709
- const configPath = path10.resolve(options2.config);
34710
- const contentDir = path10.resolve(options2.content);
34711
- const outputDir = path10.resolve(options2.output);
34712
- 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);
34713
34740
  deps.logger.log("Generating site with:");
34714
34741
  deps.logger.log(`- Config file: ${configPath}`);
34715
34742
  deps.logger.log(`- Content directory: ${contentDir}`);
@@ -34743,11 +34770,11 @@ function registerGenerateCommand(program2) {
34743
34770
  }
34744
34771
 
34745
34772
  // src/utils/image-uploader.ts
34746
- import path12 from "path";
34773
+ import path13 from "path";
34747
34774
 
34748
34775
  // src/utils/s3-uploader.ts
34749
34776
  var {S3Client } = globalThis.Bun;
34750
- import path11 from "path";
34777
+ import path12 from "path";
34751
34778
 
34752
34779
  class S3Uploader {
34753
34780
  s3Config;
@@ -34870,7 +34897,7 @@ class S3Uploader {
34870
34897
  let failedCount = 0;
34871
34898
  const uploadTasks = imageFiles.map((imageFile) => async () => {
34872
34899
  try {
34873
- const imagePath = path11.join(imagesDir, imageFile);
34900
+ const imagePath = path12.join(imagesDir, imageFile);
34874
34901
  const s3Key = keyTransform ? keyTransform(imageFile) : imageFile;
34875
34902
  const file = Bun.file(imagePath);
34876
34903
  if (process.env.BUNKI_DRY_RUN === "true") {} else {
@@ -34904,13 +34931,13 @@ function createUploader(config) {
34904
34931
  }
34905
34932
 
34906
34933
  // src/utils/image-uploader.ts
34907
- var DEFAULT_IMAGES_DIR = path12.join(process.cwd(), "assets");
34908
- 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");
34909
34936
  async function uploadImages(options2 = {}) {
34910
34937
  try {
34911
34938
  const contentAssetsMode = options2.contentAssets === true;
34912
34939
  const defaultDir = contentAssetsMode ? DEFAULT_CONTENT_DIR2 : DEFAULT_IMAGES_DIR;
34913
- const imagesDir = path12.resolve(options2.images || defaultDir);
34940
+ const imagesDir = path13.resolve(options2.images || defaultDir);
34914
34941
  if (!await fileExists(imagesDir)) {
34915
34942
  console.log(`Creating images directory at ${imagesDir}...`);
34916
34943
  await ensureDir(imagesDir);
@@ -34962,7 +34989,7 @@ async function uploadImages(options2 = {}) {
34962
34989
  const uploader = createUploader(s3Config);
34963
34990
  const imageUrlMap = await uploader.uploadImages(imagesDir, options2.minYear, keyTransform);
34964
34991
  if (options2.outputJson) {
34965
- const outputFile = path12.resolve(options2.outputJson);
34992
+ const outputFile = path13.resolve(options2.outputJson);
34966
34993
  await Bun.write(outputFile, JSON.stringify(imageUrlMap, null, 2));
34967
34994
  console.log(`Image URL mapping saved to ${outputFile}`);
34968
34995
  }
@@ -35010,7 +35037,7 @@ function registerImagesPushCommand(program2) {
35010
35037
  }
35011
35038
 
35012
35039
  // src/cli/commands/init.ts
35013
- import path13 from "path";
35040
+ import path14 from "path";
35014
35041
  var defaultDependencies = {
35015
35042
  createDefaultConfig,
35016
35043
  ensureDir,
@@ -35020,7 +35047,7 @@ var defaultDependencies = {
35020
35047
  };
35021
35048
  async function handleInitCommand(options2, deps = defaultDependencies) {
35022
35049
  try {
35023
- const configPath = path13.resolve(options2.config);
35050
+ const configPath = path14.resolve(options2.config);
35024
35051
  const configCreated = await deps.createDefaultConfig(configPath);
35025
35052
  if (!configCreated) {
35026
35053
  deps.logger.log(`
@@ -35029,19 +35056,19 @@ Skipped initialization because the config file already exists`);
35029
35056
  }
35030
35057
  deps.logger.log("Creating directory structure...");
35031
35058
  const baseDir = process.cwd();
35032
- const contentDir = path13.join(baseDir, "content");
35033
- const templatesDir = path13.join(baseDir, "templates");
35034
- const stylesDir = path13.join(templatesDir, "styles");
35035
- 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");
35036
35063
  await deps.ensureDir(contentDir);
35037
35064
  await deps.ensureDir(templatesDir);
35038
35065
  await deps.ensureDir(stylesDir);
35039
35066
  await deps.ensureDir(publicDir);
35040
35067
  for (const [filename, content] of Object.entries(getDefaultTemplates())) {
35041
- await deps.writeFile(path13.join(templatesDir, filename), content);
35068
+ await deps.writeFile(path14.join(templatesDir, filename), content);
35042
35069
  }
35043
- await deps.writeFile(path13.join(stylesDir, "main.css"), getDefaultCss());
35044
- 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());
35045
35072
  deps.logger.log(`
35046
35073
  Initialization complete! Here are the next steps:`);
35047
35074
  deps.logger.log("1. Edit bunki.config.ts to configure your site");
@@ -35061,221 +35088,302 @@ function registerInitCommand(program2, deps = defaultDependencies) {
35061
35088
  function getDefaultTemplates() {
35062
35089
  return {
35063
35090
  "base.njk": String.raw`<!DOCTYPE html>
35064
- <html lang="en">
35065
- <head>
35066
- <meta charset="UTF-8">
35067
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
35068
- <title>{% block title %}{{ site.title }}{% endblock %}</title>
35069
- <meta name="description" content="{% block description %}{{ site.description }}{% endblock %}">
35070
- <link rel="stylesheet" href="/css/style.css">
35071
- {% block head %}{% endblock %}
35072
- </head>
35073
- <body>
35074
- <header>
35075
- <div class="container">
35076
- <h1><a href="/">{{ site.title }}</a></h1>
35077
- <nav>
35078
- <ul>
35079
- <li><a href="/">Home</a></li>
35080
- <li><a href="/tags/">Tags</a></li>
35081
- </ul>
35082
- </nav>
35083
- </div>
35084
- </header>
35085
-
35086
- <main class="container">
35087
- {% block content %}{% endblock %}
35088
- </main>
35089
-
35090
- <footer>
35091
- <div class="container">
35092
- <p>&copy; {{ "now" | date("YYYY") }} {{ site.title }}</p>
35093
- </div>
35094
- </footer>
35095
- </body>
35096
- </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>`,
35097
35143
  "index.njk": String.raw`{% extends "base.njk" %}
35098
35144
 
35099
- {% block content %}
35100
- <h1>Latest Posts</h1>
35101
-
35102
- {% if posts.length > 0 %}
35103
- <div class="posts">
35104
- {% for post in posts %}
35105
- <article class="post-card">
35106
- <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35107
- <div class="post-meta">
35108
- <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35109
- {% if post.tags.length > 0 %}
35110
- <span class="tags">
35111
- {% for tag in post.tags %}
35112
- <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35113
- {% endfor %}
35114
- </span>
35115
- {% endif %}
35116
- </div>
35117
- <div class="post-excerpt">{{ post.excerpt }}</div>
35118
- <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35119
- </article>
35120
- {% endfor %}
35121
- </div>
35122
-
35123
- {% if pagination.totalPages > 1 %}
35124
- <nav class="pagination">
35125
- {% if pagination.hasPrevPage %}
35126
- <a href="{{ pagination.pagePath }}{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35127
- {% endif %}
35128
-
35129
- {% if pagination.hasNextPage %}
35130
- <a href="{{ pagination.pagePath }}page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
35131
- {% endif %}
35132
-
35133
- <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35134
- </nav>
35135
- {% endif %}
35136
- {% else %}
35137
- <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>
35138
35182
  {% endif %}
35139
- {% endblock %}`,
35183
+ {% else %}
35184
+ <p>No posts yet.</p>
35185
+ {% endif %}
35186
+ {% endblock %}`,
35140
35187
  "post.njk": String.raw`{% extends "base.njk" %}
35141
35188
 
35142
- {% block title %}{{ post.title }} | {{ site.title }}{% endblock %}
35143
- {% block description %}{{ post.excerpt }}{% endblock %}
35144
-
35145
- {% block content %}
35146
- <article class="post">
35147
- <header class="post-header">
35148
- <h1>{{ post.title }}</h1>
35149
- <div class="post-meta">
35150
- <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35151
- {% if post.tags.length > 0 %}
35152
- <span class="tags">
35153
- {% for tag in post.tags %}
35154
- <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35155
- {% endfor %}
35156
- </span>
35157
- {% endif %}
35158
- </div>
35159
- </header>
35160
-
35161
- <div class="post-content">
35162
- {{ 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 %}
35163
35225
  </div>
35164
- </article>
35165
- {% endblock %}`,
35166
- "tag.njk": String.raw`{% extends "base.njk" %}
35167
-
35168
- {% block title %}{{ tag.name }} | {{ site.title }}{% endblock %}
35169
- {% block description %}Posts tagged with {{ tag.name }} on {{ site.title }}{% endblock %}
35170
-
35171
- {% block content %}
35172
- <h1>Posts tagged "{{ tag.name }}"</h1>
35173
-
35174
- {% if tag.description %}
35175
- <div class="tag-description">{{ tag.description }}</div>
35176
- {% endif %}
35226
+ </header>
35177
35227
 
35178
- {% if tag.posts.length > 0 %}
35179
- <div class="posts">
35180
- {% for post in tag.posts %}
35181
- <article class="post-card">
35182
- <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35183
- <div class="post-meta">
35184
- <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35185
- </div>
35186
- <div class="post-excerpt">{{ post.excerpt }}</div>
35187
- <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35188
- </article>
35189
- {% 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>
35190
35247
  </div>
35248
+ </footer>
35249
+ </article>
35250
+ {% endblock %}`,
35251
+ "tag.njk": String.raw`{% extends "base.njk" %}
35191
35252
 
35192
- {% if pagination.totalPages > 1 %}
35193
- <nav class="pagination">
35194
- {% if pagination.hasPrevPage %}
35195
- <a href="{{ pagination.pagePath }}{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35196
- {% endif %}
35197
-
35198
- {% if pagination.hasNextPage %}
35199
- <a href="{{ pagination.pagePath }}page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
35200
- {% endif %}
35201
-
35202
- <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35203
- </nav>
35204
- {% endif %}
35205
- {% else %}
35206
- <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>
35207
35296
  {% endif %}
35208
- {% endblock %}`,
35297
+ {% else %}
35298
+ <p>No posts with this tag yet.</p>
35299
+ {% endif %}
35300
+ {% endblock %}`,
35209
35301
  "tags.njk": String.raw`{% extends "base.njk" %}
35210
35302
 
35211
- {% block title %}Tags | {{ site.title }}{% endblock %}
35212
- {% 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 %}
35213
35305
 
35214
- {% block content %}
35215
- <h1>All Tags</h1>
35306
+ {% block canonical %}{{ site.baseUrl }}/tags/{% endblock %}
35216
35307
 
35217
- {% if tags.length > 0 %}
35218
- <ul class="tags-list">
35219
- {% for tag in tags %}
35220
- <li>
35221
- <a href="/tags/{{ tag.slug }}/">{{ tag.name }}</a>
35222
- <span class="count">({{ tag.count }})</span>
35223
- {% if tag.description %}
35224
- <p class="description">{{ tag.description }}</p>
35225
- {% endif %}
35226
- </li>
35227
- {% endfor %}
35228
- </ul>
35229
- {% else %}
35230
- <p>No tags found!</p>
35231
- {% endif %}
35232
- {% endblock %}`,
35233
- "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 %}
35234
35311
 
35235
- {% block title %}Archive {{ year }} | {{ site.title }}{% endblock %}
35236
- {% block description %}Posts from {{ year }} on {{ site.title }}{% endblock %}
35237
-
35238
- {% block content %}
35239
- <h1>Posts from {{ year }}</h1>
35240
-
35241
- {% if posts.length > 0 %}
35242
- <div class="posts">
35243
- {% for post in posts %}
35244
- <article class="post-card">
35245
- <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35246
- <div class="post-meta">
35247
- <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35248
- {% if post.tags.length > 0 %}
35249
- <span class="tags">
35250
- {% for tag in post.tags %}
35251
- <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35252
- {% endfor %}
35253
- </span>
35254
- {% endif %}
35255
- </div>
35256
- <div class="post-excerpt">{{ post.excerpt }}</div>
35257
- <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35258
- </article>
35259
- {% endfor %}
35260
- </div>
35312
+ {% block twitter_title %}Tags | {{ site.title }}{% endblock %}
35313
+ {% block twitter_description %}Browse all tags on {{ site.title }}{% endblock %}
35261
35314
 
35262
- {% if pagination.totalPages > 1 %}
35263
- <nav class="pagination">
35264
- {% if pagination.hasPrevPage %}
35265
- <a href="/{{ year }}/{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35266
- {% endif %}
35315
+ {% block content %}
35316
+ <h1>All Tags</h1>
35267
35317
 
35268
- {% if pagination.hasNextPage %}
35269
- <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>
35270
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" %}
35271
35335
 
35272
- <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35273
- </nav>
35274
- {% endif %}
35275
- {% else %}
35276
- <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>
35277
35382
  {% endif %}
35278
- {% endblock %}`
35383
+ {% else %}
35384
+ <p>No posts from {{ year }} yet.</p>
35385
+ {% endif %}
35386
+ {% endblock %}`
35279
35387
  };
35280
35388
  }
35281
35389
  function getDefaultCss() {
@@ -35470,6 +35578,43 @@ function getDefaultCss() {
35470
35578
  font-size: 0.9rem;
35471
35579
  }
35472
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
+
35473
35618
  /* Footer */
35474
35619
  footer {
35475
35620
  text-align: center;
@@ -35533,7 +35678,7 @@ function hello() {
35533
35678
  }
35534
35679
 
35535
35680
  // src/cli/commands/new-post.ts
35536
- import path14 from "path";
35681
+ import path15 from "path";
35537
35682
  var defaultDeps4 = {
35538
35683
  writeFile: (filePath, data) => Bun.write(filePath, data),
35539
35684
  now: () => new Date,
@@ -35557,7 +35702,7 @@ async function handleNewCommand(title, options2, deps = defaultDeps4) {
35557
35702
  ` + `# ${title}
35558
35703
 
35559
35704
  `;
35560
- const filePath = path14.join(DEFAULT_CONTENT_DIR, `${slug}.md`);
35705
+ const filePath = path15.join(DEFAULT_CONTENT_DIR, `${slug}.md`);
35561
35706
  await deps.writeFile(filePath, frontmatter);
35562
35707
  deps.logger.log(`Created new post: ${filePath}`);
35563
35708
  return filePath;
@@ -35574,11 +35719,11 @@ function registerNewCommand(program2) {
35574
35719
  }
35575
35720
 
35576
35721
  // src/cli/commands/serve.ts
35577
- import path16 from "path";
35722
+ import path17 from "path";
35578
35723
 
35579
35724
  // src/server.ts
35580
35725
  import fs2 from "fs";
35581
- import path15 from "path";
35726
+ import path16 from "path";
35582
35727
  async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
35583
35728
  try {
35584
35729
  const stats = await fs2.promises.stat(outputDir);
@@ -35613,18 +35758,18 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
35613
35758
  let filePath = "";
35614
35759
  if (homePaginationMatch) {
35615
35760
  const pageNumber = homePaginationMatch[1];
35616
- filePath = path15.join(outputDir, "page", pageNumber, "index.html");
35761
+ filePath = path16.join(outputDir, "page", pageNumber, "index.html");
35617
35762
  } else if (tagPaginationMatch) {
35618
35763
  const tagSlug = tagPaginationMatch[1];
35619
35764
  const pageNumber = tagPaginationMatch[2];
35620
- filePath = path15.join(outputDir, "tags", tagSlug, "page", pageNumber, "index.html");
35765
+ filePath = path16.join(outputDir, "tags", tagSlug, "page", pageNumber, "index.html");
35621
35766
  } else if (yearPaginationMatch) {
35622
35767
  const year = yearPaginationMatch[1];
35623
35768
  const pageNumber = yearPaginationMatch[2];
35624
- filePath = path15.join(outputDir, year, "page", pageNumber, "index.html");
35769
+ filePath = path16.join(outputDir, year, "page", pageNumber, "index.html");
35625
35770
  } else {
35626
- const directPath = path15.join(outputDir, pathname);
35627
- const withoutSlash = path15.join(outputDir, pathname + ".html");
35771
+ const directPath = path16.join(outputDir, pathname);
35772
+ const withoutSlash = path16.join(outputDir, pathname + ".html");
35628
35773
  const withHtml = pathname.endsWith(".html") ? directPath : withoutSlash;
35629
35774
  const bunFileDirect = Bun.file(directPath);
35630
35775
  const bunFileHtml = Bun.file(withHtml);
@@ -35633,7 +35778,7 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
35633
35778
  } else if (await bunFileHtml.exists()) {
35634
35779
  filePath = withHtml;
35635
35780
  } else {
35636
- const indexPath = path15.join(outputDir, pathname, "index.html");
35781
+ const indexPath = path16.join(outputDir, pathname, "index.html");
35637
35782
  const bunFileIndex = Bun.file(indexPath);
35638
35783
  if (await bunFileIndex.exists()) {
35639
35784
  filePath = indexPath;
@@ -35647,7 +35792,7 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
35647
35792
  }
35648
35793
  }
35649
35794
  console.log(`Serving file: ${filePath}`);
35650
- const extname = path15.extname(filePath);
35795
+ const extname = path16.extname(filePath);
35651
35796
  let contentType = "text/html";
35652
35797
  switch (extname) {
35653
35798
  case ".js":
@@ -35706,7 +35851,7 @@ var defaultDeps5 = {
35706
35851
  };
35707
35852
  async function handleServeCommand(options2, deps = defaultDeps5) {
35708
35853
  try {
35709
- const outputDir = path16.resolve(options2.output);
35854
+ const outputDir = path17.resolve(options2.output);
35710
35855
  const port = parseInt(options2.port, 10);
35711
35856
  await deps.startServer(outputDir, port);
35712
35857
  } catch (error) {
@@ -35749,7 +35894,7 @@ function registerValidateCommand(program2) {
35749
35894
  }
35750
35895
 
35751
35896
  // src/cli/commands/validate-media.ts
35752
- import { readdirSync, readFileSync, existsSync, statSync } from "fs";
35897
+ import { readdirSync, readFileSync, existsSync as existsSync2, statSync } from "fs";
35753
35898
  import { join, dirname, resolve, basename } from "path";
35754
35899
  var imageExtensions = [".jpg", ".jpeg", ".png", ".webp", ".gif"];
35755
35900
  var videoExtensions = [".mp4", ".webm", ".mov"];
@@ -35757,7 +35902,7 @@ var mediaExtensions = [...imageExtensions, ...videoExtensions];
35757
35902
  async function handleValidateMediaCommand(options2, deps = { logger: console, exit: (code) => process.exit(code) }) {
35758
35903
  const contentDir = options2.contentDir || join(process.cwd(), "content");
35759
35904
  const assetsDir = join(process.cwd(), "assets");
35760
- if (!existsSync(contentDir)) {
35905
+ if (!existsSync2(contentDir)) {
35761
35906
  deps.logger.error(`Content directory not found: ${contentDir}`);
35762
35907
  deps.exit(1);
35763
35908
  }
@@ -35879,7 +36024,7 @@ function validateMedia(contentDir, assetsDir) {
35879
36024
  }
35880
36025
  function getAllMediaFromContentAssets(contentDir) {
35881
36026
  const mediaFiles = [];
35882
- if (!existsSync(contentDir))
36027
+ if (!existsSync2(contentDir))
35883
36028
  return [];
35884
36029
  const years = readdirSync(contentDir).filter((f) => {
35885
36030
  const fullPath = join(contentDir, f);
@@ -35887,7 +36032,7 @@ function getAllMediaFromContentAssets(contentDir) {
35887
36032
  });
35888
36033
  for (const year of years) {
35889
36034
  const assetsDir = join(contentDir, year, "_assets");
35890
- if (!existsSync(assetsDir))
36035
+ if (!existsSync2(assetsDir))
35891
36036
  continue;
35892
36037
  const files = readdirSync(assetsDir);
35893
36038
  for (const file of files) {
@@ -35909,7 +36054,7 @@ function getAllMediaFromContentAssets(contentDir) {
35909
36054
  }
35910
36055
  function getAllMediaFromAssets(assetsDir) {
35911
36056
  const mediaFiles = [];
35912
- if (!existsSync(assetsDir))
36057
+ if (!existsSync2(assetsDir))
35913
36058
  return [];
35914
36059
  const years = readdirSync(assetsDir).filter((f) => {
35915
36060
  const fullPath = join(assetsDir, f);
@@ -35941,7 +36086,7 @@ function getAllMediaFromAssets(assetsDir) {
35941
36086
  function checkMediaReference(markdownFile, lineNumber, mediaPath, type, missingReferences) {
35942
36087
  const markdownDir = dirname(markdownFile);
35943
36088
  const resolvedPath = resolve(markdownDir, mediaPath);
35944
- if (!existsSync(resolvedPath)) {
36089
+ if (!existsSync2(resolvedPath)) {
35945
36090
  missingReferences.push({
35946
36091
  file: markdownFile.replace(process.cwd() + "/", ""),
35947
36092
  line: lineNumber,