bunki 0.7.1 → 0.8.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
@@ -4,7 +4,7 @@
4
4
  [![Coverage Status](https://coveralls.io/repos/github/kahwee/bunki/badge.svg?branch=main)](https://coveralls.io/github/kahwee/bunki?branch=main)
5
5
  [![npm version](https://badge.fury.io/js/bunki.svg)](https://badge.fury.io/js/bunki)
6
6
 
7
- Fast static site generator for blogs and documentation built with Bun. Supports Markdown + frontmatter, tags, year-based archives, pagination, RSS feeds, sitemaps, secure HTML sanitization, syntax highlighting, PostCSS pipelines, media uploads (images & videos to S3/R2), incremental uploads with year filtering, and Nunjucks templating.
7
+ Fast static site generator for blogs and documentation built with Bun. Supports Markdown + frontmatter, tags, year-based archives, pagination, RSS feeds, sitemaps, JSON-LD structured data for SEO, secure HTML sanitization, syntax highlighting, PostCSS pipelines, media uploads (images & videos to S3/R2), incremental uploads with year filtering, and Nunjucks templating.
8
8
 
9
9
  ## Install
10
10
 
@@ -122,6 +122,188 @@ Create `templates/styles/main.css`:
122
122
 
123
123
  CSS is processed automatically during `bunki generate`.
124
124
 
125
+ ## JSON-LD Structured Data for SEO
126
+
127
+ Bunki automatically generates [JSON-LD](https://json-ld.org/) structured data markup for enhanced SEO and search engine visibility. JSON-LD (JavaScript Object Notation for Linked Data) is Google's recommended format for structured data.
128
+
129
+ ### What is JSON-LD?
130
+
131
+ JSON-LD helps search engines better understand your content by providing explicit, structured information about your pages. This can lead to:
132
+
133
+ - **Rich snippets** in search results (article previews, star ratings, etc.)
134
+ - **Better content indexing** and understanding by search engines
135
+ - **Improved click-through rates** from search results
136
+ - **Knowledge graph integration** with Google, Bing, and other search engines
137
+
138
+ ### Automatic Schema Generation
139
+
140
+ Bunki automatically generates appropriate schemas for different page types:
141
+
142
+ #### Blog Posts (BlogPosting Schema)
143
+
144
+ Every blog post includes comprehensive `BlogPosting` schema with:
145
+
146
+ - Headline and description
147
+ - Publication and modification dates
148
+ - Author information
149
+ - Publisher details
150
+ - Article keywords (from tags)
151
+ - Word count
152
+ - Featured image (automatically extracted)
153
+ - Language information
154
+
155
+ Example output in your HTML:
156
+
157
+ ```html
158
+ <script type="application/ld+json">
159
+ {
160
+ "@context": "https://schema.org",
161
+ "@type": "BlogPosting",
162
+ "headline": "Getting Started with Bun",
163
+ "description": "Learn how to get started with Bun, the fast JavaScript runtime.",
164
+ "url": "https://example.com/2025/getting-started-with-bun/",
165
+ "datePublished": "2025-01-15T10:30:00.000Z",
166
+ "dateModified": "2025-01-15T10:30:00.000Z",
167
+ "author": {
168
+ "@type": "Person",
169
+ "name": "John Doe",
170
+ "email": "john@example.com"
171
+ },
172
+ "publisher": {
173
+ "@type": "Organization",
174
+ "name": "My Blog",
175
+ "url": "https://example.com"
176
+ },
177
+ "keywords": "bun, javascript, performance",
178
+ "image": "https://example.com/images/bun-logo.png"
179
+ }
180
+ </script>
181
+ ```
182
+
183
+ #### Homepage (WebSite & Organization Schemas)
184
+
185
+ The homepage includes dual schemas:
186
+
187
+ 1. **WebSite Schema**: Defines the website entity
188
+ 2. **Organization Schema**: Defines the publisher/organization
189
+
190
+ ```html
191
+ <script type="application/ld+json">
192
+ {
193
+ "@context": "https://schema.org",
194
+ "@type": "WebSite",
195
+ "name": "My Blog",
196
+ "url": "https://example.com",
197
+ "description": "My thoughts and ideas",
198
+ "potentialAction": {
199
+ "@type": "SearchAction",
200
+ "target": {
201
+ "@type": "EntryPoint",
202
+ "urlTemplate": "https://example.com/search?q={search_term_string}"
203
+ }
204
+ }
205
+ }
206
+ </script>
207
+ ```
208
+
209
+ #### Breadcrumbs (BreadcrumbList Schema)
210
+
211
+ All pages include breadcrumb navigation for better site hierarchy understanding:
212
+
213
+ ```html
214
+ <script type="application/ld+json">
215
+ {
216
+ "@context": "https://schema.org",
217
+ "@type": "BreadcrumbList",
218
+ "itemListElement": [
219
+ {
220
+ "@type": "ListItem",
221
+ "position": 1,
222
+ "name": "Home",
223
+ "item": "https://example.com"
224
+ },
225
+ {
226
+ "@type": "ListItem",
227
+ "position": 2,
228
+ "name": "Getting Started with Bun",
229
+ "item": "https://example.com/2025/getting-started-with-bun/"
230
+ }
231
+ ]
232
+ }
233
+ </script>
234
+ ```
235
+
236
+ ### Configuration for SEO
237
+
238
+ Enhance your JSON-LD output by providing complete author and site information in `bunki.config.ts`:
239
+
240
+ ```typescript
241
+ import { SiteConfig } from "bunki";
242
+
243
+ export default (): SiteConfig => ({
244
+ title: "My Blog",
245
+ description: "My thoughts and ideas on web development",
246
+ baseUrl: "https://example.com",
247
+ domain: "example.com",
248
+
249
+ // Author information (used in BlogPosting schema)
250
+ authorName: "John Doe",
251
+ authorEmail: "john@example.com",
252
+
253
+ // RSS/SEO configuration
254
+ rssLanguage: "en-US", // Language code for content
255
+ copyright: "Copyright © 2025 My Blog",
256
+
257
+ // ... other config
258
+ });
259
+ ```
260
+
261
+ ### Testing Your JSON-LD
262
+
263
+ You can validate your structured data using these tools:
264
+
265
+ 1. **[Google Rich Results Test](https://search.google.com/test/rich-results)** - Test how Google sees your structured data
266
+ 2. **[Schema.org Validator](https://validator.schema.org/)** - Validate JSON-LD syntax
267
+ 3. **[Structured Data Linter](http://linter.structured-data.org/)** - Check for errors and warnings
268
+
269
+ ### Supported Schema Types
270
+
271
+ Bunki currently supports these Schema.org types:
272
+
273
+ - **BlogPosting** - Individual blog posts and articles
274
+ - **WebSite** - Homepage and site-wide metadata
275
+ - **Organization** - Publisher/organization information
276
+ - **Person** - Author information
277
+ - **BreadcrumbList** - Navigation breadcrumbs
278
+
279
+ ### How It Works
280
+
281
+ JSON-LD generation is completely automatic:
282
+
283
+ 1. **Post Creation**: When you write a post with frontmatter, Bunki extracts metadata
284
+ 2. **Site Generation**: During `bunki generate`, appropriate schemas are created
285
+ 3. **Template Injection**: JSON-LD scripts are automatically injected into `<head>`
286
+ 4. **Image Extraction**: The first image in your post content is automatically used as the featured image
287
+
288
+ No manual configuration needed - just run `bunki generate` and your site will have complete structured data!
289
+
290
+ ### Best Practices
291
+
292
+ To maximize SEO benefits:
293
+
294
+ 1. **Use descriptive titles** - Your post title becomes the schema headline
295
+ 2. **Write good excerpts** - These become schema descriptions
296
+ 3. **Include images** - First image in content is used as featured image
297
+ 4. **Tag your posts** - Tags become schema keywords
298
+ 5. **Set author info** - Complete `authorName` and `authorEmail` in config
299
+ 6. **Use ISO 8601 dates** - Format: `2025-01-15T10:30:00-07:00`
300
+
301
+ ### Further Reading
302
+
303
+ - [Schema.org Documentation](https://schema.org/)
304
+ - [Google Search Central - Structured Data](https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data)
305
+ - [JSON-LD Official Spec](https://json-ld.org/)
306
+
125
307
  ## Image Management
126
308
 
127
309
  ### Overview
@@ -568,7 +750,8 @@ dist/
568
750
  - **Styling**: Built-in PostCSS support for modern CSS frameworks
569
751
  - **Media Management**: Direct S3/R2 uploads for images and MP4 videos with URL mapping
570
752
  - **Incremental Uploads**: Year-based filtering (`--min-year`) for large media collections
571
- - **SEO**: Automatic RSS feeds, sitemaps, meta tags
753
+ - **SEO**: Automatic RSS feeds, sitemaps, meta tags, and JSON-LD structured data
754
+ - **JSON-LD Structured Data**: Automatic Schema.org markup (BlogPosting, WebSite, Organization, BreadcrumbList)
572
755
  - **Pagination**: Configurable posts per page
573
756
  - **Archives**: Year-based and tag-based organization
574
757
 
@@ -606,7 +789,20 @@ bunki/
606
789
 
607
790
  ## Changelog
608
791
 
609
- ### v0.7.0 (Current)
792
+ ### v0.8.0 (Current)
793
+
794
+ - **JSON-LD Structured Data**: Automatic Schema.org markup generation for enhanced SEO
795
+ - BlogPosting schema for individual blog posts with author, keywords, images
796
+ - WebSite schema for homepage with search action
797
+ - Organization schema for publisher information
798
+ - BreadcrumbList schema for navigation hierarchy
799
+ - Automatic featured image extraction from post content
800
+ - **Comprehensive SEO**: Complete structured data support following Google best practices
801
+ - **Zero configuration**: JSON-LD automatically generated during site build
802
+ - **Well documented**: Extensive README section with examples and validation tools
803
+ - **Fully tested**: 60+ new tests covering all JSON-LD schema types
804
+
805
+ ### v0.7.0
610
806
 
611
807
  - **Media uploads**: Added MP4 video support alongside image uploads
612
808
  - **Incremental uploads**: Year-based filtering with `--min-year` option
package/dist/cli.js CHANGED
@@ -32909,6 +32909,152 @@ async function parseMarkdownDirectory(contentDir) {
32909
32909
  }
32910
32910
  }
32911
32911
 
32912
+ // src/utils/json-ld.ts
32913
+ function generateOrganizationSchema(site) {
32914
+ const organization = {
32915
+ "@context": "https://schema.org",
32916
+ "@type": "Organization",
32917
+ name: site.title,
32918
+ url: site.baseUrl
32919
+ };
32920
+ if (site.description) {
32921
+ organization.description = site.description;
32922
+ }
32923
+ return organization;
32924
+ }
32925
+ function generateBlogPostingSchema(options2) {
32926
+ const { post, site, imageUrl, dateModified } = options2;
32927
+ const postUrl = `${site.baseUrl}${post.url}`;
32928
+ const blogPosting = {
32929
+ "@context": "https://schema.org",
32930
+ "@type": "BlogPosting",
32931
+ headline: post.title,
32932
+ description: post.excerpt,
32933
+ url: postUrl,
32934
+ mainEntityOfPage: {
32935
+ "@type": "WebPage",
32936
+ "@id": postUrl
32937
+ },
32938
+ datePublished: post.date,
32939
+ dateModified: dateModified || post.date
32940
+ };
32941
+ if (site.authorName) {
32942
+ blogPosting.author = {
32943
+ "@type": "Person",
32944
+ name: site.authorName,
32945
+ ...site.authorEmail && { email: site.authorEmail }
32946
+ };
32947
+ }
32948
+ blogPosting.publisher = {
32949
+ "@type": "Organization",
32950
+ name: site.title,
32951
+ url: site.baseUrl
32952
+ };
32953
+ if (imageUrl) {
32954
+ blogPosting.image = imageUrl;
32955
+ }
32956
+ if (post.tags && post.tags.length > 0) {
32957
+ blogPosting.keywords = post.tags.join(", ");
32958
+ }
32959
+ if (post.tags && post.tags.length > 0) {
32960
+ blogPosting.articleSection = post.tags[0];
32961
+ }
32962
+ if (post.content) {
32963
+ const wordCount = post.content.split(/\s+/).length;
32964
+ blogPosting.wordCount = wordCount;
32965
+ }
32966
+ blogPosting.inLanguage = site.rssLanguage || "en-US";
32967
+ return blogPosting;
32968
+ }
32969
+ function generateWebSiteSchema(options2) {
32970
+ const { site } = options2;
32971
+ const webSite = {
32972
+ "@context": "https://schema.org",
32973
+ "@type": "WebSite",
32974
+ name: site.title,
32975
+ url: site.baseUrl
32976
+ };
32977
+ if (site.description) {
32978
+ webSite.description = site.description;
32979
+ }
32980
+ webSite.potentialAction = {
32981
+ "@type": "SearchAction",
32982
+ target: {
32983
+ "@type": "EntryPoint",
32984
+ urlTemplate: `${site.baseUrl}/search?q={search_term_string}`
32985
+ },
32986
+ "query-input": "required name=search_term_string"
32987
+ };
32988
+ return webSite;
32989
+ }
32990
+ function generateBreadcrumbListSchema(options2) {
32991
+ const { site, post, items } = options2;
32992
+ const breadcrumbs = {
32993
+ "@context": "https://schema.org",
32994
+ "@type": "BreadcrumbList",
32995
+ itemListElement: []
32996
+ };
32997
+ if (items && items.length > 0) {
32998
+ breadcrumbs.itemListElement = items.map((item, index) => ({
32999
+ "@type": "ListItem",
33000
+ position: index + 1,
33001
+ name: item.name,
33002
+ item: item.url
33003
+ }));
33004
+ return breadcrumbs;
33005
+ }
33006
+ const homeItem = {
33007
+ "@type": "ListItem",
33008
+ position: 1,
33009
+ name: "Home",
33010
+ item: site.baseUrl
33011
+ };
33012
+ breadcrumbs.itemListElement.push(homeItem);
33013
+ if (post) {
33014
+ breadcrumbs.itemListElement.push({
33015
+ "@type": "ListItem",
33016
+ position: 2,
33017
+ name: post.title,
33018
+ item: `${site.baseUrl}${post.url}`
33019
+ });
33020
+ }
33021
+ return breadcrumbs;
33022
+ }
33023
+ function toScriptTag(jsonLd) {
33024
+ const json2 = JSON.stringify(jsonLd, null, 2);
33025
+ return `<script type="application/ld+json">
33026
+ ${json2}
33027
+ </script>`;
33028
+ }
33029
+ function extractFirstImageUrl(html, baseUrl) {
33030
+ const imgMatch = html.match(/<img[^>]+src=["']([^"']+)["']/i);
33031
+ if (!imgMatch || !imgMatch[1]) {
33032
+ return;
33033
+ }
33034
+ const src = imgMatch[1];
33035
+ if (src.startsWith("http://") || src.startsWith("https://")) {
33036
+ return src;
33037
+ }
33038
+ const cleanBaseUrl = baseUrl.replace(/\/$/, "");
33039
+ const cleanSrc = src.startsWith("/") ? src : `/${src}`;
33040
+ return `${cleanBaseUrl}${cleanSrc}`;
33041
+ }
33042
+ function generatePostPageSchemas(options2) {
33043
+ const schemas = [];
33044
+ schemas.push(generateBlogPostingSchema(options2));
33045
+ schemas.push(generateBreadcrumbListSchema({
33046
+ site: options2.site,
33047
+ post: options2.post
33048
+ }));
33049
+ return schemas;
33050
+ }
33051
+ function generateHomePageSchemas(options2) {
33052
+ const schemas = [];
33053
+ schemas.push(generateWebSiteSchema(options2));
33054
+ schemas.push(generateOrganizationSchema(options2.site));
33055
+ return schemas;
33056
+ }
33057
+
32912
33058
  // src/site-generator.ts
32913
33059
  class SiteGenerator {
32914
33060
  options;
@@ -33094,11 +33240,20 @@ class SiteGenerator {
33094
33240
  const endIndex = startIndex + pageSize;
33095
33241
  const paginatedPosts = this.site.posts.slice(startIndex, endIndex);
33096
33242
  const pagination = this.createPagination(this.site.posts, page, pageSize, "/");
33243
+ let jsonLd = "";
33244
+ if (page === 1) {
33245
+ const schemas = generateHomePageSchemas({
33246
+ site: this.options.config
33247
+ });
33248
+ jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
33249
+ `);
33250
+ }
33097
33251
  const pageHtml = import_nunjucks.default.render("index.njk", {
33098
33252
  site: this.options.config,
33099
33253
  posts: paginatedPosts,
33100
33254
  tags: this.getSortedTags(this.options.config.maxTagsOnHomepage),
33101
- pagination
33255
+ pagination,
33256
+ jsonLd
33102
33257
  });
33103
33258
  if (page === 1) {
33104
33259
  await Bun.write(path5.join(this.options.outputDir, "index.html"), pageHtml);
@@ -33114,9 +33269,18 @@ class SiteGenerator {
33114
33269
  const postPath = post.url.substring(1);
33115
33270
  const postDir = path5.join(this.options.outputDir, postPath);
33116
33271
  await ensureDir(postDir);
33272
+ const imageUrl = extractFirstImageUrl(post.html, this.options.config.baseUrl);
33273
+ const schemas = generatePostPageSchemas({
33274
+ post,
33275
+ site: this.options.config,
33276
+ imageUrl
33277
+ });
33278
+ const jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
33279
+ `);
33117
33280
  const postHtml = import_nunjucks.default.render("post.njk", {
33118
33281
  site: this.options.config,
33119
- post
33282
+ post,
33283
+ jsonLd
33120
33284
  });
33121
33285
  await Bun.write(path5.join(postDir, "index.html"), postHtml);
33122
33286
  }
package/dist/index.js CHANGED
@@ -30856,6 +30856,152 @@ function getDefaultCSSConfig() {
30856
30856
  };
30857
30857
  }
30858
30858
 
30859
+ // src/utils/json-ld.ts
30860
+ function generateOrganizationSchema(site) {
30861
+ const organization = {
30862
+ "@context": "https://schema.org",
30863
+ "@type": "Organization",
30864
+ name: site.title,
30865
+ url: site.baseUrl
30866
+ };
30867
+ if (site.description) {
30868
+ organization.description = site.description;
30869
+ }
30870
+ return organization;
30871
+ }
30872
+ function generateBlogPostingSchema(options2) {
30873
+ const { post, site, imageUrl, dateModified } = options2;
30874
+ const postUrl = `${site.baseUrl}${post.url}`;
30875
+ const blogPosting = {
30876
+ "@context": "https://schema.org",
30877
+ "@type": "BlogPosting",
30878
+ headline: post.title,
30879
+ description: post.excerpt,
30880
+ url: postUrl,
30881
+ mainEntityOfPage: {
30882
+ "@type": "WebPage",
30883
+ "@id": postUrl
30884
+ },
30885
+ datePublished: post.date,
30886
+ dateModified: dateModified || post.date
30887
+ };
30888
+ if (site.authorName) {
30889
+ blogPosting.author = {
30890
+ "@type": "Person",
30891
+ name: site.authorName,
30892
+ ...site.authorEmail && { email: site.authorEmail }
30893
+ };
30894
+ }
30895
+ blogPosting.publisher = {
30896
+ "@type": "Organization",
30897
+ name: site.title,
30898
+ url: site.baseUrl
30899
+ };
30900
+ if (imageUrl) {
30901
+ blogPosting.image = imageUrl;
30902
+ }
30903
+ if (post.tags && post.tags.length > 0) {
30904
+ blogPosting.keywords = post.tags.join(", ");
30905
+ }
30906
+ if (post.tags && post.tags.length > 0) {
30907
+ blogPosting.articleSection = post.tags[0];
30908
+ }
30909
+ if (post.content) {
30910
+ const wordCount = post.content.split(/\s+/).length;
30911
+ blogPosting.wordCount = wordCount;
30912
+ }
30913
+ blogPosting.inLanguage = site.rssLanguage || "en-US";
30914
+ return blogPosting;
30915
+ }
30916
+ function generateWebSiteSchema(options2) {
30917
+ const { site } = options2;
30918
+ const webSite = {
30919
+ "@context": "https://schema.org",
30920
+ "@type": "WebSite",
30921
+ name: site.title,
30922
+ url: site.baseUrl
30923
+ };
30924
+ if (site.description) {
30925
+ webSite.description = site.description;
30926
+ }
30927
+ webSite.potentialAction = {
30928
+ "@type": "SearchAction",
30929
+ target: {
30930
+ "@type": "EntryPoint",
30931
+ urlTemplate: `${site.baseUrl}/search?q={search_term_string}`
30932
+ },
30933
+ "query-input": "required name=search_term_string"
30934
+ };
30935
+ return webSite;
30936
+ }
30937
+ function generateBreadcrumbListSchema(options2) {
30938
+ const { site, post, items } = options2;
30939
+ const breadcrumbs = {
30940
+ "@context": "https://schema.org",
30941
+ "@type": "BreadcrumbList",
30942
+ itemListElement: []
30943
+ };
30944
+ if (items && items.length > 0) {
30945
+ breadcrumbs.itemListElement = items.map((item, index) => ({
30946
+ "@type": "ListItem",
30947
+ position: index + 1,
30948
+ name: item.name,
30949
+ item: item.url
30950
+ }));
30951
+ return breadcrumbs;
30952
+ }
30953
+ const homeItem = {
30954
+ "@type": "ListItem",
30955
+ position: 1,
30956
+ name: "Home",
30957
+ item: site.baseUrl
30958
+ };
30959
+ breadcrumbs.itemListElement.push(homeItem);
30960
+ if (post) {
30961
+ breadcrumbs.itemListElement.push({
30962
+ "@type": "ListItem",
30963
+ position: 2,
30964
+ name: post.title,
30965
+ item: `${site.baseUrl}${post.url}`
30966
+ });
30967
+ }
30968
+ return breadcrumbs;
30969
+ }
30970
+ function toScriptTag(jsonLd) {
30971
+ const json2 = JSON.stringify(jsonLd, null, 2);
30972
+ return `<script type="application/ld+json">
30973
+ ${json2}
30974
+ </script>`;
30975
+ }
30976
+ function extractFirstImageUrl(html, baseUrl) {
30977
+ const imgMatch = html.match(/<img[^>]+src=["']([^"']+)["']/i);
30978
+ if (!imgMatch || !imgMatch[1]) {
30979
+ return;
30980
+ }
30981
+ const src = imgMatch[1];
30982
+ if (src.startsWith("http://") || src.startsWith("https://")) {
30983
+ return src;
30984
+ }
30985
+ const cleanBaseUrl = baseUrl.replace(/\/$/, "");
30986
+ const cleanSrc = src.startsWith("/") ? src : `/${src}`;
30987
+ return `${cleanBaseUrl}${cleanSrc}`;
30988
+ }
30989
+ function generatePostPageSchemas(options2) {
30990
+ const schemas = [];
30991
+ schemas.push(generateBlogPostingSchema(options2));
30992
+ schemas.push(generateBreadcrumbListSchema({
30993
+ site: options2.site,
30994
+ post: options2.post
30995
+ }));
30996
+ return schemas;
30997
+ }
30998
+ function generateHomePageSchemas(options2) {
30999
+ const schemas = [];
31000
+ schemas.push(generateWebSiteSchema(options2));
31001
+ schemas.push(generateOrganizationSchema(options2.site));
31002
+ return schemas;
31003
+ }
31004
+
30859
31005
  // src/site-generator.ts
30860
31006
  class SiteGenerator {
30861
31007
  options;
@@ -31041,11 +31187,20 @@ class SiteGenerator {
31041
31187
  const endIndex = startIndex + pageSize;
31042
31188
  const paginatedPosts = this.site.posts.slice(startIndex, endIndex);
31043
31189
  const pagination = this.createPagination(this.site.posts, page, pageSize, "/");
31190
+ let jsonLd = "";
31191
+ if (page === 1) {
31192
+ const schemas = generateHomePageSchemas({
31193
+ site: this.options.config
31194
+ });
31195
+ jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
31196
+ `);
31197
+ }
31044
31198
  const pageHtml = import_nunjucks.default.render("index.njk", {
31045
31199
  site: this.options.config,
31046
31200
  posts: paginatedPosts,
31047
31201
  tags: this.getSortedTags(this.options.config.maxTagsOnHomepage),
31048
- pagination
31202
+ pagination,
31203
+ jsonLd
31049
31204
  });
31050
31205
  if (page === 1) {
31051
31206
  await Bun.write(path5.join(this.options.outputDir, "index.html"), pageHtml);
@@ -31061,9 +31216,18 @@ class SiteGenerator {
31061
31216
  const postPath = post.url.substring(1);
31062
31217
  const postDir = path5.join(this.options.outputDir, postPath);
31063
31218
  await ensureDir(postDir);
31219
+ const imageUrl = extractFirstImageUrl(post.html, this.options.config.baseUrl);
31220
+ const schemas = generatePostPageSchemas({
31221
+ post,
31222
+ site: this.options.config,
31223
+ imageUrl
31224
+ });
31225
+ const jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
31226
+ `);
31064
31227
  const postHtml = import_nunjucks.default.render("post.njk", {
31065
31228
  site: this.options.config,
31066
- post
31229
+ post,
31230
+ jsonLd
31067
31231
  });
31068
31232
  await Bun.write(path5.join(postDir, "index.html"), postHtml);
31069
31233
  }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * JSON-LD (JavaScript Object Notation for Linked Data) utility functions
3
+ * for generating structured data markup for SEO optimization.
4
+ *
5
+ * This module provides functions to generate Schema.org structured data
6
+ * in JSON-LD format, which helps search engines better understand your content.
7
+ *
8
+ * Supported schema types:
9
+ * - BlogPosting: For individual blog posts/articles
10
+ * - WebSite: For the homepage/website metadata
11
+ * - BreadcrumbList: For navigation breadcrumbs
12
+ * - Organization: For publisher/organization information
13
+ * - Person: For author information
14
+ *
15
+ * @see https://schema.org/
16
+ * @see https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data
17
+ */
18
+ import type { Post, SiteConfig } from "../types.js";
19
+ /**
20
+ * Base Schema.org Thing type
21
+ */
22
+ interface SchemaOrgThing {
23
+ "@context": "https://schema.org";
24
+ "@type": string;
25
+ [key: string]: any;
26
+ }
27
+ /**
28
+ * Options for generating BlogPosting JSON-LD
29
+ */
30
+ export interface BlogPostingOptions {
31
+ /** The post object */
32
+ post: Post;
33
+ /** Site configuration */
34
+ site: SiteConfig;
35
+ /** Optional image URL for the post (first image extracted from content) */
36
+ imageUrl?: string;
37
+ /** Optional date modified (defaults to date published) */
38
+ dateModified?: string;
39
+ }
40
+ /**
41
+ * Options for generating WebSite JSON-LD
42
+ */
43
+ export interface WebSiteOptions {
44
+ /** Site configuration */
45
+ site: SiteConfig;
46
+ }
47
+ /**
48
+ * Options for generating BreadcrumbList JSON-LD
49
+ */
50
+ export interface BreadcrumbListOptions {
51
+ /** Site configuration */
52
+ site: SiteConfig;
53
+ /** Current post (optional, for post pages) */
54
+ post?: Post;
55
+ /** Custom breadcrumb items */
56
+ items?: Array<{
57
+ name: string;
58
+ url: string;
59
+ }>;
60
+ }
61
+ /**
62
+ * Generates a Person schema for author information
63
+ *
64
+ * @param name - Author's name
65
+ * @param email - Author's email (optional)
66
+ * @returns Person schema object
67
+ *
68
+ * @example
69
+ * const author = generatePersonSchema("John Doe", "john@example.com");
70
+ * // Returns: { "@type": "Person", "name": "John Doe", "email": "john@example.com" }
71
+ */
72
+ export declare function generatePersonSchema(name: string, email?: string): SchemaOrgThing;
73
+ /**
74
+ * Generates an Organization schema for publisher information
75
+ *
76
+ * @param site - Site configuration
77
+ * @returns Organization schema object
78
+ *
79
+ * @example
80
+ * const org = generateOrganizationSchema({ title: "My Blog", baseUrl: "https://example.com" });
81
+ * // Returns: { "@type": "Organization", "name": "My Blog", "url": "https://example.com" }
82
+ */
83
+ export declare function generateOrganizationSchema(site: SiteConfig): SchemaOrgThing;
84
+ /**
85
+ * Generates BlogPosting structured data for a blog post
86
+ *
87
+ * This is the primary schema for individual blog posts/articles.
88
+ * It provides search engines with detailed information about the post
89
+ * including title, author, publication date, content, and more.
90
+ *
91
+ * @param options - BlogPosting generation options
92
+ * @returns BlogPosting schema as JSON-LD object
93
+ *
94
+ * @example
95
+ * const jsonLd = generateBlogPostingSchema({
96
+ * post: { title: "Hello World", date: "2025-01-15T10:00:00Z", ... },
97
+ * site: { title: "My Blog", baseUrl: "https://example.com", ... }
98
+ * });
99
+ *
100
+ * @see https://schema.org/BlogPosting
101
+ */
102
+ export declare function generateBlogPostingSchema(options: BlogPostingOptions): SchemaOrgThing;
103
+ /**
104
+ * Generates WebSite structured data for the homepage
105
+ *
106
+ * This schema provides search engines with information about the website
107
+ * itself, including name, description, and URL.
108
+ *
109
+ * @param options - WebSite generation options
110
+ * @returns WebSite schema as JSON-LD object
111
+ *
112
+ * @example
113
+ * const jsonLd = generateWebSiteSchema({
114
+ * site: { title: "My Blog", baseUrl: "https://example.com", description: "..." }
115
+ * });
116
+ *
117
+ * @see https://schema.org/WebSite
118
+ */
119
+ export declare function generateWebSiteSchema(options: WebSiteOptions): SchemaOrgThing;
120
+ /**
121
+ * Generates BreadcrumbList structured data for navigation
122
+ *
123
+ * Breadcrumbs help search engines understand the site's hierarchy
124
+ * and can appear in search results.
125
+ *
126
+ * @param options - BreadcrumbList generation options
127
+ * @returns BreadcrumbList schema as JSON-LD object
128
+ *
129
+ * @example
130
+ * // For a blog post
131
+ * const jsonLd = generateBreadcrumbListSchema({
132
+ * site: { title: "My Blog", baseUrl: "https://example.com" },
133
+ * post: { title: "Hello World", url: "/2025/hello-world/" }
134
+ * });
135
+ *
136
+ * @see https://schema.org/BreadcrumbList
137
+ */
138
+ export declare function generateBreadcrumbListSchema(options: BreadcrumbListOptions): SchemaOrgThing;
139
+ /**
140
+ * Converts a JSON-LD object to an HTML script tag string
141
+ *
142
+ * This helper function serializes the JSON-LD object and wraps it
143
+ * in a script tag for inclusion in HTML templates.
144
+ *
145
+ * @param jsonLd - The JSON-LD object to convert
146
+ * @returns HTML script tag string
147
+ *
148
+ * @example
149
+ * const schema = generateBlogPostingSchema({ ... });
150
+ * const scriptTag = toScriptTag(schema);
151
+ * // Returns: '<script type="application/ld+json">{"@context":"https://schema.org",...}</script>'
152
+ */
153
+ export declare function toScriptTag(jsonLd: SchemaOrgThing): string;
154
+ /**
155
+ * Extracts the first image URL from HTML content
156
+ *
157
+ * Searches for the first <img> tag in HTML content and returns its src attribute.
158
+ * This is useful for automatically finding a representative image for BlogPosting schema.
159
+ *
160
+ * @param html - HTML content to search
161
+ * @param baseUrl - Base URL to prepend to relative image URLs
162
+ * @returns Full image URL or undefined if no image found
163
+ *
164
+ * @example
165
+ * const imageUrl = extractFirstImageUrl(
166
+ * '<p>Text</p><img src="/img/photo.jpg" alt="Photo">',
167
+ * 'https://example.com'
168
+ * );
169
+ * // Returns: 'https://example.com/img/photo.jpg'
170
+ */
171
+ export declare function extractFirstImageUrl(html: string, baseUrl: string): string | undefined;
172
+ /**
173
+ * Generates multiple JSON-LD schemas for a blog post page
174
+ *
175
+ * This is a convenience function that generates all relevant schemas
176
+ * for a typical blog post page: BlogPosting, BreadcrumbList, and Organization.
177
+ *
178
+ * @param options - BlogPosting generation options
179
+ * @returns Array of JSON-LD schemas
180
+ *
181
+ * @example
182
+ * const schemas = generatePostPageSchemas({
183
+ * post: { ... },
184
+ * site: { ... }
185
+ * });
186
+ * // Returns: [BlogPosting, BreadcrumbList]
187
+ */
188
+ export declare function generatePostPageSchemas(options: BlogPostingOptions): SchemaOrgThing[];
189
+ /**
190
+ * Generates multiple JSON-LD schemas for the homepage
191
+ *
192
+ * This is a convenience function that generates all relevant schemas
193
+ * for the homepage: WebSite and Organization.
194
+ *
195
+ * @param options - WebSite generation options
196
+ * @returns Array of JSON-LD schemas
197
+ *
198
+ * @example
199
+ * const schemas = generateHomePageSchemas({
200
+ * site: { title: "My Blog", baseUrl: "https://example.com", ... }
201
+ * });
202
+ * // Returns: [WebSite, Organization]
203
+ */
204
+ export declare function generateHomePageSchemas(options: WebSiteOptions): SchemaOrgThing[];
205
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunki",
3
- "version": "0.7.1",
3
+ "version": "0.8.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",
@@ -63,7 +63,7 @@
63
63
  "@types/nunjucks": "^3.2.6",
64
64
  "@types/sanitize-html": "^2.16.0",
65
65
  "autoprefixer": "^10.4.21",
66
- "bun-types": "^1.3.1",
66
+ "bun-types": "^1.3.2",
67
67
  "husky": "^9.1.7",
68
68
  "lint-staged": "^16.2.6",
69
69
  "prettier": "^3.6.2",
@@ -84,7 +84,7 @@
84
84
  "README.md"
85
85
  ],
86
86
  "engines": {
87
- "bun": ">=1.3.0",
87
+ "bun": ">=1.3.2",
88
88
  "node": ">=18.0.0"
89
89
  },
90
90
  "engineStrict": true,