ACF is one of three patterns Matters AI uses to store editor-controlled content. Knowing when ACF is the right tool is more important than knowing how to use it — the previous agency over-used ACF and left 13 dead field groups behind. This page is the canonical guide.
There are three patterns in this codebase:
| Pattern | Best for | Example |
|---|---|---|
| Workspace + CPT + content JSON | Big editorial structures with their own admin UI | industries, use cases, videos — see CPT pattern |
| Hardcoded in code | Marketing copy, hero text, navbar, footer | Why Matters sections, About Us hero |
| ACF on existing posts/pages | Small, atomic per-entity metadata that editors update without dev help | pdf_download_url on case studies |
ACF's sweet spot is the third pattern — one to five small fields on an existing entity (post, page, category, user) where editors need to override per-instance without writing code.
If you're adding more than ~5 fields to a single ACF group, that's the signal to step back and ask: should this be a workspace module instead? Bigger ACF groups are how the previous agency ended up with 13 dead groups — they made every page template an ACF schema. Don't repeat it.
When you add a new ACF field group, in WP Admin → ACF → Field Groups → Add New:
| Setting | Required value |
|---|---|
| Show in REST API | Yes ⭐ (without this, Next.js can't read it) |
| Allow Access to Value in Editor UI | No (only needed for Gutenberg block bindings) |
| Location rule | Scope to the right entity (post, page, category, user) — typically Post Category is equal to <slug> for per-section fields |
| GraphQL Field Name | camelCase auto-fills correctly |
| GraphQL Non-Null | No (field will be null for entities that haven't been edited) |
Then, in your field group:
snake_case (this is the key in REST: post.acf.<name>)The base Post type in lib/wordpress.d.ts carries an
optional acf object:
export interface Post extends WPEntity {
// ...
acf?: {
author_name?: string;
author_position?: string;
pdf_download_url?: string;
[key: string]: any;
};
}
The index signature lets you read any field without re-typing — but add new fields to the explicit list so they're discoverable. Always guard with optional chaining:
const url = typeof post.acf?.pdf_download_url === "string"
? post.acf.pdf_download_url
: null;
These are the only ACF groups currently consumed by the Next.js app as of 2026-05-29:
| Group | Used For | Read In |
|---|---|---|
| Case Study Resources | pdf_download_url on case-study posts | app/[...slug]/page.tsx |
| Blog Overrides | All per-post editorial overrides — schema, visibility, layout, author social — see fields below | lib/post-schema.ts, BlogContent.tsx, BlogHero.tsx, lib/blogs.ts |
| Category Customisation | Per-category landing customisation on the Category taxonomy | app/[...slug]/page.tsx, BlogsHero.tsx |
| (live banner fields) | banner_text, banner_link, banner_mobile_text on the site-banner Page | lib/settings.ts |
| (live author fields) | author_name, author_position on Posts | Author bylines throughout components/blogs/ |
| (live SEO meta) | meta_title, meta_description, meta_keywords | SEO consumption is via Rank Math, not raw ACF — likely not in any field group we control |
If you add a new ACF group that becomes live, add a row here.
Single per-Post field group covering schema overrides, listing visibility, TOC layout, and per-post author social links.
Group settings:
| Setting | Value |
|---|---|
| Field group title | Blog Overrides (if you already have Blog Schema Overrides, just rename it and add the new fields below) |
| Location | Post Type is equal to Post |
| Show in REST API | Yes ⭐ |
Fields (use ACF Tab fields between sections for the post-edit UX):
| Section | Field Label | Field Name | Type | Notes |
|---|---|---|---|---|
| Schema | Disable auto-generated FAQ schema | disable_auto_faq_schema | True/False | Suppresses auto-FAQ JSON-LD on this post |
| FAQ schema override | faq_schema_override | Textarea | Raw JSON-LD object — replaces auto-FAQ | |
| Additional JSON-LD | custom_jsonld | Textarea | Raw JSON-LD object or array — always appended | |
| Visibility & layout | Featured (pin to top) | is_featured | True/False | Listing pages sort featured posts first; renders a "Featured" badge on the card |
| Show Table of Contents | show_toc | True/False (default true) | Hide TOC on this post | |
| TOC depth | toc_depth | Number (min 2, max 6, step 1, default 2) | 2 = H2 only, 3 = H2+H3, 4 = H2+H3+H4, … | |
| Author overrides | Author LinkedIn URL | author_linkedin | URL | Renders LinkedIn icon next to byline; first author only |
| Author Twitter / X URL | author_twitter | URL | Renders X/Twitter icon next to byline; first author only | |
| Author photo URL | author_photo | Image (return URL) | Overrides the WP avatar for first author on this post |
Resolution order for the schema fields on each request:
faq_schema_override parses to a valid object → that block is emitted (auto-FAQ skipped).disable_auto_faq_schema is false (or unset) and the post body contains
any matters-faq-block, the auto-extracted FAQPage JSON-LD is emitted.custom_jsonld is then appended.Invalid/unparseable JSON is silently ignored (no broken pages).
Per-category landing-page overrides. Scoped to the Category taxonomy (not Post), so it must be a separate field group.
Group settings:
| Setting | Value |
|---|---|
| Field group title | Category Customisation |
| Location | Taxonomy is equal to Category |
| Show in REST API | Yes ⭐ |
Fields:
| Field Label | Field Name | Type | Notes |
|---|---|---|---|
| Hero image | category_hero_image | Image (return URL) | Renders a banner image on /<category>/ above the carousel |
| Long description | category_description_long | Wysiwyg Editor | HTML allowed. Replaces the default plain-text description in the hero section |
| SEO intro | category_seo_intro | Textarea | Overrides the meta-description used in <head> for the listing page |
Edit any category in WP Admin → Posts → Categories → click a category → fields show in the right panel.
These 13 groups returned zero hits across the Next.js codebase when audited on 2026-05-29. They are leftovers from the previous agency's WordPress-as-frontend setup, where every page template was an ACF schema. After migrating to Next.js, all the page data was hardcoded or moved to CPTs — but the ACF groups were never cleaned up.
| Group | Fields | Earlier verdict | Real verdict |
|---|---|---|---|
| About us | 5+ tested | "Keep" (false positive on AboutUs component name) | 🔴 Dead |
| Footer | 5+ tested | "Keep" (false positive on Footer component) | 🔴 Dead |
| Header | 9 tested | "Keep" (false positive on 193 Header refs) | 🔴 Dead |
| Homepage | 9 tested | "Keep" | 🔴 Dead |
| How it works | 5 tested | "Keep" (false positive on service fn names) | 🔴 Dead |
| Stack | 3 tested | "Keep" (Options page) | 🔴 Dead |
| Testimonials | 1 tested | "Keep" (Options page) | 🔴 Dead |
| Use Case Template | 1 tested | "Keep" (data actually from CPT JSON) | 🔴 Dead |
| Why Matters | 5 tested | "Keep" (false positive on WhyMatters page) | 🔴 Dead |
| Single post | 5 tested | "Investigate" | 🔴 Dead — fields named author, job_position, content_title, post_sections not read |
| Hero Section Footer | 5 tested | "Investigate" | 🔴 Dead |
| Contact us | 3 tested | "Investigate" | 🔴 Dead — the contact_form hit was a form-source string, not ACF |
| Post category bg color | 2 tested | "Investigate" | 🔴 Dead — category_color in code is computed via getColorForLabel(), not from this ACF |
| Services page template | 1 tested | "Investigate" | 🔴 Dead |
Do not delete groups yet. Deletion is one-way; deactivation preserves the data on
each post (in wp_postmeta) and the field definitions, so reactivation is risk-free.
The two-week soak window planned below (deactivate → 2026-06-12) has now elapsed. WordPress ACF state lives outside this repo, so confirm the current Inactive/Deleted status directly in WP Admin → ACF → Field Groups before acting. Treat the steps below as the standing procedure for any group still pending cleanup.
Process:
/about-us/use-case/<any>/platform/why-matters/platform/how-it-works/industry/<any>This is the canonical "right size for ACF" implementation. Follow this when adding a new ACF field.
| Setting | Value |
|---|---|
| Field group title | Case Study Resources |
| Location | Post Category is equal to Read What Matters – Case Studies |
| Show in REST API | Yes |
| Allow Access to Value in Editor UI | No |
| Field name | pdf_download_url |
| Field type | URL |
| Required | No |
/downloads/payu-case-study.pdf (or a WP media library URL).// app/[...slug]/page.tsx
const downloadUrl = isCaseStudy
? typeof post.acf?.pdf_download_url === "string"
? post.acf.pdf_download_url
: null
: null;
return <BlogPostTemplate ... downloadUrl={downloadUrl} ... />;
Before this change, the same value lived in a static lib/case-study-downloads.ts map.
Devs had to commit + deploy for every new case-study PDF. Now editors update WP → next
revalidation pulls the new URL automatically.
Documented for visibility; not built yet as of 2026-05-29:
| Field | Entity | Why | Effort |
|---|---|---|---|
is_gated, lead_capture_form_id | Datasheet / Whitepaper | Editor-controlled gating | M |
Add new rows here when you're considering adding ACF. Don't slip into the old-agency pattern of one ACF group per page template.