Astro SEO 基础:title、meta、canonical、hreflang

Astro 站点的最低必备 SEO:每一页都要有的 head 标签、真正影响展示的像素上限,以及在 500+ 篇规模下仍成立的写法。

Astro 不附送任何 SEO 默认值。没有内置的 title 助手,没有自动 canonical,也没有 hreflang,<head> 里的每一个标签都得你自己输出。听起来像苦差事,其实是好事:把对的配置放进一个 <BaseHead> 组件,全站都用它,整个站就天然正确。这篇就是那份最短清单,附上当前(2026 年 6 月)的像素上限和 Google 真正会读的 head 标签。

一句话总结

  • 写一个 <BaseHead> 组件,每页都渲染它,绝不在单页里内联 <title><meta>
  • 四个不可省:唯一的 <title><meta name="description">、指向自己的绝对 <link rel="canonical">,以及多语言时的 <link rel="alternate" hreflang>
  • title 控制在 50-60 个字符(桌面端超过约 600 px 会被截断),description 控制在 120-158 个字符(桌面约 920 px,移动约 680 px)。
  • 用官方的 @astrojs/sitemap 集成生成 sitemap,别手写;robots.txt 指向 /sitemap-index.xml
  • 上线后用 view-source: 和 Rich Results Test 核实渲染出来的 HTML,别凭感觉信。

为什么 Astro 要你自己来

Next.js 这类框架自带 Metadata API,Astro 故意不带。<head> 完全归你管。好处是没有隐藏的默认值跟你的标签打架,而且一个内容站的整个 SEO 面足够小,一个组件文件就能读完。坏处是”分类页忘了写 canonical”是你自己挖的坑,不是框架的限制。把所有标签集中到 <BaseHead>,这一整类问题就消失了。

这件事值得早做对。到了 500+ 篇时,补一个漏掉的标签意味着全站重新抓取,再等 Google 重新处理几周。第一天把组件写对,后面零成本。

这篇适合谁

  • 准备发任何靠 Google 自然流量的 Astro 站。
  • 有,或将有,不止一个语言版本。
  • 会定期发布或更新内容,没法逐页人工检查。

只有内部工具站、不想被收录时才跳过:那种情况全站加 <meta name="robots" content="noindex" /> 就够,读到这里即可。

真正影响展示的像素上限(2026 年 6 月)

字符数只是参考,不是规则。Google 量的是像素宽度,一个 W 比一个 i 宽得多。下面是按桌面端来写的阈值:

标签最佳区间桌面截断点移动端备注
<title>50-60 个字符约 600 px略紧一些;关键词前置
<meta name="description">120-158 个字符约 920 px(约 158 字符)约 680 px(约 120 字符)

控制在 50-60 个字符,能让 title 在大约 90% 的桌面结果里完整显示。另外两条:

  • Google 觉得查询词需要别的展示时,会改写你的 title 或 description,而加粗的命中词更宽,所以别把像素额度顶满,留点余量。
  • 空 description 比短 description 更糟。Google 会从正文自动拼一段摘要,通常比你写的更生硬。

实操步骤

1. 在 astro.config.mjs 里设 site

<head> 里所有绝对 URL(canonical、hreflang、OG 图)都基于 Astro.site 拼出来。不设的话这些标签会悄悄出错,而且 @astrojs/sitemap 根本不会生成。

// astro.config.mjs
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';

export default defineConfig({
  site: 'https://yourdomain.com',
  trailingSlash: 'always',
  build: { format: 'directory' },
  integrations: [
    sitemap({
      i18n: {
        defaultLocale: 'en',
        locales: { en: 'en', zh: 'zh-CN' },
      },
    }),
  ],
});

i18n 块会让 @astrojs/sitemap 在 sitemap 内部也写出 hreflang 标注,相当于在 head 标签之外多了一层 sitemap 级别的信号。defaultLocale 必须是 locales 的某个键。(Google 会忽略 changefreqpriority,别费劲调。)

2. 写 src/components/BaseHead.astro

全站只有这一个地方输出 meta 标签。

---
export interface Props {
  title: string;
  description: string;
  lang?: 'en' | 'zh';
  translationKey?: string;
  ogImage?: string;
}

const { title, description, lang = 'en', translationKey, ogImage } = Astro.props;

if (!title || !description) {
  throw new Error(`BaseHead 必须传 title + description。当前:${Astro.url.pathname}`);
}

const canonical = new URL(Astro.url.pathname, Astro.site).toString();
const ogImg = new URL(ogImage ?? '/og-default.png', Astro.site).toString();
---
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />

<meta property="og:type" content="article" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
<meta property="og:image" content={ogImg} />
<meta property="og:locale" content={lang === 'zh' ? 'zh_CN' : 'en_US'} />

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={ogImg} />

{translationKey && (
  <>
    <link rel="alternate" hreflang="en"
          href={new URL(`/en/articles/${translationKey}/`, Astro.site).toString()} />
    <link rel="alternate" hreflang="zh"
          href={new URL(`/zh/articles/${translationKey}/`, Astro.site).toString()} />
    <link rel="alternate" hreflang="x-default"
          href={new URL(`/en/articles/${translationKey}/`, Astro.site).toString()} />
  </>
)}

这里的 throw 是有意为之。一个因缺 title 而大声失败的构建,好过一个悄悄上线、坏掉的页面。hreflang 块在 EN 和 ZH 两边都输出全部三个变体,满足 Google 的双向规则(见 FAQ)。

3. 在布局里只接一次

---
import BaseHead from '../components/BaseHead.astro';
const { title, description, lang, translationKey } = Astro.props;
---
<html lang={lang}>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <BaseHead {title} {description} {lang} {translationKey} />
  </head>
  <body><slot /></body>
</html>

4. robots.txt 指向 sitemap

构建时 @astrojs/sitemap 会往 dist/ 写出 sitemap-index.xml 和一个或多个 sitemap-0.xml 分片,超过 50,000 个 URL 时自动拆分。在 public/robots.txt 里告诉爬虫索引在哪:

User-agent: *
Allow: /

Sitemap: https://yourdomain.com/sitemap-index.xml

5. 核实渲染出来的 HTML

在已部署的文章上打开 view-source:,你要看到的正是这个结构:

<title>Astro SEO 基础:title、meta、canonical、hreflang</title>
<meta name="description" content="Astro 站点的最低必备 SEO..." />
<link rel="canonical" href="https://yourdomain.com/zh/articles/astro-seo-basics/" />
<meta property="og:title" content="Astro SEO 基础..." />
<link rel="alternate" hreflang="en" href="https://yourdomain.com/en/articles/astro-seo-basics/" />
<link rel="alternate" hreflang="zh" href="https://yourdomain.com/zh/articles/astro-seo-basics/" />
<link rel="alternate" hreflang="x-default" href="https://yourdomain.com/en/articles/astro-seo-basics/" />

6. 一行命令审上线后的站

任何缺 canonical 或三行 hreflang 的 URL 都会被打印出来:

for u in $(cat sitemap-urls.txt); do
  curl -s "$u" | grep -E 'rel="canonical"|hreflang=' \
    | wc -l | awk -v u=$u '$1 < 4 { print u, "标签缺失" }'
done

(自身 + EN + ZH + x-default = 双语页上 4 行命中;按你的标签数量调阈值。)

7. 校验并请求收录

抽一个代表性的 canonical URL 丢进 Google 的 Rich Results Test。它会解析线上的 <head>,暴露隐性问题:canonical 坏了、OG 不合法、或者漏了某个 alternate。然后打开 Search Console 的 URL Inspection,对首页和一篇文章跑一遍,点”请求编入索引”,别被动等下一次抓取。

容易踩的坑

  • 在单页里内联 <title> 十页没问题,到一百页时总有人忘写。<BaseHead> 就是解药。
  • canonical 用相对路径。 只有绝对形式安全;new URL(pathname, Astro.site) 能保证这点。
  • hreflang 不互相引用。 EN 页列了 ZH,但 ZH 页没列 EN,Google 会把整个簇丢掉。每页都输出全部变体。
  • x-default 没有它,Google 自己猜该给哪个语言,常常猜错。
  • OG 和 Twitter 的值不一致。 两者都从同一份 title/description props 推导,就不会漂移。
  • 用 JavaScript 在客户端设 meta。 爬虫可能根本不执行。Astro 在构建时就渲染好 head,这正是你想要的。

FAQ

  • 第一天就要上结构化数据吗? 不用。唯一的 title、description、canonical、hreflang 足够干净地被收录。等基础稳定、页面开始有曝光,再加 ArticleFAQPage JSON-LD。
  • canonical 总是指向当前 URL 吗? self-canonical 是安全默认,Google 也推荐这么做。例外是有意的重复:分页列表可以让第 2 页起指向第 1 页,而 noindex 页应当干脆不写 canonical。
  • 2026 年一个 title 多长合适? 50-60 个字符为宜。Google 在桌面端超过约 600 px 就截断,50-60 字符大约在 90% 的结果里能完整显示。关键词前置,让它在被截断后仍然在。
  • hreflang 需要指向自己的标签吗? Google 现在把自引用当作可选,但它是好习惯、也让整组更好推理,建议保留。可选的是双向互引:每个变体必须引用其他所有变体,否则整簇失效。
  • sitemap 里的 changefreqpriority 有用吗? 没用。Google 已明确表示两者都忽略。保持默认,把精力花在 head 标签上。
  • 该不该直接用 astro-seo 包,而不是自写 <BaseHead> 内容站通常自写组件更好:约 40 行、没有要跟踪的依赖、每个标签你都能控制。只有当你想要带类型的 prop 接口、又不介意这层抽象时,才考虑 astro-seo

相关阅读

标签: #独立开发 #Astro #SEO #Technical SEO #Canonical #hreflang