@ibalzam/codejitsu-core 0.1.0 → 0.2.1
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/CLAUDE.md +55 -39
- package/MIGRATIONS/0.2.0.md +166 -0
- package/README.md +25 -5
- package/checklist/bin/run.mjs +97 -55
- package/modules/blog/CLAUDE.md +105 -52
- package/modules/blog/src/collection.ts +176 -0
- package/modules/blog/src/fs.ts +167 -0
- package/modules/blog/src/index.ts +5 -201
- package/modules/blog/src/types.ts +71 -0
- package/modules/blog/templates/content.config.ts +27 -0
- package/modules/blog/templates/lib/blog-fs.ts +14 -0
- package/modules/blog/templates/lib/blog.ts +11 -3
- package/modules/config/CLAUDE.md +121 -0
- package/modules/config/src/define.mjs +14 -0
- package/modules/config/src/index.ts +5 -0
- package/modules/config/src/load.mjs +92 -0
- package/modules/config/src/types.ts +203 -0
- package/modules/images/CLAUDE.md +56 -39
- package/modules/images/bin/optimize.mjs +42 -34
- package/modules/images/checklist.md +15 -7
- package/modules/images/src/auto-blog.mjs +112 -0
- package/modules/images/src/index.ts +3 -18
- package/modules/images/src/optimize.mjs +7 -9
- package/modules/llms/CLAUDE.md +121 -28
- package/modules/llms/bin/generate.mjs +13 -23
- package/modules/llms/checklist.md +7 -6
- package/modules/llms/src/generate.mjs +374 -108
- package/modules/seo/CLAUDE.md +65 -21
- package/modules/seo/templates/Head.astro +99 -27
- package/package.json +11 -1
- package/src/index.ts +1 -1
- package/modules/images/templates/codejitsu-images.config.mjs +0 -18
- package/modules/llms/templates/codejitsu-llms.config.mjs +0 -39
|
@@ -3,121 +3,122 @@ import path from 'path';
|
|
|
3
3
|
import matter from 'gray-matter';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* @param {object} config
|
|
9
|
-
* @param {string} config.siteUrl e.g. 'https://acme.com'
|
|
10
|
-
* @param {string} config.siteName
|
|
11
|
-
* @param {string} config.tagline Short one-line description.
|
|
12
|
-
* @param {string} config.about Longer "About" paragraph (used in concise file).
|
|
13
|
-
* @param {string} [config.aboutFull] Full "About" content (used in full file; falls back to `about`).
|
|
14
|
-
* @param {Section[]} [config.sections]
|
|
15
|
-
* @param {string} [config.aiGuidance] "For AI Assistants" block content.
|
|
16
|
-
* @param {string} [config.blogDir] If set, auto-includes recent blog posts.
|
|
17
|
-
* @param {number} [config.blogLimit=10] How many recent posts to include in concise file.
|
|
18
|
-
* @param {number} [config.blogFullLimit=20] How many in full file.
|
|
19
|
-
* @param {string} config.outDir Where to write the files (typically the site's `public/`).
|
|
20
|
-
*
|
|
21
|
-
* @typedef {object} Section
|
|
22
|
-
* @property {string} title
|
|
23
|
-
* @property {string} [description] Short intro for the full file.
|
|
24
|
-
* @property {SectionItem[]} items
|
|
25
|
-
*
|
|
26
|
-
* @typedef {object} SectionItem
|
|
27
|
-
* @property {string} title
|
|
28
|
-
* @property {string} description
|
|
29
|
-
* @property {string} url Relative or absolute.
|
|
30
|
-
* @property {string} [fullDescription] Longer text for llms-full.txt.
|
|
6
|
+
* Top-level entry — picks 'config' or 'content-scan' mode based on `llms.mode`.
|
|
31
7
|
*/
|
|
32
|
-
export async function generateLlms(config) {
|
|
33
|
-
const {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
tagline,
|
|
37
|
-
about,
|
|
38
|
-
aboutFull,
|
|
39
|
-
sections = [],
|
|
40
|
-
aiGuidance,
|
|
41
|
-
blogDir,
|
|
42
|
-
blogLimit = 10,
|
|
43
|
-
blogFullLimit = 20,
|
|
44
|
-
outDir,
|
|
45
|
-
} = config;
|
|
46
|
-
|
|
47
|
-
if (!outDir) throw new Error('generateLlms: outDir is required.');
|
|
48
|
-
if (!siteUrl) throw new Error('generateLlms: siteUrl is required.');
|
|
8
|
+
export async function generateLlms({ config, cwd, outDir }) {
|
|
9
|
+
const llms = config.llms ?? {};
|
|
10
|
+
const mode = llms.mode ?? 'config';
|
|
11
|
+
|
|
49
12
|
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
|
50
13
|
|
|
51
|
-
|
|
52
|
-
|
|
14
|
+
if (mode === 'content-scan') {
|
|
15
|
+
const out = await generateContentScan({ config, cwd });
|
|
16
|
+
fs.writeFileSync(path.join(outDir, 'llms.txt'), out.concise);
|
|
17
|
+
fs.writeFileSync(path.join(outDir, 'llms-full.txt'), out.full);
|
|
18
|
+
} else {
|
|
19
|
+
const out = generateFromConfig({ config });
|
|
20
|
+
fs.writeFileSync(path.join(outDir, 'llms.txt'), out.concise);
|
|
21
|
+
fs.writeFileSync(path.join(outDir, 'llms-full.txt'), out.full);
|
|
22
|
+
}
|
|
23
|
+
console.log(`✓ ${path.relative(cwd, path.join(outDir, 'llms.txt'))}`);
|
|
24
|
+
console.log(`✓ ${path.relative(cwd, path.join(outDir, 'llms-full.txt'))}`);
|
|
25
|
+
}
|
|
53
26
|
|
|
54
|
-
|
|
55
|
-
siteUrl,
|
|
56
|
-
siteName,
|
|
57
|
-
tagline,
|
|
58
|
-
about,
|
|
59
|
-
sections,
|
|
60
|
-
aiGuidance,
|
|
61
|
-
today,
|
|
62
|
-
blogPosts: blogPosts.slice(0, blogLimit),
|
|
63
|
-
});
|
|
27
|
+
// ─── Common helpers ─────────────────────────────────────────────────────────
|
|
64
28
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
about: aboutFull ?? about,
|
|
70
|
-
sections,
|
|
71
|
-
aiGuidance,
|
|
72
|
-
today,
|
|
73
|
-
blogPosts: blogPosts.slice(0, blogFullLimit),
|
|
74
|
-
});
|
|
29
|
+
function getTodayUTC() {
|
|
30
|
+
const now = new Date();
|
|
31
|
+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
32
|
+
}
|
|
75
33
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
console.log(`✓ wrote ${path.relative(process.cwd(), path.join(outDir, 'llms.txt'))}`);
|
|
79
|
-
console.log(`✓ wrote ${path.relative(process.cwd(), path.join(outDir, 'llms-full.txt'))}`);
|
|
34
|
+
function isoDate() {
|
|
35
|
+
return new Date().toISOString().split('T')[0];
|
|
80
36
|
}
|
|
81
37
|
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
38
|
+
function absoluteUrl(siteUrl, url) {
|
|
39
|
+
if (/^https?:\/\//.test(url)) return url;
|
|
40
|
+
const base = siteUrl.replace(/\/$/, '');
|
|
41
|
+
return `${base}${url.startsWith('/') ? '' : '/'}${url}`;
|
|
42
|
+
}
|
|
87
43
|
|
|
44
|
+
function readBlogPosts(blogDir, dateField = 'date', draftField = null) {
|
|
45
|
+
if (!blogDir || !fs.existsSync(blogDir)) return [];
|
|
46
|
+
const today = getTodayUTC();
|
|
88
47
|
return fs
|
|
89
|
-
.readdirSync(
|
|
48
|
+
.readdirSync(blogDir)
|
|
90
49
|
.filter((n) => n.endsWith('.md'))
|
|
91
50
|
.map((fileName) => {
|
|
92
|
-
const raw = fs.readFileSync(path.join(
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
51
|
+
const raw = fs.readFileSync(path.join(blogDir, fileName), 'utf8');
|
|
52
|
+
const { data } = matter(raw);
|
|
53
|
+
const slug = data.slug || fileName.replace(/\.md$/, '');
|
|
54
|
+
const dateVal = data[dateField];
|
|
55
|
+
const date = dateVal instanceof Date
|
|
56
|
+
? dateVal.toISOString().split('T')[0]
|
|
57
|
+
: (typeof dateVal === 'string' ? dateVal : '');
|
|
58
|
+
return { ...data, slug, date };
|
|
59
|
+
})
|
|
60
|
+
.filter((p) => {
|
|
61
|
+
if (draftField && p[draftField]) return false;
|
|
62
|
+
if (!p.date) return true;
|
|
63
|
+
return new Date(p.date) <= today;
|
|
103
64
|
})
|
|
104
|
-
.filter((p) => p.date && new Date(p.date) <= today)
|
|
105
65
|
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
|
106
66
|
}
|
|
107
67
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
68
|
+
// ─── 'config' mode ──────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
function generateFromConfig({ config }) {
|
|
71
|
+
const site = config.site;
|
|
72
|
+
const llms = config.llms ?? {};
|
|
73
|
+
const blogPosts = llms.blogDir
|
|
74
|
+
? readBlogPosts(path.resolve(process.cwd(), llms.blogDir))
|
|
75
|
+
: [];
|
|
76
|
+
|
|
77
|
+
const concise = renderConcise({
|
|
78
|
+
siteUrl: site.url,
|
|
79
|
+
siteName: site.name,
|
|
80
|
+
tagline: llms.tagline,
|
|
81
|
+
about: llms.about,
|
|
82
|
+
business: site.business,
|
|
83
|
+
sections: llms.sections ?? [],
|
|
84
|
+
aiGuidance: llms.aiGuidance,
|
|
85
|
+
blogPosts: blogPosts.slice(0, llms.blogLimit ?? 10),
|
|
86
|
+
today: isoDate(),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const full = renderFull({
|
|
90
|
+
siteUrl: site.url,
|
|
91
|
+
siteName: site.name,
|
|
92
|
+
tagline: llms.tagline,
|
|
93
|
+
about: llms.aboutFull ?? llms.about,
|
|
94
|
+
business: site.business,
|
|
95
|
+
sections: llms.sections ?? [],
|
|
96
|
+
aiGuidance: llms.aiGuidance,
|
|
97
|
+
blogPosts: blogPosts.slice(0, llms.blogFullLimit ?? 20),
|
|
98
|
+
today: isoDate(),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return { concise, full };
|
|
111
102
|
}
|
|
112
103
|
|
|
113
|
-
function renderConcise({ siteUrl, siteName, tagline, about, sections, aiGuidance,
|
|
104
|
+
function renderConcise({ siteUrl, siteName, tagline, about, business, sections, aiGuidance, blogPosts, today }) {
|
|
114
105
|
let out = `# ${siteName}${tagline ? ` — ${tagline}` : ''}\n`;
|
|
115
106
|
out += `Last Updated: ${today}\n\n`;
|
|
116
|
-
out += `> ${about}\n\n`;
|
|
107
|
+
if (about) out += `> ${about}\n\n`;
|
|
108
|
+
|
|
109
|
+
if (business) {
|
|
110
|
+
out += `## Contact\n\n`;
|
|
111
|
+
if (business.telephone) out += `- Phone: ${business.telephone}\n`;
|
|
112
|
+
if (business.email) out += `- Email: ${business.email}\n`;
|
|
113
|
+
if (business.address) out += `- Address: ${formatAddress(business.address)}\n`;
|
|
114
|
+
out += `- Website: ${siteUrl}\n`;
|
|
115
|
+
if (business.license) out += `- License: ${business.license}\n`;
|
|
116
|
+
out += '\n';
|
|
117
|
+
}
|
|
117
118
|
|
|
118
119
|
for (const section of sections) {
|
|
119
120
|
if (!section.items?.length) continue;
|
|
120
|
-
out += `## ${section.title}\n`;
|
|
121
|
+
out += `## ${section.title}\n\n`;
|
|
121
122
|
for (const item of section.items) {
|
|
122
123
|
out += `- [${item.title}](${absoluteUrl(siteUrl, item.url)}): ${item.description}\n`;
|
|
123
124
|
}
|
|
@@ -125,26 +126,33 @@ function renderConcise({ siteUrl, siteName, tagline, about, sections, aiGuidance
|
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
if (blogPosts.length) {
|
|
128
|
-
out += `## Recent Blog Posts\n`;
|
|
129
|
+
out += `## Recent Blog Posts\n\n`;
|
|
129
130
|
for (const post of blogPosts) {
|
|
130
131
|
out += `- [${post.title}](${siteUrl}/blog/${post.slug}/): ${post.description}\n`;
|
|
131
132
|
}
|
|
132
133
|
out += '\n';
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
if (aiGuidance) {
|
|
136
|
-
out += `## For AI Assistants\n\n${aiGuidance}\n\n`;
|
|
137
|
-
}
|
|
138
|
-
|
|
136
|
+
if (aiGuidance) out += `## For AI Assistants\n\n${aiGuidance}\n\n`;
|
|
139
137
|
out += `---\nGenerated automatically during build\n`;
|
|
140
138
|
return out;
|
|
141
139
|
}
|
|
142
140
|
|
|
143
|
-
function renderFull({ siteUrl, siteName, tagline, about, sections, aiGuidance,
|
|
141
|
+
function renderFull({ siteUrl, siteName, tagline, about, business, sections, aiGuidance, blogPosts, today }) {
|
|
144
142
|
let out = `# ${siteName} — Complete Documentation\n`;
|
|
145
143
|
out += `Last Updated: ${today}\n\n`;
|
|
146
|
-
out += `>
|
|
147
|
-
out +=
|
|
144
|
+
out += `> For a concise navigation overview, see /llms.txt\n\n---\n\n`;
|
|
145
|
+
if (about) out += `# About\n\n${about}\n\n---\n\n`;
|
|
146
|
+
|
|
147
|
+
if (business) {
|
|
148
|
+
out += `# Contact Information\n\n`;
|
|
149
|
+
if (business.telephone) out += `- **Phone:** ${business.telephone}\n`;
|
|
150
|
+
if (business.email) out += `- **Email:** ${business.email}\n`;
|
|
151
|
+
if (business.address) out += `- **Address:** ${formatAddress(business.address)}\n`;
|
|
152
|
+
out += `- **Website:** ${siteUrl}\n`;
|
|
153
|
+
if (business.license) out += `- **License:** ${business.license}\n`;
|
|
154
|
+
out += '\n---\n\n';
|
|
155
|
+
}
|
|
148
156
|
|
|
149
157
|
for (const section of sections) {
|
|
150
158
|
if (!section.items?.length) continue;
|
|
@@ -153,8 +161,7 @@ function renderFull({ siteUrl, siteName, tagline, about, sections, aiGuidance, t
|
|
|
153
161
|
for (const item of section.items) {
|
|
154
162
|
out += `## ${item.title}\n\n`;
|
|
155
163
|
out += `**URL**: ${absoluteUrl(siteUrl, item.url)}\n\n`;
|
|
156
|
-
out += `${item.fullDescription ?? item.description}\n\n`;
|
|
157
|
-
out += `---\n\n`;
|
|
164
|
+
out += `${item.fullDescription ?? item.description}\n\n---\n\n`;
|
|
158
165
|
}
|
|
159
166
|
}
|
|
160
167
|
|
|
@@ -162,18 +169,277 @@ function renderFull({ siteUrl, siteName, tagline, about, sections, aiGuidance, t
|
|
|
162
169
|
out += `# Blog Posts\n\n`;
|
|
163
170
|
for (const post of blogPosts) {
|
|
164
171
|
out += `## ${post.title}\n\n`;
|
|
165
|
-
out += `**Published**: ${post.date}\n`;
|
|
172
|
+
if (post.date) out += `**Published**: ${post.date}\n`;
|
|
166
173
|
if (post.author) out += `**Author**: ${post.author}\n`;
|
|
167
174
|
if (post.tags?.length) out += `**Tags**: ${post.tags.join(', ')}\n`;
|
|
168
|
-
out += `**URL**: ${siteUrl}/blog/${post.slug}/\n\n`;
|
|
169
|
-
out += `${post.description}\n\n---\n\n`;
|
|
175
|
+
out += `**URL**: ${siteUrl}/blog/${post.slug}/\n\n${post.description}\n\n---\n\n`;
|
|
170
176
|
}
|
|
171
177
|
}
|
|
172
178
|
|
|
173
|
-
if (aiGuidance) {
|
|
174
|
-
|
|
179
|
+
if (aiGuidance) out += `# For AI Assistants\n\n${aiGuidance}\n\n`;
|
|
180
|
+
out += `---\nGenerated automatically during build\n`;
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function formatAddress(addr) {
|
|
185
|
+
const parts = [
|
|
186
|
+
addr.streetAddress,
|
|
187
|
+
[addr.addressLocality, addr.addressRegion].filter(Boolean).join(', '),
|
|
188
|
+
addr.postalCode,
|
|
189
|
+
addr.addressCountry,
|
|
190
|
+
].filter(Boolean);
|
|
191
|
+
return parts.join(', ');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ─── 'content-scan' mode ────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
async function generateContentScan({ config, cwd }) {
|
|
197
|
+
const site = config.site;
|
|
198
|
+
const llms = config.llms;
|
|
199
|
+
const scan = llms.contentScan ?? {};
|
|
200
|
+
|
|
201
|
+
const servicesDir = scan.servicesDir ? path.resolve(cwd, scan.servicesDir) : null;
|
|
202
|
+
const locationsDir = scan.locationsDir ? path.resolve(cwd, scan.locationsDir) : null;
|
|
203
|
+
const pagesDir = scan.pagesDir ? path.resolve(cwd, scan.pagesDir) : null;
|
|
204
|
+
const blogDir = llms.blogDir ? path.resolve(cwd, llms.blogDir) : null;
|
|
205
|
+
|
|
206
|
+
const services = readContentDir(servicesDir);
|
|
207
|
+
const locations = readContentDir(locationsDir);
|
|
208
|
+
const blogPosts = readBlogPosts(blogDir, 'pubDate', 'draft').concat(
|
|
209
|
+
// Also try 'date' field for fallback
|
|
210
|
+
blogDir && readBlogPosts(blogDir, 'date', 'draft').filter((p) => !p.pubDate) || []
|
|
211
|
+
);
|
|
212
|
+
const pages = pagesDir ? collectStaticPages(pagesDir) : [];
|
|
213
|
+
|
|
214
|
+
const dynamicRoutes = scan.dynamicRoutes ?? [];
|
|
215
|
+
const expandedUrls = expandRoutes(dynamicRoutes, {
|
|
216
|
+
services: services.map((s) => s.slug),
|
|
217
|
+
locations: locations.map((l) => l.slug),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const concise = renderContentScanConcise({
|
|
221
|
+
siteUrl: site.url,
|
|
222
|
+
siteName: site.name,
|
|
223
|
+
tagline: llms.tagline,
|
|
224
|
+
about: llms.about,
|
|
225
|
+
business: site.business,
|
|
226
|
+
services,
|
|
227
|
+
locations,
|
|
228
|
+
pages: pages.concat(expandedUrls),
|
|
229
|
+
blogPosts: blogPosts.slice(0, llms.blogLimit ?? 10),
|
|
230
|
+
aiGuidance: llms.aiGuidance,
|
|
231
|
+
today: isoDate(),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const full = renderContentScanFull({
|
|
235
|
+
siteUrl: site.url,
|
|
236
|
+
siteName: site.name,
|
|
237
|
+
tagline: llms.tagline,
|
|
238
|
+
about: llms.aboutFull ?? llms.about,
|
|
239
|
+
business: site.business,
|
|
240
|
+
services,
|
|
241
|
+
locations,
|
|
242
|
+
aiGuidance: llms.aiGuidance,
|
|
243
|
+
today: isoDate(),
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return { concise, full };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function readContentDir(dir) {
|
|
250
|
+
if (!dir || !fs.existsSync(dir)) return [];
|
|
251
|
+
const entries = walkMd(dir);
|
|
252
|
+
return entries.map((file) => {
|
|
253
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
254
|
+
const { data, content } = matter(raw);
|
|
255
|
+
const slug = path.basename(file, '.md');
|
|
256
|
+
return { file, slug, data, body: content };
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function walkMd(dir) {
|
|
261
|
+
const out = [];
|
|
262
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
263
|
+
const full = path.join(dir, entry.name);
|
|
264
|
+
if (entry.isDirectory()) out.push(...walkMd(full));
|
|
265
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) out.push(full);
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function collectStaticPages(dir) {
|
|
271
|
+
const out = [];
|
|
272
|
+
function walk(d, prefix = '') {
|
|
273
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
274
|
+
const full = path.join(d, entry.name);
|
|
275
|
+
if (entry.isDirectory()) {
|
|
276
|
+
walk(full, `${prefix}/${entry.name}`);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (!entry.isFile() || !entry.name.endsWith('.astro')) continue;
|
|
280
|
+
if (entry.name.includes('[')) continue;
|
|
281
|
+
const stem = entry.name.replace(/\.astro$/, '');
|
|
282
|
+
if (stem === '404') continue;
|
|
283
|
+
const route = stem === 'index' ? prefix || '/' : `${prefix}/${stem}`;
|
|
284
|
+
out.push(route.endsWith('/') ? route : `${route}/`);
|
|
285
|
+
}
|
|
175
286
|
}
|
|
287
|
+
walk(dir);
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
176
290
|
|
|
177
|
-
|
|
291
|
+
function expandRoutes(routes, slugSets) {
|
|
292
|
+
const out = [];
|
|
293
|
+
for (const route of routes) {
|
|
294
|
+
const template = typeof route === 'string' ? route : route.template;
|
|
295
|
+
const placeholders = (template.match(/\{(\w+)\}/g) ?? []).map((p) => p.slice(1, -1));
|
|
296
|
+
if (placeholders.length === 0) {
|
|
297
|
+
out.push(template);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const arrays = placeholders.map((name) => slugSets[name] ?? []);
|
|
301
|
+
if (arrays.some((a) => a.length === 0)) continue;
|
|
302
|
+
for (const combo of cartesian(arrays)) {
|
|
303
|
+
let url = template;
|
|
304
|
+
placeholders.forEach((name, i) => {
|
|
305
|
+
url = url.replace(`{${name}}`, combo[i]);
|
|
306
|
+
});
|
|
307
|
+
out.push(url);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
178
310
|
return out;
|
|
179
311
|
}
|
|
312
|
+
|
|
313
|
+
function cartesian(arrays) {
|
|
314
|
+
return arrays.reduce(
|
|
315
|
+
(acc, curr) => acc.flatMap((a) => curr.map((c) => [...a, c])),
|
|
316
|
+
[[]]
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function renderContentScanConcise({ siteUrl, siteName, tagline, about, business, services, locations, pages, blogPosts, aiGuidance, today }) {
|
|
321
|
+
const lines = [];
|
|
322
|
+
lines.push(`# ${siteName}${tagline ? ` — ${tagline}` : ''}`);
|
|
323
|
+
lines.push(`Last Updated: ${today}`, '');
|
|
324
|
+
if (about) lines.push(`> ${about}`, '');
|
|
325
|
+
|
|
326
|
+
if (business) {
|
|
327
|
+
lines.push('## Contact', '');
|
|
328
|
+
if (business.telephone) lines.push(`- Phone: ${business.telephone}`);
|
|
329
|
+
if (business.email) lines.push(`- Email: ${business.email}`);
|
|
330
|
+
if (business.address) lines.push(`- Address: ${formatAddress(business.address)}`);
|
|
331
|
+
lines.push(`- Website: ${siteUrl}`);
|
|
332
|
+
if (business.license) lines.push(`- License: ${business.license}`);
|
|
333
|
+
lines.push('');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (services.length) {
|
|
337
|
+
lines.push('## Services', '');
|
|
338
|
+
for (const s of services) {
|
|
339
|
+
lines.push(`- ${s.data.title ?? s.slug}`);
|
|
340
|
+
}
|
|
341
|
+
lines.push('');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (locations.length) {
|
|
345
|
+
lines.push('## Service Areas', '');
|
|
346
|
+
for (const l of locations) {
|
|
347
|
+
lines.push(`- ${l.data.name ?? l.data.title ?? l.slug}`);
|
|
348
|
+
}
|
|
349
|
+
lines.push('');
|
|
350
|
+
} else if (business?.areaServed?.length) {
|
|
351
|
+
lines.push('## Service Areas', '');
|
|
352
|
+
for (const area of business.areaServed) lines.push(`- ${area}`);
|
|
353
|
+
lines.push('');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (blogPosts.length) {
|
|
357
|
+
lines.push('## Recent Blog Posts', '');
|
|
358
|
+
for (const post of blogPosts) {
|
|
359
|
+
lines.push(`- [${post.title}](${siteUrl}/blog/${post.slug}/): ${post.description ?? ''}`);
|
|
360
|
+
}
|
|
361
|
+
lines.push('');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (pages.length) {
|
|
365
|
+
lines.push('## Site Pages', '');
|
|
366
|
+
for (const p of pages.sort()) lines.push(`- ${siteUrl}${p}`);
|
|
367
|
+
lines.push('');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (aiGuidance) lines.push('## For AI Assistants', '', aiGuidance, '');
|
|
371
|
+
lines.push('---', 'Generated automatically during build');
|
|
372
|
+
return lines.join('\n') + '\n';
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function renderContentScanFull({ siteUrl, siteName, tagline, about, business, services, locations, aiGuidance, today }) {
|
|
376
|
+
const lines = [];
|
|
377
|
+
lines.push(`# ${siteName} — Full Reference`);
|
|
378
|
+
lines.push(`Last Updated: ${today}`, '');
|
|
379
|
+
if (tagline) lines.push(`> ${tagline}`, '');
|
|
380
|
+
if (about) lines.push(about, '');
|
|
381
|
+
lines.push('---', '');
|
|
382
|
+
|
|
383
|
+
if (business) {
|
|
384
|
+
lines.push('## Contact Information', '');
|
|
385
|
+
if (business.telephone) lines.push(`- **Phone:** ${business.telephone}`);
|
|
386
|
+
if (business.email) lines.push(`- **Email:** ${business.email}`);
|
|
387
|
+
if (business.address) lines.push(`- **Address:** ${formatAddress(business.address)}`);
|
|
388
|
+
lines.push(`- **Website:** ${siteUrl}`);
|
|
389
|
+
if (business.license) lines.push(`- **License:** ${business.license}`);
|
|
390
|
+
lines.push('', '---', '');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (services.length) {
|
|
394
|
+
lines.push('## Services', '');
|
|
395
|
+
for (const svc of services) {
|
|
396
|
+
const fm = svc.data;
|
|
397
|
+
lines.push(`### ${fm.title ?? svc.slug}`, '');
|
|
398
|
+
if (fm.shortDescription) lines.push(fm.shortDescription, '');
|
|
399
|
+
else if (fm.description) lines.push(fm.description, '');
|
|
400
|
+
if (Array.isArray(fm.benefits) && fm.benefits.length) {
|
|
401
|
+
lines.push('**Key Benefits:**', '');
|
|
402
|
+
for (const b of fm.benefits) lines.push(`- ${b}`);
|
|
403
|
+
lines.push('');
|
|
404
|
+
}
|
|
405
|
+
const faqs = extractFaqsFromBody(svc.body);
|
|
406
|
+
if (faqs.length) {
|
|
407
|
+
lines.push('**Frequently Asked Questions:**', '');
|
|
408
|
+
for (const faq of faqs) {
|
|
409
|
+
lines.push(`**Q: ${faq.q}**`, `A: ${faq.a}`, '');
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
lines.push('---', '');
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (locations.length) {
|
|
417
|
+
lines.push('## Service Areas', '');
|
|
418
|
+
for (const loc of locations) {
|
|
419
|
+
const fm = loc.data;
|
|
420
|
+
const name = fm.name ?? fm.title ?? loc.slug;
|
|
421
|
+
lines.push(`### ${name}`, '');
|
|
422
|
+
if (fm.description) {
|
|
423
|
+
const desc = typeof fm.description === 'string' ? fm.description : fm.description.join(' ');
|
|
424
|
+
lines.push(desc, '');
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
lines.push('---', '');
|
|
428
|
+
} else if (business?.areaServed?.length) {
|
|
429
|
+
lines.push('## Service Areas', '');
|
|
430
|
+
for (const area of business.areaServed) lines.push(`- ${area}`);
|
|
431
|
+
lines.push('', '---', '');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
lines.push(`## Optional`, '', `- Sitemap: ${siteUrl}/sitemap-index.xml`, '');
|
|
435
|
+
if (aiGuidance) lines.push('## For AI Assistants', '', aiGuidance, '');
|
|
436
|
+
return lines.join('\n') + '\n';
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function extractFaqsFromBody(body) {
|
|
440
|
+
const faqs = [];
|
|
441
|
+
const re = /- question: ["'](.+?)["']\s*\n\s*answer: ["'](.+?)["']/gs;
|
|
442
|
+
let m;
|
|
443
|
+
while ((m = re.exec(body)) !== null) faqs.push({ q: m[1], a: m[2] });
|
|
444
|
+
return faqs;
|
|
445
|
+
}
|