Building Custom WordPress Themes with  ACF

Foundations of building and maintaining a good custom WordPress theme.

Solid custom WordPress themes are built on the fundamentals, not page builders, bloated frameworks, or plugin abuse.

When you really understand WordPress at its core, you discover a surprisingly simple system of templates, fields, and functions that can power nearly any website. Combine that with ACF, and you get clean separation of content and design without the overhead that comes with modern drag-and-drop tools.

I’ve been building themes this way since 2012, and some of the custom themes I created over a decade ago are still running strong today. These methods are time-tested and stable, though they often get overlooked as developers reach for quick solutions that end up sacrificing long-term maintainability.

For a working reference, check out my starter theme: basic-wp.

Custom WordPress Theme with ACF

Table of Contents

Theme Structure

The “theme structure” is your site’s skeleton. It’s how WordPress decides which file to load under different conditions, whether someone’s viewing a single post, a page, or an archive.

WordPress uses a template hierarchy to pick the right file. When you visit a product page, WordPress looks for single-product.php first, then falls back to single.php, and finally to index.php if nothing else matches.

Core theme files

These files define the foundation and global behavior of a WordPress theme.

Page-level templates

These templates control how different types of pages and archives are displayed.

Reusable template parts

These reusable components are included in various templates for consistency.

Conditional tags for loading logic

These tags let you control which templates or elements load based on the page type.

Custom Post Types & Taxonomies

Custom Post Types and taxonomies let you model real-world content, like “Products,” “Events,” or “Resources”, instead of cramming everything into standard posts or pages.

Register CPTs with register_post_type() to set labels, visibility, archives, and permalinks. Register taxonomies with register_taxonomy() to group content, like product categories or event types. Just remember to flush permalinks with flush_rewrite_rules() only on theme activation to keep URLs working properly.

Custom Page Templates

Custom page templates let you create unique layouts for specific pages, like landing pages or campaign hubs, without affecting the rest of your site.

Add a file header like this:

<?php
/**
 * Template Name: Landing Page
 */

Then select it in the page editor to apply that layout.

Pairing Templates with ACF Field Groups

Advanced Custom Fields (ACF) lets you define custom field groups tied to each template, turning a static template into a flexible CMS tool. For example, you can create a Landing Page field group with fields for hero text, background images, CTAs, and testimonials, and set it to appear only when the Landing Page template is selected.

Inside your template file, output these fields:

<?php the_field('hero_heading'); ?>
<?php the_field('cta_button_link'); ?>

This pairing allows non-technical editors to manage content visually through the WordPress admin while preserving the template’s layout and style, enabling scalable, highly-customizable landing pages, microsites, and campaign hubs.

Working with ACF

Advanced Custom Fields lets you define fields for custom content entry, which keeps structure separate from presentation.

Create field groups scoped to specific post types, templates, or pages. Use Flexible Content for section-based layouts that work like HubSpot-style modules. Use Repeaters for lists or grids, and Relationship fields to connect related content.

Key functions you’ll use constantly: have_rows(), the_row(), get_row_layout(), get_sub_field().

Don’t forget to add Options pages for global settings like headers, footers, or site-wide alerts.

Auto-Versioned CSS & JS

Keep styles and scripts fresh without manual cache-busting by using each file’s last-modified time as its version. This ensures browsers only reload when a file actually changes.

Enqueue assets in functions.php:

add_action('wp_enqueue_scripts', function () {
  $css_rel   = '/assets/css/app.css';
  $js_rel    = '/assets/js/app.js';
  $css_path  = get_stylesheet_directory() . $css_rel;
  $js_path   = get_stylesheet_directory() . $js_rel;
  $css_url   = get_stylesheet_directory_uri() . $css_rel;
  $js_url    = get_stylesheet_directory_uri() . $js_rel;
  $css_ver   = file_exists($css_path) ? filemtime($css_path) : null;
  $js_ver    = file_exists($js_path)  ? filemtime($js_path)  : null;

  wp_enqueue_style('theme', $css_url, [], $css_ver, 'all');
  wp_enqueue_script('theme', $js_url, ['jquery'], $js_ver, true);
});

Notes on Auto-versioning

This ensures browsers always pull the latest version when you update a file.

Use dependencies properly, load scripts in the footer when possible, and pass PHP data to JavaScript with wp_localize_script().

add_action('wp_enqueue_scripts', function () {
  wp_localize_script('theme', 'ThemeData', [
    'ajaxUrl' => admin_url('admin-ajax.php'),
    'nonce'   => wp_create_nonce('theme'),
  ]);
});

Template Tags & The Loop

The Loop is how WordPress cycles through posts. Template tags control what to display within each iteration.

Loop Basics

Pagination

Metadata

Queries & Performance

When you need to display something other than the default post loop—like a featured article slider, a list of related posts, or custom archive layouts—you’ll reach for WP_Query. It’s the backbone of WordPress querying and gives you fine-grained control over which posts you fetch and how they’re displayed.

WP_Query is ideal for building custom loops, while get_posts() is a lighter alternative for simple, one-off lists. If you need to adjust the main archive or search results, use the pre_get_posts hook. This hook lets you modify the query before it runs, preserving pagination and performance. Avoid query_posts() entirely; it overwrites the main query, breaks pagination, and can lead to confusing bugs.

Working with Queries

When creating secondary loops—for example, to showcase featured posts—instantiate WP_Query directly. Always call wp_reset_postdata() after the custom loop to restore the global $post object. For archive modifications, rely on pre_get_posts to change parameters such as post types or categories without disrupting WordPress’s default logic.

Performance Considerations

Queries can be one of the biggest performance bottlenecks on busy sites. Cache expensive queries with transients (set_transient() / get_transient()) for short-term storage, or use persistent object caching solutions like Redis or Memcached to avoid hitting the database repeatedly. If you don’t need pagination, set no_found_rows => true to skip unnecessary counting queries. Narrow parameters as much as possible—restrict by post_type, avoid broad meta_query filters, and limit returned fields.

During development, enable SAVEQUERIES or use the Query Monitor plugin to inspect and benchmark your queries. Reducing query complexity and caching frequently used results can significantly improve page load times and overall scalability.

Key Functions & Hooks

Navigation and media aren’t just decoration—they shape how users experience your site. Clean, accessible menus guide visitors, and properly optimized images keep pages fast while looking sharp on every device.

A theme should define menu locations up front so site owners can manage them in the dashboard. Register them in functions.php:

add_action('after_setup_theme', function () {
  register_nav_menus([
  'primary' => 'Primary Navigation',
  'footer' => 'Footer Links',
  ]);
});

Render them where needed using wp_nav_menu():

<?php
wp_nav_menu([
  'theme_location' => 'primary',
  'container' => false,
  'menu_class' => 'nav-primary',
]);
?>

Keep menu labels short, use a flat hierarchy for mobile usability, and ensure the markup is wrapped in a <nav> element with proper ARIA attributes.

Images

Every image you output should be generated at the optimal size. Declare sizes with add_image_size() so WordPress can crop and serve the right dimensions:

add_action('after_setup_theme', function () {
  add_image_size('hero', 1600, 600, true);
  add_image_size('card', 400, 300, true);
});

Display them with responsive markup via wp_get_attachment_image() or the_post_thumbnail():

<?php echo wp_get_attachment_image($image_id, 'card'); ?>

This automatically includes srcset and sizes attributes for modern responsive loading. Compress and optimize all images—prefer WebP or AVIF if supported.

SVGs

SVG graphics scale crisply at any size but can contain malicious code if uploaded directly. Sanitize them before use with a plugin like Safe SVG or a custom filter that strips unsafe tags and attributes. Avoid trusting raw user-uploaded SVGs.

Favicons & App Manifests

Add favicons for multiple devices to maintain a professional appearance. For PWAs, include a site.webmanifest file that defines icons, theme colors, and splash screens for mobile devices. You can upload favicons via Appearance → Customize → Site Identity, or enqueue them manually in your for full control.

Best Practices

Theme Support & Setup

A theme’s setup defines which WordPress features are enabled and how lean your front end runs. Turn on the essentials to keep things modern, and strip out defaults you don’t use to reduce requests, bytes, and complexity.

Enable the essentials

Use add_theme_support() inside after_setup_theme to enable core features your design actually needs.

add_action('after_setup_theme', function () {
    add_theme_support('title-tag');
    add_theme_support('post-thumbnails');
    add_theme_support('html5', [
        'search-form',
        'comment-form',
        'comment-list',
        'gallery',
        'caption',
        'script',
        'style'
    ]);
    add_theme_support('automatic-feed-links');
    add_theme_support('custom-logo');
    add_theme_support('align-wide');
    add_theme_support('editor-styles');
});

Disable defaults you don’t need

Remove built-in features that add noise or network requests you’re not using.

add_action('init', function () {
    remove_action('wp_head', 'print_emoji_detection_script', 7);
    remove_action('wp_print_styles', 'print_emoji_styles');
    remove_action('wp_head', 'wp_generator');
    remove_action('wp_head', 'rsd_link');
    remove_action('wp_head', 'wlwmanifest_link');
    remove_action('wp_head', 'wp_shortlink_wp_head');
    add_filter('xmlrpc_enabled', '__return_false');
});

Trim editor and block assets

If you’re not using core block styles or embeds globally, dequeue them late to avoid unnecessary CSS/JS.

add_action('wp_enqueue_scripts', function () {
    wp_dequeue_style('wp-block-library');
    wp_dequeue_style('wp-block-library-theme');
    wp_dequeue_style('classic-theme-styles');
    wp_dequeue_style('global-styles');
    wp_deregister_script('wp-embed');
}, 100);

Best practices

Forms capture leads, drive conversions, and collect user input—security and reliability are critical. Search is often overlooked but directly impacts how quickly users find what they need.

Forms

For most sites, start with a battle-tested plugin like Gravity Forms. It offers advanced field types, conditional logic, spam protection, and secure storage of submissions. Pair it with server-side validation and enable nonces to prevent CSRF attacks. Keep the plugin up to date and review data retention policies to stay compliant with privacy regulations.

If you’re using a third-party CRM or marketing platform, such as HubSpot, integrate its embedded forms directly or through their WordPress plugin. This approach lets leads flow into your CRM automatically for nurturing and tracking. Always test these integrations end to end—look for field mapping issues and confirm that submissions are recorded in both WordPress and the CRM.

For custom projects that demand a fully branded experience, build your own lead-generation forms with a JavaScript framework like Vue. Handle front-end validation in Vue for a fast, seamless UX, then send submissions to a secure WordPress REST API endpoint protected by nonces and wp_verify_nonce(). Store the data in custom tables or forward it to your CRM of choice. This hybrid approach provides the most control over design and performance while keeping the data pipeline secure.

The default WordPress search often falls short—it’s basic and ranks results by date, not relevance. Use a plugin like Relevanssi or an external service like Algolia to provide weighted indexes, fuzzy matching, and the ability to index PDFs and custom fields. Improving search makes content easier to find and improves engagement.

Best Practices

Security & Hardening

Security should be woven into every template, query, and interaction. A few small practices can prevent most common attacks.

Output Escaping

Escape all dynamic output to prevent XSS vulnerabilities:
– Use esc_html() for plain text.
– Use esc_attr() for attribute values inside tags.
– Use esc_url() for links.
– Use wp_kses() to allow only specific HTML tags in user-submitted content.

Never echo raw data without first escaping or sanitizing it.

Requests & Permissions

All forms and AJAX requests should be protected with nonces:

check_admin_referer('form_action');
wp_verify_nonce($_POST['nonce'], 'form_action');

For front-end submissions, pair nonces with capability checks like current_user_can() before processing any data. Always validate and sanitize $_POST and $_GET values server-side even if you do it client-side.

Reducing Attack Surface

Limit what’s exposed by default:

Admin Experience

A thoughtful admin experience saves time and prevents errors for editors and clients.

Custom Fields & Options

Use Advanced Custom Fields (ACF) Options Pages to centralize global settings like logos, contact details, or site-wide alerts:

if (function_exists('acf_add_options_page')) {
    acf_add_options_page([
        'page_title' => 'Site Settings',
        'menu_title' => 'Site Settings',
        'menu_slug'  => 'site-settings',
        'capability' => 'edit_posts',
        'redirect'   => false,
    ]);
}

Scope field groups by post type, page template, or user role so editors only see fields that apply to their work.

Streamlining the Editor

Remove unused metaboxes and clutter:

add_action('admin_menu', function () {
    remove_meta_box('trackbacksdiv', 'post', 'normal');
    remove_meta_box('slugdiv', 'page', 'normal');
});

You can also hide unnecessary dashboard widgets and disable comments on content types where they’re not needed.

Improving List Views

Add custom columns to list tables for key ACF data or post meta that helps editors see important info at a glance:

add_filter('manage_post_posts_columns', function ($columns) {
    $columns['featured'] = 'Featured';
    return $columns;
});

add_action('manage_post_posts_custom_column', function ($column, $post_id) {
    if ($column === 'featured') {
        echo get_field('featured', $post_id) ? 'Yes' : 'No';
    }
}, 10, 2);

Best Practices

Conclusion

A custom theme built on these fundamentals using templates, custom post types, ACF, and WordPress’s template hierarchy properly is fast, stable, and maintainable.

I’ve been using these practices for over a decade because they work. They outlast design trends and plugin churn, giving you a rock-solid foundation that keeps content separate from design and your sites reliable for years to come.

References

WordPress Documentation

Security & Performance

Tools & Plugins

Additional Resources