mirror of
https://github.com/BigMarc/Bunny-Agency-Italy.git
synced 2026-10-13 17:00:40 +02:00
75 lines
5.6 KiB
JavaScript
75 lines
5.6 KiB
JavaScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
export function validateContent(data, root) {
|
|
const errors = [];
|
|
const marketing = data.pages.filter((page) => page.category !== "legal");
|
|
const moneyPaths = new Set(["/", "/services/account-management", "/pricing", "/blog/questions/creator-taxes-guide", "/blog/questions/how-to-start-onlyfans", "/blog/questions/how-to-choose-a-creator-agency"]);
|
|
const paths = new Set(data.pages.map((page) => page.path));
|
|
const segmenter = new Intl.Segmenter(data.site.locale, { granularity: "word" });
|
|
if (marketing.length < 38) errors.push("fewer than 38 marketing/content pages");
|
|
for (const field of ["path", "title", "description", "h1"]) {
|
|
const seen = new Map();
|
|
for (const page of data.pages) {
|
|
const key = String(page[field] ?? "").trim().toLocaleLowerCase();
|
|
if (seen.has(key)) errors.push(`duplicate ${field}: ${page.path} and ${seen.get(key)}`);
|
|
seen.set(key, page.path);
|
|
}
|
|
}
|
|
for (const page of data.pages) {
|
|
for (const field of ["path", "title", "description", "h1", "summary", "image", "alt", "keyword"]) {
|
|
if (!page[field]) errors.push(`${page.path} missing ${field}`);
|
|
}
|
|
for (const [field, value] of [["summary", page.summary], ["title", page.title], ...page.faq.map((faq) => ["FAQ answer", faq.answer])]) {
|
|
if (/LLC: 2019/.test(value) || /\| Bunny\.$/.test(value.trim()) || value.startsWith(`${data.site.name} —`)) {
|
|
errors.push(`${page.path} ${field} contains generated filler`);
|
|
}
|
|
}
|
|
if (!page.faq?.length) errors.push(`${page.path} missing visible FAQ`);
|
|
if (new Set(page.faq.map((faq) => faq.question.trim().toLocaleLowerCase())).size !== page.faq.length) errors.push(`${page.path} duplicate FAQ question`);
|
|
if (page.title.length > 60) errors.push(`${page.path} title exceeds 60 characters`);
|
|
if (page.description.length > 155) errors.push(`${page.path} description exceeds 155 characters`);
|
|
if (/[….] \| Bunny\.$|…/.test(page.title)) errors.push(`${page.path} truncated title`);
|
|
if (page.citations.length < 2) errors.push(`${page.path} has fewer than 2 citations`);
|
|
if (page.category === "service" && !page.h1.toLocaleLowerCase().includes(page.keyword.toLocaleLowerCase())) errors.push(`${page.path} service keyword missing from H1`);
|
|
if (page.category === "blog" && ![page.h1, ...page.sections.map((section) => section.heading)].some((value) => value.toLocaleLowerCase().includes(page.keyword.toLocaleLowerCase()))) errors.push(`${page.path} blog keyword missing from H1/H2`);
|
|
for (const related of page.relatedPaths ?? []) if (!paths.has(related)) errors.push(`${page.path} broken related path ${related}`);
|
|
if (moneyPaths.has(page.path)) {
|
|
if (page.sections.length < 5 || page.sections.length > 7) errors.push(`${page.path} money page needs 5-7 sections`);
|
|
if (page.faq.length < 5) errors.push(`${page.path} money page needs at least 5 FAQs`);
|
|
const rendered = [page.summary, ...page.sections.flatMap((section) => [section.heading, section.body, ...(section.items ?? []), ...(section.steps ?? []), ...(section.table?.headers ?? []), ...(section.table?.rows.flat() ?? [])]), ...page.faq.flatMap((faq) => [faq.question, faq.answer])].join(" ");
|
|
if (["zh-TW", "th-TH", "ko-KR"].includes(data.site.locale)) {
|
|
if (rendered.length < 1400) errors.push(`${page.path} money page is too short`);
|
|
} else {
|
|
const words = [...segmenter.segment(rendered)].filter((item) => item.isWordLike).length;
|
|
if (words < 580) errors.push(`${page.path} money page needs at least 580 words: ${words}`);
|
|
}
|
|
}
|
|
if (root && !fs.existsSync(path.join(root, "public", page.image))) errors.push(`${page.path} missing image ${page.image}`);
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(page.datePublished) || !/^\d{4}-\d{2}-\d{2}$/.test(page.dateModified) || page.datePublished > page.dateModified) errors.push(`${page.path} invalid publication/modification dates`);
|
|
}
|
|
if (data.site.hreflang?.["it-IT"]) errors.push("unlaunched Italian hreflang target");
|
|
for (const url of data.site.schemaSameAs ?? []) {
|
|
if (url.replace(/\/+$/, "") === data.site.url.replace(/\/+$/, "")) errors.push("sameAs points to this site");
|
|
if (new URL(url).hostname === "www.bunny-agency.it") errors.push("unlaunched Italian sameAs target");
|
|
}
|
|
if (root) {
|
|
for (const file of ["public/llms.txt", "public/llms-full.txt", "src/app/robots.ts", "src/app/sitemap.ts", "src/components/TrackedHubSpotForm.tsx", "src/components/SiteJsonLd.tsx", "src/components/Breadcrumbs.tsx"]) if (!fs.existsSync(path.join(root, file))) errors.push(`missing ${file}`);
|
|
for (const file of ["SiteJsonLd.tsx", "PageJsonLd.tsx"]) {
|
|
const schema = fs.readFileSync(path.join(root, "src/components", file), "utf8");
|
|
if (/"@type": "HowTo"/.test(schema)) errors.push(`${file} contains HowTo schema`);
|
|
if (file === "SiteJsonLd.tsx" && (!schema.includes("aggregateRating:") || !schema.includes("review:"))) errors.push(`${file} must retain the existing rating and testimonial markup`);
|
|
}
|
|
}
|
|
return errors;
|
|
}
|
|
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const data = JSON.parse(fs.readFileSync(path.join(root, "src/content/site-content.json"), "utf8"));
|
|
const errors = validateContent(data, root);
|
|
if (errors.length) { console.error(errors.join("\n")); process.exitCode = 1; }
|
|
else console.log(`${data.site.code}: ${data.pages.filter((page) => page.category !== "legal").length} marketing pages, ${data.pages.length} total pages; SEO registry valid`);
|
|
}
|