好的栏目结构是隐形的。坏的栏目结构会在第 200 篇时跳出来:一半文章不知道归在哪,而改一个 hub 名字就意味着弄坏几百个已收录的 URL。趁早把分类规划好,编码进内容 schema、URL 规则、sitemap,你就能躲掉独立内容站最贵的一次返工。
一句话结论
- 用 4-8 个一级栏目(hub) 加扁平标签系统。每个 hub 都应该是一个能填进 8-15 篇以上文章 的主题簇;填不到这个量的,它是标签,不是 hub。
- 选一种 单层 URL(
/en/articles/<slug>/或/en/<hub>/<slug>/),上线前定死。URL 改动是唯一会让你赔上已收录权重的返工。 - 把 hub 写成 Astro 5
src/content.config.ts里的 Zodenum,非法分类直接挂构建,而不是漏到线上。 - 每个 hub 都配一个真实索引页(约 120 字介绍 + 文章列表 + 3-5 条 FAQ),薄标签页在攒到约 8 篇之前保持
noindex,follow。
为什么分类最不能错
栏目结构是文章与搜索引擎之间的契约。它告诉 Google”这 30 篇覆盖同一个主题”——这正是主题权威(topical authority)的信号——也告诉读者”你找 X,这里还有 5 篇 X”。一个成熟的主题簇,能比同样这批文章零散无内链地发出去,拉来明显更多的自然流量,因为 Google 奖励对单一主题的深度覆盖。
分类错误的复利比任何规划错误都快,因为一次改动会同时牵动 URL、面包屑、sitemap 和内链。一个标签打错字只是表面问题;一个 hub 改名却没做跳转,能让整个栏目掉出索引好几周。正是这种不对称,决定了这件事要在第 50 篇之前花一个下午做掉,而不是拖到第 200 篇。
你的站需要这套吗
下面大多数成立,就现在规划 hub 结构:
- 预期 18 个月内能写到 200+ 篇。
- 主题天然能划成 4-8 个明显不同的子方向。
- 读者会想浏览或筛选某个主题,而不仅仅是搜索。
- 变现(订阅、联盟、广告)的转化要靠栏目页落地,而不仅是单篇文章。
- 希望栏目页本身能跑出中等量级关键词排名。
如果你还在 30 篇以内、簇也没成形,单一扁平 collection 加标签就够了。等某个标签攒过约 10 篇、开始像一个独立主题时,再升级成 hub。
开始前准备
- 先把 URL 形态定下来。 扁平(
/en/articles/<slug>/)还是单层嵌套(/en/<hub>/<slug>/)。两种都行,规矩是单层且永久。这个选择会约束后续每个决定,所以定一次就别动。 - 确认框架支持 schema 校验。 Astro 5 Content Collections(Zod 校验)、Next.js MDX 加一道 Zod 解析、或任何能让坏 frontmatter 挂掉构建的方案。
- 带上 50-100 个候选题目。 拿真实的标题点子做归桶,不是纸上推演。一个填不到 8-15 个候选的 hub,根本不算 hub。
URL 规则怎么选
URL 规则决定了将来重组要付多大代价。下面两个单层方案都安全,会咬人的是嵌套那一行。
| URL 规则 | 示例 | 文章换栏目时的重组代价 | 结论 |
|---|---|---|---|
| 扁平,hub 放 metadata | /en/articles/<slug>/ | 零——改 category,URL 不变 | 经常重新归桶的站推荐 |
| 单层嵌套 | /en/<hub>/<slug>/ | 高——每篇换栏目都要做 301 | hub 真的永久就可以 |
| 多层嵌套 | /en/<hub>/<sub>/<slug>/ | 极高——任何重构都层层连锁 | 别用 |
本站用扁平规则(/en/articles/<slug>/),就是为了让换栏目变成改一行 frontmatter、不需要任何跳转。如果你更喜欢把 hub 放进路径让 URL 更好看,现在选定并锁死,因为改它那天,底下每个 URL 你都得欠 Google 一个跳转。
实操步骤
-
锚定 4-8 个核心名词。 写一句”这个站是干什么的”,挑出扛得起这句话的 4-8 个核心名词,那些就是 hub。比如 AI 生产力站可能是:
ai-applications、ai-tools、indie-dev、prompt-library、troubleshooting。 -
每个 hub 按簇的体量做压力测试。 一个主题簇要攒够深度看起来才够全,才挣得到权威。2026 年的经验值是 每个 pillar 配 8-15 篇支撑文章,所以每个 hub 在拍板前至少要列出这么多长尾候选。撑不到约 8 个的 hub 太窄——合到邻居里,或者降级成标签。
-
分类写进 content schema。 Astro 5 的 Content Layer 把配置挪到了
src/content.config.ts,集合也改用loader。把允许的 hub 写成const数组,再把category约束到它上面:
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
const HUBS = [
'ai-applications',
'ai-tools',
'indie-dev',
'prompt-library',
'troubleshooting',
] as const;
const articles = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/articles' }),
schema: z.object({
title: z.string().min(8).max(80),
description: z.string().min(80).max(170),
urlSlug: z.string().regex(/^[a-z0-9-]+$/),
category: z.enum(HUBS), // 严格属于一个 hub
subcategory: z.string().optional(), // 自由格式,可变
tags: z.array(z.string()).max(8),
publishedAt: z.coerce.date(),
lang: z.enum(['en', 'zh']),
translationKey: z.string(),
}),
});
export const collections = { articles };
z.enum(HUBS) 防漂移:加第 9 个 hub 得显式改数组,不是手滑打错字就漏上线。旧的 type: 'content' 写法在 Astro 5 里仍能解析但已弃用——新站直接上 glob() loader,Astro 团队实测它在大集合上构建快约 5 倍、内存约为旧方案的一半。
- URL 规则定下来后写进
astro.config.mjs。 推荐扁平 + 语言前缀:
/en/articles/<slug>/ # 文章
/en/category/<hub>/ # 栏目索引
/en/category/<hub>/page/2/ # 分页
/en/tag/<tag>/ # 标签索引(可 noindex)
Astro 路由:
src/pages/[lang]/articles/[...slug].astro
src/pages/[lang]/category/[hub]/index.astro
src/pages/[lang]/category/[hub]/page/[page].astro
src/pages/[lang]/tag/[tag].astro
- 第一天就建每个 hub 的索引页——就算是空的。 hub 模板要有真实内容,不能只是链接列表,否则 Google 会把它读成薄聚合页。骨架:
---
import { getCollection } from 'astro:content';
const { hub, lang } = Astro.params;
const all = await getCollection('articles',
(a) => a.data.category === hub && a.data.lang === lang);
const top = all.sort((a, b) => +b.data.publishedAt - +a.data.publishedAt);
---
<h1>{hubTitle(hub, lang)}</h1>
<p>{hubIntro(hub, lang)}</p> <!-- ~120 字真实介绍 -->
<ul>
{top.map((a) => (
<li><a href={`/${lang}/articles/${a.data.urlSlug}/`}>{a.data.title}</a></li>
))}
</ul>
<section class="hub-faq">…</section> <!-- 3-5 条 hub 级 FAQ -->
介绍段和 FAQ 一定要发:介绍段是 hub 跑出自己关键词排名的地方,FAQ 让它有资格拿 FAQPage 结构化数据。
-
内链按 hub-and-spoke 接好。 分类只有在内链对应上时才生效。每篇文章用描述性锚文本链回它的 hub,hub 向下链到所属文章,每篇文章再带 1-3 条横向链接 指向同 hub 内相邻的文章。一个实用密度是 每 1000 字 3-5 条上下文内链——既引导读者又不至于刷屏。正是这个结构,把一堆文章变成 Google 能识别的簇。
-
sitemap 给 hub 比文章更高 priority。 hub 0.8,文章 0.6:
<url>
<loc>https://yourdomain.com/en/category/indie-dev/</loc>
<priority>0.8</priority>
<changefreq>weekly</changefreq>
</url>
<url>
<loc>https://yourdomain.com/en/articles/some-slug/</loc>
<priority>0.6</priority>
<changefreq>monthly</changefreq>
</url>
- 标签是后加的。 起步最多 8-12 个,每个标签攒到至少 8 篇之前都保持
noindex,follow。这也是 2026 年的主流建议:自动生成、几乎为空的标签归档正是那种浪费爬取预算、稀释权重的薄页,先让它攒够价值再放 Google 进来。
{tagArticleCount < 8 && <meta name="robots" content="noindex,follow" />}
- CI 里强制单 hub 归属。 一个简单的 prebuild 检查:
# scripts/check-single-hub.mjs(节选)
for (const article of all) {
if (!HUBS.includes(article.data.category)) fail(article, 'invalid hub');
if (Array.isArray(article.data.category)) fail(article, 'multi-hub forbidden');
}
- 每 100 篇审一次。 标签低于 5 篇的合并、超过 40 的拆分,再确认每个 hub 还有至少 8-12 篇。审计日期写进 content-ops 日志,下次审计好有个基线。
执行检查清单
- content schema 用 enum 约束允许的 hub——typo 直接挂。
- 每个 hub 有自己的索引页:介绍 + FAQ + 分页列表。
- sitemap 给 hub 和文章不同 priority。
- 标签页满阈值之前保持
noindex,follow。 - prebuild 脚本对非法 hub 分配直接挂构建。
上线后验证
- Search Console,Performance,Pages:hub URL 应该在 4-8 周内开始累积 hub 级关键词曝光。两个月后某个 hub 还是零曝光,多半是它的介绍文案太薄、排不上。
- 用 URL Inspection 抽查 hub URL,确认已收录、且渲染出的 HTML 里能看到文章列表。
- sitemap 计数:
curl -s https://yourdomain.com/sitemap.xml | grep -c '<loc>',对一下预期数。底层规则以 Google 的抓取与索引文档和robotsmeta 标签参考为准,那是noindex,follow的权威来源。
容易踩的坑
- URL 里塞子子栏目,像
/hub/subhub/subsubhub/slug/,一年内就要痛苦重构。检测:任何语言前缀后有 3+ 段路径。 - 让 hub 野蛮生长,最后变成 15 个 hub 每个只有 5 篇。检测:prebuild 脚本统计每 hub 文章数,任何 hub 低于 12 就告警。
- 拿标签当 hub 用。 搜索引擎奖励的是 hub 形成的那种内链深簇,扁平标签建不起同样的主题权威。
- hub 改名却没做 301。 积累的权重全丢。必须改的话,先把跳转写进
firebase.json或vercel.json,再发改名。 - 一篇文章挂多个 hub。 要么 URL 重复,要么 canonical 弱。在 CI 里强制一篇一个
category。 - 给只有几篇的标签页放 index。 Google 当薄聚合页处理,还拖累爬取预算。
FAQ
- 多少个 hub 算多: 独立站第一年超过 8 个 hub 基本都太多,每个都填不满。每个 hub 至少要撑得起 8-15 篇,读起来才像一个真正的簇。
- hub 页面要不要写内容: 要。需要一段真实的约 120 字介绍、文章列表,外加 3-5 条 FAQ。光一串链接会被读成薄聚合页,很难单独排上。
- 标签有 SEO 价值吗: 更多是帮读者浏览,对排名作用有限。2026 年的共识是标签页攒到约 8 篇之前保持
noindex,follow,让深度内链的 hub 去扛主题权威。 - 现有 Astro 4 站要不要迁到新配置: 还在用
src/content/config.ts加type: 'content'的话,Astro 5 里仍能跑,但src/content.config.ts里的glob()loader 在规模上构建更快、内存更省。等下次动 schema 时顺手迁,不用当紧急事。 - 方向真的需要子子栏目怎么办: 用扁平 URL,把层级放进面包屑和
subcategory元数据,别把深度写进 URL 路径。 - subcategory 要进 URL 吗: 不要。
subcategory放 metadata 和面包屑,URL 保持单层。URL 改动最痛,metadata 改动几乎免费。