WordPress was born as a blogging platform, but it stopped being just that a long time ago: today it manages portfolios, real estate agencies, academies, business directories, and entire stores. The piece that makes this versatility possible are WordPress custom post types: personalized content types that coexist with posts and pages, but with their own name, their own fields, their own URLs, and their own section in the dashboard. If you have ever tried to manage "projects," "recipes," or "courses" using normal posts and categories, you know how quickly it turns into chaos. In this complete guide, you will learn exactly what a Custom Post Type (CPT) is, when it makes sense to create one and when not, how to register it with complete and correct PHP code, how to add your own taxonomies, how to display it in your theme, and what implications it has for SEO. By the end, you will be able to structure any project with the solidity of a professional developer.
What are WordPress custom post types
In WordPress, all content is technically a "post": blog posts (post), pages (page), media library attachments (attachment), revisions, or navigation menus. All are saved in the same database table (wp_posts) and are distinguished by the value of the column post_type. A custom post type is nothing more than a new value in that column, registered by you, with its own rules: what features it supports (editor, featured image, excerpt), what it's called in the menu, what URL its elements have, and whether or not it appears in search results.
Classic examples where WordPress custom post types shine:
- Portfolio: projects with image, client, and technology used.
- Real Estate: properties with price, area, and location.
- Academy: courses and lessons with duration and level.
- Restaurant: dishes with allergens and price.
- Events: with date, place, and capacity.
In fact, you already use them without knowing it: WooCommerce products are a CPT called product, and the forms of almost any plugin are also saved as their own content types.
When to create a CPT (and when not to)
The key question: is this content conceptually different from a blog post? If the answer is yes (it makes sense to list it separately, with its own archives and its own structure), it is a candidate for a CPT. If you only want to group posts by topic, the correct tools are the traditional categories and tags.
| Situation | Appropriate solution |
|---|---|
| Separate the blog by topics | Categories |
| Product sheets, courses, properties… | Custom Post Type |
| Mark posts with loose keywords | Tags |
| Classify a CPT (genre of a book, area of a property) | Custom taxonomy |
| Specific data for each item (price, date, ISBN) | Custom fields (post meta) |
Another prior decision: code or plugin? Plugins like Custom Post Type UI or Pods allow registering CPTs from the panel without writing PHP, and they are perfectly valid. In return, they create dependency: if the plugin is deactivated, the content is not deleted but disappears from the dashboard. Code registration, which we will see now, is lighter, more portable, and more controllable, and it's how all professional themes and plugins do it.
How to register a custom post type with code
Everything revolves around one function: register_post_type(), which must be executed on the hook init. This is a complete and commented registration for a portfolio CPT, ready to use:
<?php
function biblioweb_registrar_cpt_proyecto() {
$labels = array(
'name' => 'Proyectos',
'singular_name' => 'Proyecto',
'menu_name' => 'Porfolio',
'add_new' => 'Añadir nuevo',
'add_new_item' => 'Añadir nuevo proyecto',
'edit_item' => 'Editar proyecto',
'new_item' => 'Nuevo proyecto',
'view_item' => 'Ver proyecto',
'search_items' => 'Buscar proyectos',
'not_found' => 'No se han encontrado proyectos',
'not_found_in_trash' => 'No hay proyectos en la papelera',
'all_items' => 'Todos los proyectos',
);
$args = array(
'labels' => $labels,
'public' => true,
'has_archive' => true,
'menu_position'=> 20,
'menu_icon' => 'dashicons-portfolio',
'rewrite' => array( 'slug' => 'proyectos', 'with_front' => false ),
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt', 'revisions', 'custom-fields' ),
'show_in_rest' => true, // Necesario para el editor de bloques y la REST API.
'taxonomies' => array(),
);
register_post_type( 'proyecto', $args );
}
add_action( 'init', 'biblioweb_registrar_cpt_proyecto' );
Let's review the arguments that generate the most doubts:
public: intrue, the CPT has its own URLs, appears on the dashboard, and is visible. For internal content (e.g., testimonials that are only shown embedded) it can be refined withpublicly_queryableyexclude_from_search.has_archive: creates the archive page intudominio.com/proyectos/with the list of all elements.rewrite: defines the URL slug.with_front => falseavoids inheriting prefixes from the permalink structure (like/blog/).supports: which boxes appear in the editor. If your CPT doesn't need a text editor, remove it and the form will be cleaner.show_in_rest: essential intrueif you want to use Gutenberg with this CPT or consume it from the REST API.menu_icon: any icon from the Dashicons library, or the path to your own SVG.
A critical warning: after registering or modifying a CPT, visit Settings → Permalinks and click "Save Changes". This action regenerates the rewrite rules; if you don't, the new URLs will return a 404 error and you'll waste a lot of time looking for a bug that isn't in your code.
Where to put it? In your own plugin or in a snippet manager like WPCode, which we analyze in depth here. Avoid the theme's functions.php: a CPT is functionality, not design, and should survive a theme change.
Custom taxonomies: classifying your new content
Standard categories and tags belong to posts. To classify a CPT, the correct way is to register custom taxonomies with register_taxonomy(). Continuing with the portfolio, let's create a "Project Type" taxonomy:
<?php
function biblioweb_registrar_taxonomia_tipo() {
$labels = array(
'name' => 'Tipos de proyecto',
'singular_name' => 'Tipo de proyecto',
'search_items' => 'Buscar tipos',
'all_items' => 'Todos los tipos',
'edit_item' => 'Editar tipo',
'add_new_item' => 'Añadir nuevo tipo',
);
register_taxonomy(
'tipo_proyecto',
array( 'proyecto' ),
array(
'labels' => $labels,
'hierarchical' => true, // true = como categorías; false = como etiquetas.
'rewrite' => array( 'slug' => 'tipo-proyecto' ),
'show_in_rest' => true,
)
);
}
add_action( 'init', 'biblioweb_registrar_taxonomia_tipo' );
The parameter hierarchical decides the behavior: in true works like categories (with parent-child hierarchy and checkboxes); in false, like tags (free text). Each term automatically generates its own archive page: tudominio.com/tipo-proyecto/diseno-web/.
How to display custom post types in your theme
WordPress resolves the presentation using the template hierarchy. For our CPT proyecto will search, in this order:
single-proyecto.phpfor the individual entry (if it doesn't exist, usesingle.php).archive-proyecto.phpfor the listing (if it doesn't exist, usearchive.phpand thenindex.php).taxonomy-tipo_proyecto.phpfor taxonomy archives.
If your theme is block-based or you use a page builder, you can usually assign templates to the CPT from its own interface. And for custom listings in any template or shortcode, the query is made with WP_Query:
<?php
$proyectos = new WP_Query( array(
'post_type' => 'proyecto',
'posts_per_page' => 6,
'tax_query' => array(
array(
'taxonomy' => 'tipo_proyecto',
'field' => 'slug',
'terms' => 'diseno-web',
),
),
) );
if ( $proyectos->have_posts() ) :
while ( $proyectos->have_posts() ) : $proyectos->the_post();
the_title( '<h3>', '</h3>' );
the_post_thumbnail( 'medium' );
endwhile;
wp_reset_postdata();
endif;
Never forget wp_reset_postdata() after a custom loop: restores the main query and avoids strange side effects on the rest of the page. The complete reference of all available arguments is in the official post types documentation from developer.wordpress.org.
Custom fields: the data that completes the entry
A CPT defines the "container," but specific data (price, client, delivery date) lives in custom fields (post meta). You can manage them in three ways: with the native custom fields box (spartan but functional), with code using register_post_meta() y get_post_meta(), or with specialized plugins like Advanced Custom Fields (ACF) or Meta Box, which generate comfortable editing interfaces with validation and advanced field types. For serious projects, the combination CPT by code + ACF for fields is probably the industry standard.
<?php
// Registrar un campo y exponerlo en la REST API:
register_post_meta( 'proyecto', 'cliente', array(
'type' => 'string',
'single' => true,
'show_in_rest' => true,
'sanitize_callback' => 'sanitize_text_field',
) );
// Leerlo en la plantilla:
$cliente = get_post_meta( get_the_ID(), 'cliente', true );
SEO and custom post types: what you should review
A well-planned CPT is a blessing for SEO: it generates clean archives, semantic URLs, and a clear structure. But it's worth reviewing four points:
- Sitemap: check that your SEO plugin includes the new content type in the XML sitemap. Both Yoast and Rank Math, whose free version we analyzed, detect public CPTs and allow them to be activated or excluded by type.
- Selective indexing: if the CPT is supporting content (testimonials, reusable blocks), mark it as non-indexable or directly register it with
public => false. - Slug with intent:
/proyectos/says more than/cpt_portfolio/. The rewrite slug is an SEO decision, and changing it later implies redirections. - Content in the archive: the automatically generated archive page is usually a simple list; considering a page with its own introductory text can make a difference for positioning the main keyword of the content type.
Frequently Asked Questions
What happens to the content if I deactivate the plugin that registers the CPT?
Nothing is deleted: the elements remain in the database. They simply stop being displayed in the dashboard and their URLs return 404, because WordPress no longer recognizes that content type. When reactivating the registration (plugin or snippet), everything reappears intact. That's why it's important to keep the registration in something independent of the theme.
Custom post type or categories? I'm still not sure which to use
Use categories when you want to organize blog articles that share a format. Use a CPT when the content has its own nature: different fields, different template, and makes sense as an independent section of the website. A practical hint: if you find yourself wanting to hide those "posts" from the main blog, it's almost certain they should be a custom post type.
How many custom post types can I create?
There is no relevant technical limit: all share the table wp_posts and the cost of registering each type is minimal. The reasonable limit is organizational: if you go over six or eight types, check if some shouldn't actually be taxonomies or fields of the same type. Reserve names with a prefix (for example bw_proyecto) if you distribute your code, to avoid collisions with other plugins.
Why do my custom post types give a 404 error?
In 95% of cases, due to rewrite rules: WordPress has not yet "learned" the new URLs. Go to Settings → Permalinks and save without changing anything. If it persists, check that public be true, that the rewrite slug does not conflict with an existing page and that the registration is executed in the hook init on every load, not just on activation.
Conclusion
WordPress custom post types are the tool that turns a blog into a true custom CMS: portfolios, catalogs, directories, or academies, each content with its structure, its URLs, and its place in the dashboard. The professional recipe fits into three steps: register the type with register_post_type() in the hook init (and save permalinks), classify it with custom taxonomies using register_taxonomy(), and complete the entries with custom fields. From there, the template hierarchy and WP_Query give you total control over how everything is displayed. Start with a single real content type from your project, register it with code in your own snippet or plugin, and check in the dashboard what changes managing real "projects" instead of disguised posts. You, your clients, and Google will appreciate that structural clarity.