bunki 0.14.1 → 0.15.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 +42 -1
- package/dist/cli.js +152 -20
- package/dist/index.js +152 -20
- package/dist/utils/markdown-utils.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,6 +96,34 @@ performance = "Performance optimization and speed"
|
|
|
96
96
|
web = "Web development and technology"
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
### Business Location Data
|
|
100
|
+
|
|
101
|
+
Add structured business/location data with automatic validation:
|
|
102
|
+
|
|
103
|
+
```markdown
|
|
104
|
+
---
|
|
105
|
+
title: "Restaurant Review"
|
|
106
|
+
date: 2025-01-15T09:00:00-07:00
|
|
107
|
+
tags: [food, review]
|
|
108
|
+
business:
|
|
109
|
+
- type: Restaurant
|
|
110
|
+
name: "Blue Bottle Coffee"
|
|
111
|
+
address: "123 Main St, San Francisco, CA 94102"
|
|
112
|
+
lat: 37.7749
|
|
113
|
+
lng: -122.4194
|
|
114
|
+
---
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Required fields:** `type`, `name`, `lat`, `lng`
|
|
118
|
+
**Optional fields:** `address`, `cuisine`, `priceRange`, `telephone`, `url`, `openingHours`
|
|
119
|
+
|
|
120
|
+
The validator enforces:
|
|
121
|
+
- Use `business:` (not deprecated `location:`)
|
|
122
|
+
- Use `lat:`/`lng:` (not deprecated `latitude:`/`longitude:`)
|
|
123
|
+
- All required fields must be present
|
|
124
|
+
|
|
125
|
+
Validation runs automatically during `bunki generate` and `bunki validate`.
|
|
126
|
+
|
|
99
127
|
## CSS & Tailwind
|
|
100
128
|
|
|
101
129
|
To use Tailwind CSS:
|
|
@@ -718,6 +746,7 @@ export S3_PUBLIC_URL="https://img.example.com"
|
|
|
718
746
|
bunki init [--config FILE] # Initialize new site
|
|
719
747
|
bunki new <TITLE> [--tags TAG1,TAG2] # Create new post
|
|
720
748
|
bunki generate [--config FILE] # Build static site
|
|
749
|
+
bunki validate [--config FILE] # Validate frontmatter
|
|
721
750
|
bunki serve [--port 3000] # Start dev server
|
|
722
751
|
bunki css [--watch] # Process CSS
|
|
723
752
|
bunki images:push [--domain DOMAIN] # Upload images to cloud
|
|
@@ -744,6 +773,7 @@ dist/
|
|
|
744
773
|
## Features
|
|
745
774
|
|
|
746
775
|
- **Markdown Processing**: Frontmatter extraction, code highlighting, HTML sanitization
|
|
776
|
+
- **Frontmatter Validation**: Automatic validation of business location data with clear error messages
|
|
747
777
|
- **Security**: XSS protection, sanitized HTML, link hardening
|
|
748
778
|
- **Performance**: Static files, optional gzip, optimized output
|
|
749
779
|
- **Templating**: Nunjucks with custom filters and macros
|
|
@@ -789,7 +819,18 @@ bunki/
|
|
|
789
819
|
|
|
790
820
|
## Changelog
|
|
791
821
|
|
|
792
|
-
### v0.
|
|
822
|
+
### v0.15.0 (Current)
|
|
823
|
+
|
|
824
|
+
- **Frontmatter Validation**: Automatic validation of business location data
|
|
825
|
+
- Enforces `business:` field (rejects deprecated `location:`)
|
|
826
|
+
- Enforces `lat:`/`lng:` coordinates (rejects deprecated `latitude:`/`longitude:`)
|
|
827
|
+
- Validates required fields (type, name, lat, lng)
|
|
828
|
+
- Clear error messages with suggestions for fixes
|
|
829
|
+
- New `bunki validate` command for standalone validation
|
|
830
|
+
- **Enhanced Testing**: 47 tests for markdown parsing and validation
|
|
831
|
+
- **Breaking Change**: Deprecated `location:`, `latitude:`, and `longitude:` fields now rejected
|
|
832
|
+
|
|
833
|
+
### v0.8.0
|
|
793
834
|
|
|
794
835
|
- **JSON-LD Structured Data**: Automatic Schema.org markup generation for enhanced SEO
|
|
795
836
|
- BlogPosting schema for individual blog posts with author, keywords, images
|
package/dist/cli.js
CHANGED
|
@@ -33060,6 +33060,92 @@ function convertMarkdownToHtml(markdownContent) {
|
|
|
33060
33060
|
sanitized = sanitized.replace(/javascript:/gi, "").replace(/vbscript:/gi, "");
|
|
33061
33061
|
return sanitized;
|
|
33062
33062
|
}
|
|
33063
|
+
function validateBusinessLocation(business, filePath) {
|
|
33064
|
+
if (!business)
|
|
33065
|
+
return null;
|
|
33066
|
+
const locations = Array.isArray(business) ? business : [business];
|
|
33067
|
+
for (let i = 0;i < locations.length; i++) {
|
|
33068
|
+
const loc = locations[i];
|
|
33069
|
+
const locIndex = locations.length > 1 ? ` (location ${i + 1})` : "";
|
|
33070
|
+
if (!loc.type) {
|
|
33071
|
+
return {
|
|
33072
|
+
file: filePath,
|
|
33073
|
+
type: "validation",
|
|
33074
|
+
message: `Missing required field 'type' in business${locIndex}`,
|
|
33075
|
+
suggestion: "Add 'type: Restaurant' (or Market, Park, Hotel, Museum, Cafe, Zoo, etc.) to frontmatter"
|
|
33076
|
+
};
|
|
33077
|
+
}
|
|
33078
|
+
const validTypes = [
|
|
33079
|
+
"Accommodation",
|
|
33080
|
+
"Apartment",
|
|
33081
|
+
"Attraction",
|
|
33082
|
+
"Beach",
|
|
33083
|
+
"BodyOfWater",
|
|
33084
|
+
"Bridge",
|
|
33085
|
+
"Building",
|
|
33086
|
+
"BusStation",
|
|
33087
|
+
"Cafe",
|
|
33088
|
+
"Campground",
|
|
33089
|
+
"CivicStructure",
|
|
33090
|
+
"EventVenue",
|
|
33091
|
+
"Ferry",
|
|
33092
|
+
"Garden",
|
|
33093
|
+
"HistoricalSite",
|
|
33094
|
+
"Hotel",
|
|
33095
|
+
"Hostel",
|
|
33096
|
+
"Landmark",
|
|
33097
|
+
"LodgingBusiness",
|
|
33098
|
+
"Market",
|
|
33099
|
+
"Monument",
|
|
33100
|
+
"Museum",
|
|
33101
|
+
"NaturalFeature",
|
|
33102
|
+
"Park",
|
|
33103
|
+
"Playground",
|
|
33104
|
+
"Restaurant",
|
|
33105
|
+
"ServiceCenter",
|
|
33106
|
+
"ShoppingCenter",
|
|
33107
|
+
"Store",
|
|
33108
|
+
"TouristAttraction",
|
|
33109
|
+
"TrainStation",
|
|
33110
|
+
"Viewpoint",
|
|
33111
|
+
"Zoo"
|
|
33112
|
+
];
|
|
33113
|
+
if (!validTypes.includes(loc.type)) {
|
|
33114
|
+
return {
|
|
33115
|
+
file: filePath,
|
|
33116
|
+
type: "validation",
|
|
33117
|
+
message: `Invalid business type '${loc.type}' in business${locIndex}`,
|
|
33118
|
+
suggestion: `Use a valid Schema.org Place type: ${validTypes.slice(0, 10).join(", ")}, etc.`
|
|
33119
|
+
};
|
|
33120
|
+
}
|
|
33121
|
+
if (!loc.name) {
|
|
33122
|
+
return {
|
|
33123
|
+
file: filePath,
|
|
33124
|
+
type: "validation",
|
|
33125
|
+
message: `Missing required field 'name' in business${locIndex}`,
|
|
33126
|
+
suggestion: `Add 'name: "Full Business Name"' to frontmatter`
|
|
33127
|
+
};
|
|
33128
|
+
}
|
|
33129
|
+
if (loc.latitude !== undefined || loc.longitude !== undefined) {
|
|
33130
|
+
return {
|
|
33131
|
+
file: filePath,
|
|
33132
|
+
type: "validation",
|
|
33133
|
+
message: `Use 'lat' and 'lng' instead of 'latitude' and 'longitude' in business${locIndex}`,
|
|
33134
|
+
suggestion: "Replace 'latitude:' with 'lat:' and 'longitude:' with 'lng:' in frontmatter"
|
|
33135
|
+
};
|
|
33136
|
+
}
|
|
33137
|
+
const hasLatLng = loc.lat !== undefined && loc.lng !== undefined;
|
|
33138
|
+
if (!hasLatLng) {
|
|
33139
|
+
return {
|
|
33140
|
+
file: filePath,
|
|
33141
|
+
type: "validation",
|
|
33142
|
+
message: `Missing required coordinates in business${locIndex}`,
|
|
33143
|
+
suggestion: "Add 'lat: 47.6062' and 'lng: -122.3321' with numeric coordinates to frontmatter (REQUIRED)"
|
|
33144
|
+
};
|
|
33145
|
+
}
|
|
33146
|
+
}
|
|
33147
|
+
return null;
|
|
33148
|
+
}
|
|
33063
33149
|
async function parseMarkdownFile(filePath) {
|
|
33064
33150
|
try {
|
|
33065
33151
|
const fileContent = await readFileAsText(filePath);
|
|
@@ -33090,6 +33176,26 @@ async function parseMarkdownFile(filePath) {
|
|
|
33090
33176
|
}
|
|
33091
33177
|
};
|
|
33092
33178
|
}
|
|
33179
|
+
if (data.location) {
|
|
33180
|
+
return {
|
|
33181
|
+
post: null,
|
|
33182
|
+
error: {
|
|
33183
|
+
file: filePath,
|
|
33184
|
+
type: "validation",
|
|
33185
|
+
message: "Use 'business:' instead of deprecated 'location:' field",
|
|
33186
|
+
suggestion: "Replace 'location:' with 'business:' in frontmatter (business requires type, name, lat, lng)"
|
|
33187
|
+
}
|
|
33188
|
+
};
|
|
33189
|
+
}
|
|
33190
|
+
if (data.business) {
|
|
33191
|
+
const validationError = validateBusinessLocation(data.business, filePath);
|
|
33192
|
+
if (validationError) {
|
|
33193
|
+
return {
|
|
33194
|
+
post: null,
|
|
33195
|
+
error: validationError
|
|
33196
|
+
};
|
|
33197
|
+
}
|
|
33198
|
+
}
|
|
33093
33199
|
let slug = data.slug || getBaseFilename(filePath);
|
|
33094
33200
|
const sanitizedHtml = convertMarkdownToHtml(content);
|
|
33095
33201
|
const pacificDate = toPacificTime(data.date);
|
|
@@ -33104,17 +33210,6 @@ async function parseMarkdownFile(filePath) {
|
|
|
33104
33210
|
url: `/${postYear}/${slug}/`,
|
|
33105
33211
|
excerpt: data.excerpt || extractExcerpt(content),
|
|
33106
33212
|
html: sanitizedHtml,
|
|
33107
|
-
...data.location && {
|
|
33108
|
-
location: (() => {
|
|
33109
|
-
const loc = Array.isArray(data.location) ? data.location[0] : data.location;
|
|
33110
|
-
return {
|
|
33111
|
-
name: loc.name,
|
|
33112
|
-
address: loc.address,
|
|
33113
|
-
lat: loc.lat || loc.latitude,
|
|
33114
|
-
lng: loc.lng || loc.longitude
|
|
33115
|
-
};
|
|
33116
|
-
})()
|
|
33117
|
-
},
|
|
33118
33213
|
...data.category && { category: data.category },
|
|
33119
33214
|
...data.business && {
|
|
33120
33215
|
business: (() => {
|
|
@@ -33123,8 +33218,8 @@ async function parseMarkdownFile(filePath) {
|
|
|
33123
33218
|
type: biz.type,
|
|
33124
33219
|
name: biz.name,
|
|
33125
33220
|
address: biz.address,
|
|
33126
|
-
lat: biz.lat
|
|
33127
|
-
lng: biz.lng
|
|
33221
|
+
lat: biz.lat,
|
|
33222
|
+
lng: biz.lng,
|
|
33128
33223
|
...biz.cuisine && { cuisine: biz.cuisine },
|
|
33129
33224
|
...biz.priceRange && {
|
|
33130
33225
|
priceRange: biz.priceRange
|
|
@@ -33185,7 +33280,8 @@ async function parseMarkdownDirectory(contentDir, strictMode = false) {
|
|
|
33185
33280
|
`);
|
|
33186
33281
|
const yamlErrors = errors.filter((e) => e.type === "yaml");
|
|
33187
33282
|
const missingFieldErrors = errors.filter((e) => e.type === "missing_field");
|
|
33188
|
-
const
|
|
33283
|
+
const validationErrors = errors.filter((e) => e.type === "validation");
|
|
33284
|
+
const otherErrors = errors.filter((e) => e.type !== "yaml" && e.type !== "missing_field" && e.type !== "validation");
|
|
33189
33285
|
if (yamlErrors.length > 0) {
|
|
33190
33286
|
console.error(` YAML Parsing Errors (${yamlErrors.length}):`);
|
|
33191
33287
|
yamlErrors.slice(0, 5).forEach((e) => {
|
|
@@ -33209,6 +33305,19 @@ async function parseMarkdownDirectory(contentDir, strictMode = false) {
|
|
|
33209
33305
|
}
|
|
33210
33306
|
console.error("");
|
|
33211
33307
|
}
|
|
33308
|
+
if (validationErrors.length > 0) {
|
|
33309
|
+
console.error(` Validation Errors (${validationErrors.length}):`);
|
|
33310
|
+
validationErrors.slice(0, 5).forEach((e) => {
|
|
33311
|
+
console.error(` \u26A0\uFE0F ${e.file}: ${e.message}`);
|
|
33312
|
+
if (e.suggestion) {
|
|
33313
|
+
console.error(` \uD83D\uDCA1 ${e.suggestion}`);
|
|
33314
|
+
}
|
|
33315
|
+
});
|
|
33316
|
+
if (validationErrors.length > 5) {
|
|
33317
|
+
console.error(` ... and ${validationErrors.length - 5} more`);
|
|
33318
|
+
}
|
|
33319
|
+
console.error("");
|
|
33320
|
+
}
|
|
33212
33321
|
if (otherErrors.length > 0) {
|
|
33213
33322
|
console.error(` Other Errors (${otherErrors.length}):`);
|
|
33214
33323
|
otherErrors.slice(0, 3).forEach((e) => {
|
|
@@ -33222,6 +33331,11 @@ async function parseMarkdownDirectory(contentDir, strictMode = false) {
|
|
|
33222
33331
|
console.error(`\uD83D\uDCDD Tip: Fix YAML errors by quoting titles/descriptions with colons`);
|
|
33223
33332
|
console.error(` Example: title: "My Post: A Guide" (quotes required for colons)
|
|
33224
33333
|
`);
|
|
33334
|
+
if (validationErrors.length > 0) {
|
|
33335
|
+
throw new Error(`\u274C Build failed: ${validationErrors.length} validation error(s) found
|
|
33336
|
+
` + ` Business locations must have: type, name, address, lat, lng
|
|
33337
|
+
` + ` Run 'bunki validate' to see all errors`);
|
|
33338
|
+
}
|
|
33225
33339
|
if (strictMode) {
|
|
33226
33340
|
throw new Error(`Build failed: ${errors.length} parsing error(s) found (strictMode enabled)`);
|
|
33227
33341
|
}
|
|
@@ -33577,14 +33691,23 @@ class SiteGenerator {
|
|
|
33577
33691
|
const pagination = this.createPagination(yearPosts, page, pageSize, `/${year}/`);
|
|
33578
33692
|
let jsonLd = "";
|
|
33579
33693
|
if (page === 1) {
|
|
33580
|
-
const
|
|
33694
|
+
const schemas = [];
|
|
33695
|
+
schemas.push(generateCollectionPageSchema({
|
|
33581
33696
|
title: `Posts from ${year}`,
|
|
33582
33697
|
description: `Articles published in ${year}`,
|
|
33583
33698
|
url: `${this.options.config.baseUrl}/${year}/`,
|
|
33584
33699
|
posts: yearPosts,
|
|
33585
33700
|
site: this.options.config
|
|
33586
|
-
});
|
|
33587
|
-
|
|
33701
|
+
}));
|
|
33702
|
+
schemas.push(generateBreadcrumbListSchema({
|
|
33703
|
+
site: this.options.config,
|
|
33704
|
+
items: [
|
|
33705
|
+
{ name: "Home", url: `${this.options.config.baseUrl}/` },
|
|
33706
|
+
{ name: year, url: `${this.options.config.baseUrl}/${year}/` }
|
|
33707
|
+
]
|
|
33708
|
+
}));
|
|
33709
|
+
jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
|
|
33710
|
+
`);
|
|
33588
33711
|
}
|
|
33589
33712
|
const yearPageHtml = import_nunjucks.default.render("archive.njk", {
|
|
33590
33713
|
site: this.options.config,
|
|
@@ -33683,15 +33806,24 @@ class SiteGenerator {
|
|
|
33683
33806
|
const pagination = this.createPagination(tagData.posts, page, pageSize, `/tags/${tagData.slug}/`);
|
|
33684
33807
|
let jsonLd = "";
|
|
33685
33808
|
if (page === 1) {
|
|
33809
|
+
const schemas = [];
|
|
33686
33810
|
const description = tagData.description || `Articles tagged with ${tagName}`;
|
|
33687
|
-
|
|
33811
|
+
schemas.push(generateCollectionPageSchema({
|
|
33688
33812
|
title: `${tagName}`,
|
|
33689
33813
|
description,
|
|
33690
33814
|
url: `${this.options.config.baseUrl}/tags/${tagData.slug}/`,
|
|
33691
33815
|
posts: tagData.posts,
|
|
33692
33816
|
site: this.options.config
|
|
33693
|
-
});
|
|
33694
|
-
|
|
33817
|
+
}));
|
|
33818
|
+
schemas.push(generateBreadcrumbListSchema({
|
|
33819
|
+
site: this.options.config,
|
|
33820
|
+
items: [
|
|
33821
|
+
{ name: "Home", url: `${this.options.config.baseUrl}/` },
|
|
33822
|
+
{ name: tagName, url: `${this.options.config.baseUrl}/tags/${tagData.slug}/` }
|
|
33823
|
+
]
|
|
33824
|
+
}));
|
|
33825
|
+
jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
|
|
33826
|
+
`);
|
|
33695
33827
|
}
|
|
33696
33828
|
const tagPageHtml = import_nunjucks.default.render("tag.njk", {
|
|
33697
33829
|
site: this.options.config,
|
package/dist/index.js
CHANGED
|
@@ -30678,6 +30678,92 @@ function convertMarkdownToHtml(markdownContent) {
|
|
|
30678
30678
|
sanitized = sanitized.replace(/javascript:/gi, "").replace(/vbscript:/gi, "");
|
|
30679
30679
|
return sanitized;
|
|
30680
30680
|
}
|
|
30681
|
+
function validateBusinessLocation(business, filePath) {
|
|
30682
|
+
if (!business)
|
|
30683
|
+
return null;
|
|
30684
|
+
const locations = Array.isArray(business) ? business : [business];
|
|
30685
|
+
for (let i = 0;i < locations.length; i++) {
|
|
30686
|
+
const loc = locations[i];
|
|
30687
|
+
const locIndex = locations.length > 1 ? ` (location ${i + 1})` : "";
|
|
30688
|
+
if (!loc.type) {
|
|
30689
|
+
return {
|
|
30690
|
+
file: filePath,
|
|
30691
|
+
type: "validation",
|
|
30692
|
+
message: `Missing required field 'type' in business${locIndex}`,
|
|
30693
|
+
suggestion: "Add 'type: Restaurant' (or Market, Park, Hotel, Museum, Cafe, Zoo, etc.) to frontmatter"
|
|
30694
|
+
};
|
|
30695
|
+
}
|
|
30696
|
+
const validTypes = [
|
|
30697
|
+
"Accommodation",
|
|
30698
|
+
"Apartment",
|
|
30699
|
+
"Attraction",
|
|
30700
|
+
"Beach",
|
|
30701
|
+
"BodyOfWater",
|
|
30702
|
+
"Bridge",
|
|
30703
|
+
"Building",
|
|
30704
|
+
"BusStation",
|
|
30705
|
+
"Cafe",
|
|
30706
|
+
"Campground",
|
|
30707
|
+
"CivicStructure",
|
|
30708
|
+
"EventVenue",
|
|
30709
|
+
"Ferry",
|
|
30710
|
+
"Garden",
|
|
30711
|
+
"HistoricalSite",
|
|
30712
|
+
"Hotel",
|
|
30713
|
+
"Hostel",
|
|
30714
|
+
"Landmark",
|
|
30715
|
+
"LodgingBusiness",
|
|
30716
|
+
"Market",
|
|
30717
|
+
"Monument",
|
|
30718
|
+
"Museum",
|
|
30719
|
+
"NaturalFeature",
|
|
30720
|
+
"Park",
|
|
30721
|
+
"Playground",
|
|
30722
|
+
"Restaurant",
|
|
30723
|
+
"ServiceCenter",
|
|
30724
|
+
"ShoppingCenter",
|
|
30725
|
+
"Store",
|
|
30726
|
+
"TouristAttraction",
|
|
30727
|
+
"TrainStation",
|
|
30728
|
+
"Viewpoint",
|
|
30729
|
+
"Zoo"
|
|
30730
|
+
];
|
|
30731
|
+
if (!validTypes.includes(loc.type)) {
|
|
30732
|
+
return {
|
|
30733
|
+
file: filePath,
|
|
30734
|
+
type: "validation",
|
|
30735
|
+
message: `Invalid business type '${loc.type}' in business${locIndex}`,
|
|
30736
|
+
suggestion: `Use a valid Schema.org Place type: ${validTypes.slice(0, 10).join(", ")}, etc.`
|
|
30737
|
+
};
|
|
30738
|
+
}
|
|
30739
|
+
if (!loc.name) {
|
|
30740
|
+
return {
|
|
30741
|
+
file: filePath,
|
|
30742
|
+
type: "validation",
|
|
30743
|
+
message: `Missing required field 'name' in business${locIndex}`,
|
|
30744
|
+
suggestion: `Add 'name: "Full Business Name"' to frontmatter`
|
|
30745
|
+
};
|
|
30746
|
+
}
|
|
30747
|
+
if (loc.latitude !== undefined || loc.longitude !== undefined) {
|
|
30748
|
+
return {
|
|
30749
|
+
file: filePath,
|
|
30750
|
+
type: "validation",
|
|
30751
|
+
message: `Use 'lat' and 'lng' instead of 'latitude' and 'longitude' in business${locIndex}`,
|
|
30752
|
+
suggestion: "Replace 'latitude:' with 'lat:' and 'longitude:' with 'lng:' in frontmatter"
|
|
30753
|
+
};
|
|
30754
|
+
}
|
|
30755
|
+
const hasLatLng = loc.lat !== undefined && loc.lng !== undefined;
|
|
30756
|
+
if (!hasLatLng) {
|
|
30757
|
+
return {
|
|
30758
|
+
file: filePath,
|
|
30759
|
+
type: "validation",
|
|
30760
|
+
message: `Missing required coordinates in business${locIndex}`,
|
|
30761
|
+
suggestion: "Add 'lat: 47.6062' and 'lng: -122.3321' with numeric coordinates to frontmatter (REQUIRED)"
|
|
30762
|
+
};
|
|
30763
|
+
}
|
|
30764
|
+
}
|
|
30765
|
+
return null;
|
|
30766
|
+
}
|
|
30681
30767
|
async function parseMarkdownFile(filePath) {
|
|
30682
30768
|
try {
|
|
30683
30769
|
const fileContent = await readFileAsText(filePath);
|
|
@@ -30708,6 +30794,26 @@ async function parseMarkdownFile(filePath) {
|
|
|
30708
30794
|
}
|
|
30709
30795
|
};
|
|
30710
30796
|
}
|
|
30797
|
+
if (data.location) {
|
|
30798
|
+
return {
|
|
30799
|
+
post: null,
|
|
30800
|
+
error: {
|
|
30801
|
+
file: filePath,
|
|
30802
|
+
type: "validation",
|
|
30803
|
+
message: "Use 'business:' instead of deprecated 'location:' field",
|
|
30804
|
+
suggestion: "Replace 'location:' with 'business:' in frontmatter (business requires type, name, lat, lng)"
|
|
30805
|
+
}
|
|
30806
|
+
};
|
|
30807
|
+
}
|
|
30808
|
+
if (data.business) {
|
|
30809
|
+
const validationError = validateBusinessLocation(data.business, filePath);
|
|
30810
|
+
if (validationError) {
|
|
30811
|
+
return {
|
|
30812
|
+
post: null,
|
|
30813
|
+
error: validationError
|
|
30814
|
+
};
|
|
30815
|
+
}
|
|
30816
|
+
}
|
|
30711
30817
|
let slug = data.slug || getBaseFilename(filePath);
|
|
30712
30818
|
const sanitizedHtml = convertMarkdownToHtml(content);
|
|
30713
30819
|
const pacificDate = toPacificTime(data.date);
|
|
@@ -30722,17 +30828,6 @@ async function parseMarkdownFile(filePath) {
|
|
|
30722
30828
|
url: `/${postYear}/${slug}/`,
|
|
30723
30829
|
excerpt: data.excerpt || extractExcerpt(content),
|
|
30724
30830
|
html: sanitizedHtml,
|
|
30725
|
-
...data.location && {
|
|
30726
|
-
location: (() => {
|
|
30727
|
-
const loc = Array.isArray(data.location) ? data.location[0] : data.location;
|
|
30728
|
-
return {
|
|
30729
|
-
name: loc.name,
|
|
30730
|
-
address: loc.address,
|
|
30731
|
-
lat: loc.lat || loc.latitude,
|
|
30732
|
-
lng: loc.lng || loc.longitude
|
|
30733
|
-
};
|
|
30734
|
-
})()
|
|
30735
|
-
},
|
|
30736
30831
|
...data.category && { category: data.category },
|
|
30737
30832
|
...data.business && {
|
|
30738
30833
|
business: (() => {
|
|
@@ -30741,8 +30836,8 @@ async function parseMarkdownFile(filePath) {
|
|
|
30741
30836
|
type: biz.type,
|
|
30742
30837
|
name: biz.name,
|
|
30743
30838
|
address: biz.address,
|
|
30744
|
-
lat: biz.lat
|
|
30745
|
-
lng: biz.lng
|
|
30839
|
+
lat: biz.lat,
|
|
30840
|
+
lng: biz.lng,
|
|
30746
30841
|
...biz.cuisine && { cuisine: biz.cuisine },
|
|
30747
30842
|
...biz.priceRange && {
|
|
30748
30843
|
priceRange: biz.priceRange
|
|
@@ -30803,7 +30898,8 @@ async function parseMarkdownDirectory(contentDir, strictMode = false) {
|
|
|
30803
30898
|
`);
|
|
30804
30899
|
const yamlErrors = errors.filter((e) => e.type === "yaml");
|
|
30805
30900
|
const missingFieldErrors = errors.filter((e) => e.type === "missing_field");
|
|
30806
|
-
const
|
|
30901
|
+
const validationErrors = errors.filter((e) => e.type === "validation");
|
|
30902
|
+
const otherErrors = errors.filter((e) => e.type !== "yaml" && e.type !== "missing_field" && e.type !== "validation");
|
|
30807
30903
|
if (yamlErrors.length > 0) {
|
|
30808
30904
|
console.error(` YAML Parsing Errors (${yamlErrors.length}):`);
|
|
30809
30905
|
yamlErrors.slice(0, 5).forEach((e) => {
|
|
@@ -30827,6 +30923,19 @@ async function parseMarkdownDirectory(contentDir, strictMode = false) {
|
|
|
30827
30923
|
}
|
|
30828
30924
|
console.error("");
|
|
30829
30925
|
}
|
|
30926
|
+
if (validationErrors.length > 0) {
|
|
30927
|
+
console.error(` Validation Errors (${validationErrors.length}):`);
|
|
30928
|
+
validationErrors.slice(0, 5).forEach((e) => {
|
|
30929
|
+
console.error(` \u26A0\uFE0F ${e.file}: ${e.message}`);
|
|
30930
|
+
if (e.suggestion) {
|
|
30931
|
+
console.error(` \uD83D\uDCA1 ${e.suggestion}`);
|
|
30932
|
+
}
|
|
30933
|
+
});
|
|
30934
|
+
if (validationErrors.length > 5) {
|
|
30935
|
+
console.error(` ... and ${validationErrors.length - 5} more`);
|
|
30936
|
+
}
|
|
30937
|
+
console.error("");
|
|
30938
|
+
}
|
|
30830
30939
|
if (otherErrors.length > 0) {
|
|
30831
30940
|
console.error(` Other Errors (${otherErrors.length}):`);
|
|
30832
30941
|
otherErrors.slice(0, 3).forEach((e) => {
|
|
@@ -30840,6 +30949,11 @@ async function parseMarkdownDirectory(contentDir, strictMode = false) {
|
|
|
30840
30949
|
console.error(`\uD83D\uDCDD Tip: Fix YAML errors by quoting titles/descriptions with colons`);
|
|
30841
30950
|
console.error(` Example: title: "My Post: A Guide" (quotes required for colons)
|
|
30842
30951
|
`);
|
|
30952
|
+
if (validationErrors.length > 0) {
|
|
30953
|
+
throw new Error(`\u274C Build failed: ${validationErrors.length} validation error(s) found
|
|
30954
|
+
` + ` Business locations must have: type, name, address, lat, lng
|
|
30955
|
+
` + ` Run 'bunki validate' to see all errors`);
|
|
30956
|
+
}
|
|
30843
30957
|
if (strictMode) {
|
|
30844
30958
|
throw new Error(`Build failed: ${errors.length} parsing error(s) found (strictMode enabled)`);
|
|
30845
30959
|
}
|
|
@@ -31524,14 +31638,23 @@ class SiteGenerator {
|
|
|
31524
31638
|
const pagination = this.createPagination(yearPosts, page, pageSize, `/${year}/`);
|
|
31525
31639
|
let jsonLd = "";
|
|
31526
31640
|
if (page === 1) {
|
|
31527
|
-
const
|
|
31641
|
+
const schemas = [];
|
|
31642
|
+
schemas.push(generateCollectionPageSchema({
|
|
31528
31643
|
title: `Posts from ${year}`,
|
|
31529
31644
|
description: `Articles published in ${year}`,
|
|
31530
31645
|
url: `${this.options.config.baseUrl}/${year}/`,
|
|
31531
31646
|
posts: yearPosts,
|
|
31532
31647
|
site: this.options.config
|
|
31533
|
-
});
|
|
31534
|
-
|
|
31648
|
+
}));
|
|
31649
|
+
schemas.push(generateBreadcrumbListSchema({
|
|
31650
|
+
site: this.options.config,
|
|
31651
|
+
items: [
|
|
31652
|
+
{ name: "Home", url: `${this.options.config.baseUrl}/` },
|
|
31653
|
+
{ name: year, url: `${this.options.config.baseUrl}/${year}/` }
|
|
31654
|
+
]
|
|
31655
|
+
}));
|
|
31656
|
+
jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
|
|
31657
|
+
`);
|
|
31535
31658
|
}
|
|
31536
31659
|
const yearPageHtml = import_nunjucks.default.render("archive.njk", {
|
|
31537
31660
|
site: this.options.config,
|
|
@@ -31630,15 +31753,24 @@ class SiteGenerator {
|
|
|
31630
31753
|
const pagination = this.createPagination(tagData.posts, page, pageSize, `/tags/${tagData.slug}/`);
|
|
31631
31754
|
let jsonLd = "";
|
|
31632
31755
|
if (page === 1) {
|
|
31756
|
+
const schemas = [];
|
|
31633
31757
|
const description = tagData.description || `Articles tagged with ${tagName}`;
|
|
31634
|
-
|
|
31758
|
+
schemas.push(generateCollectionPageSchema({
|
|
31635
31759
|
title: `${tagName}`,
|
|
31636
31760
|
description,
|
|
31637
31761
|
url: `${this.options.config.baseUrl}/tags/${tagData.slug}/`,
|
|
31638
31762
|
posts: tagData.posts,
|
|
31639
31763
|
site: this.options.config
|
|
31640
|
-
});
|
|
31641
|
-
|
|
31764
|
+
}));
|
|
31765
|
+
schemas.push(generateBreadcrumbListSchema({
|
|
31766
|
+
site: this.options.config,
|
|
31767
|
+
items: [
|
|
31768
|
+
{ name: "Home", url: `${this.options.config.baseUrl}/` },
|
|
31769
|
+
{ name: tagName, url: `${this.options.config.baseUrl}/tags/${tagData.slug}/` }
|
|
31770
|
+
]
|
|
31771
|
+
}));
|
|
31772
|
+
jsonLd = schemas.map((schema) => toScriptTag(schema)).join(`
|
|
31773
|
+
`);
|
|
31642
31774
|
}
|
|
31643
31775
|
const tagPageHtml = import_nunjucks.default.render("tag.njk", {
|
|
31644
31776
|
site: this.options.config,
|
|
@@ -4,7 +4,7 @@ export declare function extractExcerpt(content: string, maxLength?: number): str
|
|
|
4
4
|
export declare function convertMarkdownToHtml(markdownContent: string): string;
|
|
5
5
|
export interface ParseError {
|
|
6
6
|
file: string;
|
|
7
|
-
type: "yaml" | "missing_field" | "file_not_found" | "unknown";
|
|
7
|
+
type: "yaml" | "missing_field" | "file_not_found" | "unknown" | "validation";
|
|
8
8
|
message: string;
|
|
9
9
|
suggestion?: string;
|
|
10
10
|
}
|
package/package.json
CHANGED