Files

37 KiB

PROMPT_IA — Guía completa del proyecto pflores.linkedcorp.net

Documento de referencia para asistentes de IA. Léelo completo antes de cualquier tarea. Última actualización: junio 2026.


1. ¿Qué es este proyecto?

Sitio web personal de hoja de vida / currículum ejecutivo de Patricia Soledad Flores Sánchez, gerente de operaciones financieras con 22+ años en banca y microfinanzas (Ecuador).

  • URL pública: https://pflores.linkedcorp.net
  • Stack: WordPress 6.x + PHP 8.4 + MariaDB + CMB2 (campos personalizados, 100 % gratuito)
  • Se ejecuta con el servidor PHP incorporado (php -S) en el puerto 8081
  • SSL terminado por Nginx Proxy Manager → envía X-Forwarded-Proto: https
  • IP local del servidor: 192.168.222.40

2. Estructura de directorios

pflores.linkedcorp.net/
├── backend/
│   ├── start-wordpress.sh          # Arrancar el servidor PHP (usa router.php)
│   ├── router.php                  # Router PHP — permite URLs tipo /politica-de-privacidad/
│   └── wordpress/                  # Instalación WordPress 7.0
│       ├── wp-config.php           # Config dinámica (URL, auto-updates OFF)
│       ├── favicon.ico             # Foto de Patricia, circular, 16/32/48px
│       └── wp-content/
│           ├── plugins/
│           │   ├── cmb2/           # CMB2 — campos personalizados (GRATIS)
│           │   ├── wp-mail-smtp/   # Email SMTP (pendiente config)
│           │   └── pflores-cv-fields/
│           │       ├── pflores-cv-fields.php   # Menú admin + campos CMB2
│           │       ├── pdf-generator.php       # Generador PDF con Chromium
│           │       └── populate-data.php       # Sembrador inicial (ejecutar 1 vez)
│           └── themes/
│               ├── apex-cv/        # Directorio REAL — Dark, cian, sidebar fijo
│               ├── persona-cv  -> ../../../../frontend/persona-cv-theme  # symlink relativo
│               ├── lumina      -> ../../../../frontend/lumina-cv-theme   # symlink relativo
│               └── cvolio      -> ../../../../frontend/cvfolio-theme     # symlink relativo
├── frontend/
│   ├── persona-cv-theme/           # ACTIVO — Personage-style, rojo, Raleway
│   │   ├── style.css
│   │   ├── functions.php
│   │   ├── header.php              # Nav + botón "Descargar CV PDF"
│   │   ├── footer.php
│   │   ├── front-page.php          # Plantilla principal del CV
│   │   ├── index.php
│   │   ├── assets/css/theme.css
│   │   ├── assets/js/theme.js
│   │   ├── assets/images/          # patricia-flores-profile.jpeg, qr.png
│   │   ├── assets/docs/            # CV_Patricia_Flores_Sanchez_ES_FINAL.docx
│   │   └── inc/
│   │       ├── cv-data.php         # Fallback hardcodeado (función persona_cv_data())
│   │       ├── customizer.php
│   │       └── visits.php
│   ├── lumina-cv-theme/            # Claro, teal+ámbar, sidebar izquierdo
│   └── cvfolio-theme/              # Oscuro, glassmorphism original
├── otros/                          # Archivos de referencia (foto, QR, docx original)
├── docs/
│   └── instalacion-wordpress.md
├── .env                            # Local, no versionado
├── .env.example
├── VERSION
├── CHANGELOG.md
├── VERSIONADO.md
├── PRODUCCION.md
├── DEPLOY_WEB.md
├── PROMPT_IA.md                    # ESTE ARCHIVO
└── README.md

Temas: apex-cv vive en wp-content/themes/apex-cv/ como directorio real (no symlink). Los otros 3 temas usan symlinks con ruta relativa (../../../../frontend/...) para funcionar tanto en desarrollo como en producción sin cambios.


3. Base de datos

Desarrollo

Host:     192.168.222.20
DB:       paty.wordpress
Usuario:  paty.wordpress
Password: paty.wordpress
Prefijo:  cv_          → tabla principal: cv_options
mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0

Producción

Host:     192.168.222.32
DB:       paty.wordpress
Usuario:  paty.wordpress
Password: (ver LOCAL_DEPLOY_CONTEXT.md)
Prefijo:  cv_

La BD de producción es una copia completa de la de desarrollo. Misma estructura, mismos datos del CV. Credenciales distintas.


4. Arrancar / reiniciar el servidor

# Ver si está corriendo
lsof -i tcp:8081

# Matar proceso existente
kill $(lsof -ti tcp:8081)

# Arrancar (lee variables de .env automáticamente)
bash backend/start-wordpress.sh &

El servidor escucha en 0.0.0.0:8081 (todas las interfaces) gracias a WP_HOST=0.0.0.0 en .env.


5. Plugins activos y su función

Plugin Propósito Gratuito
cmb2/init.php Campos personalizados en el admin (repeaters, options pages) ✓ GPL
pflores-cv-fields/pflores-cv-fields.php Páginas de opciones + función pflores_get_cv_data() + PDF ✓ propio

Activar / verificar plugins en la BD:

mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 \
  -e "SELECT option_value FROM cv_options WHERE option_name='active_plugins';"

El valor correcto debe ser:

a:2:{i:0;s:13:"cmb2/init.php";i:1;s:39:"pflores-cv-fields/pflores-cv-fields.php";}

Si no, corregirlo:

mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 \
  -e "UPDATE cv_options SET option_value='a:2:{i:0;s:13:\"cmb2/init.php\";i:1;s:39:\"pflores-cv-fields/pflores-cv-fields.php\";}' WHERE option_name='active_plugins';"

⚠ La serialización PHP es estricta: s:N: donde N es la longitud exacta en bytes. Siempre genera el valor con php -r "echo serialize(['cmb2/init.php','pflores-cv-fields/pflores-cv-fields.php']);".


6. Cambiar el tema activo

mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 \
  -e "UPDATE cv_options SET option_value='SLUG_TEMA' WHERE option_name IN('template','stylesheet');"
Slug Tema
persona-cv Activo por defecto — Personage-style, secciones alternas, rojo
lumina Claro, teal+ámbar, sidebar izquierdo, foto circular
apex-cv Oscuro, cian eléctrico, sidebar fijo
cvolio Oscuro glassmorphism, paleta café/tostado

7. Arquitectura de datos — CMB2

Todos los datos del CV se almacenan en wp_options (prefijo cv_) como arrays PHP serializados. Se acceden con get_option('clave').

Clave wp_options Contenido
pflores_perfil Nombre, cargo, intro, resumen, contacto, foto
pflores_experiencia Array de cargos con bullets
pflores_formacion Títulos académicos + certificaciones
pflores_competencias Grupos de habilidades con porcentajes
pflores_extras Métricas, highlights, impacto, voluntariado

Función central de lectura:

pflores_get_cv_data(): array
// Retorna la misma estructura que todos los temas esperan.
// Si CMB2 no está disponible, devuelve pflores_cv_fallback_data().

Estructura del array retornado:

[
  'hero'           => ['name','role','role2','intro','location','email',
                       'phone','linkedin','availability','focus','languages',
                       'born','id','web','eyebrow'],
  'photo'          => 'URL de la imagen de perfil (o vacío)',
  'summary'        => ['title','body'],
  'metrics'        => [['value','suffix','label'], ...],   // 5 contadores
  'highlights'     => [['value','suffix','label'], ...],   // 4 logros grandes
  'impact'         => [['title','body'], ...],             // 3 áreas
  'experience'     => [['period','title','company','location','lead','bullets'=>[...]], ...],
  'education'      => [['year','title','institution','detail'], ...],
  'certifications' => [['year','title','issuer'], ...],
  'competencies'   => ['Grupo' => [['name','pct'], ...], ...],
  'volunteer'      => ['title','year','body'],
  'additional'     => ['Idiomas'=>'...','Disponibilidad'=>'...','C.I.'=>'...', ...],
]

8. Editar datos del CV desde el admin

URL del admin: https://pflores.linkedcorp.net/wp-admin

  • Usuario: pflores
  • Contraseña: en .env como WP_ADMIN_PASSWORD

En el menú lateral aparece "CV Patricia Flores" con sub-páginas:

  • Perfil → datos personales, foto, disponibilidad
  • Experiencia → cargos (repeater, arrastrable)
  • Formación → títulos + certificaciones
  • Competencias → grupos de habilidades con porcentajes
  • Métricas / Impacto → contadores, logros, áreas de impacto, voluntariado

9. Agregar un nuevo campo al CV

9a. Campo simple (texto, email, URL)

Paso 1 — Registrar el campo en pflores-cv-fields.php dentro de la función pflores_register_pages():

// Ejemplo: añadir "Twitter/X" al perfil
$perfil->add_field([
    'id'   => 'pf_twitter',
    'name' => 'Twitter / X URL',
    'type' => 'text_url',
]);

Paso 2 — Añadir la lectura en pflores_get_cv_data():

'hero' => [
    // ... campos existentes ...
    'twitter' => $perfil['pf_twitter'] ?? '',
],

Paso 3 — Usarlo en el front-page.php de cada tema:

$twitter = $hero['twitter'] ?? '';
if ($twitter) : ?>
    <a href="<?php echo esc_url($twitter); ?>">Twitter</a>
<?php endif; ?>

Paso 4 — Si el campo debe aparecer en el PDF, añadirlo en pdf-generator.php dentro de pflores_build_cv_html().


9b. Nuevo grupo repetible (como experiencia o certificaciones)

Paso 1 — Registrar un nuevo CMB2 box con group en pflores_register_pages():

$premios = new_cmb2_box([
    'id'           => 'pflores_premios',
    'title'        => 'Premios y reconocimientos',
    'object_types' => ['options-page'],
    'option_key'   => 'pflores_premios',   // nombre de la opción en BD
    'parent_slug'  => 'pflores_perfil',    // sub-menú bajo el menú principal
    'menu_title'   => 'Premios',
    'save_button'  => 'Guardar cambios',
    'capability'   => 'manage_options',
]);

$gr = $premios->add_field([
    'id'      => 'pf_awards',
    'type'    => 'group',
    'options' => ['group_title' => 'Premio {#}', 'add_button' => 'Añadir premio',
                  'remove_button' => 'Eliminar', 'sortable' => true, 'closed' => true],
]);
$premios->add_group_field($gr, ['id' => 'year',  'name' => 'Año',         'type' => 'text']);
$premios->add_group_field($gr, ['id' => 'title', 'name' => 'Título',      'type' => 'text']);
$premios->add_group_field($gr, ['id' => 'org',   'name' => 'Organización','type' => 'text']);

Paso 2 — Leer los datos en pflores_get_cv_data():

$premiosOpts = get_option('pflores_premios', []);
$awards = [];
foreach ((array) ($premiosOpts['pf_awards'] ?? []) as $item) {
    $awards[] = ['year' => $item['year'] ?? '', 'title' => $item['title'] ?? '', 'org' => $item['org'] ?? ''];
}
// Agregar 'awards' => $awards al array retornado

Paso 3 — Renderizarlo en front-page.php de cada tema.

Paso 4 — Poblar datos iniciales añadiendo al populate-data.php:

update_option('pflores_premios', [
    'pf_awards' => [
        ['year' => '2025', 'title' => 'Premio Excelencia', 'org' => 'Fundación FACES'],
    ],
]);

10. Modificar datos existentes

Opción A — Desde el admin de WordPress (recomendado):

  • Ir a wp-admin → CV Patricia Flores → sub-página correspondiente → editar → Guardar.

Opción B — Directo en BD (para migraciones o correcciones masivas):

mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 -e "
UPDATE cv_options SET option_value = 'Nuevo valor' WHERE option_name = 'pflores_perfil';
"

Para campos dentro de arrays (repeaters), es más fácil editar desde el admin o re-ejecutar populate-data.php.

Re-ejecutar el poblador de datos (si se necesita resetear todo):

https://pflores.linkedcorp.net/wp-content/plugins/pflores-cv-fields/populate-data.php?key=pflores2024

⚠ Esto sobreescribe TODOS los datos con los valores del archivo populate-data.php.


11. Crear un nuevo tema

Convenciones obligatorias

  1. Directorio: Crear directamente en backend/wordpress/wp-content/themes/NOMBRE-cv/ con subdirectorios assets/{css,js,images,docs} e inc/.

    ⚠ No usar symlink — el tema apex-cv demostró que los archivos en frontend/ desaparecen cuando WordPress activa/desactiva temas. El directorio real dentro de wp-content/themes/ es la única forma estable.

  2. Text Domain: usar nombre (minúsculas, sin guiones).
  3. Prefijo de funciones: nombre_ (ej: nova_setup(), nova_render_nav()).
  4. Prefijo CSS: nv- (2-3 letras, sin conflictos con pn-, lm-, ax-, cv-).
  5. Copiar assets: copiar imágenes y docs desde cualquier tema existente:
    cp frontend/persona-cv-theme/assets/images/* backend/wordpress/wp-content/themes/NOMBRE-cv/assets/images/
    cp frontend/persona-cv-theme/assets/docs/*   backend/wordpress/wp-content/themes/NOMBRE-cv/assets/docs/
    

Archivos mínimos requeridos

NOMBRE-cv-theme/
├── style.css          # Cabecera del tema (Theme Name, Text Domain, Version...)
├── functions.php      # apex_setup, enqueue, nav helpers
├── header.php         # <html><head> + nav + botón PDF
├── footer.php         # </body></html>
├── front-page.php     # TODA la lógica y HTML del CV
├── index.php          # Fallback vacío o mínimo
└── inc/
    ├── cv-data.php    # Función NOMBRE_cv_data() con fallback hardcodeado
    ├── customizer.php # Opcional, puede estar vacío
    └── visits.php     # Contador de visitas (copiar de otro tema y renombrar funciones)

Estructura obligatoria de functions.php

<?php
declare(strict_types=1);

require_once get_template_directory() . '/inc/customizer.php';
require_once get_template_directory() . '/inc/cv-data.php';
require_once get_template_directory() . '/inc/visits.php';

function NOMBRE_setup(): void { /* add_theme_support, register_nav_menus */ }
add_action('after_setup_theme', 'NOMBRE_setup');

function NOMBRE_enqueue_assets(): void {
    // Cargar Google Font + CSS + JS
    wp_enqueue_style('NOMBRE-fonts', 'https://fonts.googleapis.com/...', [], null);
    wp_enqueue_style('NOMBRE-theme', get_template_directory_uri() . '/assets/css/theme.css', ['NOMBRE-fonts'], '1.0.0');
    wp_enqueue_script('NOMBRE-theme', get_template_directory_uri() . '/assets/js/theme.js', [], '1.0.0', true);
}
add_action('wp_enqueue_scripts', 'NOMBRE_enqueue_assets');

function NOMBRE_render_nav(): string { /* nav links con IDs de sección */ }

Estructura obligatoria de front-page.php

<?php
declare(strict_types=1);
get_header();

// ► SIEMPRE usar esta línea para obtener los datos:
$cv = function_exists('pflores_get_cv_data') ? pflores_get_cv_data() : NOMBRE_cv_data();

$hero           = $cv['hero'];
$metrics        = $cv['metrics'];
$highlights     = $cv['highlights'];
$summary        = $cv['summary'];
$impact         = $cv['impact'];
$experience     = $cv['experience'];
$education      = $cv['education'];
$certifications = $cv['certifications'];
$competencies   = $cv['competencies'];
$volunteer      = $cv['volunteer'];
$additional     = $cv['additional'];

// Foto de perfil: CMB2 primero, si no la imagen local del tema
$acfPhoto     = $cv['photo'] ?? '';
$profileImage = $acfPhoto ?: get_template_directory_uri() . '/assets/images/patricia-flores-profile.jpeg';

// QR y URL pública
$qrImage   = get_template_directory_uri() . '/assets/images/patricia-flores-qr.png';
$publicUrl = 'https://pflores.linkedcorp.net';
// ► NO usar $cvFile para botones de descarga — usar siempre home_url('/?cv_pdf=1')

// Variables de conveniencia del hero
$heroName     = $hero['name'];
$heroRole     = $hero['role'];
$heroEmail    = $hero['email'];
$heroPhone    = $hero['phone'];
$heroLocation = $hero['location'];
$linkedin     = $hero['linkedin'];

/* ... HTML del tema aquí ... */

get_footer();

Iteración sobre competencias (compatibilidad CMB2 + fallback)

Los temas deben manejar ambos formatos de competencias:

<?php foreach ($competencies as $group => $items) : ?>
    <h3><?php echo esc_html($group); ?></h3>
    <?php foreach ($items as $item) :
        // ► SIEMPRE usar esta normalización:
        $skillName = is_array($item) ? ($item['name'] ?? '') : (string) $item;
        $skillPct  = is_array($item) ? ($item['pct']  ?? 80) : 80;
    ?>
        <span><?php echo esc_html($skillName); ?></span>
        <?php // Si el tema muestra barras de progreso: ?>
        <div style="width:<?php echo esc_attr((string)$skillPct); ?>%"></div>
    <?php endforeach; ?>
<?php endforeach; ?>

Botón de descarga PDF (obligatorio en todos los temas)

<!-- En header.php o sidebar, reemplazar cualquier botón de "Imprimir" por: -->
<a class="btn-pdf" href="<?php echo esc_url(home_url('/?cv_pdf=1')); ?>" target="_blank">
    Descargar CV PDF
</a>

LOPDP — 3 requisitos obligatorios en todo tema nuevo

1. Navegación con URLs absolutas — en functions.php, la función que genera el menú debe usar home_url('/') como base cuando no se está en el front page:

function NOMBRE_render_nav(): string {
    // ...
    $base  = is_front_page() ? '' : home_url('/');
    $items = [
        $base . '#sobre-mi'    => 'Sobre mí',
        $base . '#experiencia' => 'Experiencia',
        // ... resto de anclas igual
    ];
    // ...
}

Sin esto, los links del menú quedan como #sobre-mi en la página de política de privacidad y no llevan de vuelta al CV.

2. index.php estilizado — reescribir con el estilo del tema, botón "Volver al CV" y tipografía coherente. Ver los existentes como referencia: frontend/persona-cv-theme/index.php.

3. Link de privacidad en el footer — en footer.php o en la sección del copyright de front-page.php:

<a href="<?php echo esc_url(home_url('/politica-de-privacidad/')); ?>"
   style="opacity:.5;text-decoration:underline;font-size:.75rem;color:inherit">
    Política de Privacidad · LOPDP
</a>

El banner LOPDP y la página de política son globales — están en el plugin pflores-cv-fields.php, no en cada tema. Solo el link del footer y el index.php deben adaptarse por tema.

inc/cv-data.php — función de fallback

Debe contener todos los datos reales de Patricia Flores, idénticos a los que están en la BD. Copiar la función de otro tema y renombrar a NOMBRE_cv_data().

⚠ Las competencias del fallback deben tener formato array: ['name' => 'Gestión operativa', 'pct' => 95]

Activar el nuevo tema

# El directorio ya existe en wp-content/themes/NOMBRE-cv/ — solo activar en la BD:
mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 \
  -e "UPDATE cv_options SET option_value='NOMBRE-cv' WHERE option_name IN('template','stylesheet');"

Versionar el nuevo tema

Al estar en wp-content/themes/ directamente, añadir al staging explícitamente:

git add backend/wordpress/wp-content/themes/NOMBRE-cv/
git commit -m "feat: nuevo tema NOMBRE-cv"

⚠ Evitar el slug apex (existe un tema comercial con ese nombre en WordPress.org). Usar siempre slugs compuestos: nova-cv, slate-cv, forma-cv, etc.


12. Crear / modificar menús de navegación

Los menús de navegación son listas de anclas (#seccion) dentro del mismo tema. No se usan menús de WordPress por defecto.

Modificar los ítems del menú en un tema

En functions.php de cada tema, hay una función NOMBRE_render_nav():

function persona_render_nav(): string {
    $items = [
        '#sobre-mi'        => 'Sobre mí',
        '#impacto'         => 'Impacto',
        '#experiencia'     => 'Experiencia',
        '#formacion'       => 'Formación',
        '#competencias'    => 'Competencias',
        '#certificaciones' => 'Certificaciones',
        '#contacto'        => 'Contacto',
    ];
    // Para añadir un ítem:  '#premios' => 'Premios',
    // Para eliminar: quitar la línea correspondiente
    // Para reordenar: cambiar el orden del array
    ...
}

El href debe coincidir con el id de la sección en front-page.php:

<section id="premios"> <!-- El ID debe ser igual al href sin el # -->

Usar menús de WordPress (avanzado)

Si se registra un menú en functions.php:

register_nav_menus(['primary' => __('Menú principal', 'persona')]);

Se puede asignar desde wp-admin → Apariencia → Menús y el tema lo detectará automáticamente si has_nav_menu('primary') retorna true.


13. Generador de PDF

Endpoint: https://pflores.linkedcorp.net/?cv_pdf=1

Cómo funciona (pdf-generator.php):

  1. Se activa con get_query_var('cv_pdf') === '1'
  2. Llama a pflores_get_cv_data() para obtener todos los datos actuales
  3. Genera HTML limpio con CSS embebido (formato A4, 2 columnas)
  4. Escribe el HTML en un archivo temporal
  5. Ejecuta Chromium headless: chromium --headless --no-sandbox --print-to-pdf=... file://...
  6. Lee el PDF resultante y lo devuelve con headers de descarga
  7. Limpia los archivos temporales

Modificar el layout del PDF:

Todo el diseño está en la función pflores_build_cv_html() dentro de pdf-generator.php. Modificar el CSS dentro de la variable $css o el HTML de cada sección.

Añadir una nueva sección al PDF (ej: Premios):

// En pflores_build_cv_html(), después de certificaciones:
$awardsHtml = '';
foreach ($awards as $a) {
    $awardsHtml .= '<div class="cert-item">...';
}

// Y en el HTML final:
'<p class="sec-title">Premios y reconocimientos</p>' . $awardsHtml .

14. Archivo .env

WP_ENV=local
WP_HOST=0.0.0.0
WP_PORT=8081
WP_URL=http://192.168.222.40:8081

DB_NAME=paty.wordpress
DB_USER=paty.wordpress
DB_PASSWORD=paty.wordpress
DB_HOST=192.168.222.20
DB_PREFIX=cv_

WP_ADMIN_USER=pflores
WP_ADMIN_PASSWORD=HXk4boxuI1QYum3jKL

15. Configuración wp-config.php clave

// Auto-updates DESACTIVADAS (protege archivos de temas y plugins locales)
define('AUTOMATIC_UPDATER_DISABLED', true);
define('WP_AUTO_UPDATE_CORE', false);

// URL dinámica — funciona con cualquier IP o dominio, y detrás de proxy SSL (NPM)
$_wpIsHttps = /* detecta X-Forwarded-Proto: https */;
define('WP_HOME',    $_wpProto . '://' . $_wpHost);
define('WP_SITEURL', $_wpProto . '://' . $_wpHost);

16. Convenciones de código

Elemento Convención
PHP declare(strict_types=1) en todos los archivos
Funciones WordPress Usar esc_html(), esc_url(), esc_attr() en toda salida HTML
Prefijos Funciones y opciones siempre con prefijo del plugin/tema para evitar conflictos
Comentarios Solo cuando el "por qué" no es obvio. Sin comentarios de "qué hace"
Serialización PHP en SQL Generar con php -r "echo serialize([...]);" — nunca manual
Auto-updates WP Siempre desactivadas — no tocar wp-config.php en ese bloque

17. Tareas comunes — referencia rápida

Verificar que todo funciona

curl -s http://127.0.0.1:8081/ | grep -c "Patricia Soledad"
# Debe retornar >= 1

Ver el tema activo

mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 \
  -e "SELECT option_value FROM cv_options WHERE option_name='template';" 2>/dev/null

Ver todos los datos del CV en BD

mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 \
  -e "SELECT option_name, LENGTH(option_value) as bytes FROM cv_options WHERE option_name LIKE 'pflores_%' ORDER BY option_name;" 2>/dev/null

Limpiar caché de WordPress (transients)

mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 \
  -e "DELETE FROM cv_options WHERE option_name LIKE '_site_transient_%' OR option_name LIKE '_transient_%';" 2>/dev/null

Verificar los 4 temas en un loop

for slug in persona-cv lumina apex-cv cvolio; do
  mysql -u "paty.wordpress" -p"paty.wordpress" -h 192.168.222.20 "paty.wordpress" --ssl=0 \
    -e "UPDATE cv_options SET option_value='$slug' WHERE option_name IN('template','stylesheet');" 2>/dev/null
  sleep 0.4
  count=$(curl -s http://127.0.0.1:8081/ | grep -c "Patricia Soledad" || true)
  echo "[$slug] Patricia aparece: $count veces"
done
# Restaurar:
mysql ... -e "UPDATE cv_options SET option_value='persona-cv' WHERE option_name IN('template','stylesheet');"

Probar la descarga del PDF

curl -sI http://127.0.0.1:8081/?cv_pdf=1 | grep -E "Content-Type|Content-Disposition"
# Debe mostrar: application/pdf y attachment; filename="CV_Patricia_Flores_Sanchez.pdf"

Comprobar PHP syntax de un tema

php -l /ruta/al/tema/front-page.php
php -l /ruta/al/tema/functions.php

18. Errores frecuentes y soluciones

Error Causa probable Solución
Página en blanco o "error crítico" PHP fatal en el tema Ver debug.log, buscar la línea del error
CSS sin cargar / página sin estilos Mixed content (HTTP vs HTTPS) Verificar wp-config.php — bloque de proxy SSL
CMB2 / plugin no carga Serialización incorrecta en active_plugins Regenerar con php -r "echo serialize([...]);"
pflores_get_cv_data() retorna vacío Datos no guardados en BD Re-ejecutar populate-data.php?key=pflores2024 + limpiar caché
Apex-cv vacío o sin archivos Tema apex-cv depende de directorio real, no symlink Los archivos van en wp-content/themes/apex-cv/, no en frontend/
CSS/JS con 404 tras clonar en servidor nuevo Symlinks corruptos o rotos Los symlinks en git usan ruta relativa (../../../../frontend/...) — si están rotos, ver PRODUCCION.md → Problemas conocidos
502 persistente en favicon u otros estáticos tras haberlo corregido NPM Cache Assets cachéo la respuesta 502/404 anterior En NPM: desactivar Cache Assets → guardar → reactivar → guardar. Para CV puede quedar desactivado.
PDF vacío o error 500 Chromium no encontrado o falla which chromium chromium-browser google-chrome
update_option no guarda Caché de WordPress Limpiar transients (ver sección 17)

19. Acerca de los temas — resumen visual

Tema Estilo Layout Fuente Acento
persona-cv Personage/Pixflow, secciones alternas Full-width, nav sticky top Raleway + Open Sans Rojo #c0392b
lumina Claro, moderno, alegre Sidebar IZQUIERDO fijo Plus Jakarta Sans Teal #0d9488 + Ámbar
apex-cv Oscuro, serio, editorial Sidebar IZQUIERDO fijo Space Grotesk Cian #22d3ee + Dorado
cvolio Oscuro glassmorphism Hero dividido + sidebar DERECHO Manrope + Cormorant Café/tostado

20. Notas importantes para la IA asistente

  1. No usar ACF (Advanced Custom Fields). Tiene Repeater solo en versión de pago. Usar CMB2.
  2. No instalar plugins de pago. Todo debe ser 100 % gratuito.
  3. No tocar el bloque de auto-updates en wp-config.php.
  4. Verificar serialización PHP antes de cualquier UPDATE en active_plugins o similar.
  5. Limpiar caché después de modificar datos en BD directamente.
  6. La foto de perfil es assets/images/patricia-flores-profile.jpeg en cada tema — NO usar el placeholder SVG como defecto.
  7. El CV en docx (CV_Patricia_Flores_Sanchez_ES_FINAL.docx) es solo para descarga — el contenido de la web se gestiona por CMB2.
  8. El PDF se genera dinámicamente con Chromium desde /?cv_pdf=1 — no hay PDF estático.
  9. Los 4 temas leen de CMB2 mediante pflores_get_cv_data(). Si esa función no existe (plugin inactivo), cada tema usa su propio fallback hardcodeado.
  10. Siempre verificar con curl -s http://127.0.0.1:8081/ | grep -c "Patricia Soledad" que el sitio responde correctamente después de cualquier cambio.

21. Git y Gitea — versionar y subir

Repositorio

  • Remoto: https://gitea.linkedcorp.net/mgonzalez/pflores.linkedcorp.net.git
  • Rama: main
  • Credenciales: archivo local .token_gitea (nunca versionado)

Flujo estándar de commit + push

# 1. Revisar qué cambió
git status
git diff --stat

# 2. Agregar archivos — SELECTIVO (nunca git add -A sin revisar)
git add frontend/         # temas propios
git add backend/wordpress/wp-content/plugins/pflores-cv-fields/
git add backend/wordpress/wp-content/themes/apex-cv/
git add backend/wordpress/wp-config.php
git add CHANGELOG.md VERSION README.md PROMPT_IA.md

# 3. Confirmar que .env y .token_gitea NO están en staging
git status | grep -E "\.env|\.token"   # debe retornar vacío

# 4. Commit
git commit -m "tipo: descripción corta"
# Prefijos: feat: / fix: / docs: / chore: / style:

# 5. Push
git push origin main

Credenciales para push

Al hacer git push por primera vez pedirá usuario y contraseña:

  • Usuario: mgonzalez
  • Contraseña: Personal Access Token (NO la contraseña web de Gitea)

Crear token: https://gitea.linkedcorp.net/user/settings/applications

Para no escribir credenciales cada vez:

git config credential.helper store
# La primera vez las guarda; las siguientes son automáticas

Guardar también en .token_gitea (local, nunca versionar):

# Token gitea
usuario: mgonzalez
token: [token de gitea.linkedcorp.net]

Qué SE versiona vs qué NO

SE versiona:

  • frontend/ (todos los temas propios)
  • backend/wordpress/wp-content/plugins/pflores-cv-fields/
  • backend/wordpress/wp-content/themes/apex-cv/
  • backend/wordpress/wp-config.php
  • backend/start-wordpress.sh, docs/, *.md, VERSION
  • CV_Patricia_Flores_Sanchez_ES_FINAL.docx, imágenes del CV

NO se versiona:

  • .env, .token_gitea, LOCAL_DEPLOY_CONTEXT.md
  • WordPress core (wp-admin/, wp-includes/)
  • Plugins de terceros (cmb2/, wp-mail-smtp/)
  • Temas por defecto de WP (twenty*/)
  • wp-content/debug.log, uploads/, languages/

Versionar antes de hacer push

Ver VERSIONADO.md para decidir si subir X, Y o Z:

# Actualizar VERSION
echo "1.0.1" > VERSION

# Anotar cambio en CHANGELOG.md
# Luego commit y push normal

22. Servidor de producción

Datos del servidor

  • Host: 192.168.222.3
  • SSH: deploy@192.168.222.3 -p 2203
  • deploy tiene sudo NOPASSWD
  • Ruta app: /opt/apps/pflores.linkedcorp.net
  • Servicio systemd: pflores-web.service
  • Puerto: 0.0.0.0:8081
  • NPM proxy: pflores.linkedcorp.net → 192.168.222.3:8081

⚠ El servicio debe escuchar en 0.0.0.0:8081, no en 127.0.0.1. NPM llega desde fuera del loopback — si queda en 127.0.0.1 el resultado es 502 Bad Gateway.

Credenciales SSH: ver LOCAL_DEPLOY_CONTEXT.md

Estructura en producción

/opt/apps/pflores.linkedcorp.net/
├── backend/wordpress/          ← WordPress + temas + plugins
├── frontend/                   ← temas (lumina, persona-cv, cvolio via symlink)
├── scripts/deploy_prod_web.sh  ← script de deploy
└── .env                        ← config producción (local, nunca versionar)

Deploy recurrente (flujo completo)

# 1. En desarrollo: commit + push a Gitea (ver sección 21)

# 2. En el servidor de producción:
ssh deploy@192.168.222.3 -p 2203
cd /opt/apps/pflores.linkedcorp.net
bash scripts/deploy_prod_web.sh

El script hace: git reset --hard + verificar plugins + systemctl restart + verificar HTTP 200.

Comandos útiles en producción

sudo systemctl status pflores-web
sudo systemctl restart pflores-web
sudo journalctl -u pflores-web -n 50 --no-pager
curl -s -o /dev/null -w "HTTP %{http_code}" http://127.0.0.1:8081/
curl -sI http://127.0.0.1:8081/?cv_pdf=1 | grep Content-Type

Plugins de terceros (no en git)

Si desaparecen tras un git reset --hard, el script de deploy los reinstala automáticamente. También se pueden instalar manualmente:

cd /opt/apps/pflores.linkedcorp.net/backend/wordpress/wp-content/plugins
# CMB2
wget -q https://downloads.wordpress.org/plugin/cmb2.latest-stable.zip -O cmb2.zip && python3 -c "import zipfile; zipfile.ZipFile('cmb2.zip').extractall('.')" && rm cmb2.zip
# WP Mail SMTP
wget -q https://downloads.wordpress.org/plugin/wp-mail-smtp.latest-stable.zip -O wpmail.zip && python3 -c "import zipfile; zipfile.ZipFile('wpmail.zip').extractall('.')" && rm wpmail.zip

WordPress core (no en git)

Si hay que reinstalar el core de WordPress en el servidor:

cd /opt/apps/pflores.linkedcorp.net/backend/wordpress
wget -q https://wordpress.org/wordpress-7.0.zip -O /tmp/wp.zip
python3 -c "
import zipfile,shutil,os
z=zipfile.ZipFile('/tmp/wp.zip')
z.extractall('/tmp/wp_extract')
src='/tmp/wp_extract/wordpress'
for d in ['wp-admin','wp-includes']:
    if not os.path.exists(d): shutil.copytree(os.path.join(src,d),d)
for f in os.listdir(src):
    if os.path.isfile(os.path.join(src,f)): shutil.copy2(os.path.join(src,f),f)
"
rm -rf /tmp/wp.zip /tmp/wp_extract

23. LOPDP Ecuador — Implementación completa

¿Qué es y por qué existe?

La Ley Orgánica de Protección de Datos Personales del Ecuador (LOPDP) exige informar a los visitantes sobre el tratamiento de sus datos. Este sitio usa estadísticas anónimas de visitas (transients de WordPress, eliminados cada hora) con base legal en el Art. 26 LOPDP — interés legítimo.

Componentes implementados

Todo el código LOPDP vive en backend/wordpress/wp-content/plugins/pflores-cv-fields/pflores-cv-fields.php (sección 3).

Componente Función Dónde
router.php Permite URLs bonitas (/politica-de-privacidad/) en el servidor PHP integrado backend/router.php
Página de política Creada automáticamente en admin_init con contenido completo BD: cv_posts, post_name = politica-de-privacidad
Banner LOPDP wp_footer — aparece en todos los temas, se acepta con localStorage pflores-cv-fields.php función pflores_lopd_banner()
Widget dashboard Estadísticas de visitas por tema + total pflores-cv-fields.php función pflores_visitas_widget()
Link en footer Cada tema tiene un enlace discreto "Política de Privacidad · LOPDP" footer.php o front-page.php de cada tema
Nav con home_url Menú funciona desde páginas internas functions.php de cada tema
index.php estilizado Páginas internas con estilo del tema + "Volver al CV" index.php de cada tema

Infraestructura de URLs (router.php)

Sin el router, PHP built-in server devuelve 404 para /politica-de-privacidad/.

El archivo backend/router.php:

<?php
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$file = __DIR__ . '/wordpress' . $path;
if ($path !== '/' && file_exists($file) && !is_dir($file)) { return false; }
require_once __DIR__ . '/wordpress/index.php';

El start-wordpress.sh lo usa así:

exec php -S "${WP_HOST}:${WP_PORT}" -t "$WP_DIR" "$ROOT_DIR/backend/router.php"

Los permalinks en BD deben estar en /%postname%/ (ya configurado en dev y prod).

Bugs conocidos del banner — NO repetir

Bug Descripción Fix correcto
display:flex!important en CSS Impide que el JS pueda ocultar el banner (style.display='none' no funciona) Usar display:flex sin !important
banner.removeAttribute('hidden') No muestra el banner correctamente en todos los navegadores Usar banner.style.display = 'flex'

Actualizar la página de política de privacidad

Desde el admin de WordPress: Páginas → Política de Privacidad → Editar.

O directamente en BD:

mysql -u "paty.wordpress" -p"PASSWORD" -h HOST "paty.wordpress" --ssl=0 -e "
UPDATE cv_posts SET post_content='NUEVO_CONTENIDO'
WHERE post_name='politica-de-privacidad' AND post_type='page';"

Si la página de política no existe en producción

# Verificar
mysql -u "paty.wordpress" -p"gS0mFSQrdnhSDN2b" -h 192.168.222.32 "paty.wordpress" --ssl=0 \
  -e "SELECT ID, post_name, post_status FROM cv_posts WHERE post_name='politica-de-privacidad';" 2>/dev/null

# Si no existe, forzar creación visitando el admin:
curl -s http://127.0.0.1:8081/wp-admin/admin.php > /dev/null
# O insertar directamente en BD (ver PRODUCCION.md sección "Problemas conocidos")

Verificación rápida

# Banner presente en el HTML
curl -s http://127.0.0.1:8081/ | grep -c "pflores-lopd-banner"   # debe ser 5

# Página de política accesible
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8081/politica-de-privacidad/  # debe ser 200

# Contenido correcto
curl -s http://127.0.0.1:8081/politica-de-privacidad/ | grep -c "Art. 26\|LOPDP"  # debe ser >= 1

# Sin cédula en el HTML público
curl -s http://127.0.0.1:8081/ | grep "1103591648"  # debe retornar vacío