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.
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.
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.
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
experienceentry through the Delivery API, paginating (limit/skip) withinclude: 2so 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.
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}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.
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}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.
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.xlsxRunning it
It runs locally or on demand in CI:
pnpm reportfor prod (master),pnpm report:stagefor Testing, and--previewto 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.
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