A good section structure is invisible. A bad one shows up at article 200, when half your articles do not fit anywhere and renaming a hub means breaking a few hundred indexed URLs. Plan the taxonomy now — encode it in your content collection schema, your URL pattern, and your sitemap — and you skip the most expensive rewrite an indie content site can hit.
TL;DR
- Use 4-8 top-level sections (hubs) plus a flat tag system. Each hub should be a topic cluster you can fill with 8-15+ articles; fewer than that and it is a tag, not a hub.
- Pick a single-level URL (
/en/articles/<slug>/or/en/<hub>/<slug>/) and lock it before you publish. URL changes are the only refactor that costs you indexed authority. - Encode hubs as a Zod
enumin your Astro 5src/content.config.tsso an invalid category fails the build instead of leaking into production. - Give every hub a real index page (~120-word intro + article list + 3-5 FAQs), and keep thin tag pages
noindex,followuntil they hold roughly 8 articles each.
Why taxonomy is the most expensive thing to get wrong
Section structure is the contract between your articles and search engines. It tells Google “these 30 articles cover the same topic” — the signal behind topical authority — and it tells readers “you came for X, here are five more X articles”. A mature topic cluster can pull noticeably more organic traffic than the same articles published as scattered, unlinked posts, because Google rewards depth on a single subject.
Taxonomy mistakes compound faster than any other planning mistake because one change touches URLs, breadcrumbs, sitemaps, and internal links at the same time. A typo in a tag is cosmetic. A renamed hub without redirects can drop a section out of the index for weeks. That asymmetry is the whole reason to spend an afternoon on this before article 50, not after article 200.
Does your site need this?
Plan a hub structure now if most of these are true:
- You expect 200+ articles within 18 months.
- Your topics naturally cluster into 4-8 distinct sub-areas.
- You can imagine a reader wanting to browse or filter a topic, not just search.
- Your conversion (newsletter, affiliate, ads) depends on readers landing on a section page, not only on individual articles.
- You want category pages to rank for medium-volume keywords on their own.
If you are below 30 articles with no clear clusters yet, a single flat collection with tags is fine. Add hubs the moment a tag crosses ~10 articles and starts feeling like its own topic.
Before you start
- Decide the URL shape first. Flat (
/en/articles/<slug>/) or single-level nesting (/en/<hub>/<slug>/). Either works; the rule is single-level and permanent. This choice constrains every downstream decision, so make it once. - Confirm your framework supports schema validation. Astro 5 Content Collections (Zod-validated), Next.js MDX + a Zod parse step, or any setup where bad frontmatter can fail the build.
- Bring 50-100 candidate topics. Do a real bucket exercise on actual title ideas, not a theoretical sketch. A hub you cannot fill with at least 8-15 candidates is not a hub.
Choosing a URL pattern
The pattern decides how much pain a future reorganization costs. The two single-level options below are both safe; the nested option is the one that bites.
| URL pattern | Example | Reorg cost when a topic moves | Verdict |
|---|---|---|---|
| Flat, hub in metadata | /en/articles/<slug>/ | Zero — change category, URL is unchanged | Recommended for sites that re-bucket often |
| Single-level nesting | /en/<hub>/<slug>/ | High — every moved article needs a 301 | Fine if hubs are truly permanent |
| Multi-level nesting | /en/<hub>/<sub>/<slug>/ | Very high — any restructure cascades | Avoid |
This site uses the flat pattern (/en/articles/<slug>/) precisely so that re-bucketing an article is a one-line frontmatter edit with no redirect. If you prefer the hub in the path for cleaner-looking URLs, pick it now and commit, because the day you change it you owe Google a redirect for every URL underneath.
Step by step
-
Anchor on 4-8 nouns. Write a one-sentence “what this site is about” statement and pull out the 4-8 nouns that carry it. Those are your hubs. For an AI-productivity site that might be:
ai-applications,ai-tools,indie-dev,prompt-library,troubleshooting. -
Stress-test each hub against the cluster math. A topic cluster earns its authority when it holds enough depth to look comprehensive. The 2026 working number is 8-15 supporting articles per pillar, so list at least that many long-tail candidates for each hub before you commit. A hub that cannot reach ~8 candidates is too narrow — fold it into a neighbor or demote it to a tag.
-
Encode the taxonomy in your content schema. Astro 5’s Content Layer moved the config to
src/content.config.tsand switched collections to aloader. Define the allowed hubs as aconstarray and constraincategoryto it:
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), // exactly one hub
subcategory: z.string().optional(), // free-form, fluid
tags: z.array(z.string()).max(8),
publishedAt: z.coerce.date(),
lang: z.enum(['en', 'zh']),
translationKey: z.string(),
}),
});
export const collections = { articles };
The z.enum(HUBS) constraint is what prevents drift: a 9th hub becomes an explicit edit to the array, not a silent typo that ships. The legacy type: 'content' API still parses in Astro 5 but is deprecated — new sites should start on the glob() loader, which the Astro team measures as roughly 5x faster builds and about half the memory of the old approach on large collections.
- Pick a URL pattern and lock it in
astro.config.mjs. Recommended flat-with-language layout:
/en/articles/<slug>/ # article
/en/category/<hub>/ # hub index
/en/category/<hub>/page/2/ # paginated
/en/tag/<tag>/ # tag index (optional noindex)
The Astro routes:
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
- Build hub index pages from day one — even if empty. A hub template must produce real content, not just a link list, or Google reads it as a thin aggregation page. Skeleton:
---
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 words of real context -->
<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-level Q&A -->
Always ship the intro paragraph and FAQ. The intro is where the hub earns its own keyword ranking; the FAQ is what gives it FAQPage schema eligibility.
-
Wire the internal links hub-and-spoke. Taxonomy only pays off if the links match it. Each article links back to its hub with descriptive anchor text, the hub links down to its articles, and every article carries 1-3 lateral links to adjacent articles in the same hub. A practical density is 3-5 contextual internal links per 1,000 words — enough to guide a reader without spamming. This is the structure that turns a folder of posts into a cluster Google can recognize.
-
Reflect hubs in the sitemap with priority hints. Hub pages should be priority 0.8, articles 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>
- Add tags only after you feel the gap. Start with 8-12 tags maximum, and keep each one
noindex,followuntil it holds at least 8 articles. This is the prevailing 2026 guidance: auto-generated, near-empty tag archives are exactly the thin pages that waste crawl budget and dilute authority, so you let them accumulate value before inviting Google in.
{tagArticleCount < 8 && <meta name="robots" content="noindex,follow" />}
- Enforce single-hub assignment in CI. A quick prebuild check:
# scripts/check-single-hub.mjs (excerpt)
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');
}
- Audit every 100 articles. Merge tags below 5 articles, split tags above ~40, and confirm each hub still holds at least 8-12 articles. Record the audit date in your content-ops log so the next pass has a baseline.
Implementation checklist
- Content schema enums the allowed hubs — typo-resistant.
- Each hub has its own index page with intro + FAQ + paginated list.
- Sitemap distinguishes hub vs article priority.
- Tag pages stay
noindex,followuntil they pass a minimum article threshold. - Prebuild script fails the build on invalid hub assignment.
After-launch verification
- Search Console, Performance, Pages: hub URLs should accumulate impressions on hub-level keywords within 4-8 weeks. If a hub gets zero impressions after two months, its intro copy is probably too thin to rank.
- Crawl a hub URL with the URL Inspection tool and confirm it is indexed and rendered with the article list visible in the rendered HTML.
- Run a sitemap probe:
curl -s https://yourdomain.com/sitemap.xml | grep -c '<loc>'and check it against your expected count. For the underlying rules, Google’s crawling and indexing docs and therobotsmeta tag reference are the authoritative source onnoindex,follow.
Common pitfalls
- Sub-sub-sections in the URL like
/hub/subhub/subsubhub/slug/. Refactoring gets painful inside a year. Detection: any URL with 3+ path segments after the language prefix. - Hubs that grow organically until you have 15 hubs of 5 articles each. Detection: a prebuild script that counts articles per hub and warns when any hub holds fewer than 12.
- Using tags as a substitute for hubs. Search engines reward the deep, internally-linked cluster a hub creates; a flat tag does not build the same topical authority.
- Renaming a hub with no 301s in place. You lose the accumulated authority. If you must rename, ship the redirects in
firebase.jsonorvercel.jsonbefore the rename goes live. - Articles assigned to multiple hubs. Duplicate URLs or weak canonicals follow. Enforce one
categoryper article in CI. - Indexing tag pages that hold only a handful of articles. Google treats them as thin aggregations and they can drag on crawl budget.
FAQ
- How many hubs is too many?: For an indie site, more than 8 hubs is almost always too many in year 1 — you will under-fill every one. Each hub should sustain at least 8-15 articles to read as a real cluster.
- Should hub pages have content or just be lists?: They need a real ~120-word introduction, the article list, and a 3-5 question FAQ. A bare list of links reads as a thin aggregation page and rarely ranks on its own.
- Do tags help SEO?: They help readers browse more than they help rankings. The 2026 consensus is to keep tag pages
noindex,followuntil each holds roughly 8 articles, so the deep, internally-linked hubs carry the topical-authority weight instead. - Should I migrate an existing Astro 4 site to the new config?: If you are still on
src/content/config.tswithtype: 'content', it keeps working in Astro 5, but theglob()loader insrc/content.config.tsbuilds faster and uses less memory at scale. Migrate when you next touch the schema, not as an emergency. - What if my niche genuinely needs sub-sub-sections?: Use a flat URL and represent the hierarchy in breadcrumbs and
subcategorymetadata. Do not encode the depth in the URL path. - Should I add
subcategoryto the URL?: No. Keepsubcategoryin metadata and breadcrumbs and keep URLs single-level. URL changes are the most painful refactor; metadata changes are free.
Related
- Planning a long-tail keyword site from day one
- Should a new content site go broad or deep first
- How to pick a niche that has search demand
- Pillar and cluster pages
- Should category pages be indexed?
Tags: #Indie dev #Website planning #Pillar / Cluster #SEO #Content ops #Technical SEO