Home/Server & Website Managed Operations/WordPress Custom Post Types and Custom Fields (CPT / ACF) in Practice
Server & Website Managed Operations

WordPress Custom Post Types and Custom Fields (CPT / ACF) in Practice

When content goes beyond just “Posts” and “Pages”—such as products, case studies, job listings, or property rentals—it’s time to use Custom Post Types (CPT). Trying to force this into standard posts with categories might work short-term, but it becomes increasingly cumbersome over the long run.

When to Use CPT

Scenario Use CPT Use Posts + Categories
Content structure is identical to posts Unnecessary Appropriate
Has its own fields (price, specs) Appropriate 勉强
Requires independent archives and lists Appropriate Inappropriate
High volume and long-term maintenance Appropriate Becomes chaotic

Criterion: If this type of content requires “its own set of fields” or “its own archive page,” use a CPT.

Registering a CPT

<?php
add_action('init', function () {
    register_post_type('product', [
        'labels' => [
            'name'          => 'Products',
            'singular_name' => 'Product',
            'add_new_item'  => 'Add New Product',
        ],
        'public'        => true,
        'has_archive'   => true,      // Has an archive page
        'show_in_rest'  => true,      // Supports Gutenberg and REST API
        'supports'      => ['title', 'editor', 'thumbnail', 'excerpt'],
        'menu_icon'     => 'dashicons-cart',
        'rewrite'       => ['slug' => 'products'],
    ]);
});

Key parameter explanations:

Parameter Purpose Note
public Whether visible on the frontend If false, only visible in admin
has_archive Whether an archive page exists Set to true when a list is needed
show_in_rest Gutenberg and REST support Recommended: true
supports Supported features Enable as needed
rewrite.slug URL prefix Affects link structure
# After registering, refresh permalink rules, otherwise you'll get 404s on the frontend
wp rewrite flush
# Or go to Admin > Settings > Permalinks and click Save once

“Visible in backend but 404 on frontend” is almost always due to forgetting to refresh rewrite rules. This is the most common issue with CPTs.

Custom Fields

Native Approach

<?php
// Saving (hooked to save_post)
add_action('save_post_product', function ($post_id) {
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
    if (!current_user_can('edit_post', $post_id)) return;

    if (isset($_POST['_price'])) {
        update_post_meta($post_id, '_price', sanitize_text_field($_POST['_price']));
    }
});

// Reading
$price = get_post_meta(get_the_ID(), '_price', true);

Meta keys starting with an underscore (like _price) will not appear in the default custom fields box in the backend. This is a convention suitable for programmatically managed fields.

Using the ACF Plugin

ACF (Advanced Custom Fields) visualizes the workflow of “defining fields → entering data in the backend → outputting on the frontend,” saving you from writing lots of boilerplate code:

  1. Create a field group in the backend and specify which CPT it applies to.
  2. Fill in the fields when editing content.
  3. Retrieve values on the frontend using get_field().
<?php
// Retrieving value with ACF
$price = get_field('price');
if ($price) {
    echo esc_html($price);
}

// Inside a loop
while (have_posts()) {
    the_post();
    echo get_field('price');
}
<?php
// Filtering by field (ACF values are stored in postmeta)
$args = [
    'post_type'      => 'product',
    'posts_per_page' => 10,
    'meta_query'     => [
        [
            'key'     => 'price',
            'value'   => 1000,
            'compare' => '>=',
            'type'    => 'NUMERIC',   // Must specify type for numeric comparison
        ],
    ],
];
$query = new WP_Query($args);

You must include 'type' => 'NUMERIC' in meta_query for numeric comparisons; otherwise, it compares as strings, meaning 9 would be considered greater than 10.

Template Files

Template Purpose
archive-product.php Archive/List Page
single-product.php Single Detail Page
<?php
// archive-product.php —— List
get_header();
if (have_posts()) {
    while (have_posts()) {
        the_post();
        the_title('<h2>', '</h2>');
        echo '<p>Price: ' . esc_html(get_field('price')) . '</p>';
    }
    the_posts_pagination();
}
get_footer();

The template naming convention is archive-{post_type}.php and single-{post_type}.php. If these templates are not created, WordPress falls back to the default archive/single templates.

Taxonomies (Optional)

<?php
add_action('init', function () {
    register_taxonomy('product_cat', 'product', [
        'labels'       => ['name' => 'Product Categories'],
        'public'       => true,
        'hierarchical' => true,      // true acts like categories, false like tags
        'show_in_rest' => true,
        'rewrite'      => ['slug' => 'product-cat'],
    ]);
});

Decision Tree

Use CPT
├─ Register via register_post_type (place in a custom plugin, not the parent theme)
├─ Must refresh rewrite rules after registration ← Main cause of frontend 404s
├─ Fields
│  ├─ Simple, program-managed → Native post_meta
│  └─ Need visual backend entry → ACF
├─ Queries
│  ├─ WP_Query + post_type
│  └─ meta_query numeric comparisons require 'type' => NUMERIC
└─ Templates
   ├─ archive-{type}.php
   └─ single-{type}.php

Scenario Analysis: Using Posts for Product Introductions Leads to Chaos

Symptom: A site treats product introductions as regular posts, distinguishing them via categories.
After six months, the content becomes completely mixed: products appear in the general posts list, and the product archive structure becomes unmanageable.

  1. Identify the root cause of the chaos → Different types of content share the same structure and templates,
    relying solely on categories for distinction, which is unsuitable for defining “types.”
  2. List the fields that truly need differentiation → Products have prices, parameters, and specs,
    which posts do not; these fields have no fixed storage location within the post system.
  3. Adopt independent types to carry the content → Register a new content type
    with its own list, templates, and field sets.
  4. Migrate existing content over → Migration must handle field mapping simultaneously,
    preserving original publish dates and URL structures.
  5. Define exclusive fields for the new type → Use custom fields to hold structured information,
    avoiding stuffing HTML directly into the post content.
  6. Confirm URLs and indexing are unaffected → After migration, verify individually that old URLs remain accessible.
# View registered types and existing fields
wp post-type list --allow-root
wp post list --post_type=product-type --fields=ID,post_title,post_date --allow-root

# Batch rewrite, first use dry-run to confirm impact scope
wp search-replace 'old-path' 'new-path' --dry-run --report-changed-only --allow-root

# Verify old URLs are still valid after migration
curl -sS -o /dev/null -w '%{http_code}n' https://example.com/old-path/

Root Cause: Using categories to distinguish fundamentally different content types.
Categories are suitable for horizontal classification, not for asserting “this is a different thing.” As scale increases, control is inevitably lost.

Two key takeaways:

  • To decide if a new type is needed, check if it has exclusive fields.
    Structured information like price, parameters, and specs should not be squeezed into generic content.
  • The focus during migration is two things: field mapping and URL accessibility.
    The former determines data integrity; the latter directly impacts accumulated external links and indexing.

8 Common Pitfalls

  1. Not refreshing rewrite rules after registration: Results in frontend 404s.
  2. Placing flush inside init to execute every time: Wastes performance.
  3. Writing code in the parent theme: CPT disappears when switching themes (data remains but becomes invisible).
  4. Omitting ‘type’ in meta_query numeric comparisons: String comparison leads to incorrect results.
  5. Forgetting show_in_rest: Gutenberg and REST API become unusable.
  6. Outputting fields without escaping: XSS risk; use esc_html.
  7. Not enabling has_archive but wanting a list page: Archive page does not exist.
  8. Slug conflicts with other types: URL rules overwrite each other.

Real Customer Feedback (From Cloud Market Reviews and Inquiries for Similar Services)

The following excerpts are taken from real buyer reviews and inquiries for similar operations services on Alibaba Cloud / Tencent Cloud / Huawei Cloud Cloud Markets (service providers include our store and peers, provided for reference only, not fictional tickets from this site):

10***09 2024-03-18 5.0 Convenient and stable | Clean interface | Comprehensive after-sales service (User did not fill in comment content)

FREE CHECKUP

Free server checkup

We will send a problem checklist and remediation advice within one business day — no obligation to engage.

135 410 16684
Weekdays Mon – Fri 10:00 – 22:00 (UTC+8) · 7×24 emergency response
Call now
CONTACT

Contact Us

How can we help?

* Required