← GPT-AI-LP
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;
}