Crear un sitio de recetas requiere una estructura de datos muy específica. Una receta no es una simple entrada de blog; tiene tiempos de cocción, listas de ingredientes, información nutricional y pasos estructurados.
En este tutorial construiremos el plugin «Recetas Pro». Al finalizar, tendrás un sistema que crea un tipo de contenido «Receta», campos personalizados seguros y una visualización automática optimizada para SEO (Google Rich Snippets).
Requisitos Previos
- Acceso a tu servidor vía FTP o al Gestor de Archivos de tu hosting.
- Un editor de código (VS Code, Sublime Text, etc.).
- No necesitas instalar nada más, usaremos PHP puro.
Paso 1: Crear la Estructura del Plugin
- Accede a la carpeta
/wp-content/plugins/de tu instalación. - Crea una carpeta llamada
recetas-pro. - Dentro, crea un archivo llamado
recetas-pro.php.
Abre ese archivo y pega la cabecera obligatoria para que WordPress lo reconozca:
<?php
/**
* Plugin Name: Recetas Pro
* Description: Sistema de gestión de recetas.
* Version: 1.0
* Author: LaboratorioWP
* Text Domain: recetas-pro
*/
// Seguridad: Evitar acceso directo al archivo
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
Paso 2: Crear el Custom Post Type (CPT)
Necesitamos separar las recetas de las noticias del blog. Usaremos la función register_post_type.
function rp_registrar_receta_cpt() {
$labels = array(
'name' => 'Recetas',
'singular_name' => 'Receta',
'menu_name' => 'Recetas Pro',
'add_new' => 'Añadir Nueva',
'add_new_item' => 'Añadir Nueva Receta',
'edit_item' => 'Editar Receta',
'view_item' => 'Ver Receta',
'search_items' => 'Buscar Recetas',
'not_found' => 'No se encontraron recetas',
);
$args = array(
'labels' => $labels,
'public' => true,
'has_archive' => true, // Permite tener una página /recetas/
'menu_icon' => 'dashicons-carrot', // Icono de zanahoria
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt', 'author' ),
'show_in_rest' => true, // IMPORTANTE: Activa el editor Gutenberg
'rewrite' => array( 'slug' => 'receta' ),
);
register_post_type( 'receta', $args );
}
add_action( 'init', 'rp_registrar_receta_cpt' );

Paso 3: Organizar con Taxonomías
Las categorías del blog no sirven. Necesitamos «Tipo de Cocina» (Italiana, Mexicana) y «Dificultad».
function rp_registrar_taxonomias() {
// 1. Tipo de Cocina (Jerárquica, como categorías)
register_taxonomy( 'tipo_cocina', 'receta', array(
'labels' => array( 'name' => 'Tipos de Cocina', 'add_new_item' => 'Añadir Tipo' ),
'hierarchical' => true,
'show_in_rest' => true,
));
// 2. Dificultad (Plana, como etiquetas)
register_taxonomy( 'dificultad', 'receta', array(
'labels' => array( 'name' => 'Dificultad', 'add_new_item' => 'Añadir Dificultad' ),
'hierarchical' => false,
'show_in_rest' => true,
));
}
add_action( 'init', 'rp_registrar_taxonomias' );


Paso 4: Campos Personalizados (La parte crucial)
Aquí guardaremos los datos: Tiempo, Raciones e Ingredientes. Crearemos una «Caja Meta» (Meta Box) en el editor.

4.1. Registrar la Caja y el HTML
// Registrar la caja
function rp_agregar_meta_boxes() {
add_meta_box( 'rp_detalles', 'Detalles de la Receta', 'rp_html_meta_box', 'receta', 'normal', 'high' );
}
add_action( 'add_meta_boxes', 'rp_agregar_meta_boxes' );
// El HTML del formulario dentro del editor
function rp_html_meta_box( $post ) {
// Recuperar valores guardados
$tiempo = get_post_meta( $post->ID, '_rp_tiempo', true );
$raciones = get_post_meta( $post->ID, '_rp_raciones', true );
$ingredientes = get_post_meta( $post->ID, '_rp_ingredientes', true );
// Campo de seguridad (Nonce)
wp_nonce_field( 'rp_guardar_seguro', 'rp_nonce_campo' );
?>
<div style="display: flex; gap: 20px; margin-bottom: 15px;">
<div>
<label><strong>⏱️ Tiempo (min):</strong></label><br>
<input type="number" name="rp_tiempo" value="<?php echo esc_attr($tiempo); ?>" style="width: 100px;">
</div>
<div>
<label><strong>👥 Raciones:</strong></label><br>
<input type="number" name="rp_raciones" value="<?php echo esc_attr($raciones); ?>" style="width: 100px;">
</div>
</div>
<div>
<label><strong>🥦 Ingredientes (uno por línea):</strong></label><br>
<textarea name="rp_ingredientes" rows="6" style="width: 100%;"><?php echo esc_textarea($ingredientes); ?></textarea>
</div>
<?php
}
4.2. Guardar los datos de forma segura
WordPress no guarda nada automáticamente en custom fields, hay que programarlo.
function rp_guardar_datos( $post_id ) {
// 1. Verificar seguridad (Nonce)
if ( ! isset( $_POST['rp_nonce_campo'] ) || ! wp_verify_nonce( $_POST['rp_nonce_campo'], 'rp_guardar_seguro' ) ) {
return;
}
// 2. Evitar guardado en autosave
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
// 3. Permisos del usuario
if ( ! current_user_can( 'edit_post', $post_id ) ) return;
// 4. Guardar y Limpiar (Sanitizar) datos
if ( isset( $_POST['rp_tiempo'] ) ) {
update_post_meta( $post_id, '_rp_tiempo', sanitize_text_field( $_POST['rp_tiempo'] ) );
}
if ( isset( $_POST['rp_raciones'] ) ) {
update_post_meta( $post_id, '_rp_raciones', sanitize_text_field( $_POST['rp_raciones'] ) );
}
if ( isset( $_POST['rp_ingredientes'] ) ) {
// sanitize_textarea_field mantiene los saltos de línea pero quita HTML malicioso
update_post_meta( $post_id, '_rp_ingredientes', sanitize_textarea_field( $_POST['rp_ingredientes'] ) );
}
}
add_action( 'save_post', 'rp_guardar_datos' );
Paso 5: Mostrar la Receta en la web (Frontend)
Para no tener que editar los archivos de tu tema (lo cual es mala práctica en un plugin), usaremos un filtro para «pegar» la ficha de la receta al final del contenido.
function rp_mostrar_ficha( $content ) {
// Solo en la página individual de una receta
if ( ! is_singular( 'receta' ) || ! in_the_loop() || ! is_main_query() ) {
return $content;
}
$id = get_the_ID();
$tiempo = get_post_meta( $id, '_rp_tiempo', true );
$raciones = get_post_meta( $id, '_rp_raciones', true );
$ingredientes_raw = get_post_meta( $id, '_rp_ingredientes', true );
// Convertir ingredientes en lista HTML
$lista = '';
$items = explode( PHP_EOL, $ingredientes_raw );
foreach( $items as $item ) {
if( !empty( trim($item) ) ) $lista .= '<li>' . esc_html($item) . '</li>';
}
// HTML de la tarjeta
$ficha = '
<div class="rp-receta-card" style="background: #fff; border: 1px solid #e2e2e2; border-radius: 8px; padding: 25px; margin-top: 30px; box-shadow: 0 4px 6px rgba(0,0,0,0.05);">
<h3 style="margin-top: 0;">📝 Ficha de Cocina</h3>
<div style="background: #f0f0f0; padding: 10px; border-radius: 4px; display: flex; justify-content: space-around; margin-bottom: 20px;">
<span><strong>⏱️ Tiempo:</strong> ' . esc_html($tiempo) . ' min</span>
<span><strong>🍽️ Raciones:</strong> ' . esc_html($raciones) . ' pers.</span>
</div>
<h4>Ingredientes:</h4>
<ul>' . $lista . '</ul>
</div>';
return $content . $ficha;
}
add_filter( 'the_content', 'rp_mostrar_ficha' );

Paso 6: Integración SEO (Schema.org)
Este es el paso que diferencia un plugin amateur de uno profesional. Inyectaremos código JSON-LD para que Google muestre la foto y el tiempo en los resultados de búsqueda.
function rp_inyectar_schema() {
if ( is_singular( 'receta' ) ) {
$id = get_the_ID();
$tiempo = get_post_meta( $id, '_rp_tiempo', true );
$ingredientes = array_map( 'trim', explode( PHP_EOL, get_post_meta( $id, '_rp_ingredientes', true ) ) );
$json = array(
'@context' => 'https://schema.org/',
'@type' => 'Recipe',
'name' => get_the_title(),
'image' => get_the_post_thumbnail_url( $id, 'large' ),
'description' => get_the_excerpt(),
'prepTime' => 'PT' . $tiempo . 'M', // Formato ISO de tiempo
'recipeIngredient' => array_filter($ingredientes)
);
echo '<script type="application/ld+json">' . json_encode($json) . '</script>';
}
}
add_action( 'wp_head', 'rp_inyectar_schema' );
Resultado Final
Ahora, al activar tu plugin:
- Tendrás un menú «Recetas Pro».
- Podrás crear recetas con categorías y datos de tiempo/ingredientes.
- Al ver la receta en la web, aparecerá automáticamente una ficha bonita al final.
- Google entenderá tu contenido gracias al Schema.org.
Glosario Técnico del Tutorial
CPT (Custom Post Type) Tipo de contenido personalizado. Extiende la funcionalidad de WordPress más allá de las «Entradas» y «Páginas» estándar, permitiendo crear entidades como «Productos», «Eventos» o «Recetas».
Meta Box Contenedor en la interfaz de administración que permite a los usuarios introducir metadatos (información adicional) asociada al contenido principal.
Nonce (Security Token) Acrónimo de «Number used Once». Es un código de seguridad único que WordPress genera para verificar que una acción (como guardar un formulario) proviene de un usuario legítimo y no de un ataque CSRF.
Sanitización Proceso de limpieza de datos de entrada. Funciones como sanitize_text_field eliminan caracteres peligrosos o código HTML no deseado antes de guardar la información en la base de datos.
Hook (Gancho) Puntos de anclaje en el código de WordPress.
- Action (
add_action): Ejecuta código en un momento específico (ej: al guardar un post). - Filter (
add_filter): Modifica datos antes de mostrarlos (ej: añadir la ficha de receta al final del contenido).
JSON-LD Formato de datos estructurados preferido por Google. Es un bloque de código JavaScript invisible para el humano pero que explica al buscador qué es el contenido (una receta, un libro, un evento).







