Back to Engineering
AuditIn production5 min readaudit.cms

Auditing Contentful Studio Atom Usage

Studio experiences are assembled from a shared library of atoms — our custom components. When we ship a new version of an atom, we can’t remove the old one until every experience using it has moved over. This tool reports exactly which experiences use which atoms, and how often, so that migration is a checklist instead of a guess.

Contentful StudioDelivery APINode / TypeScriptExcelJS
01

The problem

In Contentful Studio, authors build pages by composing atoms — our registered custom components — into an experience. That decoupling is the whole point: authors ship layout without engineering. But it also means the layout lives in Contentful, not in git.

That creates a specific, recurring pain around atom versioning. When we build a v2 of an atom, we can’t safely delete v1 until every experience still referencing it has been migrated — and there’s no way to see that from the codebase. Grepping the repo tells you nothing, because the usage lives in the CMS.

There’s a second wrinkle: patterns — reusable compositions of atoms that authors can drop into a page. An atom used inside a pattern is real usage, so a naive scan of the top-level tree under-counts.

02

What the report answers

The audit is a repeatable script that answers, on demand:

  • Which experiences use atom X, and how many times? — the exact migration list before you change or retire it.
  • How is each atom used across the whole space? — a ranking of what’s load-bearing versus barely touched.
  • What’s on a given experience? — a per-experience breakdown, atom by atom.
  • Has the catalog drifted? — any definitionId not in the registry surfaces as category Unknown, a signal the audit’s component list has fallen behind the app.
03

Approach

Each experience stores its layout as a nested tree, where every node references a component by its definitionId. So the audit is a tree walk with a couple of Contentful-specific details:

  • Pull every experience entry through the Delivery API, paginating (limit / skip) with include: 2 so linked pattern entries resolve.
  • Walk each experience’s componentTree, counting every definitionId occurrence.
  • Expand patterns: when a node matches a usedComponents entry, recurse into that pattern’s own tree — guarded so a pattern is only expanded once.
  • Map each definitionId to a friendly name and category via a registry kept in sync with the app’s registered components.

Delivery API, not Management

The report only needs to read resolved experience structure, so the Delivery API is the right tool — and a --preview flag swaps in the Preview API to include drafts. An --env flag targets master or Testing, so the same script audits any environment.

TSsrc/contentful-client.ts
1import { createClient } from 'contentful';2 3const EXPERIENCE_CONTENT_TYPE = 'experience';4const PAGE_SIZE = 20;5 6// Delivery API for published content — or the Preview API when --preview is set.7export async function fetchAllExperiences(config: AppConfig) {8  const client = createClient({9    space: config.spaceId,10    environment: config.environment, // master | Testing11    accessToken: config.accessToken,12    host: config.host,               // cdn | preview.contentful.com13  });14 15  const all: ExperienceEntry[] = [];16  let skip = 0;17  let total = Infinity;18 19  while (skip < total) {20    const res = await client.getEntries({21      content_type: EXPERIENCE_CONTENT_TYPE,22      limit: PAGE_SIZE,23      skip,24      include: 2, // resolve linked pattern entries (usedComponents)25    });26 27    total = res.total;28    all.push(...res.items.map((item) => ({29      sys: { id: item.sys.id },30      fields: item.fields as ExperienceEntry['fields'],31    })));32    skip += PAGE_SIZE;33  }34 35  return all;36}
Fetch + paginate every experience, resolving linked patterns.
04

The traversal

The interesting part is the recursion. It counts each node, and when a node turns out to be a pattern, it descends into that pattern’s tree too — with a visitedPatterns set so a shared pattern is only expanded once per experience.

TSsrc/tree-traversal.ts
1/**2 * Recursively walks a component tree and counts each definitionId.3 * Also descends into pattern nodes (usedComponents) to count their nested atoms.4 */5export function countAtoms(6  nodes: ComponentTreeNode[],7  usedComponents: UsedComponent[] = [],8  counts: Map<string, number> = new Map(),9  visitedPatterns: Set<string> = new Set(),10): Map<string, number> {11  for (const node of nodes) {12    const { definitionId } = node;13 14    if (definitionId) {15      counts.set(definitionId, (counts.get(definitionId) ?? 0) + 1);16 17      // A node is a "pattern" when its definitionId matches a usedComponent.18      const pattern = usedComponents.find((c) => c.sys?.id === definitionId);19 20      if (pattern && !visitedPatterns.has(definitionId)) {21        visitedPatterns.add(definitionId); // guard against re-expanding / cycles22        const children = pattern.fields?.componentTree?.children;23        if (children?.length) {24          countAtoms(children, usedComponents, counts, visitedPatterns);25        }26      }27    }28 29    if (node.children?.length) {30      countAtoms(node.children, usedComponents, counts, visitedPatterns);31    }32  }33 34  return counts;35}
Pattern-aware depth-first count of every atom.
05

The output

The script prints a summary table sorted by how many experiences use each atom, then writes a styled Excel workbook via ExcelJS with two sheets: a Summary (atom → experiences using it + total usage) and a Detail sheet (one row per experience/atom pair). Excel because the people deciding what to deprecate aren’t always engineers.

SHreport.log
1Contentful Studio Audit2Env: prod (master) | Mode: Published | Space: ****3 4Fetching experiences...5  Fetched 128/128 experiences...6 7Found 128 experiences.8 9-------------------------------------------------------------------------------------10Atom Name                      Category             Experiences     Total Count11-------------------------------------------------------------------------------------12Heading                        Atoms                42              18713Body Text                      Atoms                35              14214Button                         Atoms                38              9515SectionWithBackground          Layout               31              6416FAQ Accordion                  Layout               12              1217Location Search                Maps                 6               918-------------------------------------------------------------------------------------19 20Report saved: reports/atom-usage-report-prod.xlsx
Sample console run — figures are illustrative.
06

Running it

It runs locally or on demand in CI:

  • pnpm report for prod (master), pnpm report:stage for Testing, and --preview to include drafts.
  • Two manual GitLab CI jobs — one per environment — that produce the Excel workbook as a downloadable artifact.
  • A small component registry that’s kept in sync as new atoms are registered in the marketing app.
In the field

A while after this shipped, marketing wanted to retire the Google Maps atom — but first they needed to know which experiences still used it. Contentful’s UI gives you no way to answer that question.

When they reached out, I reminded them about this script. I triggered the GitLab job, downloaded the Excel, and sent back the exact list of experiences — in about two minutes. What would have been a risky guess became a quick, confident cleanup, and the team was thrilled.

Production impact

  • Atom versioning is safe: a concrete list of experiences to migrate before removing anything.
  • Pattern-aware counts, so usage nested inside reusable patterns is never missed.
  • Catalog drift surfaces on its own via the Unknown category.
  • Repeatable per environment, drafts on demand, and shareable as Excel for non-engineers.

Continue exploring

Engineering notes

Related case studies