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" . $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 = '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;
}