create-contractor-site 2.1.2 → 2.2.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 +49 -3
- package/bin/create-contractor-site.mjs +30 -2
- package/package.json +1 -1
- package/scripts/smoke-test.mjs +713 -5
- package/src/copy-template.mjs +1 -1
- package/src/prompts.mjs +360 -20
- package/src/replace-data.mjs +43 -4
package/src/copy-template.mjs
CHANGED
|
@@ -27,7 +27,7 @@ export const DEFAULT_TEMPLATE_REPO =
|
|
|
27
27
|
'https://github.com/glacayo/website-multipages.git';
|
|
28
28
|
|
|
29
29
|
export const DEFAULT_TEMPLATE_REF =
|
|
30
|
-
process.env.CREATE_CONTRACTOR_TEMPLATE_REF || 'v2.
|
|
30
|
+
process.env.CREATE_CONTRACTOR_TEMPLATE_REF || 'v2.2.0';
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* @param {string} name basename
|
package/src/prompts.mjs
CHANGED
|
@@ -1,7 +1,44 @@
|
|
|
1
1
|
import * as readline from 'node:readline/promises';
|
|
2
2
|
import { stdin as input, stdout as output } from 'node:process';
|
|
3
3
|
|
|
4
|
+
/** Default free-estimate CTA when blank/omitted. */
|
|
5
|
+
export const DEFAULT_FREE_ESTIMATE = 'Free On-Site Estimate';
|
|
6
|
+
/** Default years-of-experience text when blank/omitted. */
|
|
7
|
+
export const DEFAULT_YEARS_EXPERIENCE = '10+';
|
|
8
|
+
/** Default license statement when blank/omitted. */
|
|
9
|
+
export const DEFAULT_LICENSE = 'Licensed & Insured';
|
|
10
|
+
/** Default insurance statement when blank/omitted. */
|
|
11
|
+
export const DEFAULT_INSURANCE =
|
|
12
|
+
"Fully insured with general liability and workers' compensation.";
|
|
13
|
+
/** Default payment methods when blank/omitted/empty. */
|
|
14
|
+
export const DEFAULT_PAYMENT_METHODS = [
|
|
15
|
+
'Cash',
|
|
16
|
+
'Check',
|
|
17
|
+
'Credit Card',
|
|
18
|
+
'Financing Available',
|
|
19
|
+
];
|
|
20
|
+
/** Default 3-row business hours (Mon–Fri / Sat / Sun). */
|
|
21
|
+
export const DEFAULT_HOURS = [
|
|
22
|
+
{ days: 'Monday - Friday', time: '7:00 AM - 6:00 PM' },
|
|
23
|
+
{ days: 'Saturday', time: '8:00 AM - 2:00 PM' },
|
|
24
|
+
{ days: 'Sunday', time: 'Closed' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/** Canonical `site.json.site_type` values written by new scaffolds. */
|
|
28
|
+
export const SITE_TYPES = /** @type {const} */ (['one-page', 'multipage', 'seo']);
|
|
29
|
+
/** Default website type for NEW CLI scaffolds (all answer paths). */
|
|
30
|
+
export const DEFAULT_SITE_TYPE = /** @type {const} */ ('multipage');
|
|
31
|
+
|
|
32
|
+
/** Allowed `business.social` keys (blank values omitted, never empty strings). */
|
|
33
|
+
export const SOCIAL_NETWORK_KEYS = [
|
|
34
|
+
'facebook', 'instagram', 'youtube', 'tiktok', 'google_business', 'linkedin', 'x',
|
|
35
|
+
];
|
|
36
|
+
|
|
4
37
|
/**
|
|
38
|
+
* @typedef {'one-page' | 'multipage' | 'seo'} SiteType
|
|
39
|
+
* @typedef {{ days: string, time: string }} BusinessHourRow
|
|
40
|
+
* @typedef {{ name: string, url: string, initials?: string, icon?: string, badge_image?: string }} DirectoryRow
|
|
41
|
+
*
|
|
5
42
|
* @typedef {object} ScaffoldAnswers
|
|
6
43
|
* @property {string} businessName
|
|
7
44
|
* @property {string} legalName
|
|
@@ -13,11 +50,205 @@ import { stdin as input, stdout as output } from 'node:process';
|
|
|
13
50
|
* @property {string} state
|
|
14
51
|
* @property {string} zip
|
|
15
52
|
* @property {string} serviceArea
|
|
16
|
-
* @property {string} foundedYear
|
|
53
|
+
* @property {string} foundedYear empty string when skipped/blank
|
|
54
|
+
* @property {string} freeEstimate
|
|
55
|
+
* @property {string} yearsExperience
|
|
56
|
+
* @property {string} license
|
|
57
|
+
* @property {string} insurance
|
|
58
|
+
* @property {string[]} paymentMethods
|
|
59
|
+
* @property {BusinessHourRow[]} hours always length 3
|
|
60
|
+
* @property {Record<string, string>} social non-blank network keys only
|
|
61
|
+
* @property {DirectoryRow[]} directories empty = none (keep template row + disable)
|
|
62
|
+
* @property {boolean} enableDirectories true iff directories.length > 0
|
|
63
|
+
* @property {SiteType} siteType written to site.json.site_type
|
|
17
64
|
* @property {string[]} primaryServices
|
|
18
65
|
* @property {string} [siteUrl]
|
|
19
66
|
*/
|
|
20
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Trim and fall back when the value is blank-looking.
|
|
70
|
+
* @param {unknown} value
|
|
71
|
+
* @param {string} fallback
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
export function requiredText(value, fallback) {
|
|
75
|
+
const text = value == null ? '' : String(value).trim();
|
|
76
|
+
return text || fallback;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Optional text field: trim; blank/whitespace → "".
|
|
81
|
+
* @param {unknown} value
|
|
82
|
+
* @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
export function optionalText(value) {
|
|
85
|
+
if (value == null) return '';
|
|
86
|
+
return String(value).trim();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* CSV string or string[] → trimmed non-empty list; blank/empty → defaults (never []).
|
|
91
|
+
* @param {unknown} value
|
|
92
|
+
* @returns {string[]}
|
|
93
|
+
*/
|
|
94
|
+
export function normalizePaymentMethods(value) {
|
|
95
|
+
/** @type {string[]} */
|
|
96
|
+
let items = [];
|
|
97
|
+
if (typeof value === 'string') {
|
|
98
|
+
items = value
|
|
99
|
+
.split(',')
|
|
100
|
+
.map((s) => s.trim())
|
|
101
|
+
.filter(Boolean);
|
|
102
|
+
} else if (Array.isArray(value)) {
|
|
103
|
+
items = value.map((s) => String(s ?? '').trim()).filter(Boolean);
|
|
104
|
+
}
|
|
105
|
+
return items.length > 0 ? items : [...DEFAULT_PAYMENT_METHODS];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Normalize hours to the fixed 3-row `{ days, time }` shape.
|
|
110
|
+
* Accepts full rows, time-only strings, or compact weekday/sat/sun fields via buildAnswers.
|
|
111
|
+
* Blank/missing rows fall back to defaults — never empty array or partial shape.
|
|
112
|
+
* @param {unknown} value
|
|
113
|
+
* @returns {BusinessHourRow[]}
|
|
114
|
+
*/
|
|
115
|
+
export function normalizeHours(value) {
|
|
116
|
+
const defaults = DEFAULT_HOURS.map((row) => ({ days: row.days, time: row.time }));
|
|
117
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
118
|
+
return defaults;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return defaults.map((def, index) => {
|
|
122
|
+
const raw = value[index];
|
|
123
|
+
if (raw == null || raw === '') {
|
|
124
|
+
return { ...def };
|
|
125
|
+
}
|
|
126
|
+
if (typeof raw === 'string') {
|
|
127
|
+
const time = raw.trim();
|
|
128
|
+
return { days: def.days, time: time || def.time };
|
|
129
|
+
}
|
|
130
|
+
if (typeof raw === 'object') {
|
|
131
|
+
const days =
|
|
132
|
+
/** @type {{ days?: unknown, time?: unknown }} */ (raw).days != null
|
|
133
|
+
? String(/** @type {{ days?: unknown }} */ (raw).days).trim()
|
|
134
|
+
: '';
|
|
135
|
+
const time =
|
|
136
|
+
/** @type {{ time?: unknown }} */ (raw).time != null
|
|
137
|
+
? String(/** @type {{ time?: unknown }} */ (raw).time).trim()
|
|
138
|
+
: '';
|
|
139
|
+
return {
|
|
140
|
+
days: days || def.days,
|
|
141
|
+
time: time || def.time,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return { ...def };
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** @param {string} name @returns {string} */
|
|
149
|
+
export function initialsFromName(name) {
|
|
150
|
+
const parts = String(name || '').trim().split(/\s+/).filter(Boolean);
|
|
151
|
+
if (parts.length === 0) return '?';
|
|
152
|
+
if (parts.length === 1) return parts[0].slice(0, 3).toUpperCase();
|
|
153
|
+
return parts.map((p) => p[0]).join('').slice(0, 3).toUpperCase();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Object map or `network=url` CSV → non-blank keys only. @param {unknown} value */
|
|
157
|
+
export function normalizeSocial(value) {
|
|
158
|
+
/** @type {Record<string, string>} */
|
|
159
|
+
const out = {};
|
|
160
|
+
const allowed = new Set(SOCIAL_NETWORK_KEYS);
|
|
161
|
+
if (value == null || value === '') return out;
|
|
162
|
+
if (typeof value === 'string') {
|
|
163
|
+
for (const part of value.split(',')) {
|
|
164
|
+
const piece = part.trim();
|
|
165
|
+
const eq = piece.indexOf('=');
|
|
166
|
+
if (eq <= 0) continue;
|
|
167
|
+
const key = piece.slice(0, eq).trim().toLowerCase().replace(/-/g, '_');
|
|
168
|
+
const url = piece.slice(eq + 1).trim();
|
|
169
|
+
if (allowed.has(key) && url) out[key] = url;
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
174
|
+
for (const key of SOCIAL_NETWORK_KEYS) {
|
|
175
|
+
const raw = /** @type {Record<string, unknown>} */ (value)[key];
|
|
176
|
+
if (raw == null) continue;
|
|
177
|
+
const url = String(raw).trim();
|
|
178
|
+
if (url) out[key] = url;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Rows or `Name|url` CSV. Empty → [] (replace keeps ≥1 template row). @param {unknown} value */
|
|
185
|
+
export function normalizeDirectories(value) {
|
|
186
|
+
/** @type {DirectoryRow[]} */
|
|
187
|
+
const rows = [];
|
|
188
|
+
if (value == null || value === '') return rows;
|
|
189
|
+
if (typeof value === 'string') {
|
|
190
|
+
for (const part of value.split(',')) {
|
|
191
|
+
const piece = part.trim();
|
|
192
|
+
const pipe = piece.indexOf('|');
|
|
193
|
+
if (pipe <= 0) continue;
|
|
194
|
+
const name = piece.slice(0, pipe).trim();
|
|
195
|
+
const url = piece.slice(pipe + 1).trim();
|
|
196
|
+
if (name && url) rows.push({ name, url, initials: initialsFromName(name) });
|
|
197
|
+
}
|
|
198
|
+
return rows;
|
|
199
|
+
}
|
|
200
|
+
if (!Array.isArray(value)) return rows;
|
|
201
|
+
for (const item of value) {
|
|
202
|
+
if (!item || typeof item !== 'object') continue;
|
|
203
|
+
const rowIn = /** @type {Record<string, unknown>} */ (item);
|
|
204
|
+
const name = String(rowIn.name ?? '').trim();
|
|
205
|
+
const url = String(rowIn.url ?? '').trim();
|
|
206
|
+
if (!name || !url) continue;
|
|
207
|
+
/** @type {DirectoryRow} */
|
|
208
|
+
const row = { name, url };
|
|
209
|
+
const initials = rowIn.initials != null ? String(rowIn.initials).trim() : '';
|
|
210
|
+
row.initials = initials || initialsFromName(name);
|
|
211
|
+
if (rowIn.icon != null && String(rowIn.icon).trim()) row.icon = String(rowIn.icon).trim();
|
|
212
|
+
if (rowIn.badge_image != null && String(rowIn.badge_image).trim()) {
|
|
213
|
+
row.badge_image = String(rowIn.badge_image).trim();
|
|
214
|
+
}
|
|
215
|
+
rows.push(row);
|
|
216
|
+
}
|
|
217
|
+
return rows;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Normalize website type to a canonical `site.json.site_type` value.
|
|
222
|
+
* Accepts canonical values and common human-ish aliases; blank/invalid → multipage.
|
|
223
|
+
* Always returns only: one-page | multipage | seo (never weakens validate:data).
|
|
224
|
+
* @param {unknown} value
|
|
225
|
+
* @returns {SiteType}
|
|
226
|
+
*/
|
|
227
|
+
export function normalizeSiteType(value) {
|
|
228
|
+
if (value == null) return DEFAULT_SITE_TYPE;
|
|
229
|
+
const raw = String(value)
|
|
230
|
+
.trim()
|
|
231
|
+
.toLowerCase()
|
|
232
|
+
.replace(/[_]+/g, '-')
|
|
233
|
+
.replace(/\s+/g, '-');
|
|
234
|
+
if (!raw) return DEFAULT_SITE_TYPE;
|
|
235
|
+
|
|
236
|
+
/** @type {Record<string, SiteType>} */
|
|
237
|
+
const aliases = {
|
|
238
|
+
'one-page': 'one-page',
|
|
239
|
+
onepage: 'one-page',
|
|
240
|
+
one: 'one-page',
|
|
241
|
+
single: 'one-page',
|
|
242
|
+
'single-page': 'one-page',
|
|
243
|
+
singlepage: 'one-page',
|
|
244
|
+
multipage: 'multipage',
|
|
245
|
+
'multi-page': 'multipage',
|
|
246
|
+
multi: 'multipage',
|
|
247
|
+
seo: 'seo',
|
|
248
|
+
};
|
|
249
|
+
return aliases[raw] || DEFAULT_SITE_TYPE;
|
|
250
|
+
}
|
|
251
|
+
|
|
21
252
|
/**
|
|
22
253
|
* @param {string} label
|
|
23
254
|
* @param {{ required?: boolean, defaultValue?: string }} [opts]
|
|
@@ -31,7 +262,8 @@ async function ask(rl, label, opts = {}) {
|
|
|
31
262
|
const raw = await rl.question(`${label}${hint}: `);
|
|
32
263
|
const answer = raw.trim();
|
|
33
264
|
if (answer) return answer;
|
|
34
|
-
if (
|
|
265
|
+
if (defaultValue) return defaultValue;
|
|
266
|
+
if (!required) return '';
|
|
35
267
|
console.log(' This field is required. Please enter a value (or Ctrl+C to abort).');
|
|
36
268
|
}
|
|
37
269
|
}
|
|
@@ -59,7 +291,45 @@ export async function collectClientAnswers() {
|
|
|
59
291
|
const state = await ask(rl, 'State');
|
|
60
292
|
const zip = await ask(rl, 'ZIP');
|
|
61
293
|
const serviceArea = await ask(rl, 'Service area description');
|
|
62
|
-
const
|
|
294
|
+
const freeEstimate = await ask(rl, 'Free-estimate wording', {
|
|
295
|
+
defaultValue: DEFAULT_FREE_ESTIMATE,
|
|
296
|
+
});
|
|
297
|
+
const yearsExperience = await ask(rl, 'Years of experience', {
|
|
298
|
+
defaultValue: DEFAULT_YEARS_EXPERIENCE,
|
|
299
|
+
});
|
|
300
|
+
const license = await ask(rl, 'License text', {
|
|
301
|
+
defaultValue: DEFAULT_LICENSE,
|
|
302
|
+
});
|
|
303
|
+
const insurance = await ask(rl, 'Insurance statement', {
|
|
304
|
+
defaultValue: DEFAULT_INSURANCE,
|
|
305
|
+
});
|
|
306
|
+
const foundedYear = await ask(rl, 'Founded year (optional)', {
|
|
307
|
+
required: false,
|
|
308
|
+
defaultValue: '',
|
|
309
|
+
});
|
|
310
|
+
const paymentMethodsRaw = await ask(rl, 'Payment methods (comma-separated)', {
|
|
311
|
+
defaultValue: DEFAULT_PAYMENT_METHODS.join(', '),
|
|
312
|
+
});
|
|
313
|
+
const hoursWeekday = await ask(rl, 'Weekday hours (Mon–Fri)', {
|
|
314
|
+
defaultValue: DEFAULT_HOURS[0].time,
|
|
315
|
+
});
|
|
316
|
+
const hoursSaturday = await ask(rl, 'Saturday hours', {
|
|
317
|
+
defaultValue: DEFAULT_HOURS[1].time,
|
|
318
|
+
});
|
|
319
|
+
const hoursSunday = await ask(rl, 'Sunday hours', {
|
|
320
|
+
defaultValue: DEFAULT_HOURS[2].time,
|
|
321
|
+
});
|
|
322
|
+
const socialRaw = await ask(rl, 'Social links (network=url,…; Enter to skip)', {
|
|
323
|
+
required: false,
|
|
324
|
+
defaultValue: '',
|
|
325
|
+
});
|
|
326
|
+
const directoriesRaw = await ask(rl, 'Directories (Name|url,…; Enter to skip)', {
|
|
327
|
+
required: false,
|
|
328
|
+
defaultValue: '',
|
|
329
|
+
});
|
|
330
|
+
const siteTypeRaw = await ask(rl, 'Website type (one-page | multipage | seo)', {
|
|
331
|
+
defaultValue: DEFAULT_SITE_TYPE,
|
|
332
|
+
});
|
|
63
333
|
const servicesRaw = await ask(rl, 'Primary services (comma-separated)');
|
|
64
334
|
const siteUrl = await ask(rl, 'Site URL (optional)', {
|
|
65
335
|
required: false,
|
|
@@ -75,7 +345,7 @@ export async function collectClientAnswers() {
|
|
|
75
345
|
throw new Error('At least one primary service is required.');
|
|
76
346
|
}
|
|
77
347
|
|
|
78
|
-
return {
|
|
348
|
+
return buildAnswers({
|
|
79
349
|
businessName,
|
|
80
350
|
legalName,
|
|
81
351
|
tagline,
|
|
@@ -86,36 +356,106 @@ export async function collectClientAnswers() {
|
|
|
86
356
|
state,
|
|
87
357
|
zip,
|
|
88
358
|
serviceArea,
|
|
359
|
+
freeEstimate,
|
|
360
|
+
yearsExperience,
|
|
361
|
+
license,
|
|
362
|
+
insurance,
|
|
89
363
|
foundedYear,
|
|
364
|
+
paymentMethods: paymentMethodsRaw,
|
|
365
|
+
hoursWeekday,
|
|
366
|
+
hoursSaturday,
|
|
367
|
+
hoursSunday,
|
|
368
|
+
social: socialRaw,
|
|
369
|
+
directories: directoriesRaw,
|
|
370
|
+
siteType: siteTypeRaw,
|
|
90
371
|
primaryServices,
|
|
91
372
|
siteUrl: siteUrl || undefined,
|
|
92
|
-
};
|
|
373
|
+
});
|
|
93
374
|
} finally {
|
|
94
375
|
rl.close();
|
|
95
376
|
}
|
|
96
377
|
}
|
|
97
378
|
|
|
98
379
|
/**
|
|
99
|
-
*
|
|
380
|
+
* All answer paths funnel here. Hours: full rows or compact weekday/sat/sun.
|
|
381
|
+
* Social: object or `network=url` CSV. Directories: rows or `Name|url` CSV.
|
|
382
|
+
* siteType: canonical or human-ish → one-page | multipage | seo (default multipage).
|
|
100
383
|
*
|
|
101
|
-
* @param {Partial<ScaffoldAnswers> & {
|
|
384
|
+
* @param {Partial<ScaffoldAnswers> & {
|
|
385
|
+
* businessName: string,
|
|
386
|
+
* primaryServices: string[],
|
|
387
|
+
* paymentMethods?: string | string[],
|
|
388
|
+
* hoursWeekday?: string,
|
|
389
|
+
* hoursSaturday?: string,
|
|
390
|
+
* hoursSunday?: string,
|
|
391
|
+
* social?: string | Record<string, string>,
|
|
392
|
+
* directories?: string | DirectoryRow[],
|
|
393
|
+
* siteType?: string,
|
|
394
|
+
* }} overrides
|
|
102
395
|
* @returns {ScaffoldAnswers}
|
|
103
396
|
*/
|
|
104
397
|
export function buildAnswers(overrides) {
|
|
105
|
-
const businessName = overrides.businessName;
|
|
398
|
+
const businessName = requiredText(overrides.businessName, '');
|
|
399
|
+
if (!businessName) throw new Error('businessName is required');
|
|
400
|
+
|
|
401
|
+
const primaryServices = (overrides.primaryServices || [])
|
|
402
|
+
.map((s) => String(s || '').trim())
|
|
403
|
+
.filter(Boolean);
|
|
404
|
+
if (primaryServices.length === 0) {
|
|
405
|
+
throw new Error('primaryServices must include at least one service');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const hasCompactHours =
|
|
409
|
+
overrides.hoursWeekday != null ||
|
|
410
|
+
overrides.hoursSaturday != null ||
|
|
411
|
+
overrides.hoursSunday != null;
|
|
412
|
+
/** @type {unknown} */
|
|
413
|
+
let hoursInput = overrides.hours;
|
|
414
|
+
if (
|
|
415
|
+
hasCompactHours &&
|
|
416
|
+
(hoursInput == null || (Array.isArray(hoursInput) && hoursInput.length === 0))
|
|
417
|
+
) {
|
|
418
|
+
hoursInput = [overrides.hoursWeekday, overrides.hoursSaturday, overrides.hoursSunday];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const social = normalizeSocial(/** @type {{ social?: unknown }} */ (overrides).social);
|
|
422
|
+
const directories = normalizeDirectories(
|
|
423
|
+
/** @type {{ directories?: unknown }} */ (overrides).directories,
|
|
424
|
+
);
|
|
425
|
+
// Derived from rows only: directories:[] + enableDirectories:true → false.
|
|
426
|
+
const enableDirectories = directories.length > 0;
|
|
427
|
+
const siteType = normalizeSiteType(
|
|
428
|
+
/** @type {{ siteType?: unknown }} */ (overrides).siteType,
|
|
429
|
+
);
|
|
430
|
+
|
|
106
431
|
return {
|
|
107
432
|
businessName,
|
|
108
|
-
legalName: overrides.legalName
|
|
109
|
-
tagline: overrides.tagline
|
|
110
|
-
phone: overrides.phone
|
|
111
|
-
email: overrides.email
|
|
112
|
-
street: overrides.street
|
|
113
|
-
city: overrides.city
|
|
114
|
-
state: overrides.state
|
|
115
|
-
zip: overrides.zip
|
|
116
|
-
serviceArea:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
433
|
+
legalName: requiredText(overrides.legalName, `${businessName} LLC`),
|
|
434
|
+
tagline: requiredText(overrides.tagline, `Quality work from ${businessName}.`),
|
|
435
|
+
phone: requiredText(overrides.phone, '(555) 555-0100'),
|
|
436
|
+
email: requiredText(overrides.email, 'hello@example.com'),
|
|
437
|
+
street: requiredText(overrides.street, '123 Main St'),
|
|
438
|
+
city: requiredText(overrides.city, 'Example City'),
|
|
439
|
+
state: requiredText(overrides.state, 'VA'),
|
|
440
|
+
zip: requiredText(overrides.zip, '00000'),
|
|
441
|
+
serviceArea: requiredText(
|
|
442
|
+
overrides.serviceArea,
|
|
443
|
+
`${requiredText(overrides.city, 'Example City')} and surrounding areas`,
|
|
444
|
+
),
|
|
445
|
+
freeEstimate: requiredText(overrides.freeEstimate, DEFAULT_FREE_ESTIMATE),
|
|
446
|
+
yearsExperience: requiredText(overrides.yearsExperience, DEFAULT_YEARS_EXPERIENCE),
|
|
447
|
+
license: requiredText(overrides.license, DEFAULT_LICENSE),
|
|
448
|
+
insurance: requiredText(overrides.insurance, DEFAULT_INSURANCE),
|
|
449
|
+
foundedYear: optionalText(overrides.foundedYear),
|
|
450
|
+
paymentMethods: normalizePaymentMethods(
|
|
451
|
+
/** @type {{ paymentMethods?: unknown }} */ (overrides).paymentMethods,
|
|
452
|
+
),
|
|
453
|
+
hours: normalizeHours(hoursInput),
|
|
454
|
+
social,
|
|
455
|
+
directories,
|
|
456
|
+
enableDirectories,
|
|
457
|
+
siteType,
|
|
458
|
+
primaryServices,
|
|
459
|
+
siteUrl: overrides.siteUrl ? String(overrides.siteUrl).trim() || undefined : undefined,
|
|
120
460
|
};
|
|
121
461
|
}
|
package/src/replace-data.mjs
CHANGED
|
@@ -373,7 +373,19 @@ export function replaceTargetData(targetDir, answers) {
|
|
|
373
373
|
full: fullAddress,
|
|
374
374
|
};
|
|
375
375
|
business.service_area = answers.serviceArea;
|
|
376
|
+
business.free_estimate = answers.freeEstimate;
|
|
377
|
+
business.years_experience = answers.yearsExperience;
|
|
378
|
+
business.license = answers.license;
|
|
379
|
+
business.insurance = answers.insurance;
|
|
380
|
+
// Optional: always preserve key; skipped/blank → ""
|
|
376
381
|
business.founded_year = answers.foundedYear;
|
|
382
|
+
// Always write non-empty arrays from buildAnswers (never [] / never skip).
|
|
383
|
+
business.payment_methods = answers.paymentMethods.map((m) => String(m));
|
|
384
|
+
business.hours = answers.hours.map((row) => ({
|
|
385
|
+
days: String(row.days),
|
|
386
|
+
time: String(row.time),
|
|
387
|
+
}));
|
|
388
|
+
business.social = { ...(answers.social || {}) }; // non-blank keys only
|
|
377
389
|
business.type_of_services = typeOfServices;
|
|
378
390
|
|
|
379
391
|
if (aligned) {
|
|
@@ -385,15 +397,42 @@ export function replaceTargetData(targetDir, answers) {
|
|
|
385
397
|
if (answers.siteUrl) {
|
|
386
398
|
site.url = answers.siteUrl.replace(/\/$/, '');
|
|
387
399
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
};
|
|
400
|
+
// New scaffolds always write canonical site_type (default multipage via buildAnswers).
|
|
401
|
+
site.site_type = answers.siteType;
|
|
402
|
+
site.logo = { ...site.logo, alt: answers.businessName };
|
|
392
403
|
site.seo = {
|
|
393
404
|
...site.seo,
|
|
394
405
|
default_title: `${answers.businessName} | ${answers.city}, ${answers.state}`,
|
|
395
406
|
default_description: `${answers.tagline} Serving ${answers.serviceArea}.`,
|
|
396
407
|
};
|
|
408
|
+
site.features = {
|
|
409
|
+
...site.features,
|
|
410
|
+
enable_directories: Boolean(answers.enableDirectories),
|
|
411
|
+
};
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
updateJsonFile(path.join(dataDir, 'directories.json'), (directoriesData) => {
|
|
415
|
+
const provided = Array.isArray(answers.directories) ? answers.directories : [];
|
|
416
|
+
if (provided.length > 0) {
|
|
417
|
+
directoriesData.directories = provided.map((row) => {
|
|
418
|
+
/** @type {Record<string, unknown>} */
|
|
419
|
+
const out = { name: String(row.name), url: String(row.url) };
|
|
420
|
+
if (row.initials) out.initials = String(row.initials);
|
|
421
|
+
if (row.icon) out.icon = String(row.icon);
|
|
422
|
+
if (row.badge_image) out.badge_image = String(row.badge_image);
|
|
423
|
+
return out;
|
|
424
|
+
});
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
// none: keep existing rows; never write directories: []
|
|
428
|
+
const existing = Array.isArray(directoriesData.directories)
|
|
429
|
+
? directoriesData.directories
|
|
430
|
+
: [];
|
|
431
|
+
if (existing.length === 0) {
|
|
432
|
+
directoriesData.directories = [
|
|
433
|
+
{ name: 'Google Business', url: 'https://example.com/business', initials: 'G' },
|
|
434
|
+
];
|
|
435
|
+
}
|
|
397
436
|
});
|
|
398
437
|
|
|
399
438
|
updateJsonFile(path.join(dataDir, 'areas.json'), (areas) => {
|