feat(#8): buscador avanzado nativo (FULLTEXT + filtros)
Replica el «Buscador avanzado» del Joomla viejo sin servicios externos: - fea-search-fulltext.php: motor MySQL FULLTEXT (MATCH AGAINST, Boolean Mode, orden por relevancia); comprueba que existe el índice fea_ft y degrada al buscador nativo si no, sin romper. - fea-search-advanced.php: formulario con filtros por autor, categoría (lista curada de secciones), cita bíblica (meta _cita_evangelio por prefijo) y rango de fechas; pre_get_posts, chips de filtros, byline en tarjetas, i18n es/en/fr/it/pt. Excluye pseudo-autores «Testamento». - create_buscar_page.php: crea la página /buscar + traducciones (idempotente). - tools/e2e: suite Playwright de verificación. Desplegado y verificado en prod (índice fea_ft + página /buscar + traducciones). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* create_buscar_page.php (#8) — Crea/repara la página dedicada /buscar.
|
||||||
|
* La página sirve como destino del enlace «Búsqueda avanzada» y muestra el formulario
|
||||||
|
* avanzado aunque no haya consulta activa.
|
||||||
|
*
|
||||||
|
* Idempotente de verdad:
|
||||||
|
* - Si /buscar NO existe → la crea (es) + traducciones (en/fr/it/pt) y las vincula.
|
||||||
|
* - Si /buscar YA existe → la deja, pero REPARA traducciones Polylang faltantes o
|
||||||
|
* no publicadas (crea las que falten, publica las que estén en borrador, revincula).
|
||||||
|
*
|
||||||
|
* Uso:
|
||||||
|
* wp eval-file scripts/create_buscar_page.php # DRY-RUN
|
||||||
|
* APPLY=1 wp eval-file scripts/create_buscar_page.php # aplica
|
||||||
|
*/
|
||||||
|
$apply = getenv('APPLY') === '1';
|
||||||
|
|
||||||
|
$titles = [
|
||||||
|
'es' => 'Búsqueda avanzada',
|
||||||
|
'en' => 'Advanced Search',
|
||||||
|
'fr' => 'Recherche avancée',
|
||||||
|
'it' => 'Ricerca avanzata',
|
||||||
|
'pt' => 'Pesquisa avançada',
|
||||||
|
];
|
||||||
|
// Contenido mínimo (sin bloques Gutenberg). El formulario se inyecta vía the_content.
|
||||||
|
$content = '<p>Utiliza el formulario de búsqueda avanzada para encontrar reflexiones, artículos y más.</p>';
|
||||||
|
|
||||||
|
$has_pll = function_exists('pll_set_post_language') && function_exists('pll_save_post_translations');
|
||||||
|
$pll_langs = (function_exists('pll_languages_list'))
|
||||||
|
? pll_languages_list(['fields' => 'slug'])
|
||||||
|
: ['es'];
|
||||||
|
|
||||||
|
/** Crea (o devuelve si existe) una página por slug, con idioma Polylang. */
|
||||||
|
function fea_buscar_ensure_page(string $slug, string $title, string $content, string $lang, bool $apply, bool $has_pll) {
|
||||||
|
$existing = get_page_by_path($slug, OBJECT, 'page');
|
||||||
|
if ($existing) {
|
||||||
|
// Asegurar que está publicada
|
||||||
|
if ($apply && $existing->post_status !== 'publish') {
|
||||||
|
wp_update_post(['ID' => $existing->ID, 'post_status' => 'publish']);
|
||||||
|
echo " · ({$lang}) página '{$slug}' existía en estado {$existing->post_status} → publicada (ID {$existing->ID})\n";
|
||||||
|
} else {
|
||||||
|
echo " · ({$lang}) página '{$slug}' ya existe y publicada (ID {$existing->ID})\n";
|
||||||
|
}
|
||||||
|
// Asegurar idioma asignado
|
||||||
|
if ($apply && $has_pll && function_exists('pll_get_post_language')) {
|
||||||
|
$cur = pll_get_post_language($existing->ID);
|
||||||
|
if ($cur !== $lang) { pll_set_post_language($existing->ID, $lang); echo " idioma → {$lang}\n"; }
|
||||||
|
}
|
||||||
|
return (int) $existing->ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ($apply ? " · ({$lang}) creando" : " · ({$lang}) [dry] crearía") . " página '{$slug}'\n";
|
||||||
|
if (!$apply) return 0;
|
||||||
|
|
||||||
|
$id = wp_insert_post([
|
||||||
|
'post_type' => 'page',
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'post_name' => $slug,
|
||||||
|
'post_title' => $title,
|
||||||
|
'post_content' => $content,
|
||||||
|
'post_author' => 1,
|
||||||
|
], true);
|
||||||
|
if (is_wp_error($id)) { echo " ERROR: " . $id->get_error_message() . "\n"; return 0; }
|
||||||
|
if ($has_pll) pll_set_post_language($id, $lang);
|
||||||
|
echo " creada (ID {$id})\n";
|
||||||
|
return (int) $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ($apply ? "APLICANDO" : "DRY-RUN") . " — crear/reparar /buscar\n";
|
||||||
|
|
||||||
|
// 1) Página ES (slug 'buscar')
|
||||||
|
$translations = [];
|
||||||
|
$es_id = fea_buscar_ensure_page('buscar', $titles['es'], $content, 'es', $apply, $has_pll);
|
||||||
|
if ($es_id) $translations['es'] = $es_id;
|
||||||
|
|
||||||
|
// 2) Traducciones (en/fr/it/pt) sólo si Polylang activo y el idioma existe
|
||||||
|
if ($has_pll) {
|
||||||
|
foreach (['en', 'fr', 'it', 'pt'] as $lang) {
|
||||||
|
if (!in_array($lang, $pll_langs, true)) { echo " · ({$lang}) idioma no configurado en Polylang, omitido\n"; continue; }
|
||||||
|
|
||||||
|
// Si ya hay traducción vinculada a la ES, reusarla
|
||||||
|
$linked = ($es_id && function_exists('pll_get_post')) ? (int) pll_get_post($es_id, $lang) : 0;
|
||||||
|
if ($linked) {
|
||||||
|
$p = get_post($linked);
|
||||||
|
if ($p && $p->post_status !== 'publish' && $apply) {
|
||||||
|
wp_update_post(['ID' => $linked, 'post_status' => 'publish']);
|
||||||
|
echo " · ({$lang}) traducción vinculada (ID {$linked}) estaba {$p->post_status} → publicada\n";
|
||||||
|
} else {
|
||||||
|
echo " · ({$lang}) traducción ya vinculada (ID {$linked})\n";
|
||||||
|
}
|
||||||
|
$translations[$lang] = $linked;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear/reparar por slug
|
||||||
|
$tl_id = fea_buscar_ensure_page('buscar-' . $lang, $titles[$lang], $content, $lang, $apply, $has_pll);
|
||||||
|
if ($tl_id) $translations[$lang] = $tl_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Revincular todas las traducciones
|
||||||
|
if ($apply && count($translations) > 1) {
|
||||||
|
pll_save_post_translations($translations);
|
||||||
|
echo " · traducciones revinculadas: " . implode(', ', array_map(
|
||||||
|
fn($l, $id) => "{$l}={$id}", array_keys($translations), $translations)) . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (function_exists('wp_cache_flush')) wp_cache_flush();
|
||||||
|
echo ($apply ? "APLICADO" : "DRY-RUN") . "\n";
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
const { chromium } = require('playwright');
|
||||||
|
(async () => {
|
||||||
|
const b = await chromium.launch();
|
||||||
|
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||||
|
await p.goto('http://localhost:8081/buscar/', { waitUntil: 'networkidle' });
|
||||||
|
// abrir el select de categoría y listar sus opciones
|
||||||
|
const opts = await p.$$eval('#fea-cat option', els => els.map(e => e.textContent.trim()));
|
||||||
|
console.log('opciones categoría ('+opts.length+'):', JSON.stringify(opts));
|
||||||
|
await p.screenshot({ path: '/tmp/claude-1000/-mnt-c-Users-Chia/2802dab4-7a28-4ab7-ad16-457debcccc00/scratchpad/cat_curada.png', clip:{x:0,y:120,width:1280,height:520} });
|
||||||
|
await b.close();
|
||||||
|
})();
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
const { chromium } = require('playwright');
|
||||||
|
(async () => {
|
||||||
|
const b = await chromium.launch();
|
||||||
|
// Desktop: cabecera con barra
|
||||||
|
let p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||||
|
await p.goto('http://localhost:8081/', { waitUntil: 'networkidle' });
|
||||||
|
const bar = await p.$('.fea-search-bar');
|
||||||
|
console.log('barra presente desktop:', !!bar);
|
||||||
|
await p.screenshot({ path: '/tmp/claude-1000/search_desktop.png', clip: {x:0,y:0,width:1280,height:320} });
|
||||||
|
// Móvil
|
||||||
|
let m = await b.newPage({ viewport: { width: 390, height: 800 } });
|
||||||
|
await m.goto('http://localhost:8081/', { waitUntil: 'networkidle' });
|
||||||
|
await m.screenshot({ path: '/tmp/claude-1000/search_mobile.png', clip: {x:0,y:0,width:390,height:360} });
|
||||||
|
// Resultados
|
||||||
|
let r = await b.newPage({ viewport: { width: 1280, height: 1000 } });
|
||||||
|
await r.goto('http://localhost:8081/?s=oraci%C3%B3n', { waitUntil: 'networkidle' });
|
||||||
|
await r.screenshot({ path: '/tmp/claude-1000/search_results.png', clip: {x:0,y:0,width:1280,height:700} });
|
||||||
|
await b.close();
|
||||||
|
})();
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
const { chromium } = require('playwright');
|
||||||
|
(async () => {
|
||||||
|
const b = await chromium.launch();
|
||||||
|
// Resultados (desktop)
|
||||||
|
let r = await b.newPage({ viewport: { width: 1280, height: 1200 } });
|
||||||
|
await r.goto('http://localhost:8081/?s=oraci%C3%B3n', { waitUntil: 'networkidle' });
|
||||||
|
await r.screenshot({ path: '/tmp/claude-1000/res_desktop.png', clip:{x:0,y:180,width:1280,height:880} });
|
||||||
|
// Desktop 800 ancho: barra NO debe verse (solo el Buscador del menú)
|
||||||
|
let d = await b.newPage({ viewport: { width: 900, height: 700 } });
|
||||||
|
await d.goto('http://localhost:8081/', { waitUntil: 'networkidle' });
|
||||||
|
const visD = await d.$eval('.fea-search-bar', el => getComputedStyle(el).display).catch(()=>'noel');
|
||||||
|
console.log('barra display @900px:', visD);
|
||||||
|
// Móvil: barra SÍ
|
||||||
|
let m = await b.newPage({ viewport: { width: 390, height: 700 } });
|
||||||
|
await m.goto('http://localhost:8081/', { waitUntil: 'networkidle' });
|
||||||
|
const visM = await m.$eval('.fea-search-bar', el => getComputedStyle(el).display).catch(()=>'noel');
|
||||||
|
console.log('barra display @390px:', visM);
|
||||||
|
await b.close();
|
||||||
|
})();
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
/**
|
||||||
|
* shot_search_advanced.cjs — Verificación del buscador avanzado (#8)
|
||||||
|
* Comprueba: FULLTEXT, filtro autor, filtro tema, filtro cita bíblica,
|
||||||
|
* filtro fecha, combinado palabra+autor, multiidioma, formulario desktop/móvil.
|
||||||
|
*
|
||||||
|
* Uso: NODE_PATH=/home/rafa/joomla-migration/tools/e2e/node_modules node tools/e2e/shot_search_advanced.cjs
|
||||||
|
*/
|
||||||
|
const { chromium } = require('playwright');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const BASE = 'http://localhost:8081';
|
||||||
|
const OUT = '/tmp/claude-1000/-mnt-c-Users-Chia/2802dab4-7a28-4ab7-ad16-457debcccc00/scratchpad';
|
||||||
|
|
||||||
|
// Ensure output dir
|
||||||
|
fs.mkdirSync(OUT, { recursive: true });
|
||||||
|
|
||||||
|
function shotPath(name) {
|
||||||
|
return path.join(OUT, name + '.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function check(page, label, fn) {
|
||||||
|
try {
|
||||||
|
const result = await fn(page);
|
||||||
|
console.log(`✓ ${label}:`, result);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`✗ ${label}: ERROR`, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const b = await chromium.launch({ headless: true });
|
||||||
|
|
||||||
|
// ── 1. FULLTEXT por relevancia ──────────────────────────────────────────
|
||||||
|
{
|
||||||
|
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||||
|
await p.goto(`${BASE}/?s=oraci%C3%B3n`, { waitUntil: 'networkidle' });
|
||||||
|
// Comprueba que hay resultados
|
||||||
|
const cards = await p.$$('.fea-archive-card');
|
||||||
|
console.log(`✓ FULLTEXT /?s=oración: ${cards.length} tarjetas`);
|
||||||
|
// Comprueba que el formulario avanzado está presente
|
||||||
|
const form = await p.$('.fea-adv-form');
|
||||||
|
console.log(` formulario avanzado en resultados: ${!!form}`);
|
||||||
|
const counter = await p.$('.fea-adv-count');
|
||||||
|
const countText = counter ? await counter.textContent() : 'no counter';
|
||||||
|
console.log(` contador: ${countText}`);
|
||||||
|
await p.screenshot({ path: shotPath('1_fulltext_oración'), fullPage: false });
|
||||||
|
await p.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. Filtro autor ─────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||||
|
// Fray Marcos = 382, Florentino Ulibarri = 395
|
||||||
|
await p.goto(`${BASE}/?s=&fea_author=382`, { waitUntil: 'networkidle' });
|
||||||
|
const cards = await p.$$('.fea-archive-card');
|
||||||
|
console.log(`✓ Filtro autor (fea_author=382 Fray Marcos): ${cards.length} tarjetas`);
|
||||||
|
// Verify selected value in form
|
||||||
|
const selVal = await p.$eval('#fea-author', el => el.value).catch(() => 'n/a');
|
||||||
|
console.log(` select author value: ${selVal}`);
|
||||||
|
await p.screenshot({ path: shotPath('2_filtro_autor'), fullPage: false });
|
||||||
|
await p.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 3. Filtro categoría + verificación del desplegable ───────────────────
|
||||||
|
{
|
||||||
|
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||||
|
await p.goto(`${BASE}/?fea_cat=1650`, { waitUntil: 'networkidle' });
|
||||||
|
const cards = await p.$$('.fea-archive-card');
|
||||||
|
console.log(`✓ Filtro categoría (fea_cat=1650 Artículos): ${cards.length} tarjetas`);
|
||||||
|
const selVal = await p.$eval('#fea-cat', el => el.value).catch(() => 'n/a');
|
||||||
|
console.log(` select cat value: ${selVal}`);
|
||||||
|
// Label debe decir "Categoría"
|
||||||
|
const catLabel = await p.$eval('label[for="fea-cat"]', el => el.textContent).catch(() => 'n/a');
|
||||||
|
console.log(` label categoría: "${catLabel}"`);
|
||||||
|
// Contar y listar opciones de categoría (idioma ES)
|
||||||
|
const catOpts = await p.$$eval('#fea-cat option', els => els.map(e => e.textContent));
|
||||||
|
console.log(` nº opciones categoría: ${catOpts.length}`);
|
||||||
|
console.log(` opciones: ${catOpts.join(' | ')}`);
|
||||||
|
// No debe contener categorías de carta ni "Testament"
|
||||||
|
const badCat = catOpts.filter(t => /Testament/i.test(t));
|
||||||
|
console.log(` opciones con 'Testament': ${badCat.length}`);
|
||||||
|
await p.screenshot({ path: shotPath('3_filtro_categoria'), fullPage: false });
|
||||||
|
await p.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 3b. Desplegable de AUTOR: sin "Testamento" ───────────────────────────
|
||||||
|
{
|
||||||
|
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||||
|
await p.goto(`${BASE}/?s=fe`, { waitUntil: 'networkidle' });
|
||||||
|
const autOpts = await p.$$eval('#fea-author option', els => els.map(e => e.textContent));
|
||||||
|
console.log(`✓ Desplegable autor: ${autOpts.length} opciones`);
|
||||||
|
const badAut = autOpts.filter(t => /Testament/i.test(t));
|
||||||
|
console.log(` opciones autor con 'Testament': ${badAut.length} ${badAut.length ? '→ ' + badAut.join(', ') : ''}`);
|
||||||
|
await p.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 4. Filtro cita bíblica (PREFIJO) ─────────────────────────────────────
|
||||||
|
{
|
||||||
|
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||||
|
await p.goto(`${BASE}/?fea_cita=Jn`, { waitUntil: 'networkidle' });
|
||||||
|
const cards = await p.$$('.fea-archive-card');
|
||||||
|
console.log(`✓ Filtro cita PREFIJO (fea_cita=Jn): ${cards.length} tarjetas`);
|
||||||
|
const citaVal = await p.$eval('#fea-cita', el => el.value).catch(() => 'n/a');
|
||||||
|
console.log(` input cita value: ${citaVal}`);
|
||||||
|
const chip = await p.$('.fea-adv-chip');
|
||||||
|
const chipText = chip ? await chip.textContent() : 'no chip';
|
||||||
|
console.log(` chip activo: ${chipText}`);
|
||||||
|
// Placeholder corto + foco para ver que NO se corta
|
||||||
|
const ph = await p.$eval('#fea-cita', el => el.placeholder).catch(() => 'n/a');
|
||||||
|
console.log(` placeholder cita: "${ph}"`);
|
||||||
|
await p.screenshot({ path: shotPath('4_filtro_cita'), fullPage: false });
|
||||||
|
await p.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 4b. Cita bíblica con valor largo: comprobar que no desborda ──────────
|
||||||
|
{
|
||||||
|
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||||
|
await p.goto(`${BASE}/?fea_cita=Jn%2020%2C%2019-31`, { waitUntil: 'networkidle' });
|
||||||
|
// Medir overflow: scrollWidth vs clientWidth del input y de su fila
|
||||||
|
const metrics = await p.$eval('#fea-cita', el => ({
|
||||||
|
scrollW: el.scrollWidth, clientW: el.clientWidth,
|
||||||
|
offsetW: el.offsetWidth, parentW: el.parentElement.clientWidth,
|
||||||
|
})).catch(() => null);
|
||||||
|
console.log(`✓ Cita valor largo (Jn 20, 19-31): ${JSON.stringify(metrics)}`);
|
||||||
|
const overflow = metrics ? (metrics.offsetW > metrics.parentW + 1) : 'n/a';
|
||||||
|
console.log(` input desborda su celda: ${overflow}`);
|
||||||
|
await p.screenshot({ path: shotPath('4b_cita_largo_nocorta'), fullPage: false });
|
||||||
|
await p.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 5. Filtro fecha ─────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||||
|
await p.goto(`${BASE}/?fea_date_from=2020-01-01&fea_date_to=2020-12-31`, { waitUntil: 'networkidle' });
|
||||||
|
const cards = await p.$$('.fea-archive-card');
|
||||||
|
console.log(`✓ Filtro fecha (2020): ${cards.length} tarjetas`);
|
||||||
|
await p.screenshot({ path: shotPath('5_filtro_fecha'), fullPage: false });
|
||||||
|
await p.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 6. Combinado palabra + autor ────────────────────────────────────────
|
||||||
|
{
|
||||||
|
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||||
|
await p.goto(`${BASE}/?s=amor&fea_author=384`, { waitUntil: 'networkidle' }); // Enrique Martínez Lozano
|
||||||
|
const cards = await p.$$('.fea-archive-card');
|
||||||
|
console.log(`✓ Combinado s=amor + fea_author=384 (Enrique M.L.): ${cards.length} tarjetas`);
|
||||||
|
await p.screenshot({ path: shotPath('6_combinado_palabra_autor'), fullPage: false });
|
||||||
|
await p.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 7. Multiidioma /en/?s=love ──────────────────────────────────────────
|
||||||
|
{
|
||||||
|
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||||
|
await p.goto(`${BASE}/en/?s=love`, { waitUntil: 'networkidle' });
|
||||||
|
const cards = await p.$$('.fea-archive-card');
|
||||||
|
// Check form action points to /en/
|
||||||
|
const formAction = await p.$eval('.fea-adv-form', el => el.action).catch(() => 'n/a');
|
||||||
|
console.log(`✓ Multiidioma /en/?s=love: ${cards.length} tarjetas, form action: ${formAction}`);
|
||||||
|
// Check if labels are in English
|
||||||
|
const firstLabel = await p.$eval('.fea-adv-label', el => el.textContent).catch(() => 'n/a');
|
||||||
|
console.log(` primer label: ${firstLabel}`);
|
||||||
|
await p.screenshot({ path: shotPath('7_multiidioma_en'), fullPage: false });
|
||||||
|
await p.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 8a. Formulario visible en desktop ───────────────────────────────────
|
||||||
|
{
|
||||||
|
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||||
|
await p.goto(`${BASE}/?s=fe`, { waitUntil: 'networkidle' });
|
||||||
|
const formVisible = await p.isVisible('.fea-adv-form');
|
||||||
|
console.log(`✓ Formulario desktop (1280px): visible=${formVisible}`);
|
||||||
|
await p.screenshot({ path: shotPath('8a_form_desktop'), fullPage: false });
|
||||||
|
await p.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 8b. Formulario visible en móvil ────────────────────────────────────
|
||||||
|
{
|
||||||
|
const p = await b.newPage({ viewport: { width: 390, height: 844 } });
|
||||||
|
await p.goto(`${BASE}/?s=fe`, { waitUntil: 'networkidle' });
|
||||||
|
const formVisible = await p.isVisible('.fea-adv-form');
|
||||||
|
console.log(`✓ Formulario móvil (390px): visible=${formVisible}`);
|
||||||
|
// Check advanced link in mobile header
|
||||||
|
const advLink = await p.$('.fea-adv-link');
|
||||||
|
const advLinkVisible = advLink ? await advLink.isVisible() : false;
|
||||||
|
console.log(` enlace búsqueda avanzada móvil: visible=${advLinkVisible}`);
|
||||||
|
await p.screenshot({ path: shotPath('8b_form_mobile'), fullPage: false });
|
||||||
|
await p.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 9. Página /buscar ────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||||
|
await p.goto(`${BASE}/buscar/`, { waitUntil: 'networkidle' });
|
||||||
|
const status = p.url();
|
||||||
|
const form = await p.$('.fea-adv-form');
|
||||||
|
console.log(`✓ Página /buscar: url=${status}, form=${!!form}`);
|
||||||
|
await p.screenshot({ path: shotPath('9_pagina_buscar'), fullPage: false });
|
||||||
|
await p.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
await b.close();
|
||||||
|
console.log(`\nCapturas guardadas en: ${OUT}`);
|
||||||
|
})();
|
||||||
@@ -0,0 +1,632 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Fe Adulta — Buscador avanzado (#8)
|
||||||
|
* Description: Replica el «Buscador avanzado» K2 de Joomla con WordPress nativo.
|
||||||
|
* Filtros: palabra (FULLTEXT vía fea-search-fulltext.php), autor, tema
|
||||||
|
* (categoría), cita bíblica (_cita_evangelio), fecha.
|
||||||
|
* Formulario visible en la página de resultados (search template) y en
|
||||||
|
* la página dedicada /buscar. Multiidioma (Polylang).
|
||||||
|
* Version: 1.0
|
||||||
|
*/
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// Constantes de configuración
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IDs de usuarios a excluir del selector de autores.
|
||||||
|
* 1,890,1049,1540 = cuentas técnicas/admin.
|
||||||
|
* 408,409,1563..1570 = pseudo-autores "Nuevo/Antiguo Testamento" (y sus traducciones)
|
||||||
|
* que NO son personas. Además, abajo se excluye cualquier display_name que contenga
|
||||||
|
* "Testament" de forma robusta (por si aparecen nuevos IDs).
|
||||||
|
*/
|
||||||
|
defined('FEA_AUTORES_EXCLUIR') or define('FEA_AUTORES_EXCLUIR', [
|
||||||
|
1, 890, 1049, 1540,
|
||||||
|
408, 409, 1563, 1564, 1565, 1566, 1567, 1568, 1569, 1570,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Categorías de ESTADO DE CARTA a excluir del selector de categorías:
|
||||||
|
* 6 (cartasemana), 21 (cartas-de-otras-semanas), 22 (carta-semana-pasada).
|
||||||
|
*/
|
||||||
|
defined('FEA_CATS_CARTA_EXCLUIR') or define('FEA_CATS_CARTA_EXCLUIR', [6, 21, 22]);
|
||||||
|
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// i18n mínimo (es / en / fr / it / pt)
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function fea_adv_t(string $key): string {
|
||||||
|
$lang = function_exists('pll_current_language') ? pll_current_language() : 'es';
|
||||||
|
$strings = [
|
||||||
|
'search_advanced' => ['es' => 'Búsqueda avanzada', 'en' => 'Advanced search',
|
||||||
|
'fr' => 'Recherche avancée', 'it' => 'Ricerca avanzata', 'pt' => 'Pesquisa avançada'],
|
||||||
|
'word' => ['es' => 'Palabra o frase', 'en' => 'Word or phrase',
|
||||||
|
'fr' => 'Mot ou phrase', 'it' => 'Parola o frase', 'pt' => 'Palavra ou frase'],
|
||||||
|
'author' => ['es' => 'Autor', 'en' => 'Author',
|
||||||
|
'fr' => 'Auteur', 'it' => 'Autore', 'pt' => 'Autor'],
|
||||||
|
'all_authors' => ['es' => '— Cualquier autor —', 'en' => '— Any author —',
|
||||||
|
'fr' => '— Tout auteur —', 'it' => '— Qualsiasi autore —', 'pt' => '— Qualquer autor —'],
|
||||||
|
'topic' => ['es' => 'Categoría', 'en' => 'Category',
|
||||||
|
'fr' => 'Catégorie', 'it' => 'Categoria', 'pt' => 'Categoria'],
|
||||||
|
'all_topics' => ['es' => '— Cualquier categoría —', 'en' => '— Any category —',
|
||||||
|
'fr' => '— Toute catégorie —', 'it' => '— Qualsiasi categoria —', 'pt' => '— Qualquer categoria —'],
|
||||||
|
'biblical_ref' => ['es' => 'Cita bíblica', 'en' => 'Biblical reference',
|
||||||
|
'fr' => 'Référence biblique', 'it' => 'Citazione biblica', 'pt' => 'Referência bíblica'],
|
||||||
|
'biblical_ph' => ['es' => 'Ej: Jn 3', 'en' => 'E.g. Jn 3',
|
||||||
|
'fr' => 'Ex: Jn 3', 'it' => 'Es: Gv 3', 'pt' => 'Ex: Jo 3'],
|
||||||
|
'date_from' => ['es' => 'Desde', 'en' => 'From',
|
||||||
|
'fr' => 'Du', 'it' => 'Dal', 'pt' => 'De'],
|
||||||
|
'date_to' => ['es' => 'Hasta', 'en' => 'To',
|
||||||
|
'fr' => "Jusqu'au", 'it' => 'Al', 'pt' => 'Até'],
|
||||||
|
'search_btn' => ['es' => 'Buscar', 'en' => 'Search',
|
||||||
|
'fr' => 'Rechercher', 'it' => 'Cerca', 'pt' => 'Pesquisar'],
|
||||||
|
'reset_btn' => ['es' => 'Limpiar', 'en' => 'Clear',
|
||||||
|
'fr' => 'Effacer', 'it' => 'Cancella', 'pt' => 'Limpar'],
|
||||||
|
'results' => ['es' => 'resultado(s)', 'en' => 'result(s)',
|
||||||
|
'fr' => 'résultat(s)', 'it' => 'risultato/i', 'pt' => 'resultado(s)'],
|
||||||
|
'no_results' => ['es' => 'Sin resultados. Prueba con otros términos.',
|
||||||
|
'en' => 'No results. Try other terms.',
|
||||||
|
'fr' => 'Aucun résultat. Essayez d\'autres termes.',
|
||||||
|
'it' => 'Nessun risultato. Prova con altri termini.',
|
||||||
|
'pt' => 'Sem resultados. Tente outros termos.'],
|
||||||
|
'active_filters' => ['es' => 'Filtros activos:', 'en' => 'Active filters:',
|
||||||
|
'fr' => 'Filtres actifs:', 'it' => 'Filtri attivi:', 'pt' => 'Filtros ativos:'],
|
||||||
|
'filter_author' => ['es' => 'Autor', 'en' => 'Author',
|
||||||
|
'fr' => 'Auteur', 'it' => 'Autore', 'pt' => 'Autor'],
|
||||||
|
'filter_topic' => ['es' => 'Categoría', 'en' => 'Category',
|
||||||
|
'fr' => 'Catégorie', 'it' => 'Categoria', 'pt' => 'Categoria'],
|
||||||
|
'filter_cita' => ['es' => 'Cita', 'en' => 'Ref.',
|
||||||
|
'fr' => 'Réf.', 'it' => 'Cit.', 'pt' => 'Ref.'],
|
||||||
|
'filter_date' => ['es' => 'Fecha', 'en' => 'Date',
|
||||||
|
'fr' => 'Date', 'it' => 'Data', 'pt' => 'Data'],
|
||||||
|
'by' => ['es' => 'por', 'en' => 'by',
|
||||||
|
'fr' => 'par', 'it' => 'di', 'pt' => 'por'],
|
||||||
|
];
|
||||||
|
$row = $strings[$key] ?? [];
|
||||||
|
return $row[$lang] ?? $row['es'] ?? $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// Query vars
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
add_filter('query_vars', function (array $vars): array {
|
||||||
|
$vars[] = 'fea_author';
|
||||||
|
$vars[] = 'fea_cat';
|
||||||
|
$vars[] = 'fea_cita';
|
||||||
|
$vars[] = 'fea_date_from';
|
||||||
|
$vars[] = 'fea_date_to';
|
||||||
|
return $vars;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// pre_get_posts — aplica los filtros avanzados
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
add_action('pre_get_posts', function (WP_Query $q): void {
|
||||||
|
if (is_admin() || !$q->is_main_query()) return;
|
||||||
|
|
||||||
|
// Leer los parámetros avanzados desde $_GET directamente (más fiable en pre_get_posts)
|
||||||
|
$fea_author = isset($_GET['fea_author']) ? (int)$_GET['fea_author'] : 0;
|
||||||
|
$fea_cat = isset($_GET['fea_cat']) ? (int)$_GET['fea_cat'] : 0;
|
||||||
|
$fea_cita = isset($_GET['fea_cita']) ? sanitize_text_field($_GET['fea_cita']) : '';
|
||||||
|
$fea_dfr = isset($_GET['fea_date_from']) ? sanitize_text_field($_GET['fea_date_from']) : '';
|
||||||
|
$fea_dto = isset($_GET['fea_date_to']) ? sanitize_text_field($_GET['fea_date_to']) : '';
|
||||||
|
|
||||||
|
$has_adv = ($fea_author > 0 || $fea_cat > 0 || $fea_cita !== '' || $fea_dfr !== '' || $fea_dto !== '');
|
||||||
|
$is_search = $q->is_search();
|
||||||
|
|
||||||
|
// Activar si: es búsqueda, o si hay vars avanzadas (con o sin ?s=)
|
||||||
|
if (!$is_search && !$has_adv) return;
|
||||||
|
|
||||||
|
// Si hay filtros avanzados pero no ?s=, convertimos el query en listado de posts
|
||||||
|
// (evitamos que WP muestre la home o una 404)
|
||||||
|
if ($has_adv && !$is_search) {
|
||||||
|
$q->set('post_type', 'post');
|
||||||
|
$q->set('post_status', 'publish');
|
||||||
|
// Forzamos is_search para que el template search se active
|
||||||
|
$q->is_home = false;
|
||||||
|
$q->is_front_page = false;
|
||||||
|
$q->is_archive = false;
|
||||||
|
$q->is_search = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Autor
|
||||||
|
if ($fea_author > 0) $q->set('author', $fea_author);
|
||||||
|
|
||||||
|
// Categoría (tema)
|
||||||
|
if ($fea_cat > 0) $q->set('cat', $fea_cat);
|
||||||
|
|
||||||
|
// Cita bíblica: coincidencia por PREFIJO (el valor empieza por el término, ej. "Jn").
|
||||||
|
// Usamos REGEXP '^<term>' con el término escapado para evitar metacaracteres.
|
||||||
|
if ($fea_cita !== '') {
|
||||||
|
$regex = '^' . preg_quote($fea_cita, '/');
|
||||||
|
$q->set('meta_query', [
|
||||||
|
[
|
||||||
|
'key' => '_cita_evangelio',
|
||||||
|
'value' => $regex,
|
||||||
|
'compare' => 'REGEXP',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fechas: la UI usa <input type="date"> → formato YYYY-MM-DD (fecha completa).
|
||||||
|
// Tratamos los límites como fechas exactas, inclusive.
|
||||||
|
if ($fea_dfr !== '' || $fea_dto !== '') {
|
||||||
|
$date_query = ['inclusive' => true];
|
||||||
|
if ($fea_dfr !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $fea_dfr)) {
|
||||||
|
[$y, $m, $d] = array_map('intval', explode('-', $fea_dfr));
|
||||||
|
$date_query['after'] = ['year' => $y, 'month' => $m, 'day' => $d];
|
||||||
|
}
|
||||||
|
if ($fea_dto !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $fea_dto)) {
|
||||||
|
[$y, $m, $d] = array_map('intval', explode('-', $fea_dto));
|
||||||
|
$date_query['before'] = ['year' => $y, 'month' => $m, 'day' => $d];
|
||||||
|
}
|
||||||
|
// Sólo aplicamos si quedó al menos un límite válido
|
||||||
|
if (isset($date_query['after']) || isset($date_query['before'])) {
|
||||||
|
$q->set('date_query', [$date_query]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// Helpers: obtener datos del formulario
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Devuelve los autores elegibles (≥30 posts, sin excluidos), cacheado por request. */
|
||||||
|
function fea_adv_get_authors(): array {
|
||||||
|
static $cache = null;
|
||||||
|
if ($cache !== null) return $cache;
|
||||||
|
global $wpdb;
|
||||||
|
$excl = implode(',', array_map('intval', FEA_AUTORES_EXCLUIR));
|
||||||
|
// Excluimos por ID y, de forma robusta, cualquier display_name que contenga "Testament"
|
||||||
|
// (Nuevo/Antiguo Testamento, New/Old Testament, Nouveau/Ancien Testament, etc.).
|
||||||
|
$cache = $wpdb->get_results("
|
||||||
|
SELECT u.ID, u.display_name, COUNT(p.ID) as cnt
|
||||||
|
FROM {$wpdb->users} u
|
||||||
|
JOIN {$wpdb->posts} p ON p.post_author = u.ID
|
||||||
|
WHERE p.post_status = 'publish'
|
||||||
|
AND p.post_type = 'post'
|
||||||
|
AND u.ID NOT IN ({$excl})
|
||||||
|
AND u.display_name NOT LIKE '%Testament%'
|
||||||
|
GROUP BY u.ID
|
||||||
|
HAVING cnt >= 30
|
||||||
|
ORDER BY u.display_name
|
||||||
|
");
|
||||||
|
return $cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lista CURADA de categorías-sección reales (term_ids ES, idioma canónico), en orden
|
||||||
|
* editorial. Las 153 categorías reales de la BD incluyen categorías-autor y residuales
|
||||||
|
* de la migración K2 → desplegable inmanejable; por eso se cura a las secciones del sitio.
|
||||||
|
* En idiomas ≠ ES se traduce cada term con Polylang.
|
||||||
|
*/
|
||||||
|
defined('FEA_CATS_CURADA') or define('FEA_CATS_CURADA', [
|
||||||
|
1650, // Artículos
|
||||||
|
1647, // Comentarios al evangelio
|
||||||
|
1645, // Lecturas bíblicas
|
||||||
|
1648, // Eucaristía
|
||||||
|
1646, // Comentario editorial
|
||||||
|
1649, // Multimedia
|
||||||
|
63, // EFFA
|
||||||
|
14, // A la fuente cada día
|
||||||
|
23, // Cartas que nos llegan
|
||||||
|
41, // Noticias de alcance
|
||||||
|
24, // Tablón de anuncios
|
||||||
|
54, // Canciones religiosas
|
||||||
|
45, // Canciones-plegarias
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Etiqueta bonita en el desplegable (solo ES) para categorías cuyo nombre en BD quedó
|
||||||
|
* sin formatear en la migración K2 (no toca el dato del término).
|
||||||
|
*/
|
||||||
|
defined('FEA_CATS_LABEL') or define('FEA_CATS_LABEL', [
|
||||||
|
14 => 'A la fuente cada día', // en BD es "Alafuentecadadia"
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devuelve las categorías del selector (lista curada FEA_CATS_CURADA), traducidas al
|
||||||
|
* idioma activo (Polylang) y conservando el orden editorial.
|
||||||
|
* Devuelve array de objetos {term_id, name} con el term_id DEL IDIOMA ACTUAL.
|
||||||
|
*/
|
||||||
|
function fea_adv_get_categories(): array {
|
||||||
|
static $cache = null;
|
||||||
|
if ($cache !== null) return $cache;
|
||||||
|
|
||||||
|
$lang = function_exists('pll_current_language') ? pll_current_language() : '';
|
||||||
|
$default = function_exists('pll_default_language') ? pll_default_language() : 'es';
|
||||||
|
|
||||||
|
$out = [];
|
||||||
|
foreach (FEA_CATS_CURADA as $es_id) {
|
||||||
|
$tid = (int) $es_id;
|
||||||
|
if ($lang && $lang !== $default && function_exists('pll_get_term')) {
|
||||||
|
$tr = pll_get_term($es_id, $lang);
|
||||||
|
if ($tr) $tid = (int) $tr;
|
||||||
|
}
|
||||||
|
$term = get_term($tid, 'category');
|
||||||
|
if (!$term || is_wp_error($term)) continue;
|
||||||
|
$name = $term->name;
|
||||||
|
if ((!$lang || $lang === $default) && isset(FEA_CATS_LABEL[$es_id])) {
|
||||||
|
$name = FEA_CATS_LABEL[$es_id];
|
||||||
|
}
|
||||||
|
$out[] = (object) ['term_id' => (int) $term->term_id, 'name' => $name];
|
||||||
|
}
|
||||||
|
$cache = $out;
|
||||||
|
return $cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** URL base del idioma actual (para el action del form). */
|
||||||
|
function fea_adv_lang_base(): string {
|
||||||
|
$base = home_url('/');
|
||||||
|
if (function_exists('pll_current_language')) {
|
||||||
|
$lang = pll_current_language();
|
||||||
|
$default = function_exists('pll_default_language') ? pll_default_language() : 'es';
|
||||||
|
if ($lang && $lang !== $default) $base = home_url('/' . $lang . '/');
|
||||||
|
}
|
||||||
|
return $base;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nombre traducido de una categoría (vía Polylang si está disponible). */
|
||||||
|
function fea_adv_cat_name(int $cat_id, string $fallback): string {
|
||||||
|
if (function_exists('pll_current_language')) {
|
||||||
|
$lang = pll_current_language();
|
||||||
|
$default = function_exists('pll_default_language') ? pll_default_language() : 'es';
|
||||||
|
if ($lang !== $default) {
|
||||||
|
$translated_id = function_exists('pll_get_term') ? pll_get_term($cat_id, $lang) : 0;
|
||||||
|
if ($translated_id) {
|
||||||
|
$term = get_term($translated_id);
|
||||||
|
if ($term && !is_wp_error($term)) return $term->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$term = get_term($cat_id, 'category');
|
||||||
|
return ($term && !is_wp_error($term)) ? $term->name : $fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// Renderiza el formulario avanzado
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function fea_adv_form_html(): string {
|
||||||
|
$action = esc_url(fea_adv_lang_base());
|
||||||
|
$s = esc_attr(get_search_query());
|
||||||
|
// Leer de $_GET para fiabilidad (get_query_var puede llegar vacío si la var no estaba registrada aún)
|
||||||
|
$sel_aut = isset($_GET['fea_author']) ? (int)$_GET['fea_author'] : 0;
|
||||||
|
$sel_cat = isset($_GET['fea_cat']) ? (int)$_GET['fea_cat'] : 0;
|
||||||
|
$sel_cit = isset($_GET['fea_cita']) ? esc_attr(sanitize_text_field($_GET['fea_cita'])) : '';
|
||||||
|
$sel_dfr = isset($_GET['fea_date_from'])? esc_attr(sanitize_text_field($_GET['fea_date_from'])) : '';
|
||||||
|
$sel_dto = isset($_GET['fea_date_to']) ? esc_attr(sanitize_text_field($_GET['fea_date_to'])) : '';
|
||||||
|
|
||||||
|
$authors = fea_adv_get_authors();
|
||||||
|
$cats = fea_adv_get_categories();
|
||||||
|
|
||||||
|
$t_title = esc_html(fea_adv_t('search_advanced'));
|
||||||
|
$t_word = esc_html(fea_adv_t('word'));
|
||||||
|
$t_author = esc_html(fea_adv_t('author'));
|
||||||
|
$t_allaut = esc_html(fea_adv_t('all_authors'));
|
||||||
|
$t_topic = esc_html(fea_adv_t('topic'));
|
||||||
|
$t_alltop = esc_html(fea_adv_t('all_topics'));
|
||||||
|
$t_cita = esc_html(fea_adv_t('biblical_ref'));
|
||||||
|
$t_citaph = esc_attr(fea_adv_t('biblical_ph'));
|
||||||
|
$t_dfr = esc_html(fea_adv_t('date_from'));
|
||||||
|
$t_dto = esc_html(fea_adv_t('date_to'));
|
||||||
|
$t_btn = esc_html(fea_adv_t('search_btn'));
|
||||||
|
$t_reset = esc_html(fea_adv_t('reset_btn'));
|
||||||
|
|
||||||
|
$html = '<div class="fea-adv-wrap" id="fea-adv-search">';
|
||||||
|
$html .= '<details class="fea-adv-details" open>';
|
||||||
|
$html .= '<summary class="fea-adv-summary">' . $t_title . '</summary>';
|
||||||
|
$html .= '<form class="fea-adv-form" method="get" action="' . $action . '">';
|
||||||
|
|
||||||
|
// Fila 1: Palabra
|
||||||
|
$html .= '<div class="fea-adv-row">';
|
||||||
|
$html .= '<label class="fea-adv-label" for="fea-s">' . $t_word . '</label>';
|
||||||
|
$html .= '<input class="fea-adv-input" id="fea-s" type="search" name="s" value="' . $s . '" autocomplete="off">';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
// Fila 2: Autor
|
||||||
|
$html .= '<div class="fea-adv-row">';
|
||||||
|
$html .= '<label class="fea-adv-label" for="fea-author">' . $t_author . '</label>';
|
||||||
|
$html .= '<select class="fea-adv-select" id="fea-author" name="fea_author">';
|
||||||
|
$html .= '<option value="">' . $t_allaut . '</option>';
|
||||||
|
foreach ($authors as $a) {
|
||||||
|
$sel = selected($sel_aut, (int)$a->ID, false);
|
||||||
|
$name = esc_html($a->display_name);
|
||||||
|
$html .= "<option value=\"{$a->ID}\"{$sel}>{$name}</option>";
|
||||||
|
}
|
||||||
|
$html .= '</select></div>';
|
||||||
|
|
||||||
|
// Fila 3: Categoría (real, dinámica)
|
||||||
|
$html .= '<div class="fea-adv-row">';
|
||||||
|
$html .= '<label class="fea-adv-label" for="fea-cat">' . $t_topic . '</label>';
|
||||||
|
$html .= '<select class="fea-adv-select" id="fea-cat" name="fea_cat">';
|
||||||
|
$html .= '<option value="">' . $t_alltop . '</option>';
|
||||||
|
foreach ($cats as $c) {
|
||||||
|
$cat_name = esc_html($c->name);
|
||||||
|
$sel = selected($sel_cat, $c->term_id, false);
|
||||||
|
$html .= "<option value=\"{$c->term_id}\"{$sel}>{$cat_name}</option>";
|
||||||
|
}
|
||||||
|
$html .= '</select></div>';
|
||||||
|
|
||||||
|
// Fila 4: Cita bíblica
|
||||||
|
$html .= '<div class="fea-adv-row">';
|
||||||
|
$html .= '<label class="fea-adv-label" for="fea-cita">' . $t_cita . '</label>';
|
||||||
|
$html .= '<input class="fea-adv-input" id="fea-cita" type="text" name="fea_cita" value="' . $sel_cit . '" placeholder="' . $t_citaph . '">';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
// Fila 5: Fechas
|
||||||
|
$html .= '<div class="fea-adv-row fea-adv-dates">';
|
||||||
|
$html .= '<span class="fea-adv-label">' . $t_dfr . '</span>';
|
||||||
|
$html .= '<input class="fea-adv-input fea-adv-date" type="date" name="fea_date_from" value="' . $sel_dfr . '">';
|
||||||
|
$html .= '<span class="fea-adv-label fea-adv-to">' . $t_dto . '</span>';
|
||||||
|
$html .= '<input class="fea-adv-input fea-adv-date" type="date" name="fea_date_to" value="' . $sel_dto . '">';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
// Botones
|
||||||
|
$html .= '<div class="fea-adv-actions">';
|
||||||
|
$html .= '<button class="fea-adv-btn fea-adv-btn-primary" type="submit">' . $t_btn . '</button>';
|
||||||
|
$html .= '<a class="fea-adv-btn fea-adv-btn-secondary" href="' . $action . '">' . $t_reset . '</a>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
$html .= '</form></details></div>';
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// Chips de filtros activos + contador de resultados
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function fea_adv_chips_html(): string {
|
||||||
|
$chips = [];
|
||||||
|
|
||||||
|
$aut_id = isset($_GET['fea_author']) ? (int)$_GET['fea_author'] : 0;
|
||||||
|
if ($aut_id > 0) {
|
||||||
|
$udata = get_userdata($aut_id);
|
||||||
|
$name = $udata ? esc_html($udata->display_name) : $aut_id;
|
||||||
|
$chips[] = fea_adv_chip(fea_adv_t('filter_author') . ': ' . $name, 'fea_author');
|
||||||
|
}
|
||||||
|
|
||||||
|
$cat_id = isset($_GET['fea_cat']) ? (int)$_GET['fea_cat'] : 0;
|
||||||
|
if ($cat_id > 0) {
|
||||||
|
$term = get_term($cat_id, 'category');
|
||||||
|
$name = ($term && !is_wp_error($term)) ? esc_html($term->name) : $cat_id;
|
||||||
|
$chips[] = fea_adv_chip(fea_adv_t('filter_topic') . ': ' . $name, 'fea_cat');
|
||||||
|
}
|
||||||
|
|
||||||
|
$cita = isset($_GET['fea_cita']) ? sanitize_text_field($_GET['fea_cita']) : '';
|
||||||
|
if ($cita !== '') {
|
||||||
|
$chips[] = fea_adv_chip(fea_adv_t('filter_cita') . ': ' . esc_html($cita), 'fea_cita');
|
||||||
|
}
|
||||||
|
|
||||||
|
$dfr = isset($_GET['fea_date_from']) ? sanitize_text_field($_GET['fea_date_from']) : '';
|
||||||
|
$dto = isset($_GET['fea_date_to']) ? sanitize_text_field($_GET['fea_date_to']) : '';
|
||||||
|
if ($dfr !== '' || $dto !== '') {
|
||||||
|
$label = fea_adv_t('filter_date') . ': ' . ($dfr ?: '?') . ' – ' . ($dto ?: '?');
|
||||||
|
$chips[] = fea_adv_chip(esc_html($label), 'fea_date_from', 'fea_date_to');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($chips)) return '';
|
||||||
|
return '<div class="fea-adv-chips"><span class="fea-adv-chips-label">' .
|
||||||
|
esc_html(fea_adv_t('active_filters')) . '</span>' . implode('', $chips) . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Genera un chip con botón ✕ que elimina el filtro de la URL. */
|
||||||
|
function fea_adv_chip(string $label, string ...$remove_params): string {
|
||||||
|
$url = remove_query_arg($remove_params);
|
||||||
|
return '<span class="fea-adv-chip">' . $label .
|
||||||
|
' <a href="' . esc_url($url) . '" class="fea-adv-chip-x" aria-label="Eliminar filtro">×</a></span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// Inyección en la página de resultados (template search)
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inyecta el formulario avanzado + chips antes del primer bloque wp:query-title
|
||||||
|
* del template de búsqueda, modificando el HTML renderizado del bloque principal.
|
||||||
|
*/
|
||||||
|
add_filter('render_block', function (string $html, array $block): string {
|
||||||
|
if (is_admin()) return $html;
|
||||||
|
|
||||||
|
// Inyectar en búsquedas y cuando hay filtros avanzados activos
|
||||||
|
$has_adv_get = !empty($_GET['fea_author']) || !empty($_GET['fea_cat']) ||
|
||||||
|
!empty($_GET['fea_cita']) || !empty($_GET['fea_date_from']) || !empty($_GET['fea_date_to']);
|
||||||
|
|
||||||
|
if (!is_search() && !is_page('buscar') && !$has_adv_get) return $html;
|
||||||
|
|
||||||
|
if (($block['blockName'] ?? '') !== 'core/query-title') return $html;
|
||||||
|
|
||||||
|
$form = fea_adv_form_html();
|
||||||
|
$chips = fea_adv_chips_html();
|
||||||
|
|
||||||
|
// Contador de resultados (sólo cuando hay consulta activa)
|
||||||
|
$counter = '';
|
||||||
|
if (is_search() || $has_adv_get) {
|
||||||
|
global $wp_query;
|
||||||
|
if ($wp_query && $wp_query->found_posts !== null) {
|
||||||
|
$n = (int) $wp_query->found_posts;
|
||||||
|
$counter = '<div class="fea-adv-count">' . $n . ' ' . esc_html(fea_adv_t('results')) . '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $form . $chips . $counter . $html;
|
||||||
|
}, 10, 2);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Añade byline (autor) en cada tarjeta fea-archive-card dentro del template search.
|
||||||
|
* Lo hacemos inyectando tras wp:post-date en el contexto correcto.
|
||||||
|
*/
|
||||||
|
add_filter('render_block', function (string $html, array $block): string {
|
||||||
|
if (is_admin()) return $html;
|
||||||
|
$has_adv_get2 = !empty($_GET['fea_author']) || !empty($_GET['fea_cat']) ||
|
||||||
|
!empty($_GET['fea_cita']) || !empty($_GET['fea_date_from']) || !empty($_GET['fea_date_to']);
|
||||||
|
if (!is_search() && !is_page('buscar') && !$has_adv_get2) return $html;
|
||||||
|
if (($block['blockName'] ?? '') !== 'core/post-date') return $html;
|
||||||
|
|
||||||
|
$post_id = $block['attrs']['postId'] ?? (in_the_loop() ? get_the_ID() : 0);
|
||||||
|
if (!$post_id) $post_id = get_the_ID();
|
||||||
|
if (!$post_id) return $html;
|
||||||
|
|
||||||
|
$author_id = (int) get_post_field('post_author', $post_id);
|
||||||
|
$author_name = get_the_author_meta('display_name', $author_id);
|
||||||
|
if (!$author_name) return $html;
|
||||||
|
|
||||||
|
$author_url = get_author_posts_url($author_id);
|
||||||
|
$byline = '<div class="fea-adv-byline">' . esc_html(fea_adv_t('by')) . ' ' .
|
||||||
|
'<a href="' . esc_url($author_url) . '">' . esc_html($author_name) . '</a></div>';
|
||||||
|
|
||||||
|
return $html . $byline;
|
||||||
|
}, 10, 2);
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// Enlace «Búsqueda avanzada» desde la barra fea-search
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
add_filter('render_block', function (string $html, array $block): string {
|
||||||
|
if (is_admin()) return $html;
|
||||||
|
if (($block['blockName'] ?? '') !== 'core/template-part') return $html;
|
||||||
|
$slug = $block['attrs']['slug'] ?? '';
|
||||||
|
if (!in_array($slug, ['header', 'cabecera-portada'], true)) return $html;
|
||||||
|
|
||||||
|
// Sólo inyectamos el enlace si ya hay una barra de búsqueda (.fea-search-bar)
|
||||||
|
// Buscamos la barra y le añadimos el enlace de búsqueda avanzada.
|
||||||
|
if (strpos($html, 'fea-search-bar') === false) return $html;
|
||||||
|
|
||||||
|
$adv_url = home_url('/buscar/');
|
||||||
|
if (function_exists('pll_current_language')) {
|
||||||
|
$lang = pll_current_language();
|
||||||
|
$default = function_exists('pll_default_language') ? pll_default_language() : 'es';
|
||||||
|
if ($lang && $lang !== $default) {
|
||||||
|
// Intenta obtener la página /buscar traducida
|
||||||
|
$page = get_page_by_path('buscar');
|
||||||
|
if ($page) {
|
||||||
|
$tl = function_exists('pll_get_post') ? pll_get_post($page->ID, $lang) : 0;
|
||||||
|
if ($tl) $adv_url = get_permalink($tl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$label = esc_html(fea_adv_t('search_advanced'));
|
||||||
|
$link = '<div class="fea-adv-link-wrap"><a class="fea-adv-link" href="' . esc_url($adv_url) . '">' . $label . '</a></div>';
|
||||||
|
|
||||||
|
// Insertamos el enlace justo al final del bloque .fea-search-bar (cerrando el div externo)
|
||||||
|
// fea-search.php genera: <div class="fea-search-bar"><form ...>...</form></div>
|
||||||
|
// Reemplazamos la ÚLTIMA ocurrencia del cierre del div de .fea-search-bar
|
||||||
|
$marker = '</div>';
|
||||||
|
$pos = strpos($html, 'fea-search-bar');
|
||||||
|
if ($pos !== false) {
|
||||||
|
// Buscamos el </div> que cierra .fea-search-bar (el wrapper externo)
|
||||||
|
// La estructura es: <div class="fea-search-bar"><form>...</form></div>
|
||||||
|
// Hay dos </div>: uno cierra el form y otro cierra .fea-search-bar
|
||||||
|
// Usamos una sustitución segura: buscamos el patrón exacto del cierre
|
||||||
|
$html = preg_replace('#(</form></div>)#', '$1' . $link, $html, 1);
|
||||||
|
}
|
||||||
|
return $html;
|
||||||
|
}, 25, 2);
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// Página /buscar — inyectar formulario vía the_content
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
add_filter('the_content', function (string $content): string {
|
||||||
|
if (!is_page('buscar')) return $content;
|
||||||
|
// Envoltura: título + formulario + contenido original de la página
|
||||||
|
$form = fea_adv_form_html();
|
||||||
|
return $form . '<div class="fea-buscar-intro">' . $content . '</div>';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nota: la página /buscar se crea con scripts/create_buscar_page.php (ya ejecutado).
|
||||||
|
// El formulario se inyecta en the_content (hook arriba) y vía render_block en search.
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// CSS
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
add_action('wp_head', function (): void {
|
||||||
|
if (!is_search() && !is_page('buscar') &&
|
||||||
|
empty($_GET['fea_author']) && empty($_GET['fea_cat']) &&
|
||||||
|
empty($_GET['fea_cita']) && empty($_GET['fea_date_from']) && empty($_GET['fea_date_to'])) return;
|
||||||
|
?>
|
||||||
|
<style>
|
||||||
|
/* ── Formulario buscador avanzado ─────────────────────────────── */
|
||||||
|
.fea-adv-wrap{max-width:860px;margin:0 auto 1.5rem;padding:0 1rem}
|
||||||
|
|
||||||
|
.fea-adv-details{background:#faf6f7;border:1px solid #e8d5da;border-radius:8px;overflow:hidden}
|
||||||
|
.fea-adv-summary{
|
||||||
|
padding:.75rem 1.2rem;font-weight:600;font-size:1rem;color:#8b1a2e;
|
||||||
|
cursor:pointer;list-style:none;display:flex;align-items:center;gap:.5rem;
|
||||||
|
background:#fff0f2;border-bottom:1px solid #e8d5da;
|
||||||
|
}
|
||||||
|
.fea-adv-summary::-webkit-details-marker{display:none}
|
||||||
|
.fea-adv-summary::before{content:"▸";transition:transform .2s}
|
||||||
|
details[open] .fea-adv-summary::before{transform:rotate(90deg)}
|
||||||
|
|
||||||
|
.fea-adv-form{display:grid;grid-template-columns:1fr 1fr;gap:.75rem 1.2rem;padding:1rem 1.2rem}
|
||||||
|
@media(max-width:600px){.fea-adv-form{grid-template-columns:1fr}}
|
||||||
|
|
||||||
|
/* min-width:0 evita que el contenido fuerce a la celda del grid a desbordar */
|
||||||
|
.fea-adv-row{display:flex;flex-direction:column;gap:.25rem;min-width:0}
|
||||||
|
.fea-adv-dates{grid-column:1/-1;flex-direction:row;align-items:center;flex-wrap:wrap;gap:.5rem}
|
||||||
|
.fea-adv-dates .fea-adv-label{white-space:nowrap}
|
||||||
|
|
||||||
|
.fea-adv-label{font-size:.8rem;font-weight:600;color:#5a3a40;text-transform:uppercase;letter-spacing:.04em}
|
||||||
|
.fea-adv-to{margin-left:.5rem}
|
||||||
|
|
||||||
|
.fea-adv-input,.fea-adv-select{
|
||||||
|
box-sizing:border-box;border:1px solid #d9c4c9;border-radius:6px;padding:.5rem .75rem;
|
||||||
|
font-size:.95rem;background:#fff;color:#222;width:100%;max-width:100%;
|
||||||
|
transition:border-color .15s;
|
||||||
|
}
|
||||||
|
.fea-adv-input:focus,.fea-adv-select:focus{border-color:#8b1a2e;outline:0;box-shadow:0 0 0 2px #8b1a2e33}
|
||||||
|
.fea-adv-date{box-sizing:border-box;width:auto;min-width:150px;flex:0 0 auto}
|
||||||
|
|
||||||
|
.fea-adv-actions{grid-column:1/-1;display:flex;gap:.75rem;align-items:center;margin-top:.25rem}
|
||||||
|
.fea-adv-btn{padding:.55rem 1.4rem;border-radius:999px;font-size:.95rem;font-weight:600;cursor:pointer;text-decoration:none;border:2px solid transparent}
|
||||||
|
.fea-adv-btn-primary{background:#8b1a2e;color:#fff;border-color:#8b1a2e}
|
||||||
|
.fea-adv-btn-primary:hover{background:#6f1525;border-color:#6f1525}
|
||||||
|
.fea-adv-btn-secondary{background:transparent;color:#8b1a2e;border-color:#8b1a2e}
|
||||||
|
.fea-adv-btn-secondary:hover{background:#8b1a2e;color:#fff}
|
||||||
|
|
||||||
|
/* ── Contador y chips ─────────────────────────────────────────── */
|
||||||
|
.fea-adv-count{max-width:860px;margin:.5rem auto .25rem;padding:0 1rem;
|
||||||
|
font-size:.9rem;color:#666}
|
||||||
|
|
||||||
|
.fea-adv-chips{max-width:860px;margin:.5rem auto;padding:0 1rem;
|
||||||
|
display:flex;flex-wrap:wrap;gap:.4rem;align-items:center}
|
||||||
|
.fea-adv-chips-label{font-size:.8rem;color:#5a3a40;font-weight:600;margin-right:.25rem}
|
||||||
|
.fea-adv-chip{display:inline-flex;align-items:center;gap:.35rem;background:#f0e2e5;
|
||||||
|
border:1px solid #d9c4c9;border-radius:999px;padding:.2rem .75rem;font-size:.82rem;color:#5a3a40}
|
||||||
|
.fea-adv-chip-x{color:#8b1a2e;text-decoration:none;font-weight:700;font-size:1rem;line-height:1}
|
||||||
|
.fea-adv-chip-x:hover{color:#6f1525}
|
||||||
|
|
||||||
|
/* ── Byline en tarjetas de resultado ──────────────────────────── */
|
||||||
|
.fea-archive-card .fea-adv-byline{font-size:.78rem;color:#7a5a62;margin-top:.1rem}
|
||||||
|
.fea-archive-card .fea-adv-byline a{color:#8b1a2e;text-decoration:none}
|
||||||
|
.fea-archive-card .fea-adv-byline a:hover{text-decoration:underline}
|
||||||
|
|
||||||
|
/* ── Enlace «Búsqueda avanzada» en barra header ───────────────── */
|
||||||
|
.fea-adv-link-wrap{display:none;justify-content:center;padding:.3rem 1rem .4rem;
|
||||||
|
background:#faf6f7;border-bottom:1px solid #efe2e5}
|
||||||
|
@media(max-width:600px){.fea-adv-link-wrap{display:flex}}
|
||||||
|
.fea-adv-link{font-size:.8rem;color:#8b1a2e;text-decoration:none;font-weight:500}
|
||||||
|
.fea-adv-link:hover{text-decoration:underline}
|
||||||
|
</style>
|
||||||
|
<?php
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// CSS del enlace «Búsqueda avanzada» en header (se muestra en todas las páginas)
|
||||||
|
add_action('wp_head', function (): void {
|
||||||
|
?>
|
||||||
|
<style>
|
||||||
|
.fea-adv-link-wrap{display:none;justify-content:center;padding:.3rem 1rem .4rem;
|
||||||
|
background:#faf6f7;border-bottom:1px solid #efe2e5}
|
||||||
|
@media(max-width:600px){.fea-adv-link-wrap{display:flex}}
|
||||||
|
.fea-adv-link{font-size:.8rem;color:#8b1a2e;text-decoration:none;font-weight:500}
|
||||||
|
.fea-adv-link:hover{text-decoration:underline}
|
||||||
|
</style>
|
||||||
|
<?php
|
||||||
|
}, 15);
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Fe Adulta — Motor FULLTEXT (#8)
|
||||||
|
* Description: Sustituye el LIKE nativo de WP por MATCH AGAINST (MySQL FULLTEXT, InnoDB,
|
||||||
|
* Boolean Mode) cuando se hace una búsqueda por texto (/?s=…). Ordena por
|
||||||
|
* relevancia FULLTEXT si no se pide otro criterio de orden. Degradación elegante:
|
||||||
|
* si no hay término o el índice FULLTEXT no existe, usa el comportamiento nativo.
|
||||||
|
* Convive con fea-search-advanced.php (filtros pre_get_posts de autor/cat/cita/fecha).
|
||||||
|
* Version: 1.1
|
||||||
|
*/
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comprueba (cacheado) que existe el índice FULLTEXT 'fea_ft' en wp_posts.
|
||||||
|
* Si no existe, el motor se degrada al comportamiento nativo para no romper la búsqueda
|
||||||
|
* con un error SQL (MATCH AGAINST requiere el índice).
|
||||||
|
*/
|
||||||
|
function fea_ft_index_exists(): bool {
|
||||||
|
static $cached = null;
|
||||||
|
if ($cached !== null) return $cached;
|
||||||
|
|
||||||
|
// Cache persistente 12h vía transient para evitar el SHOW INDEX en cada request.
|
||||||
|
$t = get_transient('fea_ft_index_exists');
|
||||||
|
if ($t !== false) {
|
||||||
|
$cached = ($t === '1');
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$found = (int) $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND INDEX_NAME = 'fea_ft'",
|
||||||
|
$wpdb->posts
|
||||||
|
));
|
||||||
|
$cached = $found > 0;
|
||||||
|
set_transient('fea_ft_index_exists', $cached ? '1' : '0', 12 * HOUR_IN_SECONDS);
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcula el término FULLTEXT en Boolean Mode (cada palabra con prefijo *).
|
||||||
|
* Devuelve '' si el término sanitizado queda vacío.
|
||||||
|
*/
|
||||||
|
function fea_ft_boolean_term(string $raw): string {
|
||||||
|
$term = trim(substr(preg_replace('/[^\p{L}\p{N}\s\'\-]/u', '', $raw), 0, 200));
|
||||||
|
if ($term === '') return '';
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$words = preg_split('/\s+/', $term);
|
||||||
|
return implode('* ', array_map(fn($w) => $wpdb->esc_like($w), $words)) . '*';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reemplaza la cláusula WHERE de búsqueda por MATCH AGAINST.
|
||||||
|
* Hook: posts_search (filtra el WHERE que WP construye para /?s=).
|
||||||
|
*/
|
||||||
|
add_filter('posts_search', function (string $search, WP_Query $q): string {
|
||||||
|
if (is_admin() || !$q->is_main_query() || !$q->is_search()) return $search;
|
||||||
|
if (!fea_ft_index_exists()) return $search; // degradación elegante
|
||||||
|
|
||||||
|
$raw = trim((string) $q->get('s'));
|
||||||
|
if ($raw === '') return $search;
|
||||||
|
|
||||||
|
$ft_term = fea_ft_boolean_term($raw);
|
||||||
|
if ($ft_term === '') return $search;
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$ft_esc = esc_sql($ft_term);
|
||||||
|
|
||||||
|
// WP construye " AND (...) " para la búsqueda; devolvemos un bloque AND compatible.
|
||||||
|
return " AND (MATCH({$wpdb->posts}.post_title, {$wpdb->posts}.post_content) AGAINST ('{$ft_esc}' IN BOOLEAN MODE)) ";
|
||||||
|
}, 10, 2);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ordena por relevancia FULLTEXT cuando no se especifica otro orden.
|
||||||
|
* Hook: posts_clauses (permite modificar SELECT y ORDER BY juntos).
|
||||||
|
*/
|
||||||
|
add_filter('posts_clauses', function (array $clauses, WP_Query $q): array {
|
||||||
|
if (is_admin() || !$q->is_main_query() || !$q->is_search()) return $clauses;
|
||||||
|
if (!fea_ft_index_exists()) return $clauses; // degradación elegante
|
||||||
|
|
||||||
|
$raw = trim((string) $q->get('s'));
|
||||||
|
if ($raw === '') return $clauses;
|
||||||
|
|
||||||
|
// Sólo reordenamos por relevancia si el orderby es el nativo de búsqueda.
|
||||||
|
$ob = $q->get('orderby');
|
||||||
|
if (!in_array($ob, ['relevance', 'date', ''], true)) return $clauses;
|
||||||
|
|
||||||
|
$ft_term = fea_ft_boolean_term($raw);
|
||||||
|
if ($ft_term === '') return $clauses;
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$ft_esc = esc_sql($ft_term);
|
||||||
|
|
||||||
|
// Añadimos la columna de relevancia al SELECT y la usamos en ORDER BY.
|
||||||
|
$score_col = "MATCH({$wpdb->posts}.post_title, {$wpdb->posts}.post_content) AGAINST ('{$ft_esc}' IN BOOLEAN MODE)";
|
||||||
|
$clauses['fields'] .= ", ({$score_col}) AS fea_ft_score";
|
||||||
|
$clauses['orderby'] = "fea_ft_score DESC, {$wpdb->posts}.post_date DESC";
|
||||||
|
|
||||||
|
return $clauses;
|
||||||
|
}, 10, 2);
|
||||||
Reference in New Issue
Block a user