Snipet 1 video noti

# SNIPPET 01 β€” `vhn_core`
### Video Hub News CDMX β€” Core Foundation

Cabe en una sola parte. AquΓ­ va completo:

Β«`php
‘Videos Hub’,
‘singular_name’ => ‘Video Hub’,
‘menu_name’ => ‘Video Hub News’,
‘add_new’ => ‘Agregar video’,
‘add_new_item’ => ‘Agregar nuevo video’,
‘edit_item’ => ‘Editar video’,
‘new_item’ => ‘Nuevo video’,
‘view_item’ => ‘Ver video’,
‘view_items’ => ‘Ver videos’,
‘search_items’ => ‘Buscar videos’,
‘not_found’ => ‘No se encontraron videos.’,
‘not_found_in_trash’ => ‘No hay videos en la papelera.’,
‘all_items’ => ‘Todos los videos’,
‘archives’ => ‘Archivo de videos’,
‘attributes’ => ‘Atributos del video’,
‘parent_item_colon’ => Β»,
‘featured_image’ => ‘Thumbnail del video’,
‘set_featured_image’ => ‘Establecer thumbnail’,
‘remove_featured_image’ => ‘Quitar thumbnail’,
‘use_featured_image’ => ‘Usar como thumbnail’,
‘insert_into_item’ => ‘Insertar en el video’,
‘uploaded_to_this_item’ => ‘Subido a este video’,
‘items_list’ => ‘Lista de videos’,
‘items_list_navigation’ => ‘NavegaciΓ³n de la lista’,
‘filter_items_list’ => ‘Filtrar lista de videos’,
);

$args = array(
‘labels’ => $labels,
‘description’ => ‘Videos agregados automΓ‘ticamente desde YouTube para Video Hub News CDMX.’,
‘public’ => true,
‘publicly_queryable’ => true,
‘show_ui’ => true,
‘show_in_menu’ => true,
‘query_var’ => true,
‘rewrite’ => array(
‘slug’ => VHN_SLUG,
‘with_front’ => false, // URL limpia: /video-hub/ no /blog/video-hub/
),
‘capability_type’ => ‘post’,
‘has_archive’ => true, // Activa /video-hub/ como archive page
‘hierarchical’ => false,
‘menu_position’ => 25,
‘menu_icon’ => ‘dashicons-video-alt3’,
‘supports’ => array(
‘title’, // TΓ­tulo del video
‘thumbnail’, // Imagen destacada (usada como thumbnail)
‘custom-fields’, // Para que los postmeta sean accesibles vΓ­a REST
‘author’, // Permite asignar usuario editor
),
‘show_in_rest’ => true, // Necesario para Gutenberg y REST API
‘rest_base’ => ‘video-hub’,
‘taxonomies’ => array( ‘vhn_category’, ‘vhn_source_channel’ ),
);

register_post_type( VHN_CPT, $args );
}

// =============================================================================
// SECCIΓ“N 3 β€” REGISTRO DE TAXONOMÍAS
// =============================================================================

/**
* Registra las dos taxonomΓ­as del sistema.
* TambiΓ©n corre en priority 5 para estar disponibles junto al CPT.
*/
add_action( ‘init’, ‘vhn_register_taxonomies’, 5 );

function vhn_register_taxonomies() {

// ————————————————————————-
// TAXONOMÍA 1: vhn_category β€” CategorΓ­as editoriales
// ————————————————————————-
// JerΓ‘rquica (como categorΓ­as de WordPress).
// Ejemplos: Entretenimiento, Ciudad, MΓΊsica, GastronomΓ­a, Estilo de vida.
// El editor las crea y asigna manualmente desde el panel.

$cat_labels = array(
‘name’ => ‘CategorΓ­as VHN’,
‘singular_name’ => ‘CategorΓ­a VHN’,
‘menu_name’ => ‘CategorΓ­as’,
‘search_items’ => ‘Buscar categorΓ­as’,
‘all_items’ => ‘Todas las categorΓ­as’,
‘parent_item’ => ‘CategorΓ­a padre’,
‘parent_item_colon’ => ‘CategorΓ­a padre:’,
‘edit_item’ => ‘Editar categorΓ­a’,
‘update_item’ => ‘Actualizar categorΓ­a’,
‘add_new_item’ => ‘Agregar nueva categorΓ­a’,
‘new_item_name’ => ‘Nombre de nueva categorΓ­a’,
‘not_found’ => ‘No se encontraron categorΓ­as.’,
‘items_list’ => ‘Lista de categorΓ­as’,
‘items_list_navigation’ => ‘NavegaciΓ³n de categorΓ­as’,
‘back_to_items’ => ‘← Volver a categorΓ­as’,
);

$cat_args = array(
‘labels’ => $cat_labels,
‘hierarchical’ => true,
‘public’ => true,
‘show_ui’ => true,
‘show_in_menu’ => true,
‘show_in_nav_menus’ => true,
‘show_tagcloud’ => false,
‘show_admin_column’ => true, // Muestra columna en listado de posts
‘show_in_rest’ => true,
‘rewrite’ => array(
‘slug’ => VHN_SLUG . ‘/categoria’,
‘with_front’ => false,
‘hierarchical’ => true,
),
‘query_var’ => true,
);

register_taxonomy( ‘vhn_category’, array( VHN_CPT ), $cat_args );

// ————————————————————————-
// TAXONOMÍA 2: vhn_source_channel β€” Canal de YouTube fuente
// ————————————————————————-
// No jerΓ‘rquica (como tags).
// Se puebla automΓ‘ticamente durante la ingesta con el nombre del canal.
// Permite filtrar el archivo por canal de origen.

$ch_labels = array(
‘name’ => ‘Canales fuente’,
‘singular_name’ => ‘Canal fuente’,
‘menu_name’ => ‘Canales fuente’,
‘search_items’ => ‘Buscar canales’,
‘all_items’ => ‘Todos los canales’,
‘edit_item’ => ‘Editar canal’,
‘update_item’ => ‘Actualizar canal’,
‘add_new_item’ => ‘Agregar canal’,
‘new_item_name’ => ‘Nombre del nuevo canal’,
‘not_found’ => ‘No se encontraron canales.’,
‘items_list’ => ‘Lista de canales’,
‘items_list_navigation’ => ‘NavegaciΓ³n de canales’,
‘back_to_items’ => ‘← Volver a canales’,
‘no_terms’ => ‘Sin canal asignado’,
‘most_used’ => ‘Canales mΓ‘s usados’,
);

$ch_args = array(
‘labels’ => $ch_labels,
‘hierarchical’ => false,
‘public’ => true,
‘show_ui’ => true,
‘show_in_menu’ => true,
‘show_in_nav_menus’ => true,
‘show_tagcloud’ => false,
‘show_admin_column’ => true,
‘show_in_rest’ => true,
‘rewrite’ => array(
‘slug’ => VHN_SLUG . ‘/canal’,
‘with_front’ => false,
),
‘query_var’ => true,
);

register_taxonomy( ‘vhn_source_channel’, array( VHN_CPT ), $ch_args );
}

// =============================================================================
// SECCIΓ“N 4 β€” QUERY VAR
// =============================================================================

/**
* Registra ‘vhn_video’ como query var reconocida por WordPress.
*
* Sin esto, WordPress ignora el parΓ‘metro ?vhn_video=ID
* y get_query_var(‘vhn_video’) devuelve siempre vacΓ­o.
*
* Uso en frontend:
* $youtube_id = get_query_var( VHN_QUERY_VAR ); // retorna el ID o Β»
*/
add_filter( ‘query_vars’, ‘vhn_register_query_vars’ );

function vhn_register_query_vars( $vars ) {
$vars[] = VHN_QUERY_VAR; // ‘vhn_video’
return $vars;
}

// =============================================================================
// SECCIΓ“N 5 β€” FLUSH DE REWRITE RULES (UNA SOLA VEZ)
// =============================================================================

/**
* Hace flush de las rewrite rules ÚNICAMENTE si no se ha hecho antes.
*
* Por quΓ© es importante:
* El CPT y las taxonomΓ­as generan nuevas reglas de URL. Sin flush,
* /video-hub/ devuelve 404 aunque el CPT estΓ© correctamente registrado.
*
* Por quΓ© NO usar register_activation_hook aquΓ­:
* Code Snippets no expone hooks de activaciΓ³n. En cambio, usamos una
* opciΓ³n en wp_options como flag para garantizar que el flush ocurra
* exactamente una vez, en la primera carga tras activar el snippet.
*
* IMPORTANTE: Si cambias el slug VHN_SLUG, borra la opciΓ³n
* ‘vhn_rewrite_flushed’ desde WP Admin > Herramientas > Cualquier
* recurso que acceda a wp_options, o desde la consola con:
* delete_option(‘vhn_rewrite_flushed’);
* Esto forzarΓ‘ un nuevo flush en la siguiente carga.
*/
add_action( ‘init’, ‘vhn_maybe_flush_rewrite_rules’, 99 );

function vhn_maybe_flush_rewrite_rules() {

// Solo flushar si no se ha hecho antes con la versiΓ³n actual.
$flushed_version = get_option( VHN_FLUSH_FLAG );

if ( $flushed_version !== VHN_VERSION ) {
// El CPT y taxonomΓ­as ya estΓ‘n registrados en priority 5,
// asΓ­ que el flush de priority 99 los incluye correctamente.
flush_rewrite_rules( false ); // false = no regenerar .htaccess

// Guardar la versiΓ³n actual como flag para no repetir.
update_option( VHN_FLUSH_FLAG, VHN_VERSION, false );
// Tercer argumento false = no autocargar esta opciΓ³n en cada request.
}
}

// =============================================================================
// SECCIΓ“N 6 β€” HELPERS BASE
// =============================================================================
// Funciones utilitarias compartidas por todos los snippets posteriores.
// Ninguna hace llamadas externas ni accede a la API.

/**
* Retorna la fecha de hoy en zona horaria CDMX, formato YYYY-MM-DD.
*
* Esta es LA funciΓ³n canΓ³nica para obtener Β«hoyΒ» en todo el sistema.
* Todos los snippets deben usar esta funciΓ³n para determinar
* la ediciΓ³n activa, nunca date(‘Y-m-d’) directamente.
*
* @return string Fecha en formato Y-m-d. Ejemplo: ‘2026-05-26’
*/
function vhn_get_today_date() {
try {
$tz = new DateTimeZone( VHN_TIMEZONE );
$now = new DateTime( ‘now’, $tz );
return $now->format( ‘Y-m-d’ );
} catch ( Exception $e ) {
// Fallback con la timezone configurada en WordPress.
return wp_date( ‘Y-m-d’ );
}
}

/**
* Retorna el timestamp Unix actual en zona horaria CDMX.
*
* Útil para calcular expiración de transients con lógica de medianoche.
*
* @return int Timestamp Unix.
*/
function vhn_get_now_timestamp() {
try {
$tz = new DateTimeZone( VHN_TIMEZONE );
$now = new DateTime( ‘now’, $tz );
return (int) $now->format( ‘U’ );
} catch ( Exception $e ) {
return time();
}
}

/**
* Retorna los segundos que faltan hasta la medianoche CDMX.
*
* Uso principal: calcular TTL de transients de la ediciΓ³n de hoy,
* para que expiren exactamente cuando empieza una nueva ediciΓ³n.
*
* @return int Segundos hasta medianoche.
*/
function vhn_seconds_until_midnight() {
try {
$tz = new DateTimeZone( VHN_TIMEZONE );
$now = new DateTime( ‘now’, $tz );
$midnight = new DateTime( ‘tomorrow midnight’, $tz );
return max( 1, (int) $midnight->getTimestamp() – (int) $now->getTimestamp() );
} catch ( Exception $e ) {
// Fallback conservador: 6 horas.
return 6 * HOUR_IN_SECONDS;
}
}

/**
* Sanitiza y valida un YouTube video ID.
*
* Un video ID de YouTube tiene exactamente 11 caracteres alfanumΓ©ricos
* mΓ‘s guiΓ³n y guiΓ³n_bajo: [A-Za-z0-9_-]{11}
*
* @param string $raw_id Input sin sanitizar.
* @return string|false ID sanitizado, o false si no es vΓ‘lido.
*/
function vhn_sanitize_youtube_id( $raw_id ) {
$clean = sanitize_text_field( $raw_id );
// Validar longitud y caracteres permitidos por YouTube.
if ( preg_match( ‘/^[A-Za-z0-9_\-]{11}$/’, $clean ) ) {
return $clean;
}
return false;
}

/**
* Sanitiza y valida un YouTube channel ID.
*
* Los Channel IDs de YouTube empiezan con Β«UCΒ» seguido de 22 caracteres
* alfanumΓ©ricos mΓ‘s guiΓ³n y guiΓ³n_bajo.
*
* @param string $raw_id Input sin sanitizar.
* @return string|false Channel ID sanitizado, o false si no es vΓ‘lido.
*/
function vhn_sanitize_channel_id( $raw_id ) {
$clean = sanitize_text_field( $raw_id );
if ( preg_match( ‘/^UC[A-Za-z0-9_\-]{22}$/’, $clean ) ) {
return $clean;
}
return false;
}

/**
* Convierte un Channel ID (UCxxxxxx) a Playlist ID (UUxxxxxx).
*
* Esta conversiΓ³n permite obtener los videos recientes de un canal
* sin usar search.list (que cuesta 100 unidades de cuota).
* playlistItems.list de la playlist UU solo cuesta 1 unidad.
*
* No gasta cuota de API. Es una operaciΓ³n de string pura.
*
* @param string $channel_id Channel ID (UCxxxxxx).
* @return string|false Playlist ID (UUxxxxxx), o false si el channel ID no es vΓ‘lido.
*/
function vhn_channel_to_playlist_id( $channel_id ) {
$clean = vhn_sanitize_channel_id( $channel_id );
if ( ! $clean ) {
return false;
}
// Reemplazar solo el prefijo Β«UCΒ» β†’ Β«UUΒ». El resto del ID no cambia.
return ‘UU’ . substr( $clean, 2 );
}

/**
* Retorna el prefijo estΓ‘ndar del sistema.
*
* Usado por snippets posteriores para construir keys de opciones,
* transients y postmeta de forma consistente.
*
* @return string ‘vhn_’
*/
function vhn_prefix() {
return VHN_PREFIX;
}

/**
* Loguea un mensaje de debug del sistema si WP_DEBUG estΓ‘ activo.
*
* Todos los snippets del sistema deben usar esta funciΓ³n para logs,
* nunca error_log() directamente, para poder desactivar los logs
* en producciΓ³n con un solo switch (WP_DEBUG).
*
* @param string $message Mensaje a loguear.
* @param string $context Contexto opcional. Ejemplo: ‘ingestion’, ‘cron’, ‘api’.
*/
function vhn_log( $message, $context = ‘core’ ) {
if ( defined( ‘WP_DEBUG’ ) && WP_DEBUG ) {
// Format: [VHN][context] mensaje
error_log( ‘[VHN][‘ . $context . ‘] ‘ . $message );
}
}
Β«`

## QuΓ© activa este snippet y cΓ³mo probarlo

### Al activarlo por primera vez en Code Snippets verΓ‘s:

**En WP Admin β†’ menΓΊ lateral:**
Aparece el Γ­tem **Β»Video Hub NewsΒ»** con el Γ­cono de video (dashicons-video-alt3), con subitems: *Todos los videos*, *Agregar video*, *CategorΓ­as VHN*, *Canales fuente*.

**En WP Admin β†’ ConfiguraciΓ³n β†’ Enlaces Permanentes:**
No necesitas hacer nada β€” el flush ocurre automΓ‘ticamente en la primera carga.

**En el navegador:**
La URL `https://tusitio.com/video-hub/` debe responder con una pΓ‘gina de archivo (puede estar vacΓ­a, pero no debe dar 404).

### Pruebas que debes ejecutar

**Prueba 1 β€” CPT visible:**
Ve a `WP Admin β†’ Video Hub News β†’ Agregar video`. Debes ver el editor de posts con los campos estΓ‘ndar y el metabox de taxonomΓ­as en la barra lateral.

**Prueba 2 β€” TaxonomΓ­as funcionando:**
En el editor de un video, verifica que aparecen los metaboxes *Β»CategorΓ­as VHNΒ»* y *Β»Canales fuenteΒ»* en la barra lateral. Crea una categorΓ­a de prueba (Β«EntretenimientoΒ») y asΓ­gnala.

**Prueba 3 β€” Archive URL sin 404:**
Visita `https://tusitio.com/video-hub/` en el navegador. Debe cargar sin error 404. Si tu tema no tiene template para archives de CPT personalizados, verΓ‘s el layout por defecto del tema β€” eso es correcto en esta fase.

**Prueba 4 β€” Query var registrada:**
Visita `https://tusitio.com/video-hub/?vhn_video=dQw4w9WgXcQ`. No debe generar ningΓΊn error. El parΓ‘metro estΓ‘ registrado aunque ningΓΊn snippet lo usa todavΓ­a.

**Prueba 5 β€” Helpers funcionando:**
Crea un post temporal con este shortcode o agrΓ©galo momentΓ‘neamente a `functions.php` de tu tema hijo para verificar:

Β«`php
// Prueba rΓ‘pida β€” borrar despuΓ©s
add_shortcode(‘vhn_test’, function() {
$output = ‘Hoy CDMX: ‘ . vhn_get_today_date() . ‘
‘;
$output .= ‘Segundos hasta medianoche: ‘ . vhn_seconds_until_midnight() . ‘
‘;
$output .= ‘Playlist de UCxxxxxx: ‘ . vhn_channel_to_playlist_id(‘UCddiUEpeqJcYeBxX1855Kgg’) . ‘
‘;
$output .= ‘ID vΓ‘lido: ‘ . var_export( vhn_sanitize_youtube_id(‘dQw4w9WgXcQ’), true ) . ‘
‘;
$output .= ‘ID invΓ‘lido: ‘ . var_export( vhn_sanitize_youtube_id(‘

Scroll al inicio