koala73/worldmonitor · error
duplicate FAQ question: ' + question
Error message
duplicate FAQ question: ' + question
What it means
applyComparisonNarrative normalizes each FAQ question (trimmed, lowercased) into a Set and throws if the same question appears twice within a page's merged FAQ list. Duplicate questions would produce duplicate FAQPage JSON-LD entries and hurt SEO.
Solutions
- Find the duplicated question named in the error and remove or reword one instance.
- Prefer removing duplicates from extraFaqs rather than the base page faqs.
- Reword one question so the trimmed/lowercase forms differ while keeping 8-12 total.
- Add a local check that dedupes/casefolds questions before building the narrative.
Example fix
// before faqs: [['Is it free?', 'Yes']], extraFaqs: [[' is it free? ', 'Absolutely']] // after faqs: [['Is it free?', 'Yes']], extraFaqs: [['What does the free tier include?', 'Map layers and alerts.']]
Defensive patterns
Strategy: validation
Validate before calling
const seen = new Set();
for (const [q] of [...page.faqs, ...(narrative.extraFaqs ?? [])]) {
const k = String(q).trim().toLowerCase();
if (seen.has(k)) throw new Error(`duplicate FAQ: ${q}`);
seen.add(k);
} Type guard
const faqsUnique = (faqs) => new Set(faqs.map(([q]) => String(q).trim().toLowerCase())).size === faqs.length;
Try / catch
try {
page = applyComparisonNarrative(page);
} catch (e) {
if (String(e.message).includes('duplicate FAQ question')) {
console.error('Dedupe/reword this FAQ:', e.message);
} else throw e;
} Prevention
- Run a dedupe check before merging extraFaqs into page.faqs.
- Compare questions case-insensitively and trimmed, matching the build's normalization.
- Keep a single canonical wording per question across base page and extras.
When it happens
Trigger: The merged FAQ list for a slug (page.faqs plus narrative.extraFaqs) contains two questions that are identical after trim/lowercase, e.g. 'Is it free?' and ' is it free? '.
Common situations: Adding an extraFaq that restates a question already on the base page; copy-pasting a FAQ into two narrative sections; near-identical wording that differs only by case or whitespace.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- whyWeWinBody must not repeat whyWeWin
- /accuracy/ meta description must be 155–160 chars (got ${len
- Missing comparison narrative for ' + page.slug
- FAQ count must be 8-12, got ' + faqs.length
- is missing whyWeWinBody
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/1bf435623d38b6c0.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/build-comparison-pages.mjs:457
['What does travel risk intelligence cost?', 'World Monitor is free without signup, with Pro from $39.99/month. Assistance providers such as International SOS and Crisis24 negotiate enterprise retainers with undisclosed list pricing.'],
],
},
];
function applyComparisonNarrative(page) {
const narrative = COMPARISON_NARRATIVES[page.slug];
if (!narrative) {
throw new Error('Missing comparison narrative for ' + page.slug);
}
const faqs = [...page.faqs, ...(narrative.extraFaqs ?? [])];
if (faqs.length < 8 || faqs.length > 12) {
throw new Error(page.slug + ' FAQ count must be 8-12, got ' + faqs.length);
}
const faqNames = new Set();
for (const [question] of faqs) {
const key = String(question).trim().toLowerCase();
if (faqNames.has(key)) {
throw new Error(page.slug + ' duplicate FAQ question: ' + question);
}
faqNames.add(key);
}
const merged = { ...page, ...narrative, faqs };
delete merged.extraFaqs;
const whyBody = (merged.whyWeWinBody ?? []).join(' ').replace(/\s+/g, ' ').trim();
if (!whyBody) {
throw new Error(page.slug + ' is missing whyWeWinBody');
}
if (whyBody === merged.whyWeWin.replace(/\s+/g, ' ').trim()) {
throw new Error(page.slug + ' whyWeWinBody must not repeat whyWeWin');
}
if (merged.heading && !(merged.headingProse?.length || merged.competitorProfiles?.length)) {
throw new Error(page.slug + ' H2 "' + merged.heading + '" has no following prose');
}
if (!merged.evaluationHeading || !merged.evaluationProse?.length) {
throw new Error(page.slug + ' is missing the evaluation section');
}View on GitHub (pinned to 7d06c8633d)