Los Tipos de Contenido Personalizados (Custom Post Types, CPT) constituyen el mecanismo básico para extender el esquema de datos relacional de WordPress. En su arquitectura interna, los CPT no crean tablas independientes en la base de datos, sino que se proyectan sobre la tabla universal wp_posts, donde la identidad de la entidad se determina mediante un discriminador de texto en la columna post_type.
Registrar un post type a través de la función register_post_type() activa un orquestado pipeline de inicialización en el núcleo:
$wp_post_types: El núcleo instancia la clase WP_Post_Type y la almacena en el registro global $GLOBALS['wp_post_types'][$post_type].WP_Rewrite): Si el argumento rewrite está activo, el núcleo registra las estructuras de enlaces permanentes (Permalinks), la máscara de endpoints (ep_mask) y las reglas de redirección para archivos (has_archive) y vistas individuales (single).show_in_rest => true, el núcleo aprovisiona rutas REST /wp/v2/<rest_base> administradas por WP_REST_Posts_Controller. Esto hace accesible el CPT para el editor de bloques Gutenberg, aplicaciones móviles nativas y arquitecturas Headless con Next.js.Capabilities Mapping): Traducción de permisos contextuales de una entidad concreta a permisos atómicos mediante el método map_meta_cap().El registro de un CPT debe ejecutarse estrictamente en el hook de acción init. Invocarlo antes (por ejemplo, en plugins_loaded) corrompe la internacionalización de etiquetas y el mapeo de roles, mientras que llamarlo después (en wp_loaded) impide que el despachador WP::parse_request() reconozca las rutas entrantes.
Conciba la arquitectura de CPT como un contrato declarativo que sincroniza el esquema de datos, el subsistema de autorización y las interfaces de presentación:
WP_Post_Type map_meta_cap ep_mask template_lock Hierarchical CPT Penalty[register_post_type('property', $args)] (en el hook 'init')
│
▼
[1. Instanciación de WP_Post_Type] ──> Guardado en $GLOBALS['wp_post_types']['property']
│
▼
[2. Registro en WP_Rewrite]
├── add_rewrite_tag('%property%', '([^/]+)', 'property=')
├── Generación de reglas rewrite: 'property/([^/]+)/?$' => 'index.php?property=$matches[1]'
└── Archivo: 'properties/?$' => 'index.php?post_type=property'
│
▼
[3. Registro de Endpoints REST API]
└── WP_REST_Posts_Controller ──> /wp/v2/properties (GET, POST, PUT, DELETE)
│
▼
[4. Vinculación con Template Hierarchy]
├── Vista individual: single-property.php -> single.php -> singular.php -> index.php
└── Archivo: archive-property.php -> archive.php -> index.php
│
▼
[5. Mapeo de Capacidades (Permissions)]
└── map_meta_cap('edit_post', $user_id, $post_id) ──> Verificación de roles del usuarioDeclaración de etiquetas (labels), visibilidad, esquema de URL (rewrite) y características soportadas (supports).
Instanciación de WP_Post_Type, registro en $wp_post_types y enlace con taxonomías del sistema.
Generación de expresiones regulares de WP_Rewrite y asignación de máscaras de endpoints.
Inicialización de WP_REST_Posts_Controller, compilación del esquema JSON y activación de Gutenberg.
Construcción de la matriz de acceso dinámico mediante map_meta_cap y verificación de roles.
capability_type con control de acceso basado en roles (el gestor solo edita sus operaciones, auditoría retiene read_private_posts sin escritura, supervisión valida publicaciones).template y bloqueo template_lock => 'all', garantizando que los redactores ingresen datos únicamente en los bloques Gutenberg designados sin alterar la retícula visual.register_post_status) — como under_review, legal_approved, expired — con validaciones automatizadas entre transiciones.Al diseñar la arquitectura de entidades, es imprescindible discernir cuándo utilizar un CPT y cuándo optar por una tabla personalizada:
Un error frecuente entre desarrolladores consiste en definir 'capability_type' => 'book' sin asignar capacidades a los roles. WordPress divide las capacidades en dos tipos:
wp_user_roles para cada rol (por ejemplo, edit_books, edit_others_books, publish_books, read_private_books).current_user_can('edit_post', $post_id).Cuando se define 'map_meta_cap' => true, el núcleo mapea dinámicamente la comprobación edit_post hacia la capacidad primitiva requerida: validando si el post pertenece al usuario actual (edit_posts) o a otro autor (edit_others_posts).
A continuación se presenta una implementación para producción en PHP 8.4+ que realiza un registro tipado de CPT con plantilla de bloques de Gutenberg, inicialización segura de capacidades y prevención de degradación de enlaces permanentes.
<?php
/**
* Plugin Name: Fullstack Agentic Lab — Enterprise CPT Registrar
* Description: Orquestador estricto de registro de CPT con matriz de permisos segura y plantillas Gutenberg.
* Version: 2.1.0
* Author: Artur Yusupov (Fullstack Agentic Lab)
*/
declare(strict_types=1);
namespace AgenticLab\Core\PostTypes;
use WP_Error;
if (!defined('ABSPATH')) {
exit;
}
final readonly class CaseStudyPostType
{
public const POST_TYPE = 'case_study';
public const CAPABILITY_TYPE = 'case_study';
public static function init(): void
{
add_action('init', self::register(...), 10);
}
public static function register(): void
{
$labels = [
'name' => 'Casos de Estudio',
'singular_name' => 'Caso de Estudio',
'menu_name' => 'Casos de Estudio',
'name_admin_bar' => 'Caso de Estudio',
'add_new' => 'Añadir Caso',
'add_new_item' => 'Añadir Nuevo Caso',
'new_item' => 'Nuevo Caso',
'edit_item' => 'Editar Caso',
'view_item' => 'Ver Caso',
'all_items' => 'Todos los Casos',
'search_items' => 'Buscar Casos',
'not_found' => 'No se encontraron casos',
'not_found_in_trash' => 'No hay casos en la papelera',
];
// Plantilla de bloques fija para asegurar la consistencia del diseño
$blockTemplate = [
['core/heading', ['level' => 2, 'placeholder' => 'Objetivos y Metas del Proyecto...']],
['core/paragraph', ['placeholder' => 'Describa el problema inicial del cliente...']],
['core/columns', [], [
['core/column', [], [
['core/heading', ['level' => 3, 'content' => 'Stack Tecnológico']],
['core/list', []]
]],
['core/column', [], [
['core/heading', ['level' => 3, 'content' => 'Resultados (KPIs)']],
['core/paragraph', ['placeholder' => '+140% tasa de conversión...']]
]]
]],
['core/quote', ['placeholder' => 'Testimonio del Director de Tecnología del cliente...']]
];
$args = [
'labels' => $labels,
'description' => 'Portafolio de casos de ingeniería y proyectos corporativos ejecutados',
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => true,
'rewrite' => [
'slug' => 'cases',
'with_front' => false,
'feeds' => true,
'pages' => true,
],
'has_archive' => 'cases',
'hierarchical' => false, // Protección frente a sobrecarga de memoria
'menu_position' => 20,
'menu_icon' => 'dashicons-portfolio',
'show_in_rest' => true, // Habilita Gutenberg y la API REST
'rest_base' => 'cases',
'supports' => ['title', 'editor', 'thumbnail', 'excerpt', 'revisions', 'custom-fields'],
// Modelo de autorización explícito
'capability_type' => [self::CAPABILITY_TYPE, self::CAPABILITY_TYPE . 's'],
'map_meta_cap' => true,
// Configuración de plantilla de bloques y bloqueo
'template' => $blockTemplate,
'template_lock' => false, // 'all' para congelar la estructura completamente
];
register_post_type(self::POST_TYPE, $args);
}
/**
* Inicialización programática de permisos para roles durante la activación del plugin.
* ¡Nunca debe ejecutarse en cada petición!
*/
public static function grantCapabilities(): void
{
$adminRole = get_role('administrator');
$editorRole = get_role('editor');
$caps = [
"edit_" . self::CAPABILITY_TYPE,
"read_" . self::CAPABILITY_TYPE,
"delete_" . self::CAPABILITY_TYPE,
"edit_" . self::CAPABILITY_TYPE . "s",
"edit_others_" . self::CAPABILITY_TYPE . "s",
"publish_" . self::CAPABILITY_TYPE . "s",
"read_private_" . self::CAPABILITY_TYPE . "s",
"delete_" . self::CAPABILITY_TYPE . "s",
"delete_private_" . self::CAPABILITY_TYPE . "s",
"delete_published_" . self::CAPABILITY_TYPE . "s",
"delete_others_" . self::CAPABILITY_TYPE . "s",
"edit_private_" . self::CAPABILITY_TYPE . "s",
"edit_published_" . self::CAPABILITY_TYPE . "s",
];
foreach ($caps as $cap) {
$adminRole?->add_cap($cap);
$editorRole?->add_cap($cap);
}
}
}
// Inicialización del servicio
CaseStudyPostType::init();
// Vaciado seguro de reglas de reescritura ESTRICTAMENTE en la activación
register_activation_hook(__FILE__, function(): void {
CaseStudyPostType::register();
CaseStudyPostType::grantCapabilities();
flush_rewrite_rules(false); // false = vaciado suave sin modificar .htaccess
});Por qué lo hacen: Tras registrar el CPT, las vistas individuales retornan un 404. Sin comprender el funcionamiento de WP_Rewrite, el programador añade flush_rewrite_rules() al final de su función de registro.
Qué sucede en producción: flush_rewrite_rules() recompila miles de expresiones regulares y sobreescribe el pesado registro rewrite_rules en wp_options. Esto provoca invalidaciones continuas de caché de opciones, sobrecarga de I/O de disco, incrementos de TTFB de 200–600 ms en cada visita y bloqueos de filas en MySQL ante tráfico simultáneo.
Cómo resolverlo: Ejecutar el vaciado de reglas una sola vez — en el hook de activación del plugin (register_activation_hook) o guardando manualmente los enlaces permanentes en el panel de administración:
register_activation_hook(__FILE__, function(): void {
my_register_cpt();
flush_rewrite_rules(false); // Vaciado suave sin escribir en .htaccess
});Por qué lo hacen: Con la intención de obtener URLs jerárquicas del tipo catalog.com/catalog/categoria-padre/elemento-hijo/ de forma similar a las páginas estándar (page).
Qué sucede en producción: Con hierarchical => true, el núcleo genera un árbol jerárquico recursivo para el panel de administración. En la pantalla de edición, el desplegable de atributo superior invoca wp_dropdown_pages(), cargando todos los registros del CPT en la memoria RAM de PHP. Si el catálogo supera los 15.000 elementos, el servidor arroja un Fatal Error: Memory Size Exhausted. Además, resolver URLs anidadas requiere consultas en cadena que degradan la generación de SQL.
Cómo resolverlo: Mantener el CPT plano ('hierarchical' => false) y estructurar las categorías y facetas mediante Taxonomías Personalizadas (register_taxonomy), las cuales están optimizadas e indexadas para relaciones jerárquicas.
Por qué lo hacen: Se define un tipo de permisos exclusivo 'capability_type' => 'document' por motivos de seguridad, asumiendo que el administrador heredará el acceso de forma automática.
Qué sucede en producción: Ningún rol (ni siquiera administrator) posee capacidades sobre el nuevo CPT. La opción desaparece del menú lateral y al acceder a /wp-admin/edit.php?post_type=document se produce un error 403 («No tienes permisos suficientes para acceder a esta página»).
Cómo resolverlo: Otorgar programáticamente las capacidades a los roles pertinentes con la API WP_Roles durante la activación del plugin, o activar 'map_meta_cap' => true junto a capacidades estándar ('capability_type' => 'post') cuando no se requiera una segregación avanzada.
Por qué lo hacen: Reutilización de fragmentos antiguos de código de la época de WordPress 4.x previos a la integración con la REST API.
Qué sucede en producción: Las entradas se abren en el obsoleto Classic Editor (TinyMCE). El editor de bloques Gutenberg, las plantillas de bloques, los paneles de componentes y la Block Bindings API quedan inhabilitados. Asimismo, resulta imposible consumir las entradas mediante REST API o interfaces Headless.
Cómo resolverlo: Declarar siempre de forma explícita 'show_in_rest' => true y configurar un slug limpio mediante 'rest_base' => 'my_items'.
Configuración de etiquetas, visibilidad, capacidades soportadas y esquemas de enlace.
Aplique reglas estrictas: hierarchical => false para catálogos masivos.
Paso de argumentos a register_post_type(). Creación de la instancia WP_Post_Type en memoria.
Nunca registre CPTs en hooks como plugins_loaded o after_setup_theme.
Generación de expresiones regulares internas para el parser de URLs y etiquetas de endpoint.
Ejecute flush_rewrite_rules exclusivamente en eventos de activación.
Exposición de endpoints /wp/v2/<rest_base> para Gutenberg y clientes Headless.
El flag show_in_rest habilita la interfaz del editor de bloques.
Mapeo de primitivas capability_type hacia Administrador, Editor y roles personalizados.
La evaluación con current_user_can('edit_post', $id) valida el contexto de forma segura.