AlkantarClanX12
| Current Path : /www/capitalgmcbuickregina_830/public/wp-content/plugins/leadbox/lib/ |
| Current File : /www/capitalgmcbuickregina_830/public/wp-content/plugins/leadbox/lib/llms-txt.php |
<?php
/**
* LBT-1506 (OEM AI-Readiness A2): sirve /llms.txt dinámico por site.
*
* Markdown plano (spec llmstxt.org) con la identidad del dealer (NAP,
* horarios, marcas) desde Dealer Settings y links a los hubs del site
* (SRP new/used, service, parts, financing, contact, about). Se genera
* on-the-fly en cada request de /llms.txt, así que refleja cambios de
* settings al instante y no necesita cache ni invalidación.
*
* @package LBX
*/
// Exit if accessed directly.
if (!defined('ABSPATH')) {
exit;
}
/**
* Intercepta las requests a /llms.txt y responde text/plain.
*
* Se cuelga de `init` comparando el path del request en vez de usar
* add_rewrite_rule: funciona en los ~95 sites al deployar el plugin sin
* depender de un flush de rewrite rules por site.
*
* @return void
*/
function lbx_llms_txt_maybe_serve()
{
if (is_admin() || wp_doing_ajax() || wp_doing_cron() || (defined('WP_CLI') && WP_CLI)) {
return;
}
$path = (string) parse_url((string) ($_SERVER['REQUEST_URI'] ?? ''), PHP_URL_PATH);
// Sites servidos desde un subdirectorio: se descuenta el path del home.
$home_path = rtrim((string) parse_url(home_url('/'), PHP_URL_PATH), '/');
if ($home_path !== '' && strpos($path, $home_path) === 0) {
$path = substr($path, strlen($home_path));
}
if (trim($path, '/') !== 'llms.txt') {
return;
}
nocache_headers();
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: public, max-age=3600');
echo lbx_llms_txt_content(); // phpcs:ignore WordPress.Security.EscapeOutput -- text/plain generado internamente.
exit;
}
add_action('init', 'lbx_llms_txt_maybe_serve', 1);
/**
* Construye el contenido Markdown del llms.txt del site.
*
* Secciones y bullets sin data se omiten: cada site emite solo lo que
* tiene configurado.
*
* @return string
*/
function lbx_llms_txt_content()
{
$contact = get_option('dealer-settings-contact-options');
$contact = is_array($contact) ? $contact : [];
$lb = get_option('leadbox-settings');
$lb = is_array($lb) ? $lb : [];
$name = trim((string) ($contact['dealer_name'] ?? ''));
if ($name === '') {
$name = (string) get_bloginfo('name');
}
// Marcas: manufacturer + all_brands (mismas fuentes que el schema AutoDealer).
$brands = [];
foreach (array_merge([(string) ($lb['manufacturer'] ?? '')], explode(',', (string) ($lb['all_brands'] ?? ''))) as $brand) {
$brand = trim($brand);
$key = strtolower($brand);
if ($brand !== '' && !isset($brands[$key])) {
$brands[$key] = $brand === $key ? ucwords($brand) : $brand;
}
}
$brands = array_values($brands);
$city = trim((string) ($contact['address_city'] ?? ''));
$province = trim((string) ($contact['address_province'] ?? ''));
$street = trim((string) ($contact['address_line_1'] ?? ''));
$postal = trim((string) ($contact['address_postal_code'] ?? ''));
$lines = [];
$lines[] = '# ' . $name;
$lines[] = '';
$summary = $name . ' is a' . (!empty($brands) ? ' ' . implode(', ', $brands) : 'n automotive') . ' dealership';
if ($city !== '') {
$summary .= ' in ' . $city . ($province !== '' ? ', ' . $province : '') . ', Canada';
}
$summary .= '. New and used vehicle sales, service, parts and financing.';
$lines[] = '> ' . $summary;
$lines[] = '';
$address = trim(implode(', ', array_filter([$street, $city, trim($province . ' ' . $postal)])));
if ($address !== '') {
$lines[] = '- Address: ' . $address;
}
foreach (['Sales' => 'sale_phone', 'Service' => 'service_phone', 'Parts' => 'parts_phone'] as $label => $key) {
$phone = trim((string) ($contact[$key] ?? ''));
if ($phone !== '') {
$lines[] = '- ' . $label . ' phone: ' . $phone;
}
}
$lines[] = '- Website: ' . home_url('/');
$lines[] = '';
// Horarios por departamento, un bullet por depto.
$hours_lines = [];
foreach (['Sales' => 'sale', 'Service' => 'service', 'Parts' => 'parts'] as $label => $type) {
$line = lbx_llms_txt_hours_line($type);
if ($line !== '') {
$hours_lines[] = '- ' . $label . ': ' . $line;
}
}
if (!empty($hours_lines)) {
$lines[] = '## Hours';
$lines[] = '';
$lines = array_merge($lines, $hours_lines);
$lines[] = '';
}
// Hubs de inventario: páginas con el bloque inventory clasificadas por status.
$srp = lbx_llms_txt_find_srp_pages();
$inventory_lines = [];
if (!empty($srp['new'])) {
$inventory_lines[] = '- [New vehicles](' . $srp['new'] . '): New' . (!empty($brands) ? ' ' . implode(', ', $brands) : '') . ' inventory with pricing and availability';
}
if (!empty($srp['used'])) {
$inventory_lines[] = '- [Used vehicles](' . $srp['used'] . '): Pre-owned inventory with pricing and availability';
}
if (empty($inventory_lines) && !empty($srp['all'])) {
$inventory_lines[] = '- [Vehicle inventory](' . $srp['all'] . '): Full inventory with pricing and availability';
}
if (!empty($inventory_lines)) {
$lines[] = '## Inventory';
$lines[] = '';
$lines = array_merge($lines, $inventory_lines);
$lines[] = '';
}
// Hubs de servicios y contacto: primera página publicada que matchee un slug candidato.
$sections = [
'Services' => [
['Service', ['service', 'service-department', 'schedule-service', 'book-a-service', 'book-service', 'auto-service', 'entretien', 'rendez-vous-service'], 'Vehicle service and maintenance appointments'],
['Parts', ['parts', 'order-parts', 'parts-department', 'auto-parts', 'pieces', 'commander-des-pieces'], 'OEM parts and accessories'],
['Financing', ['financing', 'finance', 'apply-for-financing', 'get-approved', 'car-loans', 'credit', 'financement', 'demande-de-credit'], 'Vehicle financing and credit applications'],
],
'About' => [
['Contact', ['contact', 'contact-us', 'contactez-nous', 'nous-joindre'], 'Location, directions and contact form'],
['About us', ['about', 'about-us', 'dealership', 'our-dealership', 'our-story', 'a-propos'], 'Dealership information'],
],
];
foreach ($sections as $section => $entries) {
$section_lines = [];
foreach ($entries as $entry) {
list($label, $slugs, $description) = $entry;
$url = lbx_llms_txt_find_page($slugs);
if ($url !== '') {
$section_lines[] = '- [' . $label . '](' . $url . '): ' . $description;
}
}
if (!empty($section_lines)) {
$lines[] = '## ' . $section;
$lines[] = '';
$lines = array_merge($lines, $section_lines);
$lines[] = '';
}
}
return implode("\n", $lines);
}
/**
* Encuentra los SRP del site: páginas publicadas con el bloque
* leadbox/inventory-block con el filtro activo y sin preset de modelo
* (los showcases tipo "Bronco" no son hubs de inventario). Clasifica por
* el preset de status en new/used/all y devuelve el permalink de la
* primera página (ID ascendente) de cada grupo.
*
* @return array{new: string, used: string, all: string}
*/
function lbx_llms_txt_find_srp_pages()
{
global $wpdb;
$hubs = ['new' => '', 'used' => '', 'all' => ''];
$rows = $wpdb->get_results(
"SELECT ID, post_content FROM {$wpdb->posts}
WHERE post_type = 'page' AND post_status = 'publish'
AND post_content LIKE '%wp:leadbox/inventory-block%'
ORDER BY ID ASC"
);
foreach ((array) $rows as $row) {
if (!preg_match_all('/<!--\s*wp:leadbox\/inventory-block\s*(\{.*?\})?\s*\/?-->/s', (string) $row->post_content, $matches)) {
continue;
}
foreach ($matches[1] as $json) {
$attrs = $json !== '' ? json_decode($json, true) : [];
if (!is_array($attrs)) {
$attrs = [];
}
if (!filter_var($attrs['filter'] ?? false, FILTER_VALIDATE_BOOLEAN)) {
continue;
}
if (!empty($attrs['model_selected']) || (!empty($attrs['model']) && $attrs['model'] !== 'all')) {
continue;
}
$status = strtolower(trim((string) ($attrs['status'] ?? 'all')));
$key = in_array($status, ['new', 'used'], true) ? $status : 'all';
if ($hubs[$key] === '') {
$hubs[$key] = (string) get_permalink($row->ID);
}
}
}
return $hubs;
}
/**
* Permalink de la primera página publicada cuyo slug matchee la lista de
* candidatos, o '' si ninguno existe.
*
* @param array $slugs Slugs candidatos en orden de preferencia.
* @return string
*/
function lbx_llms_txt_find_page(array $slugs)
{
foreach ($slugs as $slug) {
$page = get_page_by_path($slug);
if ($page instanceof WP_Post && $page->post_status === 'publish') {
return (string) get_permalink($page);
}
}
return '';
}
/**
* Línea compacta de horarios de un departamento a partir de la option
* dealer-settings-hours-options: "Monday 9:00 am - 6:00 pm | ... | Sunday Closed".
* Días sin datos se omiten; devuelve '' si el departamento no tiene nada.
*
* @param string $type Prefijo del departamento: sale | service | parts.
* @return string
*/
function lbx_llms_txt_hours_line($type)
{
$hours = get_option('dealer-settings-hours-options');
if (!is_array($hours)) {
return '';
}
$parts = [];
foreach (['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] as $day) {
if (!empty($hours["{$type}_hours_{$day}_closed"])) {
$parts[] = ucfirst($day) . ' Closed';
continue;
}
$start = trim((string) ($hours["{$type}_hours_{$day}_start"] ?? ''));
$end = trim((string) ($hours["{$type}_hours_{$day}_end"] ?? ''));
if ($start !== '' && $end !== '') {
$parts[] = ucfirst($day) . ' ' . $start . ' - ' . $end;
}
}
return implode(' | ', $parts);
}