Pillar and Cluster Pages — The Structure Google Rewards

Design pillar and cluster pages so Google understands your topical authority. Includes content schema, link-audit script, and pillar page template.

Pillar/cluster is not an SEO trick — it is just a way of organizing content that mirrors how Google evaluates topical authority. When done right, your pillar page ranks for the broad term and each cluster page ranks for a long-tail variation, and they all link to each other in a sensible way. The trick is encoding the relationships in your content schema, not in your head.

Background

A “pillar page” is a broad page covering a topic at depth (e.g. “Setting up a content site”). A “cluster page” is a narrower page covering one subtopic in detail (e.g. “Submitting your sitemap to Google”). Each cluster links up to the pillar; the pillar links down to each cluster. Google reads the link graph and concludes “this site has depth on this topic”.

How to tell

  • You can name your top 5-10 topic clusters in one breath.
  • Each pillar page has 5-15 cluster pages linking to it.
  • No cluster page is more than 2 clicks from the homepage.
  • Internal anchor text uses the actual target keyword, not “click here”.
  • A new article naturally fits into an existing pillar without manual reshuffling.

Quick verdict

Set up pillar/cluster as data — a pillar field in frontmatter and a script that builds the link graph. Two-way linking, descriptive anchors, and orphan detection are what make it work.

Before you start

  • You have at least 20-30 published articles to organize.
  • Content schema can be extended to add a pillar field.
  • You can run a small Node script to audit links.

Step by step

  1. Add a pillar field to frontmatter. Extend the schema:
// src/content/config.ts
schema: z.object({
  // ...
  pillar: z.string().optional(),    // slug of the pillar this article belongs to
  isPillar: z.boolean().default(false),
}),

Cluster article:

---
title: "How to submit a sitemap to Search Console"
urlSlug: "submit-sitemap-search-console"
pillar: "submit-website-to-google"
isPillar: false
---

Pillar article:

---
title: "Submitting a new site to Google in 2026"
urlSlug: "submit-new-site-to-google-2026"
isPillar: true
---
  1. Brainstorm 5-10 pillar topics. Each needs at least 10-20 candidate clusters or it is too narrow. If you have more than 10 pillars, you are too wide.

  2. Write cluster articles first. A pillar drafted before the clusters comes out abstract. Ship 5-7 clusters per pillar, then write the pillar — the pillar will be richer and more honest.

  3. Generate the pillar’s “child” list automatically. In the pillar’s layout:

---
import { getCollection } from 'astro:content';
const { lang, urlSlug } = Astro.props.article.data;
const clusters = (await getCollection('articles', (a) =>
  a.data.lang === lang && a.data.pillar === urlSlug && !a.data.isPillar
)).sort((a, b) => a.data.title.localeCompare(b.data.title));
---
<h2>In this guide</h2>
<ul>
  {clusters.map((c) => (
    <li>
      <a href={`/${lang}/articles/${c.data.urlSlug}/`}>{c.data.title}</a>
      <p>{c.data.description}</p>
    </li>
  ))}
</ul>
  1. Back-link clusters to pillar automatically. In the cluster’s layout, render a “back to pillar” block:
---
const { lang, pillar } = Astro.props.article.data;
const p = pillar
  ? (await getEntry('articles', `${lang}/${pillarPath}/${pillar}`))
  : null;
---
{p && (
  <aside class="pillar-up">
    Part of: <a href={`/${lang}/articles/${p.data.urlSlug}/`}>{p.data.title}</a>
  </aside>
)}
  1. Use descriptive anchor text in both directions. Pillar → cluster anchors describe the cluster’s topic; cluster → pillar anchors describe the pillar’s topic. Avoid “click here” or the literal slug.

  2. Run a pillar/cluster audit script. Find weak pillars and orphan clusters:

// scripts/audit-pillars.mjs
import { readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import matter from 'gray-matter';

const byPillar = new Map();
const pillars = new Set();

function walk(dir) {
  for (const f of readdirSync(dir, { withFileTypes: true })) {
    const full = join(dir, f.name);
    if (f.isDirectory()) walk(full);
    else if (f.name.endsWith('.mdx')) {
      const { data } = matter(readFileSync(full, 'utf8'));
      if (data.isPillar) pillars.add(data.urlSlug);
      else if (data.pillar) {
        if (!byPillar.has(data.pillar)) byPillar.set(data.pillar, []);
        byPillar.get(data.pillar).push(data.urlSlug);
      }
    }
  }
}
walk('src/content/articles');

for (const p of pillars) {
  const count = (byPillar.get(p) || []).length;
  if (count < 4) console.warn(`WEAK PILLAR (${count}): ${p}`);
}
for (const [pillar, kids] of byPillar) {
  if (!pillars.has(pillar)) {
    console.warn(`ORPHAN CLUSTERS (no pillar published yet) for "${pillar}": ${kids.join(', ')}`);
  }
}
  1. Quarterly review. Any pillar with < 4 cluster links is weak; any orphan cluster (no pillar yet) needs a pillar built.

Implementation checklist

  • Schema includes pillar + isPillar fields.
  • Pillar pages auto-render the cluster list from frontmatter, not by hand.
  • Each cluster has a back-link block.
  • Audit script runs in prebuild and warns on weak pillars / orphans.
  • Anchor text is descriptive on both sides.

After-launch verification

  • Search Console → Performance → Pages: pillar URLs accumulate impressions for broad keywords.
  • URL Inspection on a cluster shows the pillar in “Referring URLs”.
  • Lighthouse → Accessibility → Links have discernible names — all green.

Common pitfalls

  • Making the pillar a giant 10,000-word page that tries to be everything. Better: a structured overview that links out.
  • Linking from pillar to clusters but not from clusters back. Two-way linking is what makes the cluster pattern work.
  • Using the same anchor text for every link. Vary it naturally with related keywords.
  • Building too many pillars. Five strong pillars beat fifteen half-built ones every time.
  • Letting the cluster list go stale by hard-coding it in the pillar instead of generating it.
  • Treating pillar/cluster as a URL structure (/pillar/<x>/cluster/<y>/). It is a logical structure, not a URL one.

FAQ

  • How long should a pillar page be?: 2000-4000 words is typical, but length matters less than coverage. If it links to 10 deep cluster articles, it doesn’t need to repeat them.
  • Can a cluster page link to another cluster?: Yes, and it should when relevant. The pattern is a hub-and-spoke, but spokes can connect to each other too.
  • Do I need a /pillar/ URL pattern?: No. The structure is logical, not URL-based. A pillar and its clusters can all live under /articles/.
  • What if I have overlapping pillars?: Pick one as canonical and merge the other, or scope them more narrowly so they don’t overlap.
  • Should I add JSON-LD WebPage about schema?: Optional but helpful. Mark pillars as WebPage with an about property pointing to the topic.

Tags: #Indie dev #Content ops #SEO #Website planning #Pillar / Cluster #Technical SEO