CODE · 4.8 KB
src/section-content.js
Workspace snapshot · 08/31 23:51
const text = (value, max = 600) =>
String(value ?? "")
.replace(/[\u0000-\u001f]/gu, " ")
.replace(/\s+/gu, " ")
.trim()
.slice(0, max);
const record = (value) =>
value && typeof value === "object" && !Array.isArray(value) ? value : {};
const list = (value, mapper, max = 8) =>
(Array.isArray(value) ? value : []).slice(0, max).map(mapper).filter(Boolean);
const item = (value) => {
const source = record(value);
const normalized = {
eyebrow: text(source.eyebrow, 80),
title: text(source.title, 180),
description: text(source.description, 600),
proof: text(source.proof, 240),
};
return normalized.title || normalized.description ? normalized : null;
};
const metric = (value) => {
const source = record(value);
const normalized = {
value: text(source.value, 80),
label: text(source.label, 160),
source: text(source.source, 240),
};
return normalized.value && normalized.label ? normalized : null;
};
const faq = (value) => {
const source = record(value);
const normalized = {
question: text(source.question, 240),
answer: text(source.answer, 1000),
};
return normalized.question && normalized.answer ? normalized : null;
};
const plan = (value) => {
const source = record(value);
const normalized = {
name: text(source.name, 120),
price: text(source.price, 100),
period: text(source.period, 80),
description: text(source.description, 360),
features: list(source.features, (entry) => text(entry, 180), 10).filter(Boolean),
note: text(source.note, 240),
cta: text(source.cta, 80),
};
return normalized.name && normalized.price ? normalized : null;
};
const comparison = (value) => {
const source = record(value);
const columns = list(source.columns, (entry) => text(entry, 120), 6).filter(Boolean);
const rows = list(source.rows, (row) => {
const rowSource = record(row);
const cells = list(rowSource.cells, (entry) => text(entry, 300), columns.length || 6).filter(Boolean);
return text(rowSource.label, 160) && cells.length
? { label: text(rowSource.label, 160), cells }
: null;
}, 12);
return columns.length >= 2 && rows.length ? { columns, rows } : null;
};
const beforeAfter = (value) => {
const source = record(value);
const before = item(source.before);
const after = item(source.after);
return before && after ? { before, after } : null;
};
const caseStudy = (value) => {
const source = record(value);
const normalized = {
subject: text(source.subject, 160),
issue: text(source.issue, 500),
action: text(source.action, 500),
result: text(source.result, 500),
source: text(source.source, 240),
metrics: list(source.metrics, metric, 6),
};
return normalized.issue && normalized.action && normalized.result ? normalized : null;
};
/**
* LPの本文をレイアウトへ流し込むための型付き正本。
* proseを句読点で分解してカードや数値を捏造しない。
*/
export function normalizeContentModel(value) {
const source = record(value);
return {
items: list(source.items, item, 10),
steps: list(source.steps, item, 8),
metrics: list(source.metrics, metric, 8),
faqs: list(source.faqs, faq, 12),
plans: list(source.plans, plan, 6),
comparison: comparison(source.comparison),
beforeAfter: beforeAfter(source.beforeAfter),
caseStudy: caseStudy(source.caseStudy),
quote: text(source.quote, 600),
quoteAttribution: text(source.quoteAttribution, 180),
sourceNote: text(source.sourceNote, 300),
};
}
const PLACEHOLDER = /(項目\s*\d|強みを記載|違いを記載|回答を入力|要設計|要確認|確認済みの根拠|価格は条件をご確認|個別見積もり)/u;
export function contentModelIssues(section) {
const model = normalizeContentModel(section?.contentModel);
const type = String(section?.type || "").toLowerCase();
const issues = [];
const serialized = JSON.stringify(model);
if (PLACEHOLDER.test(serialized)) issues.push("placeholder-content");
if (["problem", "risk", "differentiation", "usecase", "fit"].includes(type) && model.items.length < 2)
issues.push("typed-items-missing");
if (type === "trust" && model.items.length < 1 && !model.metrics.length && !model.caseStudy)
issues.push("typed-items-missing");
if (["mechanism", "process"].includes(type) && model.steps.length < 2 && !model.beforeAfter)
issues.push("typed-steps-missing");
if (type === "comparison" && !model.comparison) issues.push("comparison-data-missing");
if (type === "faq" && model.faqs.length < 2) issues.push("faq-data-missing");
if (type === "pricing" && model.plans.length < 1) issues.push("pricing-data-missing");
if (type === "proof" && !model.metrics.length && !model.caseStudy && !model.items.length)
issues.push("proof-data-missing");
return issues;
}