跳转到内容
严选跨境网络服务与流媒体推荐
实机测速保障 · 专属折扣
声明:以上服务由影迷漫游评测实验室进行晚高峰抽测与流媒体解锁验证。专属链接包含推广支持,不增加您的购买成本。

VitePress 博客 SEO 优化与性能提升指南 2026 ​

VitePress SEO Guide

VitePress 是静态站点生成器,构建时就把每篇文章渲染成完整 HTML,爬虫不用执行 JavaScript 就能读到正文——这是它在 SEO 上的先天便利。但预渲染只解决了「内容可读」,离「能被搜到、点了进来、还愿意停留」还差三步。本文不按功能罗列,而是按上线前审计的顺序走:先确认找得到,再确认读得懂,然后把性能做到用户留得住,最后留一份维护清单。文中配置以本站 y-m.top 的做法为例,平台规则以 Google 官方文档的现行说法为准。

第一步:让搜索引擎找得到 ​

页面进了索引,后面的一切才有意义。这一层查四件事:地图有没有生成、robots 有没有误伤、canonical 对不对、该提交的平台有没有提交。

sitemap:一行配置,但要回头验货 ​

VitePress 内置站点地图生成,在主配置里写上域名即可:

ts
// .vitepress/config.mts
export default defineConfig({
  sitemap: {
    hostname: 'https://y-m.top',
  },
})

构建后产物目录里会出现 sitemap.xml。容易被忽略的是验货环节:打开它扫一眼,别混进草稿页、测试页和本就不该收录的页面;里面的网址要与实际访问的完全一致(协议、域名、末尾斜杠、是否带 .html 后缀)。开启 cleanUrls: true 后网址不带后缀,地图里也应是同一形态,对不上时搜索引擎会把力气花在跳转上。

robots.txt:只写你真正想挡的 ​

robots.txt 放在 public 目录,构建时原样带到根目录:

txt
User-agent: *
Allow: /
Disallow: /admin/

Sitemap: https://y-m.top/sitemap.xml

两个提醒。其一,robots 挡的是「抓取」不是「收录」:被挡的网址若被别处链接,仍可能以只有链接、没有摘要的形态出现在结果里;真不想收录要用正文页的 noindex。其二,改完 robots 用站长平台的测试工具过一遍,Disallow 多一个通配符误伤全站并不罕见。顺带说明:本站早期做过繁体镜像并禁抓其目录,该镜像现已删除;正经多语言版本应给各版本互加 hreflang 并各自允许收录,而不是禁抓了事。

canonical:让每个页面自己声明标准网址 ​

同一篇内容常有好几个网址:带参数的、镜像的、分页的。canonical 的职责就是指认正本。逐页手写容易漏、容易写错,本站交给 transformPageData 在构建时统一生成(完整逻辑在第二步),规则只有三条:域名拼相对路径、去掉 .md 后缀、目录页去掉末尾的 index。生成后抽查首页、目录页、普通文章各一页,确认注入的地址能直接打开、且与地址栏一致。

提交与推送:Google 靠 GSC,Bing 可以催 ​

平台怎么交链接
Google Search Console递交地图;新文用网址检查催收录search.google.com/search-console
Bing Webmaster Tools递交地图,可另配 IndexNow 推送bing.com/webmasters
百度搜索资源平台地图递交或接口推送ziyuan.baidu.com
搜狗站长平台递交地图zhanzhang.sogou.com

IndexNow 是构建后自动推送的常见做法:在 buildEnd 钩子里读出刚生成的 sitemap.xml,提取全部网址 POST 到其接口即可,密钥是一个 8–128 位字符串,同时在站点根目录放同名 txt 文件供验证。覆盖范围要注意:Bing、Yandex 等支持,Google 不参与,百度也不走这条路。别因为配了自动推送就忘了去 Search Console 提交地图——对以 Google 流量为主的站点,GSC 那一份才是正本。

第二步:让搜索引擎读得懂 ​

能抓取只是入场券。标题、描述、结构化数据和分享卡片决定搜索引擎如何理解页面、又如何在结果页与社交平台上呈现它。

先管好 frontmatter ​

每篇文章头部的 frontmatter 是成本最低、收益最稳的一层:

yaml
---
title: 文章标题:把核心关键词放在前半段
description: 一两句人话讲清这篇解决什么问题,读完能得到什么。
head:
  - - meta
    - property: og:image
      content: https://example.com/og-image.jpg
---

几条经验。标题里核心词靠前,全站不要共用同一模板后缀堆出一片雷同。description 写给人看:它常被用作结果摘要,但 Google 也会按查询改写,别按字符数上限硬凑,更不要把关键词排成一行。一篇一图:og:image 每篇单独配。至于 keywords 字段,Google 早已明确不看它,留着只是给自己备忘。

正文内部同样讲层级:一页只有一个 H1(通常就是标题),小节依次用 H2、H3,不要为了字号跳级;图片写 alt 描述内容而不是文件名。老生常谈,但审计时最常见的扣分项恰恰出在这里——比如为了排版把 H2 当大字标题到处用。

结构化数据:用 transformPageData 一次配齐 ​

JSON-LD 的作用是把「这是一篇文章、作者是谁、什么时候发布」用机器可读的方式再说一遍,有机会换来更丰富的结果展示。它不保证展示,更不是排名外挂。本站已把逐页手写改为构建时生成,核心代码如下:

ts
// .vitepress/configs/transformPageData.ts
import type { UserConfig } from 'vitepress'

const baseUrl = 'https://y-m.top'
const defaultOgImage = '/logo/y-m-top-og.webp'

export const transformPageData: UserConfig['transformPageData'] = (pageData) => {
  pageData.frontmatter.head ??= []

  // canonical:域名 + 路径,去掉 .md,目录页去掉 /index
  let pageUrl = `${baseUrl}/${pageData.relativePath}`.replace(/\.md$/, '')
  if (pageUrl.endsWith('/index')) pageUrl = pageUrl.slice(0, -'/index'.length)

  const title = pageData.frontmatter?.hero?.name || pageData.title
  const description = pageData.frontmatter?.hero?.tagline || pageData.description || ''

  // og:image:优先用文章自己在 head 里声明的,没有则退回全站默认图
  const ogEntry = pageData.frontmatter.head.find(
    (item: any) => item[0] === 'meta' && item[1]?.property === 'og:image',
  )
  const ogImage = ogEntry?.[1]?.content || defaultOgImage

  const isHome = pageData.relativePath === 'index.md'
  const jsonLd = isHome
    ? {
        '@context': 'https://schema.org',
        '@type': 'WebSite',
        url: `${baseUrl}/`,
        name: title,
        description,
        inLanguage: 'zh-Hans',
      }
    : {
        '@context': 'https://schema.org',
        '@type': 'BlogPosting',
        headline: title,
        description,
        url: pageUrl,
        mainEntityOfPage: pageUrl,
        image: ogImage,
        inLanguage: 'zh-Hans',
        author: { '@type': 'Organization', name: 'y-m.top', url: baseUrl },
      }

  pageData.frontmatter.head.push(
    ['link', { rel: 'canonical', href: pageUrl }],
    ['meta', { property: 'og:title', content: title }],
    ['meta', { property: 'og:url', content: pageUrl }],
    ['meta', { property: 'og:description', content: description }],
    ['meta', { property: 'og:image', content: ogImage }],
    ['meta', { name: 'twitter:card', content: 'summary_large_image' }],
    ['meta', { name: 'twitter:title', content: title }],
    ['meta', { name: 'twitter:image', content: ogImage }],
    ['script', { type: 'application/ld+json' }, JSON.stringify(jsonLd)],
  )
}

选类型时对号入座:首页用 WebSite,文章用 BlogPosting 或 TechArticle,有真实面包屑层级时加 BreadcrumbList。这里有一处旧稿的错误值得点名:老版本代码把 article:published_time 写成了一个固定的 2020 年日期,所有文章共用——发布时间必须逐页取该篇自己的日期(本站以 frontmatter 的 datePublished 为准),拿构建时间或某个常量顶替,等于向搜索引擎报假账,日期类误判在文末还会再出现一次。另外,结构化数据里写的每一项都要在页面上真实存在,写页面上没有的内容属于操纵结构化数据,是会招致人工处罚的红线。

面包屑单独给一份示例,注意每一级的地址都必须真实可达,指向不存在的分类页是本站审计时真查出过的问题:

ts
const breadcrumbJsonLd = {
  '@context': 'https://schema.org',
  '@type': 'BreadcrumbList',
  itemListElement: [
    { '@type': 'ListItem', position: 1, name: '首页', item: 'https://y-m.top/' },
    { '@type': 'ListItem', position: 2, name: '技术笔记', item: 'https://y-m.top/notes/' },
    { '@type': 'ListItem', position: 3, name: 'VitePress', item: 'https://y-m.top/notes/vitepress/' },
  ],
}

还有一个流传很广的过时建议需要更新:给文章加 FAQPage 结构化数据博问答折叠展示。Google 自 2023 年起已把 FAQ 富摘要限制在权威政府与健康类站点,普通博客标注得再规范也不会展示问答卡。有真实问答内容就正常写进正文,别再为它单独维护一份 schema。

OG 与 Twitter 卡片:分享出去时的样子 ​

链接被转到微信、Telegram、X 上时,对方平台读的是 OG 标签。至少备齐六项:内容类型(og:type)、标题、描述、封面图、页面地址和站点名,外加用 twitter:card 指定卡片形态。封面图按 1200×630(比例 1.91:1)制作,重要文字放画面中央,文件尽量压到几百 KB 以内;地址必须写绝对地址,且上线前真实打开一次确认不是 404——图挂了卡片就是一片空白,而写文章的人通常是最后一个发现的。调试工具方面,Meta 的 Sharing Debugger 和多平台预览站 OpenGraph.xyz 还能用;Twitter 官方的 Card Validator(cards-dev.twitter.com/validator)已下线,验证 X 卡片直接发一条测试帖看效果即可。

全局默认标签集中放在 head 配置里统一管理,逐页不同的部分(标题、描述、图)留给 transformPageData 补:

ts
// .vitepress/configs/head.ts
import type { HeadConfig } from 'vitepress'

export const head: HeadConfig[] = [
  ['meta', { name: 'theme-color', content: '#ffffff' }],
  ['meta', { name: 'format-detection', content: 'telephone=no' }],
  ['link', { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }],
  ['link', { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' }],
  ['link', { rel: 'manifest', href: '/site.webmanifest' }],
  ['meta', { property: 'og:type', content: 'website' }],
  ['meta', { property: 'og:locale', content: 'zh-Hans' }],
  ['meta', { property: 'og:site_name', content: '你的站点名' }],
  ['meta', { name: 'robots', content: 'index, follow, max-image-preview:large' }],
]

第三步:让用户留得住 ​

搜索把人送进来之后,性能决定他是读下去还是关掉。Google 用 Core Web Vitals 量化这一层,及格线是 LCP 不超过 2.5 秒、CLS 不超过 0.1、INP 不超过 200 毫秒(INP 已取代旧的 FID)。注意 Search Console 报的是真实用户近 28 天的现场数据,实验室跑分再漂亮也不能替代它。

LCP:先伺候好首屏那张图 ​

最大内容绘制通常是首屏大图或大标题。对图片做四件事:转 WebP,体积比 JPG 再小约三成;给出确切的宽高;首屏图加高优先级,非首屏图才懒加载;能走 CDN 就走 CDN。

html
<img
  src="/cover.webp"
  alt="封面图内容描述"
  width="1200"
  height="630"
  loading="eager"
  fetchpriority="high"
/>

字体是另一个常见的首屏堵点:自定义字体没加载完之前,文字要么不显示要么跳一下。用 woff2 并声明 font-display: swap 先拿系统字体顶上,加载完再替换:

css
@font-face {
  font-family: 'CustomFont';
  src: url('/fonts/custom.woff2') format('woff2');
  font-display: swap;
}

框架层面开启 metaChunk: true 能把页面元信息拆成独立 chunk、减小首个 HTML 的体积,不用再额外折腾关键 CSS。

CLS 与 INP:别跳,别卡 ​

布局偏移的来源很固定:图片没尺寸、字体替换、广告或嵌入块晚到。逐项对应即可——图片一律带宽高;嵌入内容先给容器预留 min-height;字体除了 swap,再用 size-adjust 对齐新旧字体的度量,能消掉大半文字跳动。交互响应主要看主线程:长任务拆小、非关键脚本延后、统计代码克制。分析工具够用就好,GA4、Umami、Plausible、Cloudflare Web Analytics 选一到两个,其中 Umami 可自建、对隐私友好,Docker 起一个实例即可:

bash
docker run -d \
  --name umami \
  -p 3000:3000 \
  -e DATABASE_URL=postgresql://umami:umami@db:5432/umami \
  -e HASH_SALT=your-random-salt \
  ghcr.io/umami-software/umami:latest

脚本每多挂一个,INP 和加载都在替它买单,这笔账要心里有数。

PWA:锦上添花,别当必修课 ​

vite-plugin-pwa 可以给站点加离线缓存和安装到主屏幕的能力,配置骨架如下:

ts
import { VitePWA } from 'vite-plugin-pwa'

VitePWA({
  registerType: 'autoUpdate',
  manifest: {
    name: '你的站点名',
    short_name: '简称',
    theme_color: '#ffffff',
    icons: [
      { src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },
      { src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' },
    ],
  },
})

缓存策略按资源选:带指纹的构建产物用 CacheFirst,HTML 用 NetworkFirst 保新鲜、断网回退缓存。但 PWA 对搜索排名没有直接加成,它改善的是回访体验,先把前两步做扎实再考虑它。

第四步:持续维护,别让站点慢慢烂掉 ​

SEO 不是上线时的一次配置,而是随内容增长不断还债的过程,常态工作有四件。

死链巡检。 改栏目、删文章、换网址都会留下死链:站内的让爬虫白跑、让读者撞墙,结构化数据和面包屑里指向已删页面的更隐蔽。每次结构调整后跑一遍全站构建,VitePress 的死链检查会把断头路列出来,照单修或补重定向,不要攒。

日期要真实。 lastUpdated: true 读的是 git 提交时间,这里有个安静的坑:部署平台浅克隆仓库时,全站文件的提交时间会塌缩成部署当天,几百篇文章的更新时间一夜之间全部变成同一天。本站就中过招,后来改为单独维护一份真实日期表供构建读取。判断有没有中招很简单——随机打开几篇老文章,看更新日期是不是齐刷刷等于最近一次部署日。

内链要跟着内容长。 新文发布时给相关旧文补链接,旧文修订时顺手链向新文。文末「延伸阅读」是最顺手的位置,放 5 篇上下、主题真相关的;锚文本写目标页的主题词,比如「Netflix 国内观看指南」就好过「点这里」。流媒体栏目里这几篇就是一组天然的互链对象:

监测看四张表。 Search Console 里日常盯索引覆盖、搜索表现、核心网页指标和链接报告,看该收的有没有漏、不该收的有没有混进来、真实用户数据有没有掉出及格线、内链有没有畸形地堆在少数页面。新文一周没进索引,先自查 robots、noindex、canonical、地图四处,都对再去请求编入索引。

常见误判 ​

审计时最费时间的不是缺配置,而是一些看起来很努力、实际在帮倒忙的做法:

把构建时间当更新时间。 上面提到的浅克隆问题是典型:每次部署全站「更新于今天」,搜索引擎很快会学会不信你的日期,dateModified 与 lastmod 随之失去意义。日期字段只有真实才有价值,拿不准时宁可不写。

全站共用一个标题模板和描述。 批量生成「XX 完全指南 2026」式的标题、几十篇共用一段 description,结果页里自家页面先互相打架。危害不在「惩罚」,而在于搜索引擎分不清每页的主题,索性都不给好位置。

还在为 keywords 字段和 FAQ schema 花时间。 前者 Google 明确不用,后者的富摘要资格已收回给少数权威站点。这两份精力挪到补内链、修死链上,回报实在得多。

OG 图是坏链或全站一张图。 分享卡片是很多读者的第一印象,图 404、图与文不符、全站共用一张默认图都会让转发效果打折。这类问题在源码里看不出来,必须拿真实页面去预览工具里过一遍。

sitemap 里混着 noindex 或已删除的页面。 地图等于你亲手递出的「请收录这些」名单,与实际状态打架就是自降可信度。下线页面时同步清地图、清内链、给旧址安排去向,三者缺一不可。

上线前 SEO 自查清单 ​

把全文要点收成一张表,上线或大改版前逐项勾一遍:

找得到

读得懂

留得住

维护中

最后留一句总纲:部署平台(Cloudflare Pages、Vercel、Netlify)对 SEO 的影响微乎其微,挑顺手的即可——本站的部署细节可看 VitePress + Cloudflare Pages 搭建指南。拉开差距的从来不是某个开关,而是上面这四步有没有一遍遍做实。

延伸阅读 ​



延伸阅读 ​

免责声明

本文仅供技术交流和学习参考。涉及第三方服务的链接可能包含 sponsored 标记,请自行核实服务条款、价格和可用性,并遵守当地法律法规。