跳到主要内容
IM智引科技

研究 / 外贸与 B2B 增长

AI Skill:Author 与 Organization Schema 批量生成

Schema 是结构化数据,最适合脚本生成而不是手写——手写容易漏字段、写错嵌套、@id 引用不一致。这个 Skill 用一份实体配置文件驱动全站 Schema 生成,脚本负责结构和引用完整性,模型只负责 description 和 knowsAbout 这类需要表达的字段。

BLKTECH 编辑部2026年8月4日12 分钟难度 实战免费Node.jsJSON-LDAI Agent

Schema 生成用单一实体配置文件(entities.yaml)作为事实来源,脚本按 page_type 自动选择 Schema 类型并生成 @id 引用链。模型只负责 description、knowsAbout 等表达型字段。生成后跑校验脚本检查 @id 引用完整性和必填字段,再用 Rich Results Test 验证。

AI Skill:Author 与 Organization Schema 批量生成

为什么 Schema 不该手写

第 38 篇给了完整的 Schema 代码模板。但手动往每个页面复制粘贴会出三类问题:

  1. @id 引用断裂:文章 Schema 的 author 字段引用了 #person,但作者主页的 Person Schema 的 @id 写成了别的值 → Google 无法关联两个实体
  2. 字段遗漏:Organization Schema 在首页写全了,在 About 页少了 hasCredential
  3. 信息不一致:公司名在首页写 AquaFlow Industrial,在 Schema 里写 Aquaflow Industrial Ltd → 违反第 39 篇的 NAP 一致性要求

这三类问题都是结构性的,脚本可以 100% 避免。


一、单一事实来源:entities.yaml

所有实体信息集中在一个文件。这份文件同时也是第 39 篇要求的“品牌事实表”。

# src/data/entities.yaml

organization:
  id: "organization"              # 用于生成 @id
  name: "AquaFlow Industrial"
  legalName: "AquaFlow Industrial Co., Ltd."
  foundingDate: "2003"
  numberOfEmployees: 180
  description: >
    Manufacturer of industrial centrifugal, submersible and magnetic
    drive pumps for chemical processing, water treatment and food
    manufacturing applications.
  logo:
    url: "/logo.png"
    width: 512
    height: 512
  address:
    streetAddress: "No. 88 Industrial Road, Nanhai District"
    addressLocality: "Foshan"
    addressRegion: "Guangdong"
    postalCode: "528200"
    addressCountry: "CN"
  contactPoints:
    - telephone: "+86-757-1234-5678"
      contactType: "sales"
      email: "info@aquaflowpumps.com"
      availableLanguage: ["English", "Chinese"]
      areaServed: ["US", "GB", "DE", "AU", "CA"]
  credentials:
    - name: "ISO 9001:2015 Quality Management System"
      category: "certification"
      recognizedBy: "TÜV Rheinland"
    - name: "CE Marking (EN 809)"
      category: "certification"
      recognizedBy: "European Union"
  sameAs:
    - "https://www.linkedin.com/company/aquaflow-industrial"
    - "https://www.thomasnet.com/profile/aquaflow-industrial"
    - "https://www.europages.co.uk/AQUAFLOW-INDUSTRIAL"

persons:
  - slug: "michael-chen"
    name: "Michael Chen"
    givenName: "Michael"
    familyName: "Chen"
    jobTitle: "Senior Pump Systems Engineer"
    image: "/authors/michael-chen.jpg"
    email: "michael.chen@aquaflowpumps.com"
    description: >
      Pump systems engineer with 15 years of experience in centrifugal
      pump hydraulic design, NPSH optimization, and corrosion-resistant
      material selection for chemical service applications.
    alumniOf: "South China University of Technology"
    credentials:
      - name: "B.Eng in Mechanical Engineering"
        category: "degree"
      - name: "Hydraulic Institute Member"
        category: "membership"
    knowsAbout:
      - "Centrifugal pump hydraulic design"
      - "NPSH analysis and cavitation prevention"
      - "Corrosion-resistant material selection"
      - "API 610 compliance"
      - "Magnetic drive pump applications"
    sameAs:
      - "https://www.linkedin.com/in/michaelchen-pumps"

site:
  url: "https://www.aquaflowpumps.com"
  defaultLocale: "en-US"

这份文件的三个作用

  1. Schema 生成的数据源
  2. 第 39 篇要求的 NAP 标准(所有平台复制这里的值)
  3. 页面模板里显示公司信息时也读这份文件(保证前端显示和 Schema 一致)

二、按 page_type 自动选择 Schema 类型

映射规则写死在脚本里,不需要每次判断:

page_type Schema 类型 关联实体
首页 Organization + WebSite
about Organization
pillar Organization + BreadcrumbList Organization
cluster-sku Product + BreadcrumbList Organization(manufacturer)
cluster-app TechArticle + BreadcrumbList Person(author)+ Organization
cluster-resource TechArticle + BreadcrumbList Person(author)+ Organization
author 主页 Person Organization(worksFor)
含 FAQ 的任意页 追加 FAQPage

三、生成脚本

// scripts/schema-generate.mjs
import fs from 'fs/promises';
import yaml from 'js-yaml';

const entities = yaml.load(
  await fs.readFile('src/data/entities.yaml', 'utf8')
);
const registry = JSON.parse(await fs.readFile('registry.json', 'utf8'));

const SITE = entities.site.url;
const ORG_ID = `${SITE}/#organization`;
const personId = slug => `${SITE}/authors/${slug}/#person`;

// ── Organization Schema ──
function buildOrganization() {
  const o = entities.organization;
  return {
    "@context": "https://schema.org",
    "@type": "Organization",
    "@id": ORG_ID,
    "name": o.name,
    "legalName": o.legalName,
    "url": SITE,
    "description": o.description.trim(),
    "foundingDate": o.foundingDate,
    "logo": {
      "@type": "ImageObject",
      "url": SITE + o.logo.url,
      "width": o.logo.width,
      "height": o.logo.height,
    },
    ...(o.numberOfEmployees && {
      "numberOfEmployees": {
        "@type": "QuantitativeValue",
        "value": o.numberOfEmployees,
      },
    }),
    "address": { "@type": "PostalAddress", ...o.address },
    "contactPoint": (o.contactPoints ?? []).map(c => ({
      "@type": "ContactPoint",
      ...c,
    })),
    "hasCredential": (o.credentials ?? []).map(c => ({
      "@type": "EducationalOccupationalCredential",
      "credentialCategory": c.category,
      "name": c.name,
      ...(c.recognizedBy && {
        "recognizedBy": { "@type": "Organization", "name": c.recognizedBy },
      }),
    })),
    "sameAs": o.sameAs ?? [],
  };
}

// ── Person Schema ──
function buildPerson(p) {
  return {
    "@context": "https://schema.org",
    "@type": "Person",
    "@id": personId(p.slug),
    "name": p.name,
    "givenName": p.givenName,
    "familyName": p.familyName,
    "jobTitle": p.jobTitle,
    "url": `${SITE}/authors/${p.slug}/`,
    "image": SITE + p.image,
    "email": p.email,
    "description": p.description.trim(),
    "worksFor": { "@id": ORG_ID },      // 引用 Organization
    ...(p.alumniOf && {
      "alumniOf": { "@type": "CollegeOrUniversity", "name": p.alumniOf },
    }),
    "hasCredential": (p.credentials ?? []).map(c => ({
      "@type": "EducationalOccupationalCredential",
      "credentialCategory": c.category,
      "name": c.name,
    })),
    "knowsAbout": p.knowsAbout ?? [],
    "sameAs": p.sameAs ?? [],
  };
}

// ── Product Schema(SKU 页) ──
function buildProduct(row) {
  return {
    "@context": "https://schema.org",
    "@type": "Product",
    "name": row.product_name ?? row.pkw,
    "description": row.description,
    ...(row.model && { "model": row.model }),
    "brand": { "@type": "Brand", "name": entities.organization.name },
    "manufacturer": { "@id": ORG_ID },
    ...(row.specs && {
      "additionalProperty": Object.entries(row.specs).map(([k, v]) => ({
        "@type": "PropertyValue",
        "name": k,
        "value": String(v),
      })),
    }),
    "offers": {
      "@type": "Offer",
      "availability": "https://schema.org/InStock",
      "seller": { "@id": ORG_ID },
    },
  };
}

// ── TechArticle Schema ──
function buildTechArticle(row) {
  return {
    "@context": "https://schema.org",
    "@type": "TechArticle",
    "headline": row.title ?? row.pkw,
    "description": row.description,
    "datePublished": row.publishedAt,
    "dateModified": row.updatedAt ?? row.publishedAt,
    "author": { "@id": personId(row.author_slug ?? entities.persons[0].slug) },
    "publisher": { "@id": ORG_ID },
    "mainEntityOfPage": {
      "@type": "WebPage",
      "@id": SITE + row.page_slug,
    },
    ...(row.about && {
      "about": row.about.map(t => ({ "@type": "Thing", "name": t })),
    }),
    ...(row.citations && {
      "citation": row.citations.map(c => ({
        "@type": "CreativeWork",
        "name": c,
      })),
    }),
  };
}

// ── BreadcrumbList ──
function buildBreadcrumb(row) {
  const parts = row.page_slug.split('/').filter(Boolean);
  const items = [{ name: 'Home', url: SITE + '/' }];
  let acc = '';
  for (const part of parts) {
    acc += '/' + part;
    items.push({
      name: part.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
      url: SITE + acc + '/',
    });
  }
  return {
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": items.map((it, i) => ({
      "@type": "ListItem",
      "position": i + 1,
      "name": it.name,
      ...(i < items.length - 1 && { "item": it.url }),
    })),
  };
}

// ── FAQPage(从页面 FAQ 数据构建) ──
function buildFaqPage(faqs) {
  return {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    "mainEntity": faqs.map(f => ({
      "@type": "Question",
      "name": f.question,
      "acceptedAnswer": { "@type": "Answer", "text": f.answer },
    })),
  };
}

// ── 按 page_type 分发 ──
const output = {};

// Organization(首页 + about)
output['/'] = [buildOrganization(), {
  "@context": "https://schema.org",
  "@type": "WebSite",
  "@id": `${SITE}/#website`,
  "url": SITE,
  "name": entities.organization.name,
  "publisher": { "@id": ORG_ID },
}];
output['/about/'] = [buildOrganization()];

// Person(每个作者主页)
for (const p of entities.persons) {
  output[`/authors/${p.slug}/`] = [buildPerson(p)];
}

// 注册表页面
const seen = new Set();
for (const row of registry) {
  if (row.status === 'deprecated') continue;
  if (seen.has(row.page_slug)) continue;
  seen.add(row.page_slug);

  const schemas = [];

  switch (row.page_type) {
    case 'pillar':
      schemas.push(buildOrganization(), buildBreadcrumb(row));
      break;
    case 'cluster-sku':
      schemas.push(buildProduct(row), buildBreadcrumb(row));
      break;
    case 'cluster-app':
    case 'cluster-resource':
      schemas.push(buildTechArticle(row), buildBreadcrumb(row));
      break;
  }

  if (row.faqs?.length) schemas.push(buildFaqPage(row.faqs));

  if (schemas.length) output[row.page_slug] = schemas;
}

await fs.mkdir('src/data/generated', { recursive: true });
await fs.writeFile(
  'src/data/generated/schemas.json',
  JSON.stringify(output, null, 2)
);

console.log(JSON.stringify({
  summary: {
    pagesWithSchema: Object.keys(output).length,
    organizationSchemas: Object.values(output)
      .flat().filter(s => s['@type'] === 'Organization').length,
    personSchemas: entities.persons.length,
    productSchemas: Object.values(output)
      .flat().filter(s => s['@type'] === 'Product').length,
    techArticleSchemas: Object.values(output)
      .flat().filter(s => s['@type'] === 'TechArticle').length,
  },
  outputFile: 'src/data/generated/schemas.json',
}, null, 2));

四、引用完整性校验脚本

生成后必须验证 @id 引用链完整——这是最容易出问题也最容易被忽略的部分。

// scripts/schema-validate.mjs
import fs from 'fs/promises';

const schemas = JSON.parse(
  await fs.readFile('src/data/generated/schemas.json', 'utf8')
);

// 收集所有已定义的 @id
const definedIds = new Set();
for (const list of Object.values(schemas)) {
  for (const s of list) {
    if (s['@id']) definedIds.add(s['@id']);
  }
}

// 递归查找所有 @id 引用(形如 { "@id": "..." } 但自身没有 @type)
function findReferences(obj, path = '', refs = []) {
  if (Array.isArray(obj)) {
    obj.forEach((v, i) => findReferences(v, `${path}[${i}]`, refs));
    return refs;
  }
  if (obj && typeof obj === 'object') {
    const keys = Object.keys(obj);
    // 纯引用节点:只有 @id,没有 @type
    if (keys.length === 1 && keys[0] === '@id') {
      refs.push({ id: obj['@id'], path });
      return refs;
    }
    for (const [k, v] of Object.entries(obj)) {
      if (k === '@id') continue;
      findReferences(v, path ? `${path}.${k}` : k, refs);
    }
  }
  return refs;
}

const errors = [];
const warns = [];

// 检查 1:引用的 @id 是否已定义
for (const [page, list] of Object.entries(schemas)) {
  for (const s of list) {
    const refs = findReferences(s);
    for (const ref of refs) {
      if (!definedIds.has(ref.id)) {
        errors.push({
          type: 'dangling_reference',
          page,
          schemaType: s['@type'],
          missingId: ref.id,
          fieldPath: ref.path,
        });
      }
    }
  }
}

// 检查 2:必填字段
const REQUIRED_FIELDS = {
  Organization: ['name', 'url', 'address'],
  Person: ['name', 'jobTitle', 'worksFor'],
  Product: ['name', 'description', 'brand'],
  TechArticle: ['headline', 'author', 'publisher', 'datePublished'],
  FAQPage: ['mainEntity'],
  BreadcrumbList: ['itemListElement'],
};

for (const [page, list] of Object.entries(schemas)) {
  for (const s of list) {
    const required = REQUIRED_FIELDS[s['@type']] ?? [];
    const missing = required.filter(f => !s[f]);
    if (missing.length) {
      errors.push({
        type: 'missing_required_fields',
        page,
        schemaType: s['@type'],
        missing,
      });
    }
  }
}

// 检查 3:EEAT 建议字段(缺失不致命但影响效果)
const RECOMMENDED = {
  Organization: ['sameAs', 'hasCredential', 'foundingDate', 'logo'],
  Person: ['sameAs', 'knowsAbout', 'hasCredential', 'image'],
  Product: ['additionalProperty', 'manufacturer'],
  TechArticle: ['citation', 'dateModified'],
};

for (const [page, list] of Object.entries(schemas)) {
  for (const s of list) {
    const rec = RECOMMENDED[s['@type']] ?? [];
    const missing = rec.filter(f =>
      !s[f] || (Array.isArray(s[f]) && s[f].length === 0)
    );
    if (missing.length) {
      warns.push({
        type: 'missing_recommended_fields',
        page,
        schemaType: s['@type'],
        missing,
        impact: 'EEAT 信号减弱',
      });
    }
  }
}

// 检查 4:NAP 一致性(Organization 的 name 在所有实例中必须相同)
const orgNames = new Set(
  Object.values(schemas).flat()
    .filter(s => s['@type'] === 'Organization')
    .map(s => s.name)
);
if (orgNames.size > 1) {
  errors.push({
    type: 'inconsistent_org_name',
    found: [...orgNames],
    note: '违反 NAP 一致性要求(第 39 篇)',
  });
}

console.log(JSON.stringify({
  summary: {
    pagesChecked: Object.keys(schemas).length,
    definedIds: [...definedIds],
    errorCount: errors.length,
    warnCount: warns.length,
    verdict: errors.length === 0 ? 'PASS' : 'FAIL',
  },
  errors,
  warns,
}, null, 2));

process.exit(errors.length > 0 ? 1 : 0);

五、Skill 说明书

# Skill:Author 与 Organization Schema 批量生成

## 何时使用
- 首次为站点配置 Schema
- 新增作者或页面后同步 Schema
- 修改公司信息后全站更新

## 输入
- `src/data/entities.yaml`(实体配置,需先建好)
- `registry.json`

## 执行步骤

### Step 1:检查 entities.yaml 完整性(你来做)

必填检查:
□ organization.legalName 是法定注册名(不是简称)
□ organization.address 完整(含邮编和国家代码)
□ organization.sameAs 至少包含 LinkedIn 公司页
□ 每个 person 有 jobTitle(具体职位,不是 "Engineer")
□ 每个 person 的 sameAs 至少包含 LinkedIn 个人档案
□ 每个 person 的 knowsAbout 与站点内容主题对应

如 sameAs 为空 → 提示用户先完成第 39 篇的实体绑定,
Schema 里的 sameAs 是跨来源验证的入口,为空则 Schema 的
EEAT 价值大幅降低。

### Step 2:补全需要表达的字段(你来写)

脚本无法生成的字段:
- `organization.description`:60–100 词,含主营产品和服务行业
- `person.description`:50–80 词,含工作年限、专长、经验规模
- `person.knowsAbout`:3–6 项,必须与该作者实际写的内容主题一致

**knowsAbout 的写法要求**
用具体的技术领域,不用泛化描述
✅ "NPSH analysis and cavitation prevention"
❌ "Pump engineering"
❌ "Industrial equipment"

### Step 3:运行生成脚本
执行 `scripts/schema-generate.mjs`

### Step 4:运行校验脚本
执行 `scripts/schema-validate.mjs`

- verdict = PASS → 进入 Step 5
- verdict = FAIL → 按 errors 修复 entities.yaml 或注册表,重新生成

**dangling_reference 错误的处理**
说明某个 Schema 引用了不存在的 @id,通常是:
- TechArticle 的 author_slug 在 entities.yaml 的 persons 里没有对应项
- 修复:在 entities.yaml 添加该作者,或修正注册表的 author_slug

### Step 5:线上验证
部署后,用 Google Rich Results Test 验证 3–5 个代表性页面:
- 首页(Organization + WebSite)
- 一个 SKU 页(Product)
- 一个作者主页(Person)
- 一篇技术文章(TechArticle + FAQPage)

## 输出格式

Schema 批量生成报告

实体配置:[N] 个组织,[N] 位作者 生成页面:[N] 个 校验结果:[PASS/FAIL]

Schema 分布

类型 数量
Organization 12
Person 2
Product 8
TechArticle 24
FAQPage 18
BreadcrumbList 44

ERROR([N] 项)

E1. 引用悬空

  • 页面:/blog/npsh-explained/
  • Schema:TechArticle
  • 字段:author
  • 引用的 @id:https://…/authors/sarah-liu/#person
  • 问题:entities.yaml 的 persons 中无 sarah-liu
  • 修复:在 entities.yaml 添加该作者,或修正注册表的 author_slug

WARN([N] 项)

W1. 缺少 EEAT 建议字段

  • 页面:/authors/michael-chen/
  • Schema:Person
  • 缺少:sameAs
  • 影响:无外部档案链接,Google 无法跨来源验证该人物实体
  • 建议:补充 LinkedIn 个人档案 URL

线上验证清单

□ 首页 Rich Results Test 无 Error □ SKU 页 Product Schema 被识别 □ 作者主页 Person Schema 被识别 □ 文章页 TechArticle + FAQPage 均被识别


## 禁止行为
- 不要手工编辑 generated/schemas.json(改 entities.yaml 后重新生成)
- sameAs 为空时不要跳过警告——这是 Schema 的核心价值所在
- knowsAbout 不要写与站点内容无关的领域(Google 会核对一致性)
- 不要为不存在的人物创建 Person Schema(第 37 篇的原则)

六、在 Astro 中消费生成结果

生成的 schemas.json 由 SEOHead 组件读取:

---
// src/components/seo/SchemaFromRegistry.astro
import schemas from '../../data/generated/schemas.json';

const pathname = Astro.url.pathname;
const pageSchemas = schemas[pathname] ?? [];
---

{pageSchemas.map(schema => (
  <script type="application/ld+json" set:html={JSON.stringify(schema)} />
))}

为什么用生成文件而不是运行时构建:生成阶段可以跑校验,运行时构建无法验证引用完整性。生成 + 校验 + 消费三步分离,问题在部署前就能发现。


七、与第 38 篇的关系

第 38 篇 本篇
内容 Schema 字段含义和完整代码示例 批量生成的工程化方案
适用 理解 Schema 该写什么 站点有 20+ 页面时的规模化
数据源 手写在页面里 entities.yaml 单一事实来源
校验 靠 Rich Results Test 逐页查 脚本批量校验引用完整性

先读第 38 篇理解 Schema 结构,再用本篇的方案规模化。


→ AI Skill:竞品关键词差距分析

NEXT ACTION / 下一步

继续系列:AI 外贸站建设全系列

把读到的方法变成一个小行动,完成后再回来迭代。

继续

RELATED / 相关推荐

接着读这些

按同一分类、系列与标签为你挑选。