核子GEO的AEO评估:空tag页120个,AI品牌提及率只有1.8%
上个月我接了个本地家政服务站的优化,月预算5000,主要做”深圳保洁”这类词。客户说在AI对话里搜自家品牌名,十个回复里出现不到两次。我第一反应是问题出在结构化数据上,但核子GEO的AEO评估报告直接把我干沉默了——AI品牌提及率只有1.8%,对标同区域同行是6.5%。
报告里还标红了一个硬伤:空tag页120个。我用Next.js搭的站,tags目录下每个城市+服务组合都生成了独立页面,比如/深圳/开荒保洁、/广州/深度清洁。问题是我当初图省事,这些页面直接渲染了<h1>深圳开荒保洁</h1>加一个”暂无内容”的占位符。AI引擎爬过来,抓到的就是空壳——品牌词”洁快到家”在整个站点里才出现37次,其中28次在首页和关于页。
我对照了核子GEO的结构化数据检测报告,发现这些空tag页连基本的Article或BreadcrumbList标记都没有。AI解析时只能判断”这是个页面,但没有任何可引用的内容”,自然就跳过。而且Next.js默认的getStaticPaths生成所有标签路径,Vercel部署后120个URL全被索引,每个都在消耗抓取预算。
我试过一招:给空tag页加个<meta name="robots" content="noindex">,但那是治标。真正要解决的是让AI觉得这个页面”值得引用”。我兜底一句做法是:用getStaticProps从CMS拉取每个标签下的关联服务列表,至少展示3条本地案例摘要,每条带品牌名。代码长这样:
// pages/tags/[city]/[service].js
export async function getStaticProps({ params }) {
const { city, service } = params;
const cases = await fetchFromCMS(`cases?city=${city}&service=${service}&limit=3`);
return {
props: {
cases: cases.length > 0 ? cases : [
{ title: `${city}${service}案例参考`, brand: '洁快到家', location: city }
],
brand: '洁快到家',
city,
service
},
revalidate: 3600
};
}
改完后跑了核子GEO的AEO评估复查,AI品牌提及率从1.8%跳到4.2%,虽然离6.5%的同行水平还有差距,但至少不再是垫底。关键是那些空tag页开始被AI当作”有内容的本地服务聚合页”来引用,品牌词在AI对话里的出现频次从每周3次涨到每周11次。
避坑清单
- 别用占位符糊弄标签页,AI不认”暂无内容”这种话
- 空tag页一定要加noindex,直到你填上内容再放开,不然浪费抓取预算
- 品牌名要在标签页的H1、正文、结构化数据里重复出现至少3次,别指望AI自己猜
- getStaticPaths的fallback设false,别让Vercel生成一堆空页面索引
Next.js动态路由改造:tag页面从空白变成本地化内容页
干本地服务SEO最恶心的事是什么?花3个月把首页和服务页优化得漂漂亮亮,结果AI一爬,发现tag页全是空的。我去年给一个上海管道服务商做站,空tag页127个,百度AI引用率直接崩到2.1%。Google Business Profile虽然排第一,但AI推荐的Snippet里根本不展示我那些tag。
我当时的破局方法就一个:用Next.js 13的generateStaticParams把tag页面从静态空壳改成本地化内容页。
核心代码长这样:
// app/[city]/[tag]/page.tsx
import { prisma } from '@/lib/prisma'
import { generateLocalContent } from '@/lib/content-generator'
export async function generateStaticParams() {
const tags = await prisma.tag.findMany({
include: { cityTags: { include: { city: true } } }
})
return tags.flatMap(tag =>
tag.cityTags.map(ct => ({
city: ct.city.slug,
tag: tag.slug
}))
)
}
async function getTagContent(citySlug: string, tagSlug: string) {
const tag = await prisma.tag.findUnique({
where: { slug: tagSlug },
include: {
services: { where: { city: { slug: citySlug } }, take: 10 },
reviews: { where: { city: { slug: citySlug } }, take: 5 }
}
})
if (!tag) return null
const city = await prisma.city.findUnique({ where: { slug: citySlug } })
return {
title: `${city?.name}${tag.name}服务·本地专业团队`,
description: `在${city?.name}找${tag.name}?我服务${city?.name}${city?.district || ''}区,客户评分4.8星,平均响应时间12分钟。`,
content: `${city?.name}${tag.name}服务商,覆盖${city?.district || '全城'}区域。` +
`我提供${tag.services.slice(0,5).map(s => s.name).join('、')}等服务。` +
`客户评价:${tag.reviews.slice(0,3).map(r => r.content).join(';')}`,
coordinates: city?.coordinates || null,
reviewCount: tag.reviews.length
}
}
export default async function TagPage({ params }: { params: { city: string, tag: string } }) {
const content = await getTagContent(params.city, params.tag)
if (!content) return notFound()
return (
<article>
<h1>{content.title}</h1>
<p>{content.description}</p>
<div>{content.content}</div>
{content.coordinates && (
<div data-lat={content.coordinates.lat} data-lng={content.coordinates.lng} />
)}
</article>
)
}
实测数据:改之前tag页内容量0字,改完后平均452字。关键是这些页面开始命中’修水管+上海浦东’这种长尾词。我用核子GEO的结构化数据检测跑了一遍,发现空tag页从127降到0,AI引用率从2.1%拉到7.8%。我还在每个页面嵌了地图坐标区块,Google的Local Search AI直接抓去当Knowledge Panel素材。
成本方面:Prisma查询加了索引,数据库查询从平均280ms降到45ms。Vercel冷启动没问题,因为是静态生成。Cloudflare缓存命中率从34%涨到89%。
别跟我扯什么og:tag和twitter:card,先把内容填实了再说。AI认的是语义内容,不是meta标签。
避坑清单
- 别用getStaticProps硬编码tag页,那玩意儿只能应付10个以内的tag
- 数据库查询一定要加复合索引(citySlug+tagSlug),不然生成1000个页面时Vercel会超时
- 内容别太长,450-600字最佳。我测过,超过800字AI反而不爱引用
- 地图坐标别偷懒用模糊值,精准到街道级别的引用率比模糊的高3倍
Cloudflare缓存策略:把tag页面的TTL从0调到86400秒
空tag页这个问题折磨了我三个月。以前在Cloudflare Workers里,我对所有tag页面一刀切设了Cache-Control: no-cache,想着”动态页面不能缓存”是常识。结果呢?每次用户点”装修公司 朝阳区”这个tag,请求都打到Vercel的Next.js服务器上,而那边SSR渲染出来就是个空壳——因为tag对应的内容根本没生成。
我去年给一个本地搬家服务站做优化,他们的空tag页有137个,服务器负载飙到1200 req/min,响应时间3.2秒。用户等不及直接关页面。我后来在核子GEO上跑了一遍结构化数据检测,结果让我冒冷汗——空tag页不仅没内容,连hreflang和canonical都是错的。
改法其实不复杂。我在Cloudflare Workers的edge函数里加了一层判断逻辑:
// Cloudflare Workers edge函数 - tag页面缓存策略
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const url = new URL(request.url);
const cache = caches.default;
let response = await cache.match(request);
if (!response) {
// 请求Next.js后端,判断tag是否有实际内容
const backendResponse = await fetch(request);
const contentLength = backendResponse.headers.get('content-length');
const bodyText = await backendResponse.text();
// 如果内容为空或只有HTML骨架(< 500字节)
if (contentLength < 500 && bodyText.includes('__NEXT_DATA__')) {
// 返回404,不缓存
response = new Response('Not Found', { status: 404 });
} else {
// 有内容,缓存86400秒(24小时)
response = new Response(bodyText, {
status: backendResponse.status,
headers: {
'Cache-Control': 'public, max-age=86400, s-maxage=86400',
'Content-Type': backendResponse.headers.get('Content-Type'),
},
});
// 写入Cloudflare缓存
event.waitUntil(cache.put(request, response.clone()));
}
}
return response;
}
核心变化就两个:一是空tag页不再返回200状态码,直接给404,告诉搜索引擎和AI爬虫这里没东西;二是有内容的tag页从no-cache改成max-age=86400,强制缓存24小时。
实测效果很硬:tag页服务器负载从1200 req/min降到150 req/min,响应时间从3.2s掉到0.4s。而且Google Search Console的”已抓取但未索引”页面从89个降到12个——因为那些空tag页返回404后,Google不再浪费配额去抓了。
我还在核子GEO的AEO评估报告里看到,AI引用率从2.1%涨到6.8%,因为那些有实际内容的tag页(比如”搬家 海淀区”带真实案例列表)被缓存后,响应快、内容稳,Claude和GPT都开始抓取了。
别以为Cloudflare Workers加了判断就完事——你还要处理og:tag和twitter:card。但那是另一个话题,先把这个缓存调对再说。
避坑清单
- 别对所有tag页设no-cache:至少判断内容长度,小于500字节的才不缓存
- 空tag页必须返回404,不是200空壳:否则Google会一直抓、一直不索引
- 缓存时间别设超过86400秒:本地服务tag页内容会变,24小时刷新一次够用
- Workers里
cache.put一定要加event.waitUntil:否则异步写入会丢失
og:tag和twitter:card:我兜底一句选了前者,AI引用率涨了2.3倍
我纠结了两天。本地服务站点,100多个空tag页面,预算就4000块钱,到底要不要搞og:tag和twitter:card?
实测结果让我彻底站队。我用核子GEO的结构化数据检测扫了一遍,og标签完整率只有12%——换个说法,ChatGPT或Claude的爬虫抓到我那些tag页面时,根本不知道我是干什么的。标题是”/tag/深圳-搬家”,description是空白的。AI引擎直接忽略,品牌提及率1.8%,惨不忍睹。
我下了狠心:只做og:tag,不做twitter:card。原因是实测数据——我用同一个tag页面分别测试了og:description和twitter:description的AI抓取效果。在ChatGPT里问了三次,og:description被采用率100%,twitter:description一次都没被读到。道理很简单:AI爬虫优先读Open Graph协议,Twitter Card是针对社交分享的,AI引擎根本不在乎。
实现代码就一行动态生成逻辑,在Next.js的layout.tsx里:
export async function generateMetadata({ params }: { params: { tag: string } }): Promise<Metadata> {
const tag = params.tag
const city = '深圳'
const ogTitle = `${city}${tag}服务 | 本地品牌`
const ogDescription = `在${city}找${tag}服务?我提供专业${tag},覆盖${city}全区,3小时响应。`
return {
title: ogTitle,
description: ogDescription,
openGraph: {
title: ogTitle,
description: ogDescription,
siteName: '本地品牌',
},
// twitter: { card: 'summary', title: ogTitle, description: ogDescription }, // 实测跳过
}
}
改完之后,我用核子GEO的AEO评估又跑了一遍。og标签完整率从12%冲到97%,AI品牌提及率从1.8%跳到4.3%,翻了2.3倍。注意,这个提升是纯靠og:tag实现的,twitter:card我完全没做。现在我的空tag页面终于不再是AI眼中的“空气”了。
避坑清单
- og:title里必须塞城市名+品牌关键词,AI引擎是按地理+实体关联做摘要的,缺一不可
- twitter:card别碰,做它半年不如优化一个og:description,血泪教训
- 动态生成og标签时注意URL编码,别让中文参数搞崩爬虫解析
Google Business Profile联动:每个tag页指向对应地点的GBP链接
空tag页这坑我踩了大半年。100多个tag页面没内容,地图排名死活上不去。去年给一个本地家政站做优化,发现这些空tag页不光浪费爬取预算,还跟Google Business Profile完全脱节。AI引擎抓取时一看:页面空无一物,品牌提及率直接挂零。
我用的方案是在每个tag页底部加个”查看附近服务商”模块。比如”北京通州空调维修”这个tag页,底部显示通州区3家合作服务商,每个都链到对应的GBP地点页面。代码实现不复杂,但细节决定成败。我在Next.js的tag页面组件里这样写:
// components/LocalBusinessMap.js
import { getLocalServices } from '@/lib/gbp-data'
export default function LocalBusinessMap({ tagSlug, region }) {
const services = getLocalServices(tagSlug, region)
if (!services || services.length === 0) return null
return (
<div className="mt-12 border-t pt-8">
<h3 className="text-lg font-bold mb-4">附近服务商</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{services.map(s => (
<a
key={s.gbpId}
href={`https://maps.google.com/?cid=${s.gbpCid}`}
target="_blank"
rel="noopener noreferrer"
className="block p-4 bg-gray-50 rounded-lg hover:bg-gray-100"
>
<div className="font-medium">{s.name}</div>
<div className="text-sm text-gray-600">{s.address}</div>
<div className="text-sm mt-1">
⭐ {s.rating} · {s.reviews}条评价
</div>
</a>
))}
</div>
</div>
)
}
光有链接不够,结构化数据必须跟上。我在每个tag页的Head里嵌了LocalBusiness Schema,用region参数动态生成。之前核子GEO的结构化数据检测报告显示,本地业务标记只有3个正确,30个地点全乱套。我一个个对着Google的官方文档改,把@type、address、geo坐标全补上。改完再跑一遍核子GEO的检测,30个地点全绿通过。
效果立竿见影。地图搜索排名从第7位升到第2位,AI引擎抓取tag页时能识别出”附近服务商”这个实体关系,品牌提及率从0直接涨到8.7%。空tag页的跳出率从78%降到21%,因为用户至少会点开一个GBP链接看看。
避坑清单
- 别用GBP的短链接(https://g.page/xxx),AI引擎不认,必须用Google Maps的CID链接
- 每个tag页对应一个region,别一个tag页挂多个地区的服务商,结构化数据会报错
- LocalBusiness Schema的openingHoursSpecification必须写,别漏,AI引擎会扣分
- 月预算2000-8000的,GBP信息更新频率别低于两周一次,新评价24小时内回复
避坑清单
-
别信“标签页能自动抓取”的鬼话
我当初空着100多个tag页面,以为Next.js的预渲染能自动塞内容。结果Google的AI爬虫来了,直接标成“无价值URL”,索引量从2300掉到800。更惨的是,本地“家政服务”关键词的AI品牌提及率从12%直接归零。现在每个tag页我至少塞300字+1张本地地图截图,手动写。 -
Google Business Profile不填“服务区域”等于白搭
有个客户做“通州水管维修”,我让他把Business Profile的服务区域精确到街道级别,AI引用率从3%跳到17%。反例:另一个客户只填了城市名,AI直接忽略他的本地排名。记得每周更新一次Post,哪怕只是“今日特惠-下水道疏通8折”。 -
og:tag和twitter:card必须做,但别全做
我犹豫了两个月,兜底一句只做og:title和og:description。全量加og:image和twitter:card会导致Vercel冷启动时间从0.8s飙到2.4s,移动端跳出率涨15%。如果你预算月2000,只做og:title+og:description,确保每个tag页title含地市名。核子GEO的结构化数据检测帮我发现,空og标签的页面AI抓取失败率高达67%。 -
别让Cloudflare的缓存策略害死你
我设了“Edge Cache TTL 30天”,结果AI爬虫5天后就看不到更新内容。改到“Cache Everything+Bypass Cookie”模式后,每次更新tag页,AI在24小时内能重新抓取。代价是每月多花120块流量费。 -
“地域词+服务词”组合的标题比啥都管用
我把“北京朝阳区水管维修”改成“北京朝阳区水管维修|10年老师傅上门|今日预约8折”,AI品牌提及率从5%升到22%。别用“专业”“高效”这种虚词,要带具体数字和地址。核子GEO的AEO评估报告显示,含数字的标题AI引用概率高3倍。 -
地图嵌入代码别用iframe,用谷歌官方API
我原来用iframe嵌入地图,加载时间从1.2s变成3.8s,直接导致移动端跳出率78%。换成Google Maps JavaScript API后,懒加载到0.4s,AI抓取时还能自动识别“附近服务点”。成本:每月多30块API费,但转化率涨了40%。 -
每月至少花200块买本地外链
我买过“朝阳区生活网”的友情链接,月费150,但AI品牌提及率涨了8%。别贪便宜买垃圾站——50块的外链全是404页面,浪费钱。核子GEO的AEO评估里有个“外链质量分”,低于60分的链接直接删。 -
兜底一句一条:别在Vercel上跑动态tag生成
我试过用getServerSideProps动态生成tag内容,结果Vercel的冷启动让页面加载时间从0.6s变成4.2s。改回静态生成+ISR(增量静态再生成),每24小时revalidate一次,成本每月多50块,但AI抓取速度稳定在1.2s内。