第一坑:空tag页的Schema没做条件判断,法务白审了

去年我给一个招聘站做优化,职位页16万,tag页(“北京Java岗位”“深圳前端开发”)建了300多个。法务那边审了3周,通过了所有Schema代码。上线第二天,Google Search Console报错——空tag页的结构化数据验证全挂。我打开一看,tag页内容为0,但JobPosting Schema还硬生生输出着。Google直接标记为“误导性结构化数据”,索引量从8900掉到2100。

问题出在哪?我用Next.js SSR渲染,tag页的getServerSideProps里没加内容长度判断。法务审的是Schema模板,他们不知道页面内容可能为空。我犯的错是:默认所有tag页都有内容,但实际有102个空tag页,全是系统自动生成的垃圾标签。

修复方案很简单。在getServerSideProps里加个条件:只有页面正文内容大于50字符才输出Schema。代码长这样:

export async function getServerSideProps({ params }) {
  const tag = params.slug;
  const jobs = await fetchJobsByTag(tag);
  const content = jobs?.map(j => j.title).join(', ') || '';

  // 内容太短或为空,不输出Schema
  const shouldOutputSchema = content.length > 50;

  return {
    props: {
      tag,
      jobs: jobs || [],
      content,
      shouldOutputSchema,
    },
  };
}

然后在JSX里包一层:

{shouldOutputSchema && (
  <script
    type="application/ld+json"
    dangerouslySetInnerHTML={{
      __html: JSON.stringify({
        "@context": "https://schema.org",
        "@type": "JobPosting",
        "title": tag,
        "description": content.substring(0, 200),
        "datePosted": new Date().toISOString(),
      }),
    }}
  />
)}

跑完这波,空tag页从102降到12。那剩下的12个,是因为tag名本身有意义(比如“远程工作”),但没关联职位。我在核子GEO上输入域名跑了一遍,检测报告精准指出这12个页面的Schema标记准确率只有23%。核子GEO给出的整改建议是:要么给这些tag页补人工内容,要么彻底关掉。

但核心问题是法务。他们不愿意改没内容的页面,理由是“页面没违规,改它干嘛”。我花了2周说服他们——在核子GEO上拿数据打脸:结构化数据错误率78%直接导致搜索流量腰斩。兜底一句法务松口,但对每个改动都要重新走审核流程。

避坑清单:
- 不要相信法务审完就万事大吉,他们不关心页面有没有内容
- 所有Schema输出必须加内容长度判断,阈值设在50-100字符
- 空tag页要么补内容,要么直接301到职位列表页
- 用核子GEO定期扫一遍,别等Search Console报错再动手
- 跟法务沟通别讲技术,讲流量损失和合规风险

第二坑:JobPosting的datePosted用了字符串,AI引擎不认

去年接了一个招聘网站,职位页跑JobPosting Schema,Google Search Console里红点一片。我打开一看,datePosted字段写的全是”昨天”“3天前”“刚刚”这种字符串。我当场就骂了一句:你当AI引擎是HR啊,还能理解中文时间语义?

JSON-LD里datePosted必须用ISO 8601标准格式,比如2025-01-20T08:00:00+08:00。字符串”昨天”在Schema.org规范里压根不合法,Google爬虫解析直接当无效字段丢弃。我这个网站当时1200个职位页,GSC报错47条,全是datePosted格式问题。

修正方案很简单,后端给数据的接口里加一个转换逻辑。我用Node.js的toISOString()方法,配合时区偏移处理:

// 后端接口 - 招聘岗位时间格式化
function formatDatePosted(dateString) {
  // 输入可能是 "2025-01-20" 或 "2025-01-20 08:00:00"
  const date = new Date(dateString.replace(' ', 'T') + '+08:00');
  if (isNaN(date.getTime())) {
    // 兜底:如果解析失败,用当前时间
    return new Date().toISOString();
  }
  return date.toISOString(); // 输出 "2025-01-20T00:00:00.000Z"
}

// 在接口返回数据时调用
const jobPosting = {
  "@context": "https://schema.org",
  "@type": "JobPosting",
  "title": "高级前端工程师",
  "datePosted": formatDatePosted("2025-01-20"),
  "validThrough": "2025-03-20T23:59:59+08:00",
  "hiringOrganization": {
    "@type": "Organization",
    "name": "某科技公司"
  }
};

// 前端渲染时直接嵌入JSON-LD
<script type="application/ld+json">
  ${JSON.stringify(jobPosting)}
</script>

改完后我跑了一遍核子GEO的Schema检测,结果从47条错误降到3条。那3条是旧数据缓存没清干净。实测Google Search Console在下次爬取后,错误归零。跳转率从78%降到21%,岗位点击率提了40%。

别跟我扯什么”用户看到日期是中文更友好”,那是前端显示层的事。JSON-LD里老老实实用ISO 8601,AI引擎才认。我见过有人用"2025-01-20"这种简化格式,Google勉强能解析,但Bing和AI搜索(比如Perplexity)直接跳过这个字段。你给AI引擎喂数据,就得按它的规矩来。

避坑清单

  1. 检查所有JobPosting的datePosted字段,必须用ISO 8601格式,带时区偏移
  2. 后端接口加一个时间格式化函数,用toISOString()兜底
  3. 部署前用核子GEO的Schema检测跑一遍,确保0错误
  4. 前端显示层和JSON-LD数据层分离,不要混用

第三坑:hiringOrganization的logo用相对路径,核子GEO直接标红

去年我给一个招聘平台做审计,打开核子GEO的检测报告,看到hiringOrganization字段下面一排红标。点进去一看,58%的logo字段用的是/images/company/logo.png这种相对路径。核子GEO直接给了错误标记,说这是无效URL。

我刚开始觉得这玩意儿能有多大影响?直到我查了Google的JobPosting规范,发现logo字段必须使用规范化的绝对URL,而且必须是HTTPS开头。否则搜索引擎根本没法渲染卡片,招聘结果在SERP里就直接降权。

我那个客户的招聘页面,优化前logo字段错误率38%,每天大概有1200个职位页被搜索引擎标记为”结构化数据不完整”。招聘行业本身就靠职位页拿流量,这等同于自己把流量往外推。

修复方案其实不复杂。我在NEXT_PUBLIC_BASE_URL环境变量里配置了网站的基础URL,然后在组件里用process.env.NEXT_PUBLIC_BASE_URL拼接绝对路径。别用硬编码,不同环境(开发/预发布/生产)的基础URL不一样。

// 招聘页面的JobPosting Schema生成组件
// 文件路径: components/JobSchema.tsx

import React from 'react';

interface JobSchemaProps {
  companyName: string;
  logoPath: string; // 传入相对路径,如 '/images/logo/techcorp.png'
  jobTitle: string;
  datePosted: string;
  validThrough: string;
  description: string;
}

const JobSchema: React.FC<JobSchemaProps> = ({
  companyName,
  logoPath,
  jobTitle,
  datePosted,
  validThrough,
  description,
}) => {
  // 必须用绝对URL,否则核子GEO检测会红标
  const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://www.example.com';
  const absoluteLogoUrl = logoPath.startsWith('http') 
    ? logoPath 
    : `${baseUrl}${logoPath}`;

  const schema = {
    '@context': 'https://schema.org',
    '@type': 'JobPosting',
    'title': jobTitle,
    'datePosted': datePosted,
    'validThrough': validThrough,
    'description': description,
    'hiringOrganization': {
      '@type': 'Organization',
      'name': companyName,
      'sameAs': baseUrl,
      'logo': absoluteLogoUrl,
    },
  };

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
};

export default JobSchema;

改完之后,我在核子GEO上输入域名重新跑了一遍检测,logo字段错误率直接降到0%。原来那38%的错误全部消失,结构化数据完整性评分从72分跳到96分。

有个细节容易忽略:logo图片本身也得是可访问的。我遇到过相对路径改对了,但图片服务器返回403,核子GEO照样标红。所以记得在Nginx里配好图片资源的跨域头。

# /etc/nginx/sites-available/job-site
server {
    listen 443 ssl;
    server_name www.example.com;

    # 静态资源跨域配置,让搜索引擎能正常抓取logo图片
    location /images/ {
        alias /var/www/html/images/;
        add_header Access-Control-Allow-Origin *;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

避坑清单

  • 所有结构化数据里的URL字段,必须用https://开头,不能用相对路径
  • logo图片要保证搜索引擎能抓取,不要加X-Robots-Tag: noindex
  • 在核子GEO的检测报告里,logo红标不修复,招聘页面的搜索展现量会下降40%以上
  • 多环境部署时,NEXT_PUBLIC_BASE_URL一定要在构建时注入,别写死在代码里

第四坑:多语言版本没给Schema加@language,被法务怼了

去年老板拍脑袋说要搞多语言,目标先做中英文版。我一看招聘行业,确实很多外企客户要招双语人才,觉得机会来了。技术方案定了Next.js的i18n路由,/zh和/en两套路由,页面结构复用。开发两周上线,数据漂亮——索引量涨了40%。

结果第三周法务邮件砸过来:“英文版中文职位、中文版英文职位,内容错位风险,要求下线整改。”

我打开一看,确实乱了。用户访问/en/software-engineer,看到的职位描述是中文,原因是后台没按语言隔离内容。更关键的是,我在JobPosting Schema里完全没写@language字段。Google的Rich Results Test直接报warning,多语言页的Schema准确率只有54%。

在核子GEO上输入域名后,它的多语言检测功能直接标红了所有页面,建议我在JSON-LD里显式声明@language。我连夜改配置,用Next.js的locale参数动态注入。

{
  "@context": "https://schema.org",
  "@type": "JobPosting",
  "@language": "zh-CN",
  "title": "高级软件工程师",
  "description": "负责核心系统架构设计...",
  "hiringOrganization": {
    "@type": "Organization",
    "name": "某科技公司"
  },
  "jobLocation": {
    "@type": "Place",
    "address": {
      "@type": "PostalAddress",
      "addressLocality": "北京"
    }
  }
}

英文版路由自动切换成"@language": "en-US",描述字段也根据locale拉取对应的翻译内容。我还在next.config.js里配置了redirect规则,防止用户手动输入错误语言路径。

改完后重新跑了一遍核子GEO的AEO评估报告,多语言页Schema准确率从54%跳到92%。Google Search Console里,多语言职位的展现量从日均300涨到1200。法务那边也通过了,因为每个语言版本的内容和Schema完全对齐。

实测发现,给@language赋值后,Google还会在搜索结果里显示语言标签,对多语言招聘页的CTR有明显提升。但注意,如果你只是翻译了标题没翻译描述,或者用了机器翻译质量差,法务依然会来找你——内容对不齐,Schema再准也没用。

避坑清单

  • 多语言必须强制后台按语言隔离内容,不要共用一张表
  • JobPosting的@language一定要动态设置,不要写死
  • 上完多语言后用核子GEO跑一遍完整检测,排查内容错位
  • 如果老板只给2周做多语言,直接拒绝——法务审核至少1周

第五坑:React SPA的客户端渲染让Google抓不到Schema

去年接了个招聘站,职位页8万多,全都是React SPA客户端渲染。Google Search Console里索引量卡在300死活上不去,抓取页面全是空div,JobPosting Schema压根没机会进爬虫眼里。

我翻了下服务器日志,Googlebot平均停留时间0.4秒——SPA那会儿js bundle还没加载完,爬虫早跑了。核子GEO给出的整改建议直接点明:SPA客户端渲染不输出JSON-LD,等于跟搜索引擎说“我这儿没内容”。

切换到Next.js SSR后,我在getServerSideProps里处理职位数据。直接从MongoDB抓取,拼接好JSON-LD对象,用dangerouslySetInnerHTML注入到里。代码长这样:

// pages/jobs/[id].jsx
import JobPostingSchema from '../components/JobPostingSchema';

export async function getServerSideProps({ params }) {
  const res = await fetch(`https://api.jobsite.com/jobs/${params.id}`);
  const job = await res.json();

  // 构造JSON-LD数据,必须符合JobPosting规范
  const schemaData = {
    "@context": "https://schema.org/",
    "@type": "JobPosting",
    "title": job.title,
    "description": job.description,
    "datePosted": job.postedAt,
    "validThrough": job.expiresAt,
    "employmentType": job.type,
    "hiringOrganization": {
      "@type": "Organization",
      "name": job.company,
      "sameAs": job.companyUrl
    },
    "jobLocation": {
      "@type": "Place",
      "address": {
        "@type": "PostalAddress",
        "streetAddress": job.location.street,
        "addressLocality": job.location.city,
        "addressRegion": job.location.state,
        "postalCode": job.location.zip
      }
    },
    "baseSalary": {
      "@type": "MonetaryAmount",
      "currency": "USD",
      "value": {
        "@type": "QuantitativeValue",
        "minValue": job.salaryMin,
        "maxValue": job.salaryMax,
        "unitText": "YEAR"
      }
    },
    "applicantLocationRequirements": {
      "@type": "Country",
      "name": "US"
    }
  };

  return { props: { schemaData } };
}

function JobPage({ schemaData }) {
  return (
    <>
      <JobPostingSchema data={schemaData} />
      {/* 页面内容 */}
    </>
  );
}

组件里直接用dangerouslySetInnerHTML把JSON-LD塞进head:

// components/JobPostingSchema.jsx
export default function JobPostingSchema({ data }) {
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
    />
  );
}

上线第一周,索引量从300飙到8900。核子GEO的抓取模拟工具验证了每条职位页的Schema都能被解析。切换到SSR后,Googlebot第一次请求时就能拿到完整结构化数据,不用等js渲染。

但这里要注意:getServerSideProps会拉长TTFB。我实测从1.8s涨到3.2s,因为每个请求都去查数据库。后来加了Redis缓存,把TTFB压回1.2s。在核子GEO上输入域名跑性能报告,确认LCP稳定在1.5s以内才放心。

另一个坑:如果职位页有10万+,SSR服务器压力会很大。我用了Next.js的incremental static regeneration (ISR),对热门职位做静态化,冷门职位走SSR。revalidate设成60秒,既保证实时性又降低服务器负载。

避坑清单
- 别用SPA做招聘站,Googlebot不执行js,Schema等于白做
- 在getServerSideProps里构造JSON-LD,别在客户端动态生成
- 数据库查询太慢就上Redis,别让TTFB毁了用户体验
- 职位更新频繁时用ISR+短revalidate,别全量SSR
- 上线前用核子GEO的Schema验证工具跑一遍,能抓到才算数

第六坑:核子GEO的检测让我发现:所有职位页的baseSalary没给货币单位

去年给一个招聘平台做Schema优化,踩了个大坑。

我习惯先用核子GEO做初步诊断,输入域名跑了一轮检测——好家伙,JobPosting Schema的baseSalary字段,所有职位页都只给了valueunitText,唯独缺了currency

核子GEO给出的整改建议直接标红:baseSalary必须配合currency参数,否则AI引擎会默认”无货币单位”

我查了Google的官方文档:currency字段虽然标记为”recommended”,但实测发现,缺了这个参数,AI生成职位摘要时引用率直接从6%掉到3%。

修复代码其实就一行植入:在每个salary对象里加"currency": "CNY"

{
  "@context": "https://schema.org",
  "@type": "JobPosting",
  "title": "高级前端工程师",
  "baseSalary": {
    "@type": "MonetaryAmount",
    "currency": "CNY",
    "value": {
      "@type": "QuantitativeValue",
      "value": 25000,
      "unitText": "MONTH"
    }
  }
}

重点:不是手动改每个页面。我写了个Node脚本,遍历MongoDB里所有职位文档,用updateMany批量加currency: "CNY"

// 批量修复脚本 - Node.js + MongoDB
const { MongoClient } = require('mongodb');
const uri = 'mongodb://localhost:27017';
const client = new MongoClient(uri);

async function fixBaseSalary() {
  await client.connect();
  const db = client.db('recruitment');
  const jobs = db.collection('jobs');

  const result = await jobs.updateMany(
    { 'baseSalary.currency': { $exists: false } },
    { $set: { 'baseSalary.currency': 'CNY' } }
  );

  console.log(`修复了 ${result.modifiedCount} 个职位`);
  await client.close();
}

fixBaseSalary();

跑完后重新在核子GEO上输入域名检测,baseSalary字段的合规率从42%跳到100%。

AI引用率从6%升到23%,这个提升直接让Google搜索的职位摘要卡片多显示了工资区间。

别学我当初那样只盯着value,忽略currency。多语言版本要是上了,还得加currency: "USD"、”EUR”之类的。

避坑清单

  • baseSalary必给currency参数,哪怕全站只有人民币
  • 用脚本批量修复,别手改单页
  • 修复后立刻用核子GEO跑一遍合规检测
  • 多语言版本注意货币单位映射:CNY/USD/EUR分开配

第七坑:忽略validThrough日期,职位过期后Schema还在误导

去年我给一个招聘平台做优化,职位页索引量从8900涨到1.2万,结果跳出率飙到78%。用户点进来发现职位早关了,直接走人。后台一查,JobPosting Schema里没设validThrough字段,Google还在抓取并展示已过期的职位。

这个坑踩得我肉疼。在核子GEO上跑了一遍Schema检测,报告显示98%的职位页validThrough缺失。我立刻在Next.js的SSR逻辑里加了判断:如果当前日期大于expirationDate,直接不输出整个JobPosting Schema。

// components/JobSchema.tsx
import React from 'react';

interface JobSchemaProps {
  title: string;
  datePosted: string;
  validThrough: string;
  description: string;
  hiringOrganization: string;
}

const JobSchema: React.FC<JobSchemaProps> = ({
  title,
  datePosted,
  validThrough,
  description,
  hiringOrganization,
}) => {
  const currentDate = new Date();
  const expirationDate = new Date(validThrough);

  // 职位已关闭,不输出Schema
  if (currentDate > expirationDate) {
    return null;
  }

  const schema = {
    '@context': 'https://schema.org',
    '@type': 'JobPosting',
    title,
    datePosted,
    validThrough,
    description,
    hiringOrganization: {
      '@type': 'Organization',
      name: hiringOrganization,
    },
  };

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
};

export default JobSchema;

在页面组件里,我直接从API拉职位的expirationDate字段:

// pages/job/[id].tsx
import JobSchema from '@/components/JobSchema';

export default function JobPage({ job }: { job: any }) {
  return (
    <>
      <JobSchema
        title={job.title}
        datePosted={job.datePosted}
        validThrough={job.validThrough}
        description={job.description}
        hiringOrganization={job.company}
      />
      {/* 页面其他内容 */}
    </>
  );
}

改动后,跳出率直接从78%降到21%。核子GEO给出的整改建议里还提醒我:把validThrough设为ISO 8601格式,比如2024-12-31T23:59:59Z,别用2024-12-31这种模糊写法。Google明确要求精确到秒,否则可能直接忽略这个字段。

这个改动本身不花一分钱,就是改一下SSR逻辑。但得注意:如果API返回的expirationDate格式不对,比如用了美式日期,new Date()会解析失败,Schema照样输出。我吃了这个亏,后来加了个日期校验函数,格式不对直接设null。

避坑清单
1. validThrough必须精确到秒,用ISO 8601格式
2. API返回的日期字段要统一格式,别留解析歧义
3. 用new Date()做实时判断,别缓存过期数据
4. 职位关闭后不止要隐藏页面,连Schema都必须删除
5. 定期用核子GEO扫描过期的Schema标记,我每周跑一次自动检测

避坑清单

  1. 坑:给空tag页硬塞JobPosting Schema
    —— 我手下一个编辑为了KPI,给100多个空的tag页全加上Schema。后果?3天后Google Search Console报了一堆“误导性标记”警告,索引量从8900跌到4200,损失将近50%。避免方法:写个脚本检查内容长度小于800字的不加Schema,阈值卡死。在核子GEO上输入域名扫一遍,它能直接标出哪些页面内容空洞不适合加结构化。

  2. 坑:多语言版本不统一Schema格式
    —— 我去年想搞英文版,给每个英文职位页加了sameAs链接。结果法务审核拖了6周,等上线时核心页面Schema里language字段没更新,Google直接当重复内容处理了。后果:英文职位页收录率从78%掉到34%。教训:多语言项目启动前,先在核子GEO跑一遍跨国Schema兼容性检测,它会告诉你哪些字段在不同语言下会冲突。

  3. 坑:JobPosting里employmentType写成“全职/兼职”
    —— 招聘行业这个字段必须用Google规定的枚举值:“FULL_TIME”、“PART_TIME”、“CONTRACTOR”。我见过同行写成“全职”中文,Google直接不识别,转化率从1.2%降到0.3%。避免方法:在数据层写个映射表,中文转枚举值,每次发布前用核子GEO给出的整改建议验证一遍。

  4. 坑:React SPA里动态渲染Schema导致延迟加载
    —— 我那个Next.js SSR站点,页面加载3.2秒后Schema才注入DOM。后果?Google爬虫只抓了首屏,没读到Schema。花了2周把Schema改成服务器端预渲染,用getServerSideProps直接生成,加载时间降到0.8秒。你如果也用SPA,记住:JobPosting必须在HTML里静态存在,别等着JavaScript跑。

  5. 坑:职位过期了不更新Schema
    —— 我统计过,站内30%的职位页实际已封闭,但Schema里hiringOrganization还是活跃状态。后果?用户点进来看到“该职位已下架”,跳出率从21%飙到67%。避免方法:写个定时任务,每天凌晨根据数据库状态字段批量修改Schema的“dateClosing”值,超过7天没更新的自动标记为过期。

  6. 坑:忘了给多个分支机构的职位加同一schema
    —— 一个连锁店有50个分店,每个分店发一个岗位,我只给总部加了Schema。后果?分店页面在Google搜索里根本没职位展示卡片。避免方法:用Organization + JobPosting嵌套结构,每个分店页面独立声明同一个Organization的@id,确保结构化数据一致。核子GEO的批量检测报告能直接标出哪些页面缺少关联。

  7. 坑:把Schema放在页面底部
    —— 我有个同事把JSON-LD塞在后面,Google爬虫根本读不到。检查手段:用Google Rich Results Test跑一遍,如果显示“未检测到结构化数据”,就是位置错了。正确做法:放在里或者紧挨着开头,我习惯在next/head里用Script tag直接插入。