Reading Field Values in Templates
Reliable template output starts with deterministic value retrieval and context-correct escaping, not direct field echoing.
You will implement robust retrieval patterns for scalar, structured, and relational ACF values, choose correct escaping per output context, and validate real runtime payloads with WP-CLI before release.
Concept Overview
ACF retrieval in templates is a data-contract problem, not a convenience problem. get_field() returns values for logic, while direct output helpers can hide validation and escaping decisions. In production-grade templates, you should retrieve first, validate type and emptiness, then render with context-specific escaping.
Different fields return different shapes: simple scalar strings, arrays (Group/Image), object arrays (Relationship), row sets (Repeater/Flexible), and option-scoped globals. If rendering code assumes the wrong shape, failures often appear as empty output, warnings, or malformed markup.
The safest pattern is explicit normalization. Convert uncertain payloads into predictable internal variables before output. That enables easy fallbacks, consistent escaping, and testable component behavior. In code-first teams, you should also prove payload shape via WP-CLI commands, not visual guessing in admin screens.
Read values with get_field(), normalize shape, escape by output context, and render only when contract checks pass.
Why It Matters
| Approach | What Happens | Impact in Production |
|---|---|---|
| Retrieve to variable then escape on render | Validation and output concerns stay explicit | Lower XSS risk and fewer rendering regressions |
| Echo field values directly in markup | Hard to test shape/emptiness and escape correctness | Inconsistent output and hidden security debt |
| Normalize arrays/IDs/objects before rendering | Return-format drift becomes manageable | Fewer runtime notices and faster incident resolution |
| Use context-aware fallback rules | Components remain usable when optional fields are empty | Better UX continuity during editorial drafting |
| Assume payload shape without checks (wrong pattern) | Array/object/int mismatches break templates at runtime | Production defects that are hard to reproduce |
Reference Table
| Term/API | Signature/Syntax | Purpose | Key Notes |
|---|---|---|---|
get_field() | `get_field(string $selector, int | string $post_id = false, bool $format_value = true): mixed` | Retrieve value for logic and rendering |
the_field() | `the_field(string $selector, int | string $post_id = false): void` | Directly echo a field value |
esc_html() | esc_html(string $text): string | Escape plain text in HTML body | Use for headings, labels, and inline text |
esc_attr() | esc_attr(string $text): string | Escape attribute values | Use for tel:, id, data-*, and class fragments |
esc_url() | esc_url(string $url): string | Escape URL output for links/src | Required for href and image URLs |
wp_kses_post() | wp_kses_post(string $content): string | Allow safe subset of HTML | Use for trusted rich-text field output |
| Option context read | get_field('field_name', 'option') | Retrieve global option-scoped value | ACF Pro Required when Options Pages are used |
have_rows() | `have_rows(string $selector, int | string $post_id = false): bool` | Iterate Repeater/Flexible rows |
wp eval | wp eval '<php-code>' | Inspect payload type and value in runtime | Critical for debugging return-format assumptions |
Practical Use Cases
Use Case 1 — Harden hero component output with shape and context guards
A marketing hero component depends on four fields: title, subtitle, CTA label, and CTA URL. Editors sometimes leave one field blank during drafts. You need safe fallback behavior and strict output escaping.
- Retrieve all hero values with
get_field(). - Normalize each to string and apply fallback only where allowed.
- Render CTA only when both label and URL are valid.
- Escape text, URLs, and attributes per context.
- Verify value retrieval and fallback behavior via CLI.
<?php
declare(strict_types=1);
add_action('template_redirect', function (): void {
if (!is_page()) {
return;
}
$title = (string) get_field('hero_title');
$subtitle = (string) get_field('hero_subtitle');
$ctaLabel = (string) get_field('hero_cta_label');
$ctaUrl = (string) get_field('hero_cta_url');
if ($title === '') {
$title = 'Talk to our specialists today';
}
echo '<section class="hero">';
echo '<h1>' . esc_html($title) . '</h1>';
if ($subtitle !== '') {
echo '<p class="hero-subtitle">' . esc_html($subtitle) . '</p>';
}
if ($ctaLabel !== '' && $ctaUrl !== '') {
echo '<a class="hero-cta" href="' . esc_url($ctaUrl) . '">' . esc_html($ctaLabel) . '</a>';
}
echo '</section>';
});
add_action('wp', function (): void {
if (!is_page()) {
return;
}
$title = (string) get_field('hero_title');
$ctaUrl = (string) get_field('hero_cta_url');
error_log('[hero-read] page=' . get_the_ID() . ' title_len=' . strlen($title) . ' cta_len=' . strlen($ctaUrl));
});
wp post create --post_title="Template Read Drill" --post_status=publish --post_type=page
wp eval '$id=(int)get_page_by_title("Template Read Drill", OBJECT, "page")->ID; update_field("hero_title","",$id); update_field("hero_subtitle","Launch in days, not months.",$id); update_field("hero_cta_label","Book Demo",$id); update_field("hero_cta_url","/book-demo",$id); echo (get_field("hero_title",$id) ?: "fallback") . "|" . get_field("hero_cta_url",$id) . PHP_EOL;'
Success: Created post 1360.
fallback|/book-demo
Fallbacks should be policy-driven. Apply them to strategic fields (like hero title), not globally to every value.
Use Case 2 — Render option-level defaults plus post-level overrides safely
A service page should use page-specific support phone if present, otherwise global support phone from options. You need clear retrieval precedence and safe tel-link rendering.
- Read page-level value first.
- Read option-level fallback with
'option'context. - Select final value with explicit precedence.
- Render tel-link only when final value is non-empty.
- Verify both paths (override and fallback) through CLI.
<?php
declare(strict_types=1);
add_action('acf/init', function (): void {
if (!function_exists('acf_add_options_page')) {
return;
}
acf_add_options_page([
'page_title' => 'Clinic Global Contact',
'menu_title' => 'Global Contact',
'menu_slug' => 'clinic-global-contact',
'capability' => 'manage_options',
'redirect' => false,
]);
});
add_action('template_redirect', function (): void {
if (!is_page()) {
return;
}
$pagePhone = (string) get_field('page_contact_phone');
$globalPhone = (string) get_field('global_support_phone', 'option');
$finalPhone = $pagePhone !== '' ? $pagePhone : $globalPhone;
if ($finalPhone === '') {
return;
}
$telHref = preg_replace('/[^0-9+]/', '', $finalPhone);
echo '<a class="contact-phone" href="tel:' . esc_attr((string) $telHref) . '">' . esc_html($finalPhone) . '</a>';
});
add_action('wp', function (): void {
if (!is_page()) {
return;
}
$pagePhone = (string) get_field('page_contact_phone');
$globalPhone = (string) get_field('global_support_phone', 'option');
$source = $pagePhone !== '' ? 'page' : ($globalPhone !== '' ? 'option' : 'none');
error_log('[contact-phone] source=' . $source . ' page=' . get_the_ID());
});
wp eval 'update_field("global_support_phone", "+65 6700 1234", "option"); echo get_field("global_support_phone", "option") . PHP_EOL;'
wp eval '$id=(int)get_page_by_title("Template Read Drill", OBJECT, "page")->ID; update_field("page_contact_phone", "", $id); $page=(string)get_field("page_contact_phone",$id); $global=(string)get_field("global_support_phone","option"); echo ($page !== "" ? $page : $global) . PHP_EOL;'
+65 6700 1234
+65 6700 1234
Options page retrieval (get_field(..., 'option')) in this workflow assumes ACF Pro options-page usage.
Use Case 3 — Edge case: image return-format mismatch (array vs ID)
A template assumes image field returns an array (url, alt) but field settings were changed to return attachment ID. The component starts throwing notices and broken <img> tags.
❌ Fragile Pattern
<?php
declare(strict_types=1);
add_action('template_redirect', function (): void {
if (!is_page()) {
return;
}
$image = get_field('hero_image');
echo '<img src="' . esc_url($image['url']) . '" alt="' . esc_attr($image['alt']) . '">';
});
✅ Robust Pattern
<?php
declare(strict_types=1);
add_action('template_redirect', function (): void {
if (!is_page()) {
return;
}
$image = get_field('hero_image');
$url = '';
$alt = '';
if (is_array($image)) {
$url = (string) ($image['url'] ?? '');
$alt = (string) ($image['alt'] ?? '');
} elseif (is_numeric($image)) {
$url = (string) wp_get_attachment_image_url((int) $image, 'large');
$alt = (string) get_post_meta((int) $image, '_wp_attachment_image_alt', true);
}
if ($url === '') {
return;
}
echo '<img class="hero-image" src="' . esc_url($url) . '" alt="' . esc_attr($alt) . '">';
});
add_action('wp', function (): void {
if (!is_page()) {
return;
}
$image = get_field('hero_image');
error_log('[hero-image-shape] page=' . get_the_ID() . ' type=' . gettype($image));
});
wp eval '$id=(int)get_page_by_title("Template Read Drill", OBJECT, "page")->ID; $image=get_field("hero_image",$id); echo gettype($image) . PHP_EOL;'
wp eval '$id=(int)get_page_by_title("Template Read Drill", OBJECT, "page")->ID; $image=get_field("hero_image",$id); if(is_array($image)){echo "array" . PHP_EOL;} elseif(is_numeric($image)){echo "id" . PHP_EOL;} else {echo "none" . PHP_EOL;}'
integer
id
Return format can change during schema edits. Normalize payloads before rendering to make template behavior resilient.
Common Mistakes
| Mistake | Root Cause | What Breaks in Production | Correct Pattern |
|---|---|---|---|
Using the_field() in complex templates | Output and validation coupled | Hard-to-debug malformed output | Use get_field() + explicit render logic |
| Escaping with wrong function | URL escaped as text or vice versa | Broken links/attributes or over-escaped output | Match esc_html / esc_attr / esc_url to context |
| Assuming fixed return format | Field setting changed without template update | Array/ID/object mismatch notices | Normalize by is_array, is_numeric, instanceof guards |
| Rendering wrappers before emptiness checks | Empty blocks clutter layout | Visual artifacts and QA noise | Guard section-level output with contract checks |
| No option-vs-page precedence strategy | Inconsistent fallback behavior | Different pages render conflicting contact data | Define explicit precedence and log source used |
| Skipping CLI payload checks | Drift discovered only in browser | Slower debugging and incident response | Add wp eval payload-type checks to release process |
Deep Dive: Why Escaping Bugs Often Survive Code Review
Escaping bugs are subtle because the rendered page may look fine in normal content states. Problems usually surface with unusual values: special characters in titles, malformed URLs, or rich text in plain contexts. Reviewers often focus on component structure and miss context mismatches (esc_html used for URLs, no esc_attr in attributes). A practical fix is to include context-focused CLI checks and value-shape assertions during QA. Treat escaping as part of the component contract, not an afterthought.
wp eval '$id=(int)get_page_by_title("Template Read Drill", OBJECT, "page")->ID; echo "title=" . gettype(get_field("hero_title",$id)) . PHP_EOL; echo "cta_url=" . gettype(get_field("hero_cta_url",$id)) . PHP_EOL;'
Best Practices
- Retrieve first, render second: never mix retrieval and unescaped output in one expression.
- Use one normalization step per field group component: shape-check arrays/IDs/objects early.
- Escape at the output edge:
esc_htmlfor text,esc_urlfor links,esc_attrfor attributes. - Define fallback policy per field, not globally: critical text may fallback; optional sections may hide.
- Log source selection for option/page precedence in critical components.
- Audit payload types with
wp evalbefore releases involving schema changes. - Prefer explicit variables over nested function calls in template markup for readability and safety.
Hands-On Practice
Exercise 1: Refactor one component to retrieval-first pattern
Refactor a template partial to use get_field() variables and run:
wp eval '$id=(int)get_page_by_title("Template Read Drill", OBJECT, "page")->ID; echo (string)get_field("hero_title",$id) . PHP_EOL;'
After completing this exercise, output should be a plain string (or blank if intentionally empty).
Exercise 2: Implement CTA fallback policy
Run:
wp eval '$id=(int)get_page_by_title("Template Read Drill", OBJECT, "page")->ID; update_field("hero_title","",$id); update_field("hero_cta_label","Book Demo",$id); update_field("hero_cta_url","/book-demo",$id); echo (get_field("hero_title",$id) ?: "Talk to our specialists today") . PHP_EOL;'
After completing this exercise, output should be:
Talk to our specialists today
Exercise 3: Test option-level fallback behavior
Run:
wp eval 'update_field("global_support_phone", "+65 6700 1234", "option");'
wp eval '$id=(int)get_page_by_title("Template Read Drill", OBJECT, "page")->ID; update_field("page_contact_phone","",$id); $page=(string)get_field("page_contact_phone",$id); $global=(string)get_field("global_support_phone","option"); echo ($page!==""?$page:$global) . PHP_EOL;'
After completing this exercise, output should be:
+65 6700 1234
Exercise 4: Audit image return shape
Run:
wp eval '$id=(int)get_page_by_title("Template Read Drill", OBJECT, "page")->ID; $image=get_field("hero_image",$id); echo gettype($image) . PHP_EOL;'
After completing this exercise, output should be one of:
array
or
integer
Exercise 5: Build a release check for field shape assumptions
Run:
wp eval '$id=(int)get_page_by_title("Template Read Drill", OBJECT, "page")->ID; echo "hero_title=" . gettype(get_field("hero_title",$id)) . PHP_EOL; echo "hero_image=" . gettype(get_field("hero_image",$id)) . PHP_EOL; echo "related_resources=" . gettype(get_field("related_resources",$id)) . PHP_EOL;'
After completing this exercise, output pattern should be:
hero_title=string
hero_image=integer
related_resources=array
CLI Reference
| Command | Purpose | Real Example Output |
|---|---|---|
wp eval '$id=1360; echo (string)get_field("hero_title",$id) . PHP_EOL;' | Read scalar field value | Welcome to Clinic Pro |
wp eval '$id=1360; echo (string)get_field("hero_cta_url",$id) . PHP_EOL;' | Read URL contract field | /book-demo |
wp eval '$id=1360; $v=get_field("hero_image",$id); echo gettype($v) . PHP_EOL;' | Inspect image return shape | integer |
wp eval 'echo get_field("global_support_phone", "option") . PHP_EOL;' | Read option-context fallback value | +65 6700 1234 |
wp post meta get 1360 hero_title | Inspect raw stored hero title | Talk to our specialists today |
wp eval '$id=1360; $r=get_field("related_resources",$id); echo gettype($r) . PHP_EOL;' | Inspect relational payload top-level type | array |
wp eval '$id=1360; $rows=get_field("service_faq_items",$id); echo count((array)$rows) . PHP_EOL;' | Count structural rows for template branch checks | 2 |
| `wp plugin list --status=active | grep advanced-custom-fields-pro` | Confirm ACF Pro dependency |
What's Next
- Continue to Repeater and Flexible Content Render Loops.
- Return to Module 3 Overview to keep rendering patterns consistent.
- Related lesson: Conditional Rendering, Fallbacks, and Empty States.
- Related lesson: Load Value, Update Value, and Format Value Filters.
Revisit this lesson whenever schema settings change (return formats, option contexts, field types), because those changes often require template retrieval updates.