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