Field Groups, Location Rules, and Naming Conventions
A maintainable ACF system depends less on how many fields you have and more on whether groups are correctly scoped and named.
You will learn to design purpose-driven field groups, apply deterministic location rules, and enforce naming conventions with code and CLI verification. This is critical because ambiguous group scope and weak naming cause the majority of long-term ACF maintenance pain.
Concept Overview
Field groups are not just editor forms. They are schema boundaries. A healthy boundary means each group serves one rendering responsibility, appears only where it should, and exposes field names that are unambiguous in templates, APIs, and migration scripts.
Location rules are your routing layer for schema. If location logic is broad, unrelated editors see irrelevant fields and may save values in the wrong context. If location logic is too narrow or misconfigured, required fields disappear where templates expect them. Precision matters more than convenience.
Naming conventions are your long-term compatibility contract. service_headline communicates both domain and purpose. title_text communicates nothing and eventually collides with other components. Teams that enforce naming policies at registration-time avoid subtle bugs during refactors and staff handovers.
Small, clearly owned field groups + strict location targeting + deterministic naming = predictable runtime behavior and faster team delivery.
Mental Model
| Think of it as... | Because... |
|---|---|
| Field group as a bounded context | It should model one business area (service hero, staff profile, case study stats) |
| Location rules as route guards | They control where schema is allowed to appear and be edited |
| Field names as public API contracts | Templates and automation rely on them staying stable |
| Prefix strategy as namespace management | Prevents collisions between teams and modules |
Why It Matters
| Approach | What Happens | Impact in Production |
|---|---|---|
| One group per template responsibility | Editors see relevant fields only | Fewer input errors and faster publishing |
Broad location rules (post_type == page) everywhere | Unrelated pages inherit irrelevant fields | Noise, misuse, and inconsistent data entry |
Prefix naming (service_, staff_, case_) | Retrieval code is clear and collision-resistant | Safer refactors and lower onboarding cost |
| Enforce naming policy in code | Invalid field names are rejected early | Prevents hidden technical debt in schema layer |
| Overlapping groups with generic names (wrong pattern) | Two groups target same context with ambiguous fields | Template confusion, duplicate data points, and production regressions |
Reference Table
| Term/API | Signature/Syntax | Purpose | Key Notes |
|---|---|---|---|
acf_add_local_field_group() | `acf_add_local_field_group(array $field_group): array | false` | Register group with explicit location and field names |
| Location rule structure | 'location' => [ [ [ 'param' => 'page_template', 'operator' => '==', 'value' => 'template-service.php' ] ] ] | Scope fields to exact admin context | Nested arrays represent OR/AND rule groups |
acf/prepare_field | `add_filter('acf/prepare_field', callable $callback): array | false` | Modify/hide field before render |
acf/validate_value | add_filter('acf/validate_value', callable $callback, int $priority, int $accepted_args): mixed | Validate field input before save | Good for policy enforcement by field name/key |
acf_get_field_groups() | acf_get_field_groups(array $filter = []): array | Query runtime groups and location metadata | Essential CLI audit primitive |
acf_get_fields() | `acf_get_fields(int | string | array $parent): array` |
wp eval | wp eval '<php-code>' | Run runtime audits for naming/scope | Works in CI and maintenance workflows |
| Repeater/Flexible naming | service_highlights, staff_sections, case_stat_rows | Structured collection naming pattern | ACF Pro Required when using Repeater/Flexible |
Practical Use Cases
Use Case 1 — Segment field groups by template responsibility
An agency maintains two page families: service landing pages and case-study pages. Editors previously saw every field on every page. The team now creates two groups with strict location targeting and domain-prefixed names.
- Register one group for service template pages.
- Register another group for case-study post type.
- Prefix all service fields with
service_and case fields withcase_. - Keep location rules mutually exclusive.
- Verify group keys, names, and locations with WP-CLI.
<?php
declare(strict_types=1);
add_action('acf/init', function (): void {
acf_add_local_field_group([
'key' => 'group_service_template_fields',
'title' => 'Service Template Fields',
'fields' => [
[
'key' => 'field_service_headline',
'label' => 'Service Headline',
'name' => 'service_headline',
'type' => 'text',
],
[
'key' => 'field_service_cta_label',
'label' => 'Service CTA Label',
'name' => 'service_cta_label',
'type' => 'text',
],
[
'key' => 'field_service_cta_url',
'label' => 'Service CTA URL',
'name' => 'service_cta_url',
'type' => 'url',
],
],
'location' => [
[[
'param' => 'page_template',
'operator' => '==',
'value' => 'template-service.php',
]],
],
]);
acf_add_local_field_group([
'key' => 'group_case_study_fields',
'title' => 'Case Study Fields',
'fields' => [
[
'key' => 'field_case_outcome_summary',
'label' => 'Outcome Summary',
'name' => 'case_outcome_summary',
'type' => 'textarea',
],
[
'key' => 'field_case_roi_percent',
'label' => 'ROI Percent',
'name' => 'case_roi_percent',
'type' => 'number',
],
],
'location' => [
[[
'param' => 'post_type',
'operator' => '==',
'value' => 'case_study',
]],
],
]);
});
wp eval '$groups=acf_get_field_groups(); print_r(array_column($groups,"key"));'
wp eval '$g=acf_get_field_groups(["key"=>"group_service_template_fields"]); print_r($g[0]["location"]);'
wp eval '$g=acf_get_field_groups(["key"=>"group_case_study_fields"]); $fields=acf_get_fields($g[0]); print_r(array_column($fields,"name"));'
Array
(
[0] => group_service_template_fields
[1] => group_case_study_fields
)
Array
(
[0] => Array
(
[0] => Array
(
[param] => page_template
[operator] => ==
[value] => template-service.php
)
)
)
Array
(
[0] => case_outcome_summary
[1] => case_roi_percent
)
Scope first, then fields. If scope is wrong, naming discipline alone will not save model quality.
Use Case 2 — Enforce naming policy at runtime with validation hooks
A distributed team keeps introducing generic field names like title_text and description. You need a policy that blocks invalid names and surfaces violations during save operations.
- Define allowed prefixes (
service_,staff_,case_,global_). - Add
acf/prepare_fieldto annotate policy failures during editing. - Add
acf/validate_valueto block save when invalid names are used. - Log violations for audit.
- Verify policy behavior with WP-CLI by inspecting field names.
<?php
declare(strict_types=1);
add_action('acf/init', function (): void {
if (!defined('CLINIC_ACF_ALLOWED_PREFIXES')) {
define('CLINIC_ACF_ALLOWED_PREFIXES', ['service_', 'staff_', 'case_', 'global_']);
}
});
add_filter('acf/prepare_field', function ($field) {
if (!is_array($field) || empty($field['name'])) {
return $field;
}
$name = (string) $field['name'];
$allowed = CLINIC_ACF_ALLOWED_PREFIXES;
$isAllowed = false;
foreach ($allowed as $prefix) {
if (str_starts_with($name, $prefix)) {
$isAllowed = true;
break;
}
}
if (!$isAllowed) {
$field['instructions'] = trim((string) ($field['instructions'] ?? ''))
. ' [Naming policy warning: use service_/staff_/case_/global_ prefix]';
}
return $field;
}, 20);
add_filter('acf/validate_value', function ($valid, $value, $field, $input) {
if (!is_array($field) || empty($field['name'])) {
return $valid;
}
$name = (string) $field['name'];
foreach (CLINIC_ACF_ALLOWED_PREFIXES as $prefix) {
if (str_starts_with($name, $prefix)) {
return $valid;
}
}
error_log('[acf-naming-policy] rejected=' . $name);
return 'Field name must start with service_, staff_, case_, or global_';
}, 10, 4);
wp eval '$groups=acf_get_field_groups(); foreach($groups as $group){$fields=acf_get_fields($group); foreach($fields as $field){echo $field["name"] . PHP_EOL;}}'
wp eval '$invalid=[]; $allowed=["service_","staff_","case_","global_"]; foreach(acf_get_field_groups() as $group){foreach(acf_get_fields($group) as $field){$ok=false; foreach($allowed as $p){if(str_starts_with($field["name"],$p)){$ok=true; break;}} if(!$ok){$invalid[]=$field["name"];}}} print_r($invalid);'
service_headline
service_cta_label
service_cta_url
case_outcome_summary
case_roi_percent
Array
(
)
Policy hooks should enforce conventions, not hide bad design. If many names fail, revisit model ownership and review process.
Use Case 3 — Edge case: overlapping location rules create duplicate editor contexts
A rushed release adds a broad post_type == page group that overlaps a service-template group. Editors now see duplicate or conflicting fields, and developers receive inconsistent values depending on which fields were edited last.
❌ Fragile Pattern
<?php
declare(strict_types=1);
add_action('acf/init', function (): void {
acf_add_local_field_group([
'key' => 'group_generic_page_fields',
'title' => 'Generic Page Fields',
'fields' => [
[
'key' => 'field_title_text',
'label' => 'Title Text',
'name' => 'title_text',
'type' => 'text',
],
],
'location' => [[[
'param' => 'post_type',
'operator' => '==',
'value' => 'page',
]]],
]);
});
✅ Robust Pattern
<?php
declare(strict_types=1);
add_action('acf/init', function (): void {
acf_add_local_field_group([
'key' => 'group_service_template_fields',
'title' => 'Service Template Fields',
'fields' => [
[
'key' => 'field_service_headline',
'label' => 'Service Headline',
'name' => 'service_headline',
'type' => 'text',
],
],
'location' => [[[
'param' => 'page_template',
'operator' => '==',
'value' => 'template-service.php',
]]],
]);
acf_add_local_field_group([
'key' => 'group_default_page_fields',
'title' => 'Default Page Fields',
'fields' => [
[
'key' => 'field_page_intro',
'label' => 'Page Intro',
'name' => 'global_page_intro',
'type' => 'textarea',
],
],
'location' => [[[
'param' => 'page_template',
'operator' => '!=',
'value' => 'template-service.php',
]]],
]);
});
wp eval '$service=acf_get_field_groups(["key"=>"group_service_template_fields"]); $default=acf_get_field_groups(["key"=>"group_default_page_fields"]); print_r($service[0]["location"]); print_r($default[0]["location"]);'
wp eval '$all=[]; foreach(acf_get_field_groups() as $g){foreach(acf_get_fields($g) as $f){$all[]=$f["name"];}} print_r(array_filter(array_count_values($all), fn($n)=>$n>1));'
Array
(
[0] => Array
(
[0] => Array
(
[param] => page_template
[operator] => ==
[value] => template-service.php
)
)
)
Array
(
[0] => Array
(
[0] => Array
(
[param] => page_template
[operator] => !=
[value] => template-service.php
)
)
)
Array
(
)
If editors report "duplicate fields," inspect location overlap first. It is usually a scoping bug, not a rendering bug.
Common Mistakes
| Mistake | Root Cause | What Breaks in Production | Correct Pattern |
|---|---|---|---|
| Creating one mega group for many templates | No schema ownership boundaries | Editors save irrelevant values and introduce noise | Split groups by component/template and verify with acf_get_field_groups() filters |
Generic names like title_text | No naming policy enforcement | Name collisions and unreadable templates | Enforce prefixes via acf/validate_value and audit with wp eval |
| Overlapping location rules | Broad rule logic added for speed | Duplicate inputs and conflicting data | Use explicit template/post-type constraints and CLI location dumps |
| Renaming names after launch without migration | Name treated as label, not API contract | Existing values appear missing in templates | Keep old names or migrate with wp post meta + update_field() |
| Missing required flags for mandatory render inputs | No distinction between optional and mandatory fields | Empty headings/CTAs on live pages | Set required => 1 and run content completeness audits |
| Ignoring policy drift over time | No recurring model audits | Technical debt accumulates in schema layer | Add monthly wp eval naming compliance checks |
Deep Dive: Why Overlapping Location Rules Are Harder to Debug Than They Look
Overlaps rarely fail loudly. Editors simply see extra fields and may fill whichever one looks familiar, which creates divergent data paths. Templates then read one field while editors updated another, producing seemingly random frontend mismatches. Because both fields can have similar labels, support teams often suspect caching or deployment issues first. The fastest diagnosis is to inspect each group's location arrays and identify mutually non-exclusive conditions.
wp eval 'foreach(acf_get_field_groups() as $g){echo $g["key"] . PHP_EOL; print_r($g["location"]);} '
Best Practices
- Define one owner and one purpose per field group: e.g., "Service Hero" owned by marketing-engineering team.
- Use prefixes that encode domain context:
service_,staff_,case_,global_. - Design location rules as explicit constraints, not broad guesses: avoid blanket
post_type == pagewhere possible. - Audit naming compliance via CLI regularly:
wp evaloveracf_get_fields()output. - Treat field names as stable contracts: prefer additive change over destructive rename.
- Model optional vs required intentionally: required fields only where template cannot safely fallback.
- Document location intent in code comments and team docs, then verify in runtime outputs.
Hands-On Practice
Exercise 1: Split one mega group into two scoped groups
Create scoped group registrations similar to Use Case 1, then run:
wp eval 'print_r(array_column(acf_get_field_groups(), "title"));'
After completing this exercise, output should include two distinct groups:
Service Template Fields
Case Study Fields
Exercise 2: Verify location exclusivity for service template
Run:
wp eval '$g=acf_get_field_groups(["key"=>"group_service_template_fields"]); print_r($g[0]["location"]);'
After completing this exercise, output should include:
[param] => page_template
[value] => template-service.php
Exercise 3: Enforce naming policy with hooks
Create wp-content/themes/clinic-pro/inc/acf/naming-policy.php from Use Case 2 and run:
wp eval '$invalid=[]; $allowed=["service_","staff_","case_","global_"]; foreach(acf_get_field_groups() as $group){foreach(acf_get_fields($group) as $field){$ok=false; foreach($allowed as $p){if(str_starts_with($field["name"],$p)){$ok=true; break;}} if(!$ok){$invalid[]=$field["name"];}}} print_r($invalid);'
After completing this exercise, expected output:
Array
(
)
Exercise 4: Create test content objects for each model
Run:
wp post create --post_title="Service Rule Drill" --post_status=publish --post_type=page
wp post create --post_title="Case Rule Drill" --post_status=publish --post_type=case_study
After completing this exercise, expected output pattern:
Success: Created post
Success: Created post
Exercise 5: Detect duplicate field names across all groups
Run:
wp eval '$names=[]; foreach(acf_get_field_groups() as $group){foreach(acf_get_fields($group) as $field){$names[]=$field["name"];}} print_r(array_filter(array_count_values($names), fn($n)=>$n>1));'
After completing this exercise, expected output should be empty:
Array
(
)
CLI Reference
| Command | Purpose | Real Example Output |
|---|---|---|
wp eval 'print_r(array_column(acf_get_field_groups(), "key"));' | List runtime group keys | group_service_template_fields group_case_study_fields |
wp eval '$g=acf_get_field_groups(["key"=>"group_service_template_fields"]); print_r($g[0]["location"]);' | Inspect one group's location rules | param => page_template |
wp eval '$g=acf_get_field_groups(["key"=>"group_case_study_fields"]); print_r(array_column(acf_get_fields($g[0]),"name"));' | List names for one group | case_outcome_summary case_roi_percent |
wp eval '$invalid=[]; ... ; print_r($invalid);' | Run naming-prefix compliance check | Array ( ) |
wp post create --post_title="Service Rule Drill" --post_status=publish --post_type=page | Create test page for location verification | Success: Created post 1330. |
wp post create --post_title="Case Rule Drill" --post_status=publish --post_type=case_study | Create test case-study object | Success: Created post 1331. |
wp eval '$names=[]; ... ; print_r(array_filter(array_count_values($names), fn($n)=>$n>1));' | Detect duplicate field names | Array ( ) |
| `wp plugin list --status=active | grep advanced-custom-fields-pro` | Confirm dependency availability |
What's Next
- Continue to Choosing the Right Field Types.
- Return to Module 1 Overview for the full foundations sequence.
- Related lesson: Repeater, Group, Clone, and Flexible Content Patterns.
- Related lesson: Programmatic Registration and PHP Exports.
Revisit this lesson when your team adds a new template or post type and needs to decide how to scope field groups without creating overlap or naming debt.