Register Field Groups in Code, Not the UI
The ACF UI is great for prototyping. For production, always export field groups to PHP and register them in code. This keeps your schema in version control and eliminates database drift between environments.
Use ACF > Tools > Export > PHP to generate the code, then put it in a must-use plugin or your theme's functions.php:
add_action( 'acf/init', function() {
acf_add_local_field_group( [
'key' => 'group_hero',
'title' => 'Hero Section',
'fields' => [
[
'key' => 'field_hero_headline',
'label' => 'Headline',
'name' => 'hero_headline',
'type' => 'text',
],
[
'key' => 'field_hero_image',
'label' => 'Background Image',
'name' => 'hero_image',
'type' => 'image',
'return_format' => 'array',
'preview_size' => 'medium',
],
],
'location' => [
[ [ 'param' => 'page_template', 'operator' => '==', 'value' => 'template-home.php' ] ],
],
] );
} );
Flexible Content for Page Builders
The Flexible Content field type is ACF's most powerful feature. It lets editors compose pages from a library of predefined sections — without giving them a visual page builder that breaks performance.
// In your template — loop over flexible content layouts
if ( have_rows( 'page_sections' ) ) :
while ( have_rows( 'page_sections' ) ) : the_row();
$layout = get_row_layout();
get_template_part( 'template-parts/sections/section', $layout );
endwhile;
endif;
This pattern loads template-parts/sections/section-hero.php, section-text-image.php, etc. Each layout is an isolated component: its own HTML, CSS, and ACF field access. Easy to add, easy to maintain, impossible to conflict.
Defining layouts in code
[
'key' => 'field_page_sections',
'label' => 'Page Sections',
'name' => 'page_sections',
'type' => 'flexible_content',
'layouts' => [
'hero' => [
'key' => 'layout_hero',
'name' => 'hero',
'label' => 'Hero',
'display' => 'block',
'sub_fields' => [
[ 'key' => 'field_hero_title', 'name' => 'title', 'type' => 'text', 'label' => 'Title' ],
[ 'key' => 'field_hero_bg', 'name' => 'bg', 'type' => 'image', 'label' => 'Background' ],
],
],
'cta' => [
'key' => 'layout_cta',
'name' => 'cta',
'label' => 'Call to Action',
'sub_fields' => [
[ 'key' => 'field_cta_text', 'name' => 'text', 'type' => 'textarea', 'label' => 'Text' ],
[ 'key' => 'field_cta_button', 'name' => 'button', 'type' => 'link', 'label' => 'Button' ],
],
],
],
]
ACF Blocks
ACF Blocks (PRO) bring your custom fields into the Gutenberg editor as native blocks — no React knowledge required. This is the best bridge between the classic ACF workflow and the block editor.
// functions.php
add_action( 'acf/init', function() {
acf_register_block_type( [
'name' => 'testimonial',
'title' => 'Testimonial',
'description' => 'A quote with author attribution.',
'render_template' => 'template-parts/blocks/testimonial.php',
'category' => 'formatting',
'icon' => 'format-quote',
'keywords' => [ 'quote', 'testimonial' ],
'supports' => [ 'align' => false, 'jsx' => true ],
] );
} );
The render template is standard PHP with get_field() — no JavaScript. Editors get a real block preview in the editor, and you ship clean, server-rendered HTML.
Options Pages for Global Settings
ACF Options Pages let you create global settings fields (header/footer content, social links, API keys for display, etc.) accessible from any template via get_field( 'field_name', 'option' ):
add_action( 'acf/init', function() {
if ( function_exists( 'acf_add_options_page' ) ) {
acf_add_options_page( [
'page_title' => 'Site Settings',
'menu_title' => 'Site Settings',
'menu_slug' => 'site-settings',
'capability' => 'manage_options',
'redirect' => false,
] );
acf_add_options_sub_page( [
'page_title' => 'Social Media',
'menu_title' => 'Social Media',
'parent_slug' => 'site-settings',
] );
}
} );
// In any template:
$twitter = get_field( 'twitter_url', 'option' );
Query Patterns with ACF
ACF data is stored in wp_postmeta. Querying it efficiently requires understanding what WP_Query does under the hood.
The right way to query by meta value
$query = new WP_Query( [
'post_type' => 'project',
'posts_per_page' => 12,
'meta_query' => [
[
'key' => 'project_featured', // ACF field name
'value' => '1',
'compare' => '=',
],
],
'orderby' => 'meta_value_num',
'meta_key' => 'project_year',
'order' => 'DESC',
] );
Avoid querying by meta when possible
Meta queries are expensive — they add JOIN clauses to the SQL. For filterable taxonomies (categories, tags, custom taxonomies), always prefer tax_query over meta_query. It's significantly faster on large datasets because taxonomies are indexed.
ACF Performance Considerations
- Use
get_fields()once per post — it returns all fields in one DB call. Avoid callingget_field()dozens of times in a loop. - Cache expensive queries — use
wp_cache_get/setor object caching (Redis/Memcached) for queries that run on every page load. - Disable ACF admin UI in production — when fields are registered in code, you can hide the ACF admin menu from editors for a cleaner interface:
add_filter( 'acf/settings/show_admin', function() {
return current_user_can( 'manage_options' ) && WP_DEBUG;
} );