POR QUE EXISTE producto.html es una sola pagina para los 500 productos: el titulo, la foto y el precio los pone JavaScript despues de cargar. Un navegador lo ve bien. Pero WhatsApp, Facebook, Instagram y el correo NO corren JavaScript: al compartir un producto leian "Producto โ€” SIC Tienda", sin foto y sin precio. En la tienda anterior (Shopify) el mismo enlace salia con la foto del equipo. Y Google tampoco tenia de donde sacar el precio ni la disponibilidad. QUE HACE prototipo/.htaccess manda aca las visitas a producto.html?p=... Esto lee producto.html TAL CUAL y le agrega al lo del producto pedido: , descripcion, direccion canonica, Open Graph y los datos de producto que lee Google (JSON-LD). El cuerpo de la pagina no cambia: sigue siendo el mismo producto.html, con el mismo JavaScript. SI ALGO FALLA, SE SIRVE producto.html SIN TOCAR: la base caida, un producto que no existe o despublicado, lo que sea. Esto es un adorno para quien comparte y para Google; no puede botar la pagina de venta. El precio que se publica es producto.precio, el mismo que cobra el servidor con tarjeta. No se inventa nada. ========================================================= */ declare(strict_types=1); ini_set('display_errors', '0'); /* urlPagina(): la direccion de la ficha sin /prototipo en un dominio propio */ require_once dirname(__DIR__) . '/api/sitio.php'; $pagina = @file_get_contents(__DIR__ . '/producto.html'); if ($pagina === false) { http_response_code(404); exit; } header('Content-Type: text/html; charset=utf-8'); header('Cache-Control: no-cache, must-revalidate'); /* lo mismo que el .htaccess le pone a los .html */ $entregar = function (string $html): void { echo $html; exit; }; try { $slug = isset($_GET['p']) && is_string($_GET['p']) ? trim($_GET['p']) : ''; if ($slug === '' || mb_strlen($slug) > 200 || preg_match('//u', $slug) !== 1) $entregar($pagina); $CFG = require dirname(__DIR__) . '/api/config.php'; $db = new PDO(sprintf('mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', $CFG['host'], $CFG['puerto'], $CFG['base']), $CFG['usuario'], $CFG['clave'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_TIMEOUT => 4]); $s = $db->prepare('SELECT p.id, p.slug, p.titulo, p.precio, p.origen, p.condicion, p.descripcion, p.controla_stock, p.stock_disponible, m.nombre AS marca FROM producto p LEFT JOIN marca m ON m.id = p.marca_id WHERE p.slug = ? AND p.publicado = 1'); $s->execute([$slug]); $p = $s->fetch(); if (!$p) $entregar($pagina); $s = $db->prepare('SELECT ruta FROM producto_imagen WHERE producto_id = ? ORDER BY posicion, id LIMIT 4'); $s->execute([(int) $p['id']]); $sitio = rtrim((string) ($CFG['sitio'] ?? ''), '/'); $fotos = []; foreach ($s->fetchAll(PDO::FETCH_COLUMN) as $ruta) { $ruta = (string) $ruta; if (preg_match('~^https?://~i', $ruta)) $fotos[] = $ruta; elseif (strpos($ruta, '//') === 0) $fotos[] = 'https:' . $ruta; elseif ($sitio !== '') $fotos[] = $sitio . '/' . implode('/', array_map('rawurlencode', explode('/', $ruta))); } /* Lo mismo que decide api/catalogo.php */ $controla = (int) $p['controla_stock'] === 1; $disponible = $p['origen'] !== 'sin-stock' && (!$controla || (int) $p['stock_disponible'] > 0); $entrega = !$disponible ? 'Agotado por ahora' : ($p['origen'] === 'importacion' ? 'A pedido, por importacion' : 'Disponible en Chile'); $titulo = trim((string) $p['titulo']); $precio = (int) $p['precio']; $pesos = '$' . number_format($precio, 0, ',', '.'); $url = urlPagina($sitio, 'producto.html?p=' . rawurlencode((string) $p['slug'])); $texto = trim((string) preg_replace('/\s+/u', ' ', html_entity_decode(strip_tags((string) $p['descripcion']), ENT_QUOTES | ENT_HTML5, 'UTF-8'))); $desc = $pesos . ' ยท ' . $entrega . '. ' . ($texto !== '' ? $texto : 'Despacho a todo Chile y retiro en Talca. Garantia y servicio tecnico propio en SIC Tienda.'); if (mb_strlen($desc) > 200) $desc = rtrim(mb_substr($desc, 0, 197)) . '...'; $e = fn(string $v): string => htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); $ld = [ '@context' => 'https://schema.org', '@type' => 'Product', 'name' => $titulo, 'description' => $desc, 'url' => $url, 'offers' => [ '@type' => 'Offer', 'url' => $url, 'priceCurrency' => 'CLP', 'price' => $precio, 'availability' => 'https://schema.org/' . (!$disponible ? 'OutOfStock' : ($p['origen'] === 'importacion' ? 'BackOrder' : 'InStock')), 'seller' => ['@type' => 'Organization', 'name' => 'SIC Tienda'], ], ]; if ($fotos) $ld['image'] = $fotos; if (trim((string) $p['marca']) !== '') $ld['brand'] = ['@type' => 'Brand', 'name' => (string) $p['marca']]; $cabeza = "\n<title>" . $e($titulo) . " โ€” SIC Tienda\n" . '\n" . '\n" . '' . "\n" . '' . "\n" . '' . "\n" . '\n" . '\n" . '\n" . ($fotos ? '\n" : '') . '\n" . '' . "\n" . '\n" /* JSON_HEX_TAG: un "" escrito en un titulo no puede cerrar el bloque */ . '\n" /* La descripcion guardada en la base (en los PC de RS Tech, sus notas). El catalogo no la trae para no cargar cientos de KB en cada pagina; va solo en la del producto. producto.html la muestra cuando no hay descripcion de SIC en descripciones.js, y el modo edicion la usa para editarla. 27-09-2026. */ . '\n"; /* Se reemplaza el generico, una sola vez. Si la pagina ya no lo trae como se espera, no se adivina: va sin tocar. */ $generico = '<title>Producto โ€” SIC Tienda'; if (substr_count($pagina, $generico) !== 1) $entregar($pagina); $entregar(str_replace($generico, ltrim($cabeza), $pagina)); } catch (Throwable $ex) { error_log('prototipo/producto.php: ' . get_class($ex) . ' ' . $ex->getCode()); /* sin el mensaje: el de PDO trae host y usuario */ echo $pagina; }