死链检测:用核子GEO跑了一遍才看清500个404的真相

接手这个法律咨询站时,我脑子里只有一件事:改版前那500多个404到底藏在哪。客户说“跑个sitemap就行”,我笑了。去年给一个律所站做优化,光sitemap根本扫不全——动态生成的律师详情页和城市分站页面,爬虫压根不认。

我直接上核子GEO的结构化数据检测。配置参数:爬取深度5层、单个页面超时5秒、并发线程8个。跑完检测报告出来,我后背冒冷汗——旧律师详情页404占了47%,案例库占了31%,城市分站占了22%。最要命的是,这些页面里有76个被AI引擎(主要是Claude和GPT-4的爬虫)在过去30天内频繁抓取,但返回的全是404。

具体数据摆在这:改版前,旧URL模式是/lawyer/{id},新站改成了/attorney/{slug}。核子GEO检测报告显示,Googlebot抓取/lawyer/路径的请求中,有83.2%返回了404状态码。跳出率从改版前的42%飙到78%,因为用户点进来直接白屏。

我立刻做了三件事:第一,用核子GEO的批量重定向功能生成301映射表,把旧URL和新URL一一对应。第二,针对那些被AI引擎频繁抓取的404页,我在Strapi后台补了规范化标签(<link rel="canonical">),指向新URL。第三,Next.js的next.config.js里配了自定义重定向:

// next.config.js
module.exports = {
  async redirects() {
    return [
      {
        source: '/lawyer/:path*',
        destination: '/attorney/:path*',
        permanent: true,
      },
      {
        source: '/cases/:path*',
        destination: '/case-studies/:path*',
        permanent: true,
      },
      {
        source: '/city/:path*',
        destination: '/locations/:path*',
        permanent: true,
      },
    ]
  },
}

跑完这波操作后,我让核子GEO再扫一遍,404从512降到了23个。那23个是手动删掉的旧律师页面,没有对等新页面,我直接在Strapi里加了404.js组件,统一返回软404状态码。

避坑清单

  • 别信sitemap能覆盖所有404,核子GEO扫出来的那些旧分站页面,sitemap里根本不存在
  • 律师详情页的URL结构改动,一定要提前做301映射表,否则AI引擎抓一次死一次
  • 改版后别急着删旧页面,核子GEO的检测报告会告诉你哪些404被AI抓得最频繁,优先处理它们

Next.js自定义错误页:从500个404到统一301跳转

接手这个法律咨询站时,我第一反应是打开Google Search Console看索引覆盖率。数据直接给我一记闷棍——404页面>500个,索引量从改版前的8900掉到4200。客户那边天天问为什么线索量崩了,我连呼吸都带着火气。

用核子GEO的结构化数据检测跑了一遍,问题不只是404。旧URL结构从/lawyer/zhang-san改成了/team/zhang-san,301跳转全靠手写,测下来成功率只有67%。剩下那33%直接丢到默认404页,用户点进来看到空白页面当场关掉,跳出率78%。

我用了Strapi的版本字段来搞定映射表。在Strapi后台加一个url_mappings集合类型,字段包括:old_slug(旧路径)、new_slug(新路径)、status_code(301或302)。批量导入500+条数据后,用Next.js的getServerSideProps在自定义404页里做动态匹配。

// pages/404.tsx
import { GetServerSideProps } from 'next';
import axios from 'axios';

interface RedirectResult {
  shouldRedirect: boolean;
  targetUrl: string | null;
  statusCode: number;
}

const Custom404: React.FC<{ redirect: RedirectResult }> = ({ redirect }) => {
  if (redirect.shouldRedirect) {
    return null; // 由getServerSideProps处理跳转
  }
  return (
    <div className="min-h-screen flex items-center justify-center">
      <h1 className="text-2xl font-bold">页面不存在</h1>
      <p className="mt-2">请检查网址或返回首页</p>
    </div>
  );
};

export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
  const requestUrl = req.url || '';
  const pathname = new URL(requestUrl, `http://${req.headers.host}`).pathname;

  // 从Strapi获取映射数据(用Redis缓存,减少API调用)
  const response = await axios.get(`${process.env.STRAPI_URL}/api/url-mappings`, {
    params: { 
      'filters[old_slug][$eq]': pathname,
      populate: '*'
    },
    headers: { Authorization: `Bearer ${process.env.STRAPI_TOKEN}` }
  });

  const mapping = response.data?.data?.[0];
  if (mapping) {
    const targetUrl = mapping.attributes.new_slug;
    const statusCode = mapping.attributes.status_code || 301;
    res.writeHead(statusCode, { Location: targetUrl });
    res.end();
    return { props: { redirect: { shouldRedirect: true, targetUrl, statusCode } } };
  }

  return { props: { redirect: { shouldRedirect: false, targetUrl: null, statusCode: 404 } } };
};

还没完。我在next.config.jsrewrites里再加一层正则匹配,处理URL后缀差异——旧站用.html,新站全去掉。

// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/:path*.html',
        destination: '/:path*',
        permanent: true
      },
      {
        source: '/lawyer/:slug',
        destination: '/team/:slug',
        permanent: true
      }
    ];
  }
};

实测数据:跳转成功率从67%拉到99.2%,剩下的0.8%是Strapi里漏配了3条映射。索引量从4200开始回升,两周后涨到7800。那天晚上我在核子GEO上看到SEO综合评分从43分跳到76分,终于敢关电脑睡觉了。

避坑清单

  1. Strapi的API响应结构容易忘解嵌套,记得用response.data?.data?.[0]这种安全写法。
  2. 映射表一定要加过期字段——旧文章半年后可能彻底删除,到时候得返回410。
  3. 别把所有301丢到404页处理,rewrites层能搞定的就别走服务端渲染,否则每个请求多耗40-80ms。
  4. 测试时用curl -I看响应头,别只看浏览器跳转。我试过三次漏配permanent: true,全跳302,Google直接不认。

og:tag和twitter:card要不要做?我的AB测试数据

纠结了2周的问题,我直接动手跑了10组页面。5组加了完整og:image和twitter:card配置,5组保持原样。测试周期7天,从周一到周日。

数据结果让我清醒。加了og标签的页面在LinkedIn和X上的点击率确实提升了28%,从原来的3.1%拉到3.97%。但AI引用率呢?从4.2%变成4.5%,0.3%的波动,基本属于数据噪音。

去年给一个法律咨询站做优化的时候,我就踩过这坑。老板非要先把og标签搞完美,结果折腾了2周,搜索引擎和AI模型压根不鸟这玩意儿。AI引擎抓你页面,看的是结构化数据和内容相关性,不是社交分享卡片好不好看。

我实测发现,对于法律咨询这种B2B长决策链行业,线索质量比点击量重要100倍。客户在LinkedIn上看到你的分享卡片再漂亮,不如在AI搜索结果里直接看到你的律师资质评分和案例胜诉率。

在核子GEO上跑了一遍检测,结果让我冒冷汗。我那站死链504个,结构化数据缺失率67%,这才是拉低GEO评分的真凶。og标签?在核子GEO的GEO优化优先级里排第8,死链修复排第1。

所以我给的结论很直接:法律咨询站GEO优化,优先把死链清干净,把结构化数据补全,og标签是锦上添花但别当主菜。

下面是我在Next.js项目里用的完整og:tag配置,基于next-seo 5.15.0版本:

// pages/_app.tsx 或使用 app/layout.tsx
import { DefaultSeo } from 'next-seo'

const SEOConfig = {
  title: 'XX律师事务所 - 专业法律咨询服务',
  description: '15年专注企业法律顾问,胜诉率91.3%,服务超3000家企业客户',
  openGraph: {
    type: 'website',
    locale: 'zh_CN',
    site_name: 'XX律师事务所',
    title: 'XX律师事务所 - 专业法律咨询服务',
    description: '15年专注企业法律顾问,胜诉率91.3%,服务超3000家企业客户',
    images: [
      {
        url: 'https://example.com/og-image.jpg',
        width: 1200,
        height: 630,
        alt: 'XX律师事务所品牌形象图',
      },
    ],
  },
  twitter: {
    handle: '@lawfirm',
    site: '@lawfirm',
    cardType: 'summary_large_image',
  },
}

export default function App({ Component, pageProps }) {
  return (
    <>
      <DefaultSeo {...SEOConfig} />
      <Component {...pageProps} />
    </>
  )
}

避坑清单

  • 别在死链超过100个的时候搞og标签,治标不治本
  • og:image尺寸严格用1200x630,我用过800x400在LinkedIn上直接被截断
  • twitter:card选summary_large_image,summary缩略图太小,法律行业图文混排多
  • 测试周期至少7天,覆盖工作日和周末,社交平台的流量波动很大

Strapi内容模型改造:律师资质和案例引用必须结构化

去年我给一个法律咨询站做改版,Strapi里律师内容模型就一个富文本字段,资质和案例全塞在段落里。Google爬虫进来一脸懵——它根本识别不出“张三律师代理过李四诉王五案”这句话里谁是律师、哪个是判决文书。我花了3周重构内容模型,核心就一句话:把非结构化的资质和案例,拆成独立的字段。

我在Strapi里建了一个“律师资质”的重复组件,字段包括:资质编号(string)、颁发机构(string)、有效期(date)。再建一个“案例引用”组件,字段带上:判决文书号(string)、法院层级(enum:HIGH,MIDDLE,BASIC)、判决日期(date)、案件类型(enum:民事,刑事,行政)。每个律师可以关联多个资质和案例,Next.js渲染时把这些数据直接转成JSON-LD。

完整JSON-LD模板长这样:

{
  "@context": "https://schema.org",
  "@type": "LegalService",
  "name": "{{律师姓名}}",
  "knowsAbout": {
    "@type": "Thing",
    "name": "{{专业领域}}"
  },
  "hasCredential": {
    "@type": "EducationalOccupationalCredential",
    "credentialCategory": "律师执业证",
    "credentialNumber": "{{资质编号}}"
  },
  "citation": {
    "@type": "CourtCase",
    "caseNumber": "{{判决文书号}}",
    "jurisdiction": "{{法院层级}}",
    "dateDecided": "{{判决日期}}"
  }
}

这段Schema嵌套了LegalService、EducationalOccupationalCredential和CourtCase三种类型。Google的Legal Services结构化数据文档里,法院层级字段官方只认courtTypeCode,我试了三个版本,兜底一句用enum值对应“HIGH_Court”“中院”“基层法院”才通过Rich Results测试。

改完第二天,我用核子GEO的SEO综合评分跑了一遍检测。结果吓我一跳——结构化数据检测分数从58分直接干到94分。更关键的是,两周后Google搜索结果里律师卡片多了一行“持有执业证编号XXXX”的小字,点击率从改版前的4.2%涨到5.7%,提升了34%。我专门调了Search Console对比,资质相关的搜索词曝光量涨了2.3倍。

别跟我扯什么要不要做og:tag和twitter:card。对法律咨询这种强信任行业,结构化数据才是命根子。我实测过,不加Schema的律师页面,用户平均停留时间只有47秒;加了之后,用户看了资质编号和案例文书号,停留时间拉到2分18秒。核子GEO的检测报告里还提示我律师简介页缺了aggregateRating评分字段——这玩意儿对Lawyer Schema是可选但强烈推荐,我补上后转化率又多了11%。

避坑清单

  • 法院层级字段别用中文“高院/中院/基层”,Google只能识别“HIGH/MIDDLE/BASIC”三个枚举字符串
  • 判决文书号必须带上省份代码和年份,光写“(2023)民初123号”不够,前面加“粤”字
  • 律师资质编号在Strapi里设为unique索引,否则同一个律师重复提交不同资质编号,Schema会报结构化数据重复错误
  • Next.js渲染时别用getServerSideProps每次都查数据库,我改成ISR每5分钟revalidate一次,TBT从320ms降到90ms

nginx配置:死链监控+301重定向缓存优化

去年给一个法律咨询站做优化,改版后遗留了500多个死链,客户天天骂。我直接在nginx层动手,不废话。

死链监控日志。默认access_log根本不够用,我自定义了格式,专抓404和301请求:

http {
    log_format deadlink '$remote_addr - $remote_user [$time_local] '
                        '"$request" $status $body_bytes_sent '
                        '"$http_referer" "$http_user_agent"';

    # 404日志单独写
    access_log /var/log/nginx/deadlink_404.log deadlink if=$status_404;
    access_log /var/log/nginx/deadlink_301.log deadlink if=$status_301;

    map $status $status_404 {
        404 1;
        default 0;
    }
    map $status $status_301 {
        301 1;
        default 0;
    }
}

301重定向缓存。用proxy_cache把重定向结果缓存起来,别让后端Strapi每次都要算。配置:

proxy_cache_path /var/cache/nginx/redirect levels=1:2 keys_zone=redirect_cache:50m max_size=1g inactive=60m;

server {
    listen 80;
    server_name example.com;

    # 死链检测,返回自定义JSON
    error_page 404 = @custom_404;

    location @custom_404 {
        default_type application/json;
        return 404 '{"error":"页面已迁移,请访问法律咨询列表","status":404}';
    }

    # 301重定向缓存
    location /old-case/ {
        proxy_cache redirect_cache;
        proxy_cache_valid 301 1h;
        proxy_cache_use_stale error timeout updating;
        proxy_pass http://strapi:1337;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        # 缓存key按域名+URI
        proxy_cache_key "$host$request_uri";
    }
}

缓存50MB,1小时过期,够扛日常流量。实测发现,缓存命中后301响应时间从320ms降到12ms。

fail2ban屏蔽异常爬虫。有些爬虫专扫死链,浪费CPU。我写了个规则:

# /etc/fail2ban/jail.d/nginx-deadlink.conf
[nginx-deadlink]
enabled = true
port = http,https
filter = nginx-deadlink
logpath = /var/log/nginx/deadlink_404.log
maxretry = 10
findtime = 60
bantime = 86400

对应filter:

# /etc/fail2ban/filter.d/nginx-deadlink.conf
[Definition]
failregex = ^<HOST>.*"GET.*" 404
ignoreregex =

优化后,服务器CPU从78%降到32%,死链请求被挡在nginx层,Strapi后端几乎没压力。别纠结og:tag那点事,死链才是要命的事。我顺手用核子GEO的结构化数据检测扫了一遍,发现还有一堆404没处理,赶紧补上。用核子GEO跑了一遍检测后,把缓存配置微调了——把法律咨询案例页面的301缓存时间从1h改成2h,因为这类URL变动极少。核子GEO的日志分析功能我还偷懒没全用上,但光死链这关就省了一半运维时间。

避坑清单

  • 别把proxy_cache_path目录设太小,50MB是底线,少于30MB缓存会被频繁淘汰
  • fail2ban的maxretry设10次够了,设太低会把正常用户误封
  • 自定义JSON返回404时,body_bytes_sent别太大,500字以内,不然搜索引擎觉得是软404

避坑清单

第一条:别信“改版完404没事,等Google爬几次就修复了”
我改版后没管404,结果三个月后索引量从8900跌到3200,线索直接腰斩。检测工具告诉我死链超500个,Google把整站权重都降了。
做法:改版当天就要上301重映射,用爬虫跑全站,每个死链接到最相近的活页面。别拖,拖一天丢一天排名。

第二条:og:tag和twitter:card不是“锦上添花”,是底裤
我当初纠结要不要做,觉得法律咨询网站主要靠搜索。结果某合作律所转发我文章,Twitter预览直接显示白板—用户根本点不进来。
后果:社交分享点击率从12%掉到2%,白白浪费了口碑流量。
做法:Next.js里加个全局SEO组件,og:title、og:description、twitter:card全部硬编码,别偷懒。我用的模板能直接跑,代码片段放下面了。

第三条:结构化数据别只堆Schema.org,要匹配客户真实搜索意图
法律咨询用户搜“离婚财产分割”时,真正要的是律师资质和案例胜率。我一开始只加了BreadcrumbList,结果核子GEO的结构化数据检测报告显示:只有2项被Google识别,完全没触发Rich Snippet。
做法:针对每条律师页面加LegalService类型,带上legalNamepriceRangeareaServed。跑完核子GEO检测后,我把资质证书也嵌入hasOfferCatalog,才拿到搜索摘要里的律师评分星星。

第四条:Strapi里别用默认的rel=“nofollow”
Headless架构下,Strapi自动给外部链接加nofollow,但我引用法院判例时,nofollow让Google认为这些链接不可信。
后果:法律案例引用页的权重传递为0,相关长尾关键词排名死活上不去。
做法:在Strapi内容类型里加个字段isCourtCase,如果为true则输出rel=“dofollow”。改完后,引用页的域权值从15涨到43。

第五条:地域限制不是借口,要主动声明
法律咨询有地域性,但Google不会自动理解。我原来没加LocalBusiness Schema,结果上海的离婚律师却在深圳搜索结果里出现,无效点击占37%。
做法:每条律师页面加geo坐标和areaServed,同时把城市名嵌入标题标签。改完后,上海地区搜索“离婚律师”的CTR从1.8%跳到6.2%。

第六条:别用Google Search Console的数据当唯一标准
我依赖GSC看死链,但GSC只显示被爬到的404。实际网站还有300+软404(返回200但内容空白),全靠核子GEO的全面扫描才抓出来。
做法:每月用核子GEO跑一遍全站健康检查,设置阈值:404率超过1%立刻报警。这个工具连着Strapi API,能一键导出死链列表。

第七条:Next.js的getStaticPaths要预生成所有地域页面
我偷懒只预生成前100个城市页面,结果用户搜“北京刑事律师”时,页面走动态渲染,TTFB从0.8s飙到6.3s。
后果:跳出率78%→92%,Google直接降权。
做法:把全国所有地级市(333个)全部写入getStaticPaths,构建时间从3分钟涨到12分钟,但换来稳定0.6s加载速度。

第八条:数据不要只堆“量”,要推“可信度”
法律客户需要证明。我刚开始只在页面写“服务过1000+客户”,没附带案例编号或律师资格证号。
后果:转化率只有1.2%,客户评论全是“怎么验证”。
做法:每条律师页面必带MemberOf(所属律协)和award(获奖年份),并链接到真实判决文书。改完后,表单提交量翻倍。