A Custom Post Type is needed when content has a distinct structure, lifecycle, taxonomies, and output templates. For example, it's more convenient to store portfolio projects or services not as regular posts and pages, but as separate entities with their own URLs, fields, and editing rules.

In this article, you will understand how WordPress Custom Post Type development works: from data design to registration through register_post_type, settings rewrite, REST API support using show_in_rest and connecting ACF fields. Before implementation, test the solution on a staging site with a backup, taking into account the WordPress version, active theme, plugins, and access rights.

When CPT is needed and how it is related to the theme, blocks and API is shown in Review of custom WordPress development. Here we will focus on the data model.

What is Custom Post Type?

How is a Custom Post Type different from pages and posts?
How is a Custom Post Type different from pages and posts?

A Custom Post Type, or CPT, is a custom post type in WordPress for content with its own structure and purpose. It's useful when content is logically distinct from blog posts and regular pages, such as projects, services, real estate, specialists, or job openings.

By default, CPT uses the standard WordPress storage system and is defined by a separate value post_type. This doesn't necessarily require a new database table. WordPress Custom Post Type allows you to separate entities in the admin panel and assign them their own templates, categories, and display rules.

When standard records are not enough

Choose CPT if your content has a separate lifecycle. For example, you publish an article by date, but update a service when conditions change and display it in a permanent catalog. Mixing these entities complicates filtering, management, and template preparation.

Criterion Regular recording Page CPT
Main purpose News and articles Permanent sections of the site Similar business entities
Organization Categories, tags, date Page hierarchy Custom taxonomies and fields
Templates General publication template Page template Separate list and object templates
Editability Suitable for editorial flow Convenient for single content Customizable for a specific entity

CPT is useful if you need specialized taxonomies, a repeatable card structure, and a separate view in the admin panel. Additional characteristics can be stored as metadata; for a user-friendly editor interface, it is often used. WordPress ACF fields. In this case, the set of fields should be determined based on the content's objectives, and not added "for the future.".

If only the appearance of a single page is different, a new post type may be overkill. Sometimes a page template, block template, or existing taxonomy is sufficient. Consider future URLs in advance: changing an already indexed structure may require redirects.

Examples: cases and services

For project portfolios, a separate WordPress Custom Post Type helps store the case title, industry, completed projects, and related images. Taxonomy can group projects by client area or focus. You can display a general portfolio archive and a separate post card for each project, without mixing them with articles.

For a service catalog, CPT is suitable when each service has a similar basic structure: description, benefits, stages, associated specialists, or projects. Separate taxonomies are useful for grouping by area or client type.

Before choosing an architecture, consider three questions: does the entity have its own lifecycle, does it require separate taxonomies and templates, and can the editor support the data without manual layout editing. The solution's compatibility with the theme, plugins, and WordPress version being used should be checked separately.

Design the structure

Designing Record Type Fields, Taxonomies, and Relationships
Designing Record Type Fields, Taxonomies, and Relationships

Before registering a Custom Post Type, determine what data you'll store, how editors will manage it, and where the content will appear on the site. This structure helps avoid turning individual content properties into categories, custom fields, or unrelated pages.

Post Type and Taxonomies

Create a separate post type if the content has its own lifecycle, set of templates, and classification rules. For example, portfolio projects are regularly added and filtered by industry and technology, but don't appear in the news feed. The "Projects" CPT is appropriate for these projects, while "Industries" and "Technologies" can be configured as taxonomies.

For a service catalog, first check the scope. If there are multiple services and each page has a unique structure, standard pages may be sufficient. CPT is appropriate when services need to be systematically grouped, displayed in archives, linked to specialists, or transferred via an API. Regions are more often modeled as taxonomies or fields rather than as separate record types.

Design permanent URLs in advance. Make system entity names stable and independent of marketing language. Decide whether a shared archive is needed, which pages should be indexed, and whether there will be any duplicates between archives, taxonomies, and landing pages.

Use a taxonomy if a value is repeated across multiple records and requires archiving or filtering. If a parameter is specific to a specific record—for example, a project's budget range or service duration—a field makes more sense.

Fields and access rights

Describe each field before configuring the editor. Specify the name, format, whether it's required, acceptable values, and output location. For a portfolio, this might include the client, year, link to the result, and gallery. For a service, this might include a brief description, stages, expected result, and associated specialists. ACF simplifies field interface creation, but it doesn't replace schema design, validation, or data escaping during output.

Also define user roles: who can create, edit, publish, and delete posts. Don't rely solely on hiding elements in the admin interface—restrictions should be checked through WordPress permissions. If you plan to transfer data to the REST API, separate public and private fields in advance.

Data structure checklist:

  • content has a separate life cycle and a clear entity name;
  • Taxonomies are used for repeatable classification, and fields are used for record properties;
  • archives, single templates and URL logic are defined;
  • the type, mandatory nature and acceptable values of each field are described;
  • assigned rights to create, edit, publish and delete;
  • It is noted what data can be disclosed via the API;
  • Possible duplicate pages and conflicting URLs were checked.

Before implementing, check the schema against your current theme, plugins, and WordPress version: compatibility cannot be guaranteed without this check. Test changes first on a staging copy of your site and maintain a backup of your production database.

Register the entry type

Registering Custom Post Type and accessing the control panel
Registering Custom Post Type and accessing the control panel

Plugin or custom code

You can register a WordPress Custom Post Type using the plugin builder or your own code. The plugin is convenient if you need an interface for basic setup and the team doesn't plan to support PHP code. However, before choosing, check the compatibility of the solution with your current WordPress version, theme, and other plugins.

Custom code is preferred when the post type is part of the site's architecture. Place it in a separate, functional plugin, not in the theme: this way, posts will remain accessible in the control panel after changing the design. First, make changes on a test copy and back up the files and database.

Function register_post_type() usually called at an event init. The system type name must be unique, contain no more than 20 characters, and use lowercase Latin letters, numbers, hyphens, or underscores. Add your own prefix to reduce the risk of conflict.

function ua_site_register_project_cpt() {
    $labels = array(
        'name'          => 'Проекты',
        'singular_name' => 'Проект',
        'add_new_item'  => 'Добавить проект',
        'edit_item'     => 'Редактировать проект',
    );

    register_post_type(
        'ua_project',
        array(
            'labels'             => $labels,
            'public'             => true,
            'publicly_queryable' => true,
            'show_ui'            => true,
            'show_in_rest'       => true,
            'has_archive'        => true,
            'rewrite'            => array(
                'slug'       => 'projects',
                'with_front' => false,
            ),
            'supports'           => array(
                'title',
                'editor',
                'thumbnail',
                'excerpt',
            ),
            'capability_type'    => 'post',
            'map_meta_cap'       => true,
        )
    );
}
add_action( 'init', 'ua_site_register_project_cpt' );

show_in_rest Opens the record type for the REST API and is required for the block editor to work. In this example, permissions are inherited from regular records. If projects only need to edit specific roles, configure your own capabilities and test them using current_user_can(). Data from custom fields must be validated when saved and escaped when output.

Slugs and permanent links

Parameter rewrite defines the URL. For type ua_project public slug selected projects, so the archive can be located at /projects/, and a separate entry - /projects/project-name/. The system name and URL slug do not have to match.

Before publishing, check that the slug isn't already occupied by a page, category, product, or other content type. For a service catalog, you can similarly use a system name with a prefix and a user-friendly URL. /services/. It's best to use Latin characters for the slug, keep it short, and don't change it unless necessary: after changing it, redirects from the old addresses will be required.

After first registration or change rewrite Update your permalink rules: save the relevant settings page or call flush_rewrite_rules() Once per plugin activation. Don't call this function for every request—it creates unnecessary overhead.

The availability of a URL does not determine its indexing. Directive noindex, Check the URL, canonical, and sitemap separately in the SEO settings. Before launching, open the archive and a separate post without logging in, ensure there are no 404 errors, and recheck the routes after updating the theme or plugins.

Configure the editor and output

The path of CPT data from the editor to the page and archive
The path of CPT data from the editor to the page and archive

REST API support

To make your custom post type work with the block editor and be accessible via the WordPress REST API, please specify when registering ''show_in_rest' => true. After this, WordPress will create standard REST routes for reading and managing objects, taking into account the current user's permissions.

The editor's tool set is determined by the parameter supports. A portfolio may require a title, body text, thumbnail, summary, and revisions:

''supports' => array( 'title', 'editor', 'thumbnail', 'excerpt', 'revisions' )

For the service catalog, keep only the elements that are actually used. For example, if the description, price, and duration are in structured ACF fields, don't duplicate them in the main editor. This simplifies data entry and reduces the risk of inconsistencies.

Availability of ACF fields via the API depends on the field group settings, plugin version, and project configuration. Verify the actual JSON response on a staging environment rather than assuming compatibility. For custom REST fields, define the schema, permissions check, validation, and input value handling. Public read access to a record does not mean anonymous access is allowed.

Archive and single entry template

For a type with an identifier project create files in the topic single-project.php for a separate project and archive-project.php for the list of projects. If there are no special files, WordPress will continue searching the template hierarchy and may use common ones. single.php, archive.php or index.php.

The archive will be available only if configured accordingly. has_archive. Its URL also depends on parameters rewrite. Before changing your URL structure, make a backup and test the results on staging: changing the slug can create old, broken URLs that will require redirects.

  • In your portfolio, display the cover image, title, industry or other taxonomy, and a link to the detail page.
  • In your service catalog, provide a clear archive, taxonomy filtering, and a consistent card template.
  • Escape plain text via esc_html(), URL - via esc_url(), and the resolved HTML is through wp_kses_post().
  • After additional cycles, recover the global write data using wp_reset_postdata().

Changing a template alone doesn't guarantee a page will be crawled again. If URLs, canonical URLs, or content accessibility have changed, check your sitemap, redirects, HTTP statuses, and indexing rules. Check template, REST API, and ACF compatibility for your WordPress versions, theme, and plugins.

Pre-publication review

Before migrating a Custom Post Type to a live site, test it on a staging copy and back up the files and database. Compatibility cannot be guaranteed in advance: results depend on the WordPress version, active theme, plugins, permalink rules, and server configuration.

Please complete the following checklist:

  • Registration: Make sure the post type appears in the correct sections of the control panel and that its system ID does not conflict with the page name, taxonomy, or existing rewrite rule.
  • URL: Open a single post and the archive, if enabled. Check for human-readable URLs, pagination, and the absence of 404 errors. After changing rewrite settings, resave the permalinks once or update the rules when activating the plugin. Don't force them to update with every request.
  • Canonical: Check the primary URL the SEO plugin specifies for the archive and individual materials. If you're just getting started, What is a canonical URL?, think of it as a hint to the search engine about the preferred version of the page among the available duplicates.
  • Rights: Log in as an administrator, editor, or other project member. Users should only be able to see, create, edit, and delete content they have permission to. When processing forms, additionally check the capability and nonce.
  • Data: Test required and optional ACF fields, empty values, images, and repeating blocks. Validate and sanitize input data, and apply context-sensitive escaping to output: text, attribute, URL, or HTML.
  • REST API: if used show_in_rest, check that records are retrieved via the API, that the required fields are accessible, and that private data is blocked for unauthorized requests. The parameter itself does not automatically make custom fields public.
  • Templates: Open a single page, archive, search results, and related taxonomies on mobile and desktop screens. Check for instances without an image, description, or assigned term.
  • Integrations: Separately test caching, SEO plugin, multilingualism, search, and page builder if they are involved in the project.

After the migration, repeat the test on the production environment. Even if the test and production environments are similar, the PHP versions, cache, web server, and routing rules may differ. First, publish a test post, check the URL and API, and then upload the main content.

Document the CPT identifier, slug, taxonomy, field set, and access rules in the project documentation. This will simplify maintenance and reduce the risk of future custom post type development breaking existing URLs or data structure.

Official documentation: registering custom post types.

Interesting on the topic