bunki 0.14.0 → 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 +171 -33
- package/dist/index.js +171 -33
- 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,34 +33210,29 @@ 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
|
-
name: data.location.name,
|
|
33110
|
-
address: data.location.address,
|
|
33111
|
-
lat: data.location.lat,
|
|
33112
|
-
lng: data.location.lng
|
|
33113
|
-
}
|
|
33114
|
-
},
|
|
33115
33213
|
...data.category && { category: data.category },
|
|
33116
33214
|
...data.business && {
|
|
33117
|
-
business: {
|
|
33118
|
-
|
|
33119
|
-
|
|
33120
|
-
|
|
33121
|
-
|
|
33122
|
-
|
|
33123
|
-
|
|
33124
|
-
|
|
33125
|
-
|
|
33126
|
-
|
|
33127
|
-
|
|
33128
|
-
|
|
33129
|
-
|
|
33130
|
-
|
|
33131
|
-
|
|
33132
|
-
|
|
33133
|
-
|
|
33134
|
-
|
|
33215
|
+
business: (() => {
|
|
33216
|
+
const biz = Array.isArray(data.business) ? data.business[0] : data.business;
|
|
33217
|
+
return {
|
|
33218
|
+
type: biz.type,
|
|
33219
|
+
name: biz.name,
|
|
33220
|
+
address: biz.address,
|
|
33221
|
+
lat: biz.lat,
|
|
33222
|
+
lng: biz.lng,
|
|
33223
|
+
...biz.cuisine && { cuisine: biz.cuisine },
|
|
33224
|
+
...biz.priceRange && {
|
|
33225
|
+
priceRange: biz.priceRange
|
|
33226
|
+
},
|
|
33227
|
+
...biz.telephone && {
|
|
33228
|
+
telephone: biz.telephone
|
|
33229
|
+
},
|
|
33230
|
+
...biz.url && { url: biz.url },
|
|
33231
|
+
...biz.openingHours && {
|
|
33232
|
+
openingHours: biz.openingHours
|
|
33233
|
+
}
|
|
33234
|
+
};
|
|
33235
|
+
})()
|
|
33135
33236
|
}
|
|
33136
33237
|
};
|
|
33137
33238
|
return { post, error: null };
|
|
@@ -33179,7 +33280,8 @@ async function parseMarkdownDirectory(contentDir, strictMode = false) {
|
|
|
33179
33280
|
`);
|
|
33180
33281
|
const yamlErrors = errors.filter((e) => e.type === "yaml");
|
|
33181
33282
|
const missingFieldErrors = errors.filter((e) => e.type === "missing_field");
|
|
33182
|
-
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");
|
|
33183
33285
|
if (yamlErrors.length > 0) {
|
|
33184
33286
|
console.error(` YAML Parsing Errors (${yamlErrors.length}):`);
|
|
33185
33287
|
yamlErrors.slice(0, 5).forEach((e) => {
|
|
@@ -33203,6 +33305,19 @@ async function parseMarkdownDirectory(contentDir, strictMode = false) {
|
|
|
33203
33305
|
}
|
|
33204
33306
|
console.error("");
|
|
33205
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
|
+
}
|
|
33206
33321
|
if (otherErrors.length > 0) {
|
|
33207
33322
|
console.error(` Other Errors (${otherErrors.length}):`);
|
|
33208
33323
|
otherErrors.slice(0, 3).forEach((e) => {
|
|
@@ -33216,6 +33331,11 @@ async function parseMarkdownDirectory(contentDir, strictMode = false) {
|
|
|
33216
33331
|
console.error(`\uD83D\uDCDD Tip: Fix YAML errors by quoting titles/descriptions with colons`);
|
|
33217
33332
|
console.error(` Example: title: "My Post: A Guide" (quotes required for colons)
|
|
33218
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
|
+
}
|
|
33219
33339
|
if (strictMode) {
|
|
33220
33340
|
throw new Error(`Build failed: ${errors.length} parsing error(s) found (strictMode enabled)`);
|
|
33221
33341
|
}
|
|
@@ -33571,14 +33691,23 @@ class SiteGenerator {
|
|
|
33571
33691
|
const pagination = this.createPagination(yearPosts, page, pageSize, `/${year}/`);
|
|
33572
33692
|
let jsonLd = "";
|
|
33573
33693
|
if (page === 1) {
|
|
33574
|
-
const
|
|
33694
|
+
const schemas = [];
|
|
33695
|
+
schemas.push(generateCollectionPageSchema({
|
|
33575
33696
|
title: `Posts from ${year}`,
|
|
33576
33697
|
description: `Articles published in ${year}`,
|
|
33577
33698
|
url: `${this.options.config.baseUrl}/${year}/`,
|
|
33578
33699
|
posts: yearPosts,
|
|
33579
33700
|
site: this.options.config
|
|
33580
|
-
});
|
|
33581
|
-
|
|
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
|
+
`);
|
|
33582
33711
|
}
|
|
33583
33712
|
const yearPageHtml = import_nunjucks.default.render("archive.njk", {
|
|
33584
33713
|
site: this.options.config,
|
|
@@ -33677,15 +33806,24 @@ class SiteGenerator {
|
|
|
33677
33806
|
const pagination = this.createPagination(tagData.posts, page, pageSize, `/tags/${tagData.slug}/`);
|
|
33678
33807
|
let jsonLd = "";
|
|
33679
33808
|
if (page === 1) {
|
|
33809
|
+
const schemas = [];
|
|
33680
33810
|
const description = tagData.description || `Articles tagged with ${tagName}`;
|
|
33681
|
-
|
|
33811
|
+
schemas.push(generateCollectionPageSchema({
|
|
33682
33812
|
title: `${tagName}`,
|
|
33683
33813
|
description,
|
|
33684
33814
|
url: `${this.options.config.baseUrl}/tags/${tagData.slug}/`,
|
|
33685
33815
|
posts: tagData.posts,
|
|
33686
33816
|
site: this.options.config
|
|
33687
|
-
});
|
|
33688
|
-
|
|
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
|
+
`);
|
|
33689
33827
|
}
|
|
33690
33828
|
const tagPageHtml = import_nunjucks.default.render("tag.njk", {
|
|
33691
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,34 +30828,29 @@ 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
|
-
name: data.location.name,
|
|
30728
|
-
address: data.location.address,
|
|
30729
|
-
lat: data.location.lat,
|
|
30730
|
-
lng: data.location.lng
|
|
30731
|
-
}
|
|
30732
|
-
},
|
|
30733
30831
|
...data.category && { category: data.category },
|
|
30734
30832
|
...data.business && {
|
|
30735
|
-
business: {
|
|
30736
|
-
|
|
30737
|
-
|
|
30738
|
-
|
|
30739
|
-
|
|
30740
|
-
|
|
30741
|
-
|
|
30742
|
-
|
|
30743
|
-
|
|
30744
|
-
|
|
30745
|
-
|
|
30746
|
-
|
|
30747
|
-
|
|
30748
|
-
|
|
30749
|
-
|
|
30750
|
-
|
|
30751
|
-
|
|
30752
|
-
|
|
30833
|
+
business: (() => {
|
|
30834
|
+
const biz = Array.isArray(data.business) ? data.business[0] : data.business;
|
|
30835
|
+
return {
|
|
30836
|
+
type: biz.type,
|
|
30837
|
+
name: biz.name,
|
|
30838
|
+
address: biz.address,
|
|
30839
|
+
lat: biz.lat,
|
|
30840
|
+
lng: biz.lng,
|
|
30841
|
+
...biz.cuisine && { cuisine: biz.cuisine },
|
|
30842
|
+
...biz.priceRange && {
|
|
30843
|
+
priceRange: biz.priceRange
|
|
30844
|
+
},
|
|
30845
|
+
...biz.telephone && {
|
|
30846
|
+
telephone: biz.telephone
|
|
30847
|
+
},
|
|
30848
|
+
...biz.url && { url: biz.url },
|
|
30849
|
+
...biz.openingHours && {
|
|
30850
|
+
openingHours: biz.openingHours
|
|
30851
|
+
}
|
|
30852
|
+
};
|
|
30853
|
+
})()
|
|
30753
30854
|
}
|
|
30754
30855
|
};
|
|
30755
30856
|
return { post, error: null };
|
|
@@ -30797,7 +30898,8 @@ async function parseMarkdownDirectory(contentDir, strictMode = false) {
|
|
|
30797
30898
|
`);
|
|
30798
30899
|
const yamlErrors = errors.filter((e) => e.type === "yaml");
|
|
30799
30900
|
const missingFieldErrors = errors.filter((e) => e.type === "missing_field");
|
|
30800
|
-
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");
|
|
30801
30903
|
if (yamlErrors.length > 0) {
|
|
30802
30904
|
console.error(` YAML Parsing Errors (${yamlErrors.length}):`);
|
|
30803
30905
|
yamlErrors.slice(0, 5).forEach((e) => {
|
|
@@ -30821,6 +30923,19 @@ async function parseMarkdownDirectory(contentDir, strictMode = false) {
|
|
|
30821
30923
|
}
|
|
30822
30924
|
console.error("");
|
|
30823
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
|
+
}
|
|
30824
30939
|
if (otherErrors.length > 0) {
|
|
30825
30940
|
console.error(` Other Errors (${otherErrors.length}):`);
|
|
30826
30941
|
otherErrors.slice(0, 3).forEach((e) => {
|
|
@@ -30834,6 +30949,11 @@ async function parseMarkdownDirectory(contentDir, strictMode = false) {
|
|
|
30834
30949
|
console.error(`\uD83D\uDCDD Tip: Fix YAML errors by quoting titles/descriptions with colons`);
|
|
30835
30950
|
console.error(` Example: title: "My Post: A Guide" (quotes required for colons)
|
|
30836
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
|
+
}
|
|
30837
30957
|
if (strictMode) {
|
|
30838
30958
|
throw new Error(`Build failed: ${errors.length} parsing error(s) found (strictMode enabled)`);
|
|
30839
30959
|
}
|
|
@@ -31518,14 +31638,23 @@ class SiteGenerator {
|
|
|
31518
31638
|
const pagination = this.createPagination(yearPosts, page, pageSize, `/${year}/`);
|
|
31519
31639
|
let jsonLd = "";
|
|
31520
31640
|
if (page === 1) {
|
|
31521
|
-
const
|
|
31641
|
+
const schemas = [];
|
|
31642
|
+
schemas.push(generateCollectionPageSchema({
|
|
31522
31643
|
title: `Posts from ${year}`,
|
|
31523
31644
|
description: `Articles published in ${year}`,
|
|
31524
31645
|
url: `${this.options.config.baseUrl}/${year}/`,
|
|
31525
31646
|
posts: yearPosts,
|
|
31526
31647
|
site: this.options.config
|
|
31527
|
-
});
|
|
31528
|
-
|
|
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
|
+
`);
|
|
31529
31658
|
}
|
|
31530
31659
|
const yearPageHtml = import_nunjucks.default.render("archive.njk", {
|
|
31531
31660
|
site: this.options.config,
|
|
@@ -31624,15 +31753,24 @@ class SiteGenerator {
|
|
|
31624
31753
|
const pagination = this.createPagination(tagData.posts, page, pageSize, `/tags/${tagData.slug}/`);
|
|
31625
31754
|
let jsonLd = "";
|
|
31626
31755
|
if (page === 1) {
|
|
31756
|
+
const schemas = [];
|
|
31627
31757
|
const description = tagData.description || `Articles tagged with ${tagName}`;
|
|
31628
|
-
|
|
31758
|
+
schemas.push(generateCollectionPageSchema({
|
|
31629
31759
|
title: `${tagName}`,
|
|
31630
31760
|
description,
|
|
31631
31761
|
url: `${this.options.config.baseUrl}/tags/${tagData.slug}/`,
|
|
31632
31762
|
posts: tagData.posts,
|
|
31633
31763
|
site: this.options.config
|
|
31634
|
-
});
|
|
31635
|
-
|
|
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
|
+
`);
|
|
31636
31774
|
}
|
|
31637
31775
|
const tagPageHtml = import_nunjucks.default.render("tag.njk", {
|
|
31638
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