bunki 0.17.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -799,16 +799,134 @@ export S3_PUBLIC_URL="https://img.example.com"
799
799
  - WebP for photos
800
800
  - SVG for icons/graphics
801
801
 
802
+ ## Incremental Builds
803
+
804
+ Bunki supports incremental builds for significantly faster rebuild times during development. When enabled, only changed content is reprocessed while unchanged files are loaded from cache.
805
+
806
+ ### Performance Impact
807
+
808
+ **Large site example (455 posts):**
809
+ - Full build: 3,128ms
810
+ - Incremental build (no changes): 985ms (**3.2x faster**)
811
+
812
+ **Speedup breakdown:**
813
+ - Markdown parsing: 1,202ms → 55ms (**22x faster**)
814
+ - CSS processing: 1,024ms → 1ms (**1024x faster**)
815
+ - Overall: **68% faster builds**
816
+
817
+ ### Usage
818
+
819
+ ```bash
820
+ # Enable incremental builds
821
+ bunki generate --incremental
822
+
823
+ # First run (creates cache)
824
+ # Config changed, full rebuild required
825
+ # Total: 3,128ms (same as normal build)
826
+
827
+ # Subsequent runs (no changes)
828
+ # No content changes detected, using cached posts
829
+ # ✨ Loaded 455 posts from cache (0ms parsing)
830
+ # ⏭️ Skipping CSS (unchanged)
831
+ # Total: 985ms (3.2x faster!)
832
+
833
+ # When one file changes
834
+ # 📦 Incremental build: 1/456 files changed (~2730ms saved)
835
+ # Parsed: 1 new/changed, loaded: 455 from cache
836
+ # Total: ~1,000ms
837
+ ```
838
+
839
+ ### How It Works
840
+
841
+ 1. **First build** creates `.bunki-cache.json` with:
842
+ - File hashes and modification times
843
+ - Parsed post data (title, content, metadata)
844
+ - CSS file checksums
845
+ - Config file hash
846
+
847
+ 2. **Subsequent builds** detect changes by comparing:
848
+ - Config file hash (triggers full rebuild if changed)
849
+ - Markdown file hashes/mtimes
850
+ - CSS file hashes
851
+
852
+ 3. **Selective processing**:
853
+ - Only parse changed markdown files
854
+ - Load unchanged posts from cache
855
+ - Skip CSS if unchanged
856
+ - Regenerate all pages (currently not selective)
857
+
858
+ ### Cache Management
859
+
860
+ The cache is stored in `.bunki-cache.json` at your project root:
861
+
862
+ ```bash
863
+ # View cache status
864
+ cat .bunki-cache.json | jq '.version, .configHash'
865
+
866
+ # Clear cache (force full rebuild)
867
+ rm .bunki-cache.json
868
+
869
+ # Exclude from version control
870
+ echo ".bunki-cache.json" >> .gitignore
871
+ ```
872
+
873
+ ### When to Use
874
+
875
+ **Recommended for:**
876
+ - Large sites (100+ posts)
877
+ - Development workflow with frequent rebuilds
878
+ - Sites with slow CSS processing (Tailwind, PostCSS)
879
+
880
+ **Not needed for:**
881
+ - Small sites (<50 posts) - already fast enough
882
+ - CI/CD builds - prefer clean full builds
883
+ - Production deployments - always use full builds
884
+
885
+ ### Cache Format
886
+
887
+ Version 2.0.0 cache structure:
888
+
889
+ ```json
890
+ {
891
+ "version": "2.0.0",
892
+ "configHash": "abc123",
893
+ "files": {
894
+ "/path/to/post.md": {
895
+ "hash": "def456",
896
+ "mtime": 1771720766417,
897
+ "post": {
898
+ "title": "Post Title",
899
+ "date": "2024-01-01",
900
+ "content": "...",
901
+ "html": "..."
902
+ }
903
+ },
904
+ "/path/to/main.css": {
905
+ "hash": "ghi789",
906
+ "mtime": 1771720800000
907
+ }
908
+ }
909
+ }
910
+ ```
911
+
912
+ ### Future Optimizations
913
+
914
+ Current implementation (v0.18.0) optimizes parsing and CSS processing. Future versions may add:
915
+ - Selective page regeneration (only rebuild changed posts)
916
+ - Incremental sitemap/RSS updates
917
+ - Smart index page regeneration
918
+
802
919
  ## CLI Commands
803
920
 
804
921
  ```bash
805
- bunki init [--config FILE] # Initialize new site
806
- bunki new <TITLE> [--tags TAG1,TAG2] # Create new post
807
- bunki generate [--config FILE] # Build static site
808
- bunki validate [--config FILE] # Validate frontmatter
809
- bunki serve [--port 3000] # Start dev server
810
- bunki css [--watch] # Process CSS
811
- bunki images:push [--domain DOMAIN] # Upload images to cloud
922
+ bunki init [--config FILE] # Initialize new site
923
+ bunki new <TITLE> [--tags TAG1,TAG2] # Create new post
924
+ bunki generate [--config FILE] # Build static site (full)
925
+ bunki generate --incremental # Build with caching (3x faster)
926
+ bunki validate [--config FILE] # Validate frontmatter
927
+ bunki serve [--port 3000] # Start dev server
928
+ bunki css [--watch] # Process CSS
929
+ bunki images:push [--domain DOMAIN] # Upload images to cloud
812
930
  ```
813
931
 
814
932
  ## Output Structure
@@ -895,6 +1013,7 @@ site-generator.ts (orchestrator)
895
1013
  - **Frontmatter Validation**: Automatic validation of business location data with clear error messages
896
1014
  - **Security**: XSS protection, sanitized HTML, link hardening
897
1015
  - **High Performance**:
1016
+ - **Incremental builds** with smart caching (3.2x faster, 68% speedup)
898
1017
  - Parallel page generation (40-60% faster builds)
899
1018
  - Batched post processing (10x faster for 100+ posts)
900
1019
  - Pre-compiled regex patterns (2-3x faster parsing)
@@ -973,7 +1092,24 @@ bunki/
973
1092
 
974
1093
  ## Changelog
975
1094
 
976
- ### v0.17.0 (Current)
1095
+ ### v0.18.0 (Current)
1096
+
1097
+ - **Incremental Builds**: Smart caching for 3.2x faster development builds
1098
+ - File change detection using content hashing and modification times
1099
+ - Selective markdown parsing (only parse changed files)
1100
+ - CSS caching (skip processing if unchanged)
1101
+ - Cache format v2.0.0 stores full parsed post data
1102
+ - Automatic config change detection triggers full rebuilds
1103
+ - `.bunki-cache.json` stores file hashes, mtimes, and parsed posts
1104
+ - **Performance Results** (455 posts):
1105
+ - Full build: 3,128ms
1106
+ - Incremental (no changes): 985ms (68% faster)
1107
+ - Markdown parsing: 1,202ms → 55ms (22x faster)
1108
+ - CSS processing: 1,024ms → 1ms (1024x faster)
1109
+ - **CLI Enhancement**: New `--incremental` flag for `bunki generate`
1110
+ - **Code Cleanup**: Removed unused imports, reverted template extraction
1111
+
1112
+ ### v0.17.0
977
1113
 
978
1114
  - **Major Architecture Refactoring**: Modular design with single responsibility modules
979
1115
  - Split `site-generator.ts` from 957 to 282 lines (-70%)
package/dist/cli.js CHANGED
@@ -34387,8 +34387,7 @@ class SiteGenerator {
34387
34387
  this.metrics = new MetricsCollector;
34388
34388
  const env = import_nunjucks2.default.configure(this.options.templatesDir, {
34389
34389
  autoescape: true,
34390
- watch: false,
34391
- noCache: false
34390
+ watch: false
34392
34391
  });
34393
34392
  env.addFilter("date", (date, format) => {
34394
34393
  const d2 = toPacificTime(date);
@@ -34917,437 +34916,475 @@ function registerImagesPushCommand(program2) {
34917
34916
 
34918
34917
  // src/cli/commands/init.ts
34919
34918
  import path13 from "path";
34919
+ var defaultDependencies = {
34920
+ createDefaultConfig,
34921
+ ensureDir,
34922
+ writeFile: (filePath, data) => Bun.write(filePath, data),
34923
+ logger: console,
34924
+ exit: (code) => process.exit(code)
34925
+ };
34926
+ async function handleInitCommand(options2, deps = defaultDependencies) {
34927
+ try {
34928
+ const configPath = path13.resolve(options2.config);
34929
+ const configCreated = await deps.createDefaultConfig(configPath);
34930
+ if (!configCreated) {
34931
+ deps.logger.log(`
34932
+ Skipped initialization because the config file already exists`);
34933
+ return;
34934
+ }
34935
+ deps.logger.log("Creating directory structure...");
34936
+ const baseDir = process.cwd();
34937
+ const contentDir = path13.join(baseDir, "content");
34938
+ const templatesDir = path13.join(baseDir, "templates");
34939
+ const stylesDir = path13.join(templatesDir, "styles");
34940
+ const publicDir = path13.join(baseDir, "public");
34941
+ await deps.ensureDir(contentDir);
34942
+ await deps.ensureDir(templatesDir);
34943
+ await deps.ensureDir(stylesDir);
34944
+ await deps.ensureDir(publicDir);
34945
+ for (const [filename, content] of Object.entries(getDefaultTemplates())) {
34946
+ await deps.writeFile(path13.join(templatesDir, filename), content);
34947
+ }
34948
+ await deps.writeFile(path13.join(stylesDir, "main.css"), getDefaultCss());
34949
+ await deps.writeFile(path13.join(contentDir, "welcome.md"), getSamplePost());
34950
+ deps.logger.log(`
34951
+ Initialization complete! Here are the next steps:`);
34952
+ deps.logger.log("1. Edit bunki.config.ts to configure your site");
34953
+ deps.logger.log("2. Add markdown files to the content directory");
34954
+ deps.logger.log('3. Run "bunki generate" to build your site');
34955
+ deps.logger.log('4. Run "bunki serve" to preview your site locally');
34956
+ } catch (error) {
34957
+ deps.logger.error("Error initializing site:", error);
34958
+ deps.exit(1);
34959
+ }
34960
+ }
34961
+ function registerInitCommand(program2, deps = defaultDependencies) {
34962
+ return program2.command("init").description("Initialize a new site with default structure").option("-c, --config <file>", "Path to config file", "bunki.config.ts").action(async (options2) => {
34963
+ await handleInitCommand(options2, deps);
34964
+ });
34965
+ }
34966
+ function getDefaultTemplates() {
34967
+ return {
34968
+ "base.njk": String.raw`<!DOCTYPE html>
34969
+ <html lang="en">
34970
+ <head>
34971
+ <meta charset="UTF-8">
34972
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
34973
+ <title>{% block title %}{{ site.title }}{% endblock %}</title>
34974
+ <meta name="description" content="{% block description %}{{ site.description }}{% endblock %}">
34975
+ <link rel="stylesheet" href="/css/style.css">
34976
+ {% block head %}{% endblock %}
34977
+ </head>
34978
+ <body>
34979
+ <header>
34980
+ <div class="container">
34981
+ <h1><a href="/">{{ site.title }}</a></h1>
34982
+ <nav>
34983
+ <ul>
34984
+ <li><a href="/">Home</a></li>
34985
+ <li><a href="/tags/">Tags</a></li>
34986
+ </ul>
34987
+ </nav>
34988
+ </div>
34989
+ </header>
34920
34990
 
34921
- // src/cli/commands/templates/base-njk.ts
34922
- var baseNjk = String.raw`<!DOCTYPE html>
34923
- <html lang="en">
34924
- <head>
34925
- <meta charset="UTF-8">
34926
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
34927
- <title>{% block title %}{{ site.title }}{% endblock %}</title>
34928
- <meta name="description" content="{% block description %}{{ site.description }}{% endblock %}">
34929
- <link rel="stylesheet" href="/css/style.css">
34930
- {% block head %}{% endblock %}
34931
- </head>
34932
- <body>
34933
- <header>
34934
- <div class="container">
34935
- <h1><a href="/">{{ site.title }}</a></h1>
34936
- <nav>
34937
- <ul>
34938
- <li><a href="/">Home</a></li>
34939
- <li><a href="/tags/">Tags</a></li>
34940
- </ul>
34941
- </nav>
34942
- </div>
34943
- </header>
34944
-
34945
- <main class="container">
34946
- {% block content %}{% endblock %}
34947
- </main>
34948
-
34949
- <footer>
34950
- <div class="container">
34951
- <p>&copy; {{ "now" | date("YYYY") }} {{ site.title }}</p>
34952
- </div>
34953
- </footer>
34954
- </body>
34955
- </html>`;
34956
-
34957
- // src/cli/commands/templates/index-njk.ts
34958
- var indexNjk = String.raw`{% extends "base.njk" %}
34959
-
34960
- {% block content %}
34961
- <h1>Latest Posts</h1>
34962
-
34963
- {% if posts.length > 0 %}
34964
- <div class="posts">
34965
- {% for post in posts %}
34966
- <article class="post-card">
34967
- <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
34968
- <div class="post-meta">
34969
- <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
34970
- {% if post.tags.length > 0 %}
34971
- <span class="tags">
34972
- {% for tag in post.tags %}
34973
- <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
34974
- {% endfor %}
34975
- </span>
34976
- {% endif %}
34977
- </div>
34978
- <div class="post-excerpt">{{ post.excerpt }}</div>
34979
- <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
34980
- </article>
34981
- {% endfor %}
34982
- </div>
34983
-
34984
- {% if pagination.totalPages > 1 %}
34985
- <nav class="pagination">
34986
- {% if pagination.hasPrevPage %}
34987
- <a href="{{ pagination.pagePath }}{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
34988
- {% endif %}
34989
-
34990
- {% if pagination.hasNextPage %}
34991
- <a href="{{ pagination.pagePath }}page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
34992
- {% endif %}
34993
-
34994
- <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
34995
- </nav>
34991
+ <main class="container">
34992
+ {% block content %}{% endblock %}
34993
+ </main>
34994
+
34995
+ <footer>
34996
+ <div class="container">
34997
+ <p>&copy; {{ "now" | date("YYYY") }} {{ site.title }}</p>
34998
+ </div>
34999
+ </footer>
35000
+ </body>
35001
+ </html>`,
35002
+ "index.njk": String.raw`{% extends "base.njk" %}
35003
+
35004
+ {% block content %}
35005
+ <h1>Latest Posts</h1>
35006
+
35007
+ {% if posts.length > 0 %}
35008
+ <div class="posts">
35009
+ {% for post in posts %}
35010
+ <article class="post-card">
35011
+ <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35012
+ <div class="post-meta">
35013
+ <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35014
+ {% if post.tags.length > 0 %}
35015
+ <span class="tags">
35016
+ {% for tag in post.tags %}
35017
+ <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35018
+ {% endfor %}
35019
+ </span>
35020
+ {% endif %}
35021
+ </div>
35022
+ <div class="post-excerpt">{{ post.excerpt }}</div>
35023
+ <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35024
+ </article>
35025
+ {% endfor %}
35026
+ </div>
35027
+
35028
+ {% if pagination.totalPages > 1 %}
35029
+ <nav class="pagination">
35030
+ {% if pagination.hasPrevPage %}
35031
+ <a href="{{ pagination.pagePath }}{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35032
+ {% endif %}
35033
+
35034
+ {% if pagination.hasNextPage %}
35035
+ <a href="{{ pagination.pagePath }}page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
35036
+ {% endif %}
35037
+
35038
+ <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35039
+ </nav>
35040
+ {% endif %}
35041
+ {% else %}
35042
+ <p>No posts yet!</p>
34996
35043
  {% endif %}
34997
- {% else %}
34998
- <p>No posts yet!</p>
34999
- {% endif %}
35000
- {% endblock %}`;
35001
-
35002
- // src/cli/commands/templates/post-njk.ts
35003
- var postNjk = String.raw`{% extends "base.njk" %}
35004
-
35005
- {% block title %}{{ post.title }} | {{ site.title }}{% endblock %}
35006
- {% block description %}{{ post.excerpt }}{% endblock %}
35007
-
35008
- {% block content %}
35009
- <article class="post">
35010
- <header class="post-header">
35011
- <h1>{{ post.title }}</h1>
35012
- <div class="post-meta">
35013
- <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35014
- {% if post.tags.length > 0 %}
35015
- <span class="tags">
35016
- {% for tag in post.tags %}
35017
- <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35018
- {% endfor %}
35019
- </span>
35020
- {% endif %}
35044
+ {% endblock %}`,
35045
+ "post.njk": String.raw`{% extends "base.njk" %}
35046
+
35047
+ {% block title %}{{ post.title }} | {{ site.title }}{% endblock %}
35048
+ {% block description %}{{ post.excerpt }}{% endblock %}
35049
+
35050
+ {% block content %}
35051
+ <article class="post">
35052
+ <header class="post-header">
35053
+ <h1>{{ post.title }}</h1>
35054
+ <div class="post-meta">
35055
+ <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35056
+ {% if post.tags.length > 0 %}
35057
+ <span class="tags">
35058
+ {% for tag in post.tags %}
35059
+ <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35060
+ {% endfor %}
35061
+ </span>
35062
+ {% endif %}
35063
+ </div>
35064
+ </header>
35065
+
35066
+ <div class="post-content">
35067
+ {{ post.html | safe }}
35021
35068
  </div>
35022
- </header>
35069
+ </article>
35070
+ {% endblock %}`,
35071
+ "tag.njk": String.raw`{% extends "base.njk" %}
35072
+
35073
+ {% block title %}{{ tag.name }} | {{ site.title }}{% endblock %}
35074
+ {% block description %}Posts tagged with {{ tag.name }} on {{ site.title }}{% endblock %}
35023
35075
 
35024
- <div class="post-content">
35025
- {{ post.html | safe }}
35026
- </div>
35027
- </article>
35028
- {% endblock %}`;
35029
-
35030
- // src/cli/commands/templates/tag-njk.ts
35031
- var tagNjk = String.raw`{% extends "base.njk" %}
35032
-
35033
- {% block title %}{{ tag.name }} | {{ site.title }}{% endblock %}
35034
- {% block description %}Posts tagged with {{ tag.name }} on {{ site.title }}{% endblock %}
35035
-
35036
- {% block content %}
35037
- <h1>Posts tagged "{{ tag.name }}"</h1>
35038
-
35039
- {% if tag.description %}
35040
- <div class="tag-description">{{ tag.description }}</div>
35041
- {% endif %}
35042
-
35043
- {% if tag.posts.length > 0 %}
35044
- <div class="posts">
35045
- {% for post in tag.posts %}
35046
- <article class="post-card">
35047
- <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35048
- <div class="post-meta">
35049
- <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35050
- </div>
35051
- <div class="post-excerpt">{{ post.excerpt }}</div>
35052
- <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35053
- </article>
35054
- {% endfor %}
35055
- </div>
35056
-
35057
- {% if pagination.totalPages > 1 %}
35058
- <nav class="pagination">
35059
- {% if pagination.hasPrevPage %}
35060
- <a href="{{ pagination.pagePath }}{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35061
- {% endif %}
35062
-
35063
- {% if pagination.hasNextPage %}
35064
- <a href="{{ pagination.pagePath }}page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
35065
- {% endif %}
35066
-
35067
- <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35068
- </nav>
35076
+ {% block content %}
35077
+ <h1>Posts tagged "{{ tag.name }}"</h1>
35078
+
35079
+ {% if tag.description %}
35080
+ <div class="tag-description">{{ tag.description }}</div>
35069
35081
  {% endif %}
35070
- {% else %}
35071
- <p>No posts with this tag yet!</p>
35072
- {% endif %}
35073
- {% endblock %}`;
35074
-
35075
- // src/cli/commands/templates/tags-njk.ts
35076
- var tagsNjk = String.raw`{% extends "base.njk" %}
35077
-
35078
- {% block title %}Tags | {{ site.title }}{% endblock %}
35079
- {% block description %}Browse all tags on {{ site.title }}{% endblock %}
35080
-
35081
- {% block content %}
35082
- <h1>All Tags</h1>
35083
-
35084
- {% if tags.length > 0 %}
35085
- <ul class="tags-list">
35086
- {% for tag in tags %}
35087
- <li>
35088
- <a href="/tags/{{ tag.slug }}/">{{ tag.name }}</a>
35089
- <span class="count">({{ tag.count }})</span>
35090
- {% if tag.description %}
35091
- <p class="description">{{ tag.description }}</p>
35082
+
35083
+ {% if tag.posts.length > 0 %}
35084
+ <div class="posts">
35085
+ {% for post in tag.posts %}
35086
+ <article class="post-card">
35087
+ <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35088
+ <div class="post-meta">
35089
+ <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35090
+ </div>
35091
+ <div class="post-excerpt">{{ post.excerpt }}</div>
35092
+ <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35093
+ </article>
35094
+ {% endfor %}
35095
+ </div>
35096
+
35097
+ {% if pagination.totalPages > 1 %}
35098
+ <nav class="pagination">
35099
+ {% if pagination.hasPrevPage %}
35100
+ <a href="{{ pagination.pagePath }}{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35101
+ {% endif %}
35102
+
35103
+ {% if pagination.hasNextPage %}
35104
+ <a href="{{ pagination.pagePath }}page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
35092
35105
  {% endif %}
35093
- </li>
35094
- {% endfor %}
35095
- </ul>
35096
- {% else %}
35097
- <p>No tags found!</p>
35098
- {% endif %}
35099
- {% endblock %}`;
35100
-
35101
- // src/cli/commands/templates/archive-njk.ts
35102
- var archiveNjk = String.raw`{% extends "base.njk" %}
35103
-
35104
- {% block title %}Archive {{ year }} | {{ site.title }}{% endblock %}
35105
- {% block description %}Posts from {{ year }} on {{ site.title }}{% endblock %}
35106
-
35107
- {% block content %}
35108
- <h1>Posts from {{ year }}</h1>
35109
-
35110
- {% if posts.length > 0 %}
35111
- <div class="posts">
35112
- {% for post in posts %}
35113
- <article class="post-card">
35114
- <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35115
- <div class="post-meta">
35116
- <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35117
- {% if post.tags.length > 0 %}
35118
- <span class="tags">
35119
- {% for tag in post.tags %}
35120
- <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35121
- {% endfor %}
35122
- </span>
35106
+
35107
+ <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35108
+ </nav>
35109
+ {% endif %}
35110
+ {% else %}
35111
+ <p>No posts with this tag yet!</p>
35112
+ {% endif %}
35113
+ {% endblock %}`,
35114
+ "tags.njk": String.raw`{% extends "base.njk" %}
35115
+
35116
+ {% block title %}Tags | {{ site.title }}{% endblock %}
35117
+ {% block description %}Browse all tags on {{ site.title }}{% endblock %}
35118
+
35119
+ {% block content %}
35120
+ <h1>All Tags</h1>
35121
+
35122
+ {% if tags.length > 0 %}
35123
+ <ul class="tags-list">
35124
+ {% for tag in tags %}
35125
+ <li>
35126
+ <a href="/tags/{{ tag.slug }}/">{{ tag.name }}</a>
35127
+ <span class="count">({{ tag.count }})</span>
35128
+ {% if tag.description %}
35129
+ <p class="description">{{ tag.description }}</p>
35123
35130
  {% endif %}
35124
- </div>
35125
- <div class="post-excerpt">{{ post.excerpt }}</div>
35126
- <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35127
- </article>
35128
- {% endfor %}
35129
- </div>
35130
-
35131
- {% if pagination.totalPages > 1 %}
35132
- <nav class="pagination">
35133
- {% if pagination.hasPrevPage %}
35134
- <a href="/{{ year }}/{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35135
- {% endif %}
35136
-
35137
- {% if pagination.hasNextPage %}
35138
- <a href="/{{ year }}/page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
35139
- {% endif %}
35140
-
35141
- <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35142
- </nav>
35131
+ </li>
35132
+ {% endfor %}
35133
+ </ul>
35134
+ {% else %}
35135
+ <p>No tags found!</p>
35143
35136
  {% endif %}
35144
- {% else %}
35145
- <p>No posts from {{ year }}!</p>
35146
- {% endif %}
35147
- {% endblock %}`;
35148
-
35149
- // src/cli/commands/templates/default-css.ts
35150
- var defaultCss = String.raw`/* Reset & base styles */
35151
- * {
35152
- margin: 0;
35153
- padding: 0;
35154
- box-sizing: border-box;
35155
- }
35137
+ {% endblock %}`,
35138
+ "archive.njk": String.raw`{% extends "base.njk" %}
35139
+
35140
+ {% block title %}Archive {{ year }} | {{ site.title }}{% endblock %}
35141
+ {% block description %}Posts from {{ year }} on {{ site.title }}{% endblock %}
35142
+
35143
+ {% block content %}
35144
+ <h1>Posts from {{ year }}</h1>
35145
+
35146
+ {% if posts.length > 0 %}
35147
+ <div class="posts">
35148
+ {% for post in posts %}
35149
+ <article class="post-card">
35150
+ <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
35151
+ <div class="post-meta">
35152
+ <time datetime="{{ post.date }}">{{ post.date | date("MMMM D, YYYY") }}</time>
35153
+ {% if post.tags.length > 0 %}
35154
+ <span class="tags">
35155
+ {% for tag in post.tags %}
35156
+ <a href="/tags/{{ post.tagSlugs[tag] }}/">{{ tag }}</a>{% if not loop.last %}, {% endif %}
35157
+ {% endfor %}
35158
+ </span>
35159
+ {% endif %}
35160
+ </div>
35161
+ <div class="post-excerpt">{{ post.excerpt }}</div>
35162
+ <a href="{{ post.url }}" class="read-more">Read more \u2192</a>
35163
+ </article>
35164
+ {% endfor %}
35165
+ </div>
35156
35166
 
35157
- body {
35158
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
35159
- line-height: 1.6;
35160
- color: #333;
35161
- background-color: #f8f9fa;
35162
- padding-bottom: 2rem;
35163
- }
35167
+ {% if pagination.totalPages > 1 %}
35168
+ <nav class="pagination">
35169
+ {% if pagination.hasPrevPage %}
35170
+ <a href="/{{ year }}/{% if pagination.prevPage > 1 %}page/{{ pagination.prevPage }}/{% endif %}" class="prev">\u2190 Previous</a>
35171
+ {% endif %}
35164
35172
 
35165
- a {
35166
- color: #0066cc;
35167
- text-decoration: none;
35168
- }
35173
+ {% if pagination.hasNextPage %}
35174
+ <a href="/{{ year }}/page/{{ pagination.nextPage }}/" class="next">Next \u2192</a>
35175
+ {% endif %}
35169
35176
 
35170
- a:hover {
35171
- text-decoration: underline;
35177
+ <span class="page-info">Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
35178
+ </nav>
35179
+ {% endif %}
35180
+ {% else %}
35181
+ <p>No posts from {{ year }}!</p>
35182
+ {% endif %}
35183
+ {% endblock %}`
35184
+ };
35172
35185
  }
35186
+ function getDefaultCss() {
35187
+ return String.raw`/* Reset & base styles */
35188
+ * {
35189
+ margin: 0;
35190
+ padding: 0;
35191
+ box-sizing: border-box;
35192
+ }
35173
35193
 
35174
- .container {
35175
- max-width: 800px;
35176
- margin: 0 auto;
35177
- padding: 0 1.5rem;
35178
- }
35194
+ body {
35195
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
35196
+ line-height: 1.6;
35197
+ color: #333;
35198
+ background-color: #f8f9fa;
35199
+ padding-bottom: 2rem;
35200
+ }
35179
35201
 
35180
- /* Header */
35181
- header {
35182
- background-color: #fff;
35183
- padding: 1.5rem 0;
35184
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
35185
- margin-bottom: 2rem;
35186
- }
35202
+ a {
35203
+ color: #0066cc;
35204
+ text-decoration: none;
35205
+ }
35187
35206
 
35188
- header h1 {
35189
- font-size: 1.8rem;
35190
- margin: 0;
35191
- }
35207
+ a:hover {
35208
+ text-decoration: underline;
35209
+ }
35192
35210
 
35193
- header h1 a {
35194
- color: #333;
35195
- text-decoration: none;
35196
- }
35211
+ .container {
35212
+ max-width: 800px;
35213
+ margin: 0 auto;
35214
+ padding: 0 1.5rem;
35215
+ }
35197
35216
 
35198
- header nav {
35199
- margin-top: 0.5rem;
35200
- }
35217
+ /* Header */
35218
+ header {
35219
+ background-color: #fff;
35220
+ padding: 1.5rem 0;
35221
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
35222
+ margin-bottom: 2rem;
35223
+ }
35201
35224
 
35202
- header nav ul {
35203
- display: flex;
35204
- list-style: none;
35205
- gap: 1.5rem;
35206
- }
35225
+ header h1 {
35226
+ font-size: 1.8rem;
35227
+ margin: 0;
35228
+ }
35207
35229
 
35208
- /* Main content */
35209
- main {
35210
- background-color: #fff;
35211
- padding: 2rem;
35212
- border-radius: 5px;
35213
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
35214
- }
35230
+ header h1 a {
35231
+ color: #333;
35232
+ text-decoration: none;
35233
+ }
35215
35234
 
35216
- /* Posts */
35217
- .posts {
35218
- display: flex;
35219
- flex-direction: column;
35220
- gap: 2rem;
35221
- }
35235
+ header nav {
35236
+ margin-top: 0.5rem;
35237
+ }
35222
35238
 
35223
- .post-card {
35224
- border-bottom: 1px solid #eee;
35225
- padding-bottom: 1.5rem;
35226
- }
35239
+ header nav ul {
35240
+ display: flex;
35241
+ list-style: none;
35242
+ gap: 1.5rem;
35243
+ }
35227
35244
 
35228
- .post-card:last-child {
35229
- border-bottom: none;
35230
- }
35245
+ /* Main content */
35246
+ main {
35247
+ background-color: #fff;
35248
+ padding: 2rem;
35249
+ border-radius: 5px;
35250
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
35251
+ }
35231
35252
 
35232
- .post-card h2 {
35233
- margin-bottom: 0.5rem;
35234
- }
35253
+ /* Posts */
35254
+ .posts {
35255
+ display: flex;
35256
+ flex-direction: column;
35257
+ gap: 2rem;
35258
+ }
35235
35259
 
35236
- .post-meta {
35237
- font-size: 0.9rem;
35238
- color: #6c757d;
35239
- margin-bottom: 1rem;
35240
- }
35260
+ .post-card {
35261
+ border-bottom: 1px solid #eee;
35262
+ padding-bottom: 1.5rem;
35263
+ }
35241
35264
 
35242
- .post-excerpt {
35243
- margin-bottom: 1rem;
35244
- }
35265
+ .post-card:last-child {
35266
+ border-bottom: none;
35267
+ }
35245
35268
 
35246
- .read-more {
35247
- font-weight: 500;
35248
- }
35269
+ .post-card h2 {
35270
+ margin-bottom: 0.5rem;
35271
+ }
35249
35272
 
35250
- /* Single post */
35251
- .post-header {
35252
- margin-bottom: 2rem;
35253
- }
35273
+ .post-meta {
35274
+ font-size: 0.9rem;
35275
+ color: #6c757d;
35276
+ margin-bottom: 1rem;
35277
+ }
35254
35278
 
35255
- .post-content {
35256
- line-height: 1.8;
35257
- }
35279
+ .post-excerpt {
35280
+ margin-bottom: 1rem;
35281
+ }
35258
35282
 
35259
- .post-content p,
35260
- .post-content ul,
35261
- .post-content ol,
35262
- .post-content blockquote {
35263
- margin-bottom: 1.5rem;
35264
- }
35283
+ .read-more {
35284
+ font-weight: 500;
35285
+ }
35265
35286
 
35266
- .post-content h2,
35267
- .post-content h3,
35268
- .post-content h4 {
35269
- margin-top: 2rem;
35270
- margin-bottom: 1rem;
35271
- }
35287
+ /* Single post */
35288
+ .post-header {
35289
+ margin-bottom: 2rem;
35290
+ }
35272
35291
 
35273
- .post-content img {
35274
- max-width: 100%;
35275
- height: auto;
35276
- display: block;
35277
- margin: 2rem auto;
35278
- }
35292
+ .post-content {
35293
+ line-height: 1.8;
35294
+ }
35279
35295
 
35280
- .post-content pre {
35281
- background-color: #f5f5f5;
35282
- padding: 1rem;
35283
- border-radius: 4px;
35284
- overflow-x: auto;
35285
- margin-bottom: 1.5rem;
35286
- }
35296
+ .post-content p,
35297
+ .post-content ul,
35298
+ .post-content ol,
35299
+ .post-content blockquote {
35300
+ margin-bottom: 1.5rem;
35301
+ }
35287
35302
 
35288
- .post-content code {
35289
- font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
35290
- font-size: 0.9em;
35291
- background-color: #f5f5f5;
35292
- padding: 0.2em 0.4em;
35293
- border-radius: 3px;
35294
- }
35303
+ .post-content h2,
35304
+ .post-content h3,
35305
+ .post-content h4 {
35306
+ margin-top: 2rem;
35307
+ margin-bottom: 1rem;
35308
+ }
35295
35309
 
35296
- .post-content pre code {
35297
- padding: 0;
35298
- background-color: transparent;
35299
- }
35310
+ .post-content img {
35311
+ max-width: 100%;
35312
+ height: auto;
35313
+ display: block;
35314
+ margin: 2rem auto;
35315
+ }
35300
35316
 
35301
- /* Tags */
35302
- .tags a {
35303
- display: inline-block;
35304
- margin-left: 0.5rem;
35305
- }
35317
+ .post-content pre {
35318
+ background-color: #f5f5f5;
35319
+ padding: 1rem;
35320
+ border-radius: 4px;
35321
+ overflow-x: auto;
35322
+ margin-bottom: 1.5rem;
35323
+ }
35306
35324
 
35307
- .tags-list {
35308
- list-style: none;
35309
- }
35325
+ .post-content code {
35326
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
35327
+ font-size: 0.9em;
35328
+ background-color: #f5f5f5;
35329
+ padding: 0.2em 0.4em;
35330
+ border-radius: 3px;
35331
+ }
35310
35332
 
35311
- .tags-list li {
35312
- margin-bottom: 1rem;
35313
- }
35333
+ .post-content pre code {
35334
+ padding: 0;
35335
+ background-color: transparent;
35336
+ }
35314
35337
 
35315
- .tags-list .count {
35316
- color: #6c757d;
35317
- font-size: 0.9rem;
35318
- }
35338
+ /* Tags */
35339
+ .tags a {
35340
+ display: inline-block;
35341
+ margin-left: 0.5rem;
35342
+ }
35319
35343
 
35320
- .tags-list .description {
35321
- margin-top: 0.25rem;
35322
- font-size: 0.9rem;
35323
- color: #6c757d;
35324
- }
35344
+ .tags-list {
35345
+ list-style: none;
35346
+ }
35325
35347
 
35326
- /* Pagination */
35327
- .pagination {
35328
- display: flex;
35329
- justify-content: space-between;
35330
- align-items: center;
35331
- margin-top: 2rem;
35332
- padding-top: 1rem;
35333
- border-top: 1px solid #eee;
35334
- }
35348
+ .tags-list li {
35349
+ margin-bottom: 1rem;
35350
+ }
35335
35351
 
35336
- .pagination .page-info {
35337
- color: #6c757d;
35338
- font-size: 0.9rem;
35339
- }
35352
+ .tags-list .count {
35353
+ color: #6c757d;
35354
+ font-size: 0.9rem;
35355
+ }
35340
35356
 
35341
- /* Footer */
35342
- footer {
35343
- text-align: center;
35344
- padding: 2rem 0;
35345
- color: #6c757d;
35346
- font-size: 0.9rem;
35347
- }`;
35357
+ .tags-list .description {
35358
+ margin-top: 0.25rem;
35359
+ font-size: 0.9rem;
35360
+ color: #6c757d;
35361
+ }
35348
35362
 
35349
- // src/cli/commands/templates/sample-post.ts
35350
- var samplePost = `---
35363
+ /* Pagination */
35364
+ .pagination {
35365
+ display: flex;
35366
+ justify-content: space-between;
35367
+ align-items: center;
35368
+ margin-top: 2rem;
35369
+ padding-top: 1rem;
35370
+ border-top: 1px solid #eee;
35371
+ }
35372
+
35373
+ .pagination .page-info {
35374
+ color: #6c757d;
35375
+ font-size: 0.9rem;
35376
+ }
35377
+
35378
+ /* Footer */
35379
+ footer {
35380
+ text-align: center;
35381
+ padding: 2rem 0;
35382
+ color: #6c757d;
35383
+ font-size: 0.9rem;
35384
+ }`;
35385
+ }
35386
+ function getSamplePost() {
35387
+ return `---
35351
35388
  title: Welcome to Bunki
35352
35389
  date: 2025-01-15T12:00:00Z
35353
35390
  tags: [getting-started, bunki]
@@ -35398,64 +35435,6 @@ function hello() {
35398
35435
  4. Run \`bunki generate\` to build your site
35399
35436
  5. Run \`bunki serve\` to preview your site locally
35400
35437
  `;
35401
-
35402
- // src/cli/commands/templates/index.ts
35403
- var nunjucks3 = {
35404
- "base.njk": baseNjk,
35405
- "index.njk": indexNjk,
35406
- "post.njk": postNjk,
35407
- "tag.njk": tagNjk,
35408
- "tags.njk": tagsNjk,
35409
- "archive.njk": archiveNjk
35410
- };
35411
-
35412
- // src/cli/commands/init.ts
35413
- var defaultDependencies = {
35414
- createDefaultConfig,
35415
- ensureDir,
35416
- writeFile: (filePath, data) => Bun.write(filePath, data),
35417
- logger: console,
35418
- exit: (code) => process.exit(code)
35419
- };
35420
- async function handleInitCommand(options2, deps = defaultDependencies) {
35421
- try {
35422
- const configPath = path13.resolve(options2.config);
35423
- const configCreated = await deps.createDefaultConfig(configPath);
35424
- if (!configCreated) {
35425
- deps.logger.log(`
35426
- Skipped initialization because the config file already exists`);
35427
- return;
35428
- }
35429
- deps.logger.log("Creating directory structure...");
35430
- const baseDir = process.cwd();
35431
- const contentDir = path13.join(baseDir, "content");
35432
- const templatesDir = path13.join(baseDir, "templates");
35433
- const stylesDir = path13.join(templatesDir, "styles");
35434
- const publicDir = path13.join(baseDir, "public");
35435
- await deps.ensureDir(contentDir);
35436
- await deps.ensureDir(templatesDir);
35437
- await deps.ensureDir(stylesDir);
35438
- await deps.ensureDir(publicDir);
35439
- for (const [filename, content] of Object.entries(nunjucks3)) {
35440
- await deps.writeFile(path13.join(templatesDir, filename), content);
35441
- }
35442
- await deps.writeFile(path13.join(stylesDir, "main.css"), defaultCss);
35443
- await deps.writeFile(path13.join(contentDir, "welcome.md"), samplePost);
35444
- deps.logger.log(`
35445
- Initialization complete! Here are the next steps:`);
35446
- deps.logger.log("1. Edit bunki.config.ts to configure your site");
35447
- deps.logger.log("2. Add markdown files to the content directory");
35448
- deps.logger.log('3. Run "bunki generate" to build your site');
35449
- deps.logger.log('4. Run "bunki serve" to preview your site locally');
35450
- } catch (error) {
35451
- deps.logger.error("Error initializing site:", error);
35452
- deps.exit(1);
35453
- }
35454
- }
35455
- function registerInitCommand(program2, deps = defaultDependencies) {
35456
- return program2.command("init").description("Initialize a new site with default structure").option("-c, --config <file>", "Path to config file", "bunki.config.ts").action(async (options2) => {
35457
- await handleInitCommand(options2, deps);
35458
- });
35459
35438
  }
35460
35439
 
35461
35440
  // src/cli/commands/new-post.ts
package/dist/index.js CHANGED
@@ -32333,8 +32333,7 @@ class SiteGenerator {
32333
32333
  this.metrics = new MetricsCollector;
32334
32334
  const env = import_nunjucks2.default.configure(this.options.templatesDir, {
32335
32335
  autoescape: true,
32336
- watch: false,
32337
- noCache: false
32336
+ watch: false
32338
32337
  });
32339
32338
  env.addFilter("date", (date, format) => {
32340
32339
  const d2 = toPacificTime(date);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunki",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "An opinionated static site generator built with Bun featuring PostCSS integration and modern web development workflows",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -1,4 +0,0 @@
1
- /**
2
- * Year archive page template
3
- */
4
- export declare const archiveNjk: string;
@@ -1,4 +0,0 @@
1
- /**
2
- * Base template for all pages
3
- */
4
- export declare const baseNjk: string;
@@ -1,4 +0,0 @@
1
- /**
2
- * Default CSS stylesheet for new Bunki sites
3
- */
4
- export declare const defaultCss: string;
@@ -1,4 +0,0 @@
1
- /**
2
- * Homepage template with post listing and pagination
3
- */
4
- export declare const indexNjk: string;
@@ -1,14 +0,0 @@
1
- import { defaultCss } from "./default-css";
2
- import { samplePost } from "./sample-post";
3
- /**
4
- * Nunjucks template files
5
- */
6
- export declare const nunjucks: Record<string, string>;
7
- /**
8
- * Default CSS stylesheet
9
- */
10
- export { defaultCss };
11
- /**
12
- * Sample markdown post
13
- */
14
- export { samplePost };
@@ -1,4 +0,0 @@
1
- /**
2
- * Individual post page template
3
- */
4
- export declare const postNjk: string;
@@ -1,4 +0,0 @@
1
- /**
2
- * Sample blog post for new Bunki sites
3
- */
4
- export declare const samplePost = "---\ntitle: Welcome to Bunki\ndate: 2025-01-15T12:00:00Z\ntags: [getting-started, bunki]\n---\n\n# Welcome to Your New Bunki Site\n\nThis is a sample blog post to help you get started with Bunki. You can edit this file or create new markdown files in the `content` directory.\n\n## Features\n\n- Markdown support with frontmatter\n- Syntax highlighting for code blocks\n- Tag-based organization\n- Pagination for post listings\n- RSS feed generation\n- Sitemap generation\n\n## Adding Content\n\nCreate new markdown files in the `content` directory with frontmatter like this:\n\n```markdown\n---\ntitle: Your Post Title\ndate: 2025-01-01T12:00:00Z\ntags: [tag1, tag2]\n---\n\nYour post content goes here...\n```\n\n## Code Highlighting\n\nBunki supports syntax highlighting for code blocks:\n\n```javascript\nfunction hello() {\n console.log('Hello, world!');\n}\n```\n\n## Next Steps\n\n1. Edit the site configuration in `bunki.config.ts`\n2. Create your own templates in the `templates` directory\n3. Add more blog posts in the `content` directory\n4. Run `bunki generate` to build your site\n5. Run `bunki serve` to preview your site locally\n";
@@ -1,4 +0,0 @@
1
- /**
2
- * Single tag page template with posts list
3
- */
4
- export declare const tagNjk: string;
@@ -1,4 +0,0 @@
1
- /**
2
- * All tags listing page template
3
- */
4
- export declare const tagsNjk: string;