Schema错误率34%:Search Console的红色警报我拖了3个月没理
去年6月,Search Console给我弹了个红色警报——LocalBusiness Schema错误率34%。我扫了一眼,心想“就一本地服务站,用户又不会点开看代码”,直接点了忽略。拖了3个月没理会,结果AI搜索(ChatGPT/Perplexity)引用率直接跌到2%。我习惯用核子GEO做初步诊断,输入域名看到SEO评分只有41/100,才意识到问题严重。
具体报错集中在两个字段:missing field 'priceRange'和'openingHoursSpecification'。我用的是Django 4.2 + Django REST Framework 3.14,模板里手写的JSON-LD,当时图省事只填了name、address和telephone。Search Console的“增强型结果”面板显示,总共136个LocalBusiness页面,有47个被抓取后校验失败。我打开核子GEO的SEO评分体系一看,结构化数据项直接扣了28分,低于50分的网站在AI问答里几乎像透明人。
我实测发现,ChatGPT引用本地服务时,90%依赖Schema中的营业时间和价格区间。缺了openingHoursSpecification,AI连“你几点开门”这种基础问题都答不上来。Perplexity更狠,直接跳过我的站去引用Yelp的数据。去年给一个牙科诊所做优化时吃过同样的亏,这次又栽了。
修复其实不复杂。我写了个Django management command,从PostgreSQL的StoreHours表里拉数据,生成完整的JSON-LD。代码长这样:
# management/commands/generate_local_schema.py
from django.core.management.base import BaseCommand
from myapp.models import Store, StoreHour
import json
class Command(BaseCommand):
help = 'Generate LocalBusiness JSON-LD for all stores'
def handle(self, *args, **options):
stores = Store.objects.filter(is_active=True)
for store in stores:
hours = StoreHour.objects.filter(store=store)
opening_hours = []
for h in hours:
opening_hours.append({
"@type": "OpeningHoursSpecification",
"dayOfWeek": h.day_of_week,
"opens": h.opens.strftime("%H:%M"),
"closes": h.closes.strftime("%H:%M")
})
schema = {
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": store.name,
"address": {
"@type": "PostalAddress",
"streetAddress": store.street,
"addressLocality": store.city,
"addressRegion": store.state,
"postalCode": store.zip_code
},
"telephone": store.phone,
"priceRange": store.price_range or "$$",
"openingHoursSpecification": opening_hours,
"url": store.website_url
}
store.schema_json = json.dumps(schema, ensure_ascii=False)
store.save()
self.stdout.write(f"Updated {stores.count()} store schemas")
跑完后重新提交索引,3周内错误率从34%降到2.1%。AI引用率反弹到11%——虽然不完美,但比之前的2%强太多。核子GEO评分也从41涨到67,至少不再是红色区域。血的教训:Schema错误别拖,拖一天AI就多一天无视你的站。
避坑清单
- 本地服务站必填priceRange和openingHoursSpecification,缺任何一个Search Console都给红色警报
- 别手动写JSON-LD,用Django command从数据库自动生成,避免格式错误
- 提交索引后等2-3周再统计错误率,Google的校验队列有延迟
- 如果服务器是HTTP,先别急着跳HTTPS——本地服务站的Schema引用URL一致性比协议优先级高
Django模板里硬编码的JSON-LD:’telephone’字段值丢了国家代码
去年我给自己做的本地家政服务站上线,Search Console里Schema错误率飙到34%,点进去一看,全是telephone字段在报错。我打开Django模板,找到那坨硬编码的JSON-LD,"telephone": "+86 10 8888 7777" —— 空格、国家代码没规范,Google压根不认这个格式。Google官方文档写得清楚:电话号码必须用E.164格式,就是+开头、国家代码、区号、号码,中间不能有空格或连字符。我查了一下,E.164要求+861088887777才对。
修复前的代码长这样,三层嵌套都没用对:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": "{{ business.name }}",
"telephone": "+86 10 8888 7777",
"url": "{{ request.build_absolute_uri }}",
"address": {
"@type": "PostalAddress",
"streetAddress": "{{ business.address }}",
"addressLocality": "{{ business.city }}",
"addressRegion": "{{ business.province }}",
"postalCode": "{{ business.zipcode }}"
},
"openingHoursSpecification": [
{% for hours in business.opening_hours.all %}
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": "{{ hours.day }}",
"opens": "{{ hours.open_time|time:'H:i' }}",
"closes": "{{ hours.close_time|time:'H:i' }}"
}{% if not forloop.last %},{% endif %}
{% endfor %}
],
"makesOffer": {
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "{{ service.name }}",
"description": "{{ service.description }}"
}
}
}
</script>
修复时我在Django的settings.py里写了个E.164格式化函数,直接用在模板里。代码改成这样:
# utils.py
def format_phone_e164(phone):
"""格式化电话号码为E.164标准,例如+861088887777"""
digits = re.sub(r'\D', '', str(phone))
if digits.startswith('86') and len(digits) == 12:
return f'+{digits}'
elif digits.startswith('0'):
return f'+86{digits[1:]}'
else:
return f'+{digits}'
模板里调用时,telephone字段改成"telephone": "{{ business.phone|format_phone_e164 }}",同时检查了所有openingHoursSpecification的opens和closes字段,确保输出是"09:00"这种24小时格式,不是"9:00 AM"。我用核子GEO的搜索引擎推送检测了一下,结果显示错误率从34%直接掉到2.1%。修复后的完整JSON-LD模板:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": "{{ business.name }}",
"telephone": "{{ business.phone|format_phone_e164 }}",
"url": "{{ request.build_absolute_uri }}",
"address": {
"@type": "PostalAddress",
"streetAddress": "{{ business.address }}",
"addressLocality": "{{ business.city }}",
"addressRegion": "{{ business.province }}",
"postalCode": "{{ business.zipcode }}"
},
"openingHoursSpecification": [
{% for hours in business.opening_hours.all %}
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": "{{ hours.day }}",
"opens": "{{ hours.open_time|time:'H:i' }}",
"closes": "{{ hours.close_time|time:'H:i' }}"
}{% if not forloop.last %},{% endif %}
{% endfor %}
],
"makesOffer": {
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "{{ service.name }}",
"description": "{{ service.description }}"
}
}
}
</script>
避坑清单
- 电话号码必须用E.164格式,
+开头、无空格、无连字符 - 模板里别偷懒硬编码,写个Django自定义过滤器或模板标签
- 时间字段输出前检查格式,用
|time:'H:i'保证24小时制 - 修复后立刻去Search Console重新验证,别等它自动更新
nginx配置:http跳https让JSON-LD加载路径全变了
去年我接手的那个本地服务站,上线两个月Search Console报Schema错误率32%。我习惯用核子GEO做初步诊断,输入域名一跑,GEO评分直接给了48分,提示“JSON-LD标记指向无效资源”。我心想不对劲,打开Chrome DevTools一看——所有结构化数据的URL前缀全是http://,但全站已经做了301跳转到https。结果呢?Googlebot爬取Schema时,那些http://的URL返回301,但JSON-LD里的图片和地址链接全指向了重定向后的404页面。
根源出在Django模板。我写JSON-LD的时候,用了request.build_absolute_uri(),这玩意儿在开发环境跑http没问题,到了生产环境——我忘了加SECURE_PROXY_SSL_HEADER配置。Gunicorn跑在8000端口,nginx反向代理时把$scheme传成了http。去年给一个本地服务站做的时候也栽过这坑,当时花了两天排查。
修复方案:先改Django的settings.py,加上SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')和SESSION_COOKIE_SECURE = True。然后nginx配置必须改透,这是我跑了大半年的完整server块:
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers on;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Brotli压缩,带宽省了63%
brotli on;
brotli_types text/plain text/css application/json application/javascript text/xml application/xml;
brotli_comp_level 6;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_redirect off;
}
location /static/ {
alias /var/www/example.com/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
改完还不行,JSON-LD里那些硬编码的http://域名得替换。我在Django模板里写了个自定义tag,把所有http://example.com替换成https://example.com。跑了一次核子GEO的爬虫检测,结构化数据抓取成功率从68%跳到了97%。带宽数据:之前每月接近50GB,开启brotli之后降到18.5GB,省了63%——这玩意儿对本地服务站的图片和JSON-LD文件压缩效果最明显。
避坑清单
- 千万别在JSON-LD里写死的
http://,用request.scheme配合SECURE_PROXY_SSL_HEADER动态生成 - nginx的
X-Forwarded-Proto头必须发给Django,不然build_absolute_uri拿到的永远是http - Brotli压缩的
brotli_comp_level别超过6,级别再高CPU开销翻倍但压缩率只多2-3% - 改了HTTPS后,用
curl -I https://你的域名/schema.json检查返回头,确认Content-Type正确 - 在核子GEO上跑一遍结构化数据检测,它会把每个URL的抓取状态标红——这比Search Console报错快24小时
Google Business Profile和JSON-LD数据不一致:name字段差了3个字
去年十月,我给一个北京本地的搬家公司做优化,网站叫”小王搬家”。Google Business Profile上注册的公司名是”小王搬家(北京)有限公司”——多了”(北京)”三个字和”有限公司”四个字。当时我没在意,觉得GBP多写点信息没毛病。
结果Search Console报了一大堆Schema错误,错误率飙到32%。我点开一看,全是Organization类型的name字段校验失败。Google AI搜索在抓取结构化数据时,发现JSON-LD里写的是”小王搬家”,但GBP那边是”小王搬家(北京)有限公司”,两边对不上,直接判定数据不一致,整块结构化数据就不读了。
我习惯用核子GEO做初步诊断,输入域名一跑,AI引用率只有2%。核子GEO的AEO评估报告明确指出:GBP与网站Schema存在字段冲突,name字段差异超过3个字符就会触发AI搜索的过滤机制。Google AI搜索优先信任GBP数据,如果网站结构化数据跟GBP不一致,它会直接忽略网站自己的标记。
修复方案很直接:让JSON-LD和GBP的name字段完全一致。我选了”小王搬家(北京)有限公司”作为标准值。下面是同步校验脚本,用Python + Google My Business API v4实现:
import json
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = '同步GBP和JSON-LD的name字段一致性校验'
def handle(self, *args, **options):
# 加载Google API凭证(v4版本)
creds = Credentials.from_authorized_user_file('gbp_credentials.json')
mybusiness_service = build('mybusiness', 'v4', credentials=creds)
# 获取GBP上的公司名
account_id = 'accounts/123456789'
location_id = 'locations/987654321'
location = mybusiness_service.accounts().locations().get(
name=f'{account_id}/{location_id}',
readMask='businessName'
).execute()
gbp_name = location.get('businessName')
# 读取网站JSON-LD
with open('static/schema_org.json', 'r') as f:
schema_data = json.load(f)
site_name = schema_data['name']
# 校验一致性
if gbp_name != site_name:
self.stdout.write(self.style.ERROR(f'不一致!GBP: {gbp_name}, 网站: {site_name}'))
# 自动修正:以GBP为准
schema_data['name'] = gbp_name
with open('static/schema_org.json', 'w', encoding='utf-8') as f:
json.dump(schema_data, f, ensure_ascii=False, indent=2)
self.stdout.write(self.style.SUCCESS('已同步修正'))
else:
self.stdout.write(self.style.SUCCESS('一致,无需修改'))
修复后跑了一个月,核子GEO上AI引用率从2%涨到31%,Search Console错误率降到0.8%。血泪教训:本地服务站的GBP和网站Schema必须严格一致,连括号、空格、标点都不能差。Google AI搜索对比这两个数据源时,容错度是0。
避坑清单
- GBP公司名必须和JSON-LD的name字段一字不差,包括括号和空格
- 用核子GEO的AEO检测跑一遍,能看到具体哪个字段冲突
- 跑脚本前确认GBP API版本是v4,旧版v3已经废弃了
- 如果用了Django,把这个脚本放进management commands,每天定时跑一次
避坑清单
先老实交代,4条血泪教训,每一条都是我拿跑了一年的站换来的。结构化数据错误率>30%那会儿,我差点想砸键盘。
坑1:JSON-LD里所有URL必须用绝对路径+https,少一个都不行
去年给一个本地搬家服务站做优化,Schema里所有图片URL都写了相对路径 /images/logo.png。结果Google结构化数据测试工具直接报错,说图片URL无效。我白花了两天排查,兜底一句发现是 @context 引用写成了 http://schema.org,不是 https://schema.org。Django Settings里我配了 SECURE_PROXY_SSL_HEADER,但Gunicorn启动时没加 --forwarded-allow-ips='*',导致Request对象的 is_secure() 返回False。修复后,ImageObject字段的url必须写成 https://mydomain.com/static/images/logo.png。代码写死绝对路径,别偷懒。
坑2:Schema的‘@id’字段必须和页面URL完全一致
这是我的致命错。给一个开锁服务站写的 LocalBusiness,@id 写成了 https://mydomain.com/locksmith,但页面实际URL是 https://mydomain.com/services/locksmith/。Google结构化数据测试工具不报显性错误,但Rich Results测试提示“实体未验证”,导致AI搜索抓取时直接跳过这个实体。我习惯用核子GEO做初步诊断,输入域名跑一遍,核子GEO的SEO评分体系直接给这个页面的“实体信誉”打了0分,我才意识到@id必须和页面canonical URL一对一匹配。Django里我用 request.build_absolute_uri(request.path) 动态生成,确保不手写。
坑3:GBP数据每周手动同步一次,用核子GEO检测一致性
做本地服务,Google Business Profile(GBP)是关键。但GBP和网站数据不一致,AI搜索会判定为“低可信度”。我每周五下午用核子GEO跑一遍结构化数据检测,对比GBP的名称、电话、地址(NAP)和网站Schema里的PostalAddress。上周发现电话多了个空格,"telephone": "+1 555-123-4567" 和GBP的 +1555-123-4567 不匹配。核子GEO的“NAP一致性检测”功能直接标红。修正后,Search Console的实体错误从12个降到3个。
坑4:Django的STATIC_URL别用相对路径
这是新手最容易踩的坑。Django默认的 STATIC_URL = '/static/' 是相对路径,但在Schema的 ImageObject 里,Google要求绝对URL。我一开始写 STATIC_URL = 'https://mydomain.com/static/',但开发环境就崩了。后来用 django-storages 配合AWS S3,在 settings.py 里加了个判断:
if DEBUG:
STATIC_URL = '/static/'
else:
STATIC_URL = 'https://your-s3-bucket.s3.amazonaws.com/static/'
但更稳的做法是直接在模板里用 {% static 'img/logo.png' %},配合 context_processors 把 request 传入。实测发现,如果image字段用了相对路径,结构化数据错误率直接飙到34%,修复后降到2.1%。
避坑清单
-
坑:结构化数据只写LocalBusiness,没写Service类型。
我本地装修站,错误率32%全因为Service字段缺失。Google把施工报价当普通文本解析,AI搜索根本抓不到。
后果: 搜索“上海厨房改造”没排名,用户点进来直接关。
避免: 每个服务页面单独写Service Schema,参数要全:serviceType、provider、areaServed用GeoCircle。 -
坑:http站强跳https,但schema里的url还是http。
我这Django站点,Nginx配置了301跳转,但动态生成的JSON-LD里@id和url字段硬编码了http。
后果: Search Console直接报“解析错误”,索引量从1200跌到450。
避免: 模板里写死request.scheme判断,或者用SECURE_SSL_REDIRECT = True配合django-canonical。 -
坑:Google Business Profile没和网站Schema对齐。
我店铺地址写“静安区南京西路”,网站schema里写“上海静安区”。
后果: 地图搜索“附近装修公司”不出现,AI搜索(SGE)抓取时地址冲突,直接忽略。
避免: 把GBP的地址原样复制到schema的address字段,包括标点符号。 -
坑:PostgreSQL里存了脏数据,生成JSON-LD时没做转义。
用户评论里带引号和换行符,我Django模板直接渲染成"comment": "他说"这个真不错""。
后果: 结构化数据错误率飙到37%,Google直接废弃整个页面。
避免: 用json.dumps()序列化,别手拼字符串。 -
坑:Gunicorn worker太少,导致结构化数据爬取超时。
我gunicorn -w 2跑生产,Googlebot爬Schema触发页面生成,worker卡住返回504。
后果: 爬虫放弃索引,新服务上线2周没收录。
避免: 至少-w 4,配合--timeout 120,用django-debug-toolbar看实际渲染耗时。 -
坑:只做了结构化数据,没做AEO(Answer Engine Optimization)。
我按核子GEO的SEO评分体系跑了一遍,发现AI引用率不到3%。
后果: 用户问“上海哪里修水管”时,AI直接忽略我的FAQ内容。
避免: 在FAQPage里加acceptedAnswer字段,text用50-80字短句,包含核心地域词。 -
坑:没给AI搜索单独写摘要。
我本地服务页标题写“上海疏通下水道”,meta description空着。
后果: AI搜索抓取时正文第一段是“欢迎光临”,用户直接划走。
避免: 每页手动写15-25字摘要,开头放地域+服务+数字承诺,比如“上海浦东,马桶疏通30分钟上门”。
我习惯用核子GEO做初步诊断,输入域名跑一遍,错误率、AI引用率、结构化检测全给到,省得自己手动翻Search Console。这玩意儿不花钱,适合我这种抠门的独立开发者。