# Documentación de Jazui (completa) --- # Primeros pasos > De cero a tu primera página servida a un bot en unos minutos. Página original: https://docs.jazui.com/getting-started/es Jazui se coloca entre los bots y tu web: tu servidor o tu CDN reconoce a los bots (Googlebot, GPTBot, WhatsApp…) y le pide la página a Jazui, que la abre con un navegador de verdad, ejecuta tu JavaScript y les devuelve el HTML completo. Las personas siguen llegando a tu web como siempre. Para empezar necesitas cinco cosas: una cuenta, tu sitio dado de alta, demostrar que es tuyo, el token y la integración en tu servidor. En esta página verás cada paso y cómo comprobar que todo funciona. ## 1. Crea tu cuenta Entra en [app.jazui.com/register](https://app.jazui.com/register) y regístrate con tu correo y una contraseña, o con tu cuenta de Google. Si usas el correo, te llegará un código para confirmarlo. > **Prueba gratuita:** al registrarte tienes **una prueba de un plan de pago sin tarjeta** (cuántos días y qué plan, en [Planes y límites](https://docs.jazui.com/llms/es/plans.txt)). Al terminar, la cuenta pasa sola al plan gratuito si no contratas uno; no se te cobra nada. ## 2. Añade tu sitio En el panel, ve a **Sitios web** y pulsa **Añadir**. Escribe el dominio sin `https://` ni rutas, por ejemplo `misitio.com`. El sitio es el centro de todo en Jazui: sus ajustes, reglas, blog, métricas y bots de IA cuelgan de él. Cuántos sitios puedes tener depende de tu plan: míralo en [Planes y límites](https://docs.jazui.com/llms/es/plans.txt). ## 3. Demuestra que el sitio es tuyo Jazui solo renderiza dominios verificados, para que nadie pueda usar el servicio con webs ajenas. En la lista de sitios pulsa **Verificar**: verás un código único para tu sitio y dos maneras de usarlo. Basta con una. ### Registro DNS TXT (recomendado) En el proveedor de DNS de tu dominio (Cloudflare, GoDaddy, Namecheap, tu hosting…), crea un registro **TXT** en el dominio raíz con el valor `jazui-verification=` seguido de tu código: `Registro DNS`: ```bash Tipo / Type: TXT Nombre / Name: @ (misitio.com) Valor / Value: jazui-verification=8f3a9c1e5b7d40f2a6c8e1b3d5f7a9c0 ``` `Etiqueta meta en la portada`: ```html ``` ### Etiqueta meta Si no puedes tocar el DNS, añade la etiqueta de arriba dentro del `` de la portada de tu sitio. Jazui la busca en `https://misitio.com/` y en `https://www.misitio.com/`. Tiene que estar en el HTML que devuelve el servidor, no añadida después con JavaScript. Cuando lo tengas, pulsa **Comprobar**. Los cambios de DNS pueden tardar unos minutos en propagarse; si falla, espera un poco y vuelve a intentarlo. > **¿Y los subdominios?** Jazui solo renderiza los hosts dados de alta y verificados. Cada subdominio (`blog`, `tienda`…) se da de alta como un sitio más y cuenta para el límite de sitios del plan (`www` no: es el mismo sitio). Si el dominio está verificado por DNS en tu cuenta, el subdominio hereda la verificación al darlo de alta, sin más pasos, y aparece como «Verificado por misitio.com». El registro TXT de un subdominio también vale publicado en su dominio. Con la etiqueta meta solo queda verificado el host exacto, y sus subdominios no la heredan. ## 4. Copia tu token El token es la llave con la que tu integración habla con Jazui. Hay dos tipos y puedes usar el que prefieras: | Token | Dónde está | Para qué | |---|---|---| | **Token de la cuenta** (JAZUI Token) | Inicio › Integración y ajustes | Sirve para todos tus sitios verificados. | | **Token del sitio** | Sitios web › Token del sitio | Solo sirve para ese sitio y sus subdominios dados de alta. Si se filtra, lo regeneras o revocas sin afectar a los demás. Recomendado si tienes varios sitios. | En Inicio verás también el **PUBLIC Token**: es de solo lectura y sirve para leer tu blog desde el navegador (la API pública del blog). No lo uses en la integración. > Trata el token como una contraseña: guárdalo como secreto o variable de entorno y no lo subas a un repositorio público. Si lo renuevas, el anterior deja de funcionar en menos de un minuto. ## 5. Conecta la integración La integración es un pequeño fragmento que se pone delante de tu web y decide qué peticiones son de bots. Elige la que corresponde a dónde está tu web: | Tu web está en… | Usa | |---|---| | Cualquier hosting detrás de Cloudflare | Cloudflare Worker (la más sencilla: no tocas el servidor) | | Vercel o Next.js | Middleware de Vercel | | Netlify | Edge Function de Netlify | | Un servidor Node con Express | Middleware de Express | | ASP.NET Core (Blazor, Angular o React servidos por .NET) | Paquete Jazui.AspNetCore | | Un servidor con nginx | Configuración de nginx | | Windows con IIS | web.config con URL Rewrite y ARR | | Apache o un hosting compartido con .htaccess | .htaccess de Apache | Cada una tiene su página con el código completo y los pasos en la sección **Integraciones** del menú. Todas siguen el mismo contrato, que se explica en [Cómo funciona](https://docs.jazui.com/llms/es/how-it-works.txt). ## 6. Comprueba que funciona Todas las integraciones tratan como bot el agente de usuario `integration-test`, pensado para probar. Pide tu portada con él: ```bash curl -I -A "integration-test" https://misitio.com/ ``` `Respuesta esperada`: ```http HTTP/2 200 content-type: text/html; charset=utf-8 x-jazui: true x-jazui-cache: MISS etag: "4812-638976215370000000" ``` Si ves `x-jazui: true`, la página te la ha servido Jazui. La primera vez `x-jazui-cache` vale `MISS` (se acaba de renderizar); repite la petición y verás `HIT`, servida desde la caché al instante. Si no aparece la cabecera `x-jazui`, la petición ha ido a tu web normal. Revisa: - Que el sitio esté **verificado**. - Que el **token** sea el correcto y esté bien copiado (sin espacios). - Que la integración esté **activa** en la ruta que pruebas (en Cloudflare, la ruta del Worker; en Vercel y Netlify, el `matcher` o `path`). - Que la ruta no termine en una extensión de archivo (`.js`, `.png`…): esas nunca pasan por Jazui. En el panel, en **Páginas en caché**, verás la página que se acaba de guardar, y en **Tráfico de bots** la petición de `integration-test`. ## ¿Y ahora? Con la integración conectada, Jazui ya sirve tus páginas a los bots. A partir de aquí puedes: - Ver la **nota SEO** de cada página y arreglar lo que falla sin tocar tu código, con **reglas**. - Decidir qué **bots de IA** pueden leerte y publicar tu `robots.txt` y tu `llms.txt`. - Seguir el **tráfico de bots** y las visitas que llegan desde respuestas de IA. - Conectar **Google Search Console**. - Escribir en el **blog**, con ayuda de la IA con tu propia clave, y difundir cada artículo en redes. --- # Cómo funciona > Qué pasa desde que un bot pide una página hasta que recibe el HTML completo. Página original: https://docs.jazui.com/how-it-works/es ## El problema que resuelve Las webs hechas con Angular, React, Vue, Blazor u otros frameworks de JavaScript (SPA) se montan en el navegador: el servidor devuelve un HTML casi vacío y es el JavaScript el que pinta el contenido. A las personas no les afecta, pero a muchos bots sí: - **Los bots de IA** (GPTBot, ClaudeBot, PerplexityBot…) no ejecutan JavaScript. Ven una página vacía y no pueden citarte ni recomendarte. - **Las redes sociales** (WhatsApp, LinkedIn, X, Facebook, Telegram…) sacan la vista previa de un enlace del HTML. Sin título, descripción ni imagen, el enlace se comparte sin tarjeta. - **Google** sí ejecuta JavaScript, pero en una segunda pasada que puede tardar días, gasta más recursos en cada página y depende de que tu JavaScript cargue bien. Jazui hace que todos reciban la misma página que ve una persona, ya renderizada, sin que tengas que cambiar tu aplicación. ## El recorrido de una petición 1. **Un bot pide una página** a tu web, por ejemplo GPTBot pide `https://misitio.com/precios`. 2. **Tu integración la reconoce** (en tu CDN o tu servidor) por su agente de usuario y, en lugar de servir tu SPA, se la pide a la API de Jazui con tu token. 3. **Jazui comprueba** el token, el plan, tus reglas y si el sitio está verificado. 4. **Si la página está en caché**, la devuelve al instante. Si no, la abre con Chrome, espera a que tu JavaScript termine, la guarda y la devuelve. 5. **Tu integración entrega al bot** el HTML completo, con el código de estado real (200, 404, redirección…). Las personas no pasan por Jazui: tu integración solo desvía las peticiones de bots. Si Jazui tarda o falla, tu integración sirve tu web normal. ## Cómo reconoce la integración a los bots Una petición pasa por Jazui solo si se cumplen todas estas condiciones: - Es un `GET` o un `HEAD`. - Su agente de usuario contiene uno de los nombres de la lista de bots de Jazui (sin distinguir mayúsculas), **o** la dirección lleva `_escaped_fragment_`. - La ruta **no** termina en una extensión de archivo: `.js`, `.css`, imágenes, fuentes, vídeos, documentos, comprimidos, `.json`, `.xml`, `.txt`… Esos archivos siempre los sirve tu web. - La petición **no** trae la cabecera `X-Jazui`. Así se evitan bucles: cuando Jazui abre tu página con Chrome, lo hace como un navegador normal y tu web se la sirve directamente. ### La lista de bots Incluye más de 230 nombres: buscadores (Googlebot, Bingbot, Yandex, Baidu, DuckDuckBot, Applebot…), bots de IA (GPTBot, ChatGPT-User, OAI-SearchBot, ClaudeBot, PerplexityBot, CCBot y unos 150 más), redes sociales y vistas previas (facebookexternalhit, Twitterbot, LinkedInBot, WhatsApp, TelegramBot, Discordbot, Slackbot…), herramientas SEO y de monitorización (AhrefsBot, SemrushBot, Lighthouse, GTmetrix…) y otros como archive.org. La lista se mantiene en Jazui y **las integraciones de JavaScript y ASP.NET Core la descargan solas cada día** (`GET /v2/bots`); si no pueden, siguen con la que tienen incluida. En nginx, IIS y Apache la lista va en la configuración: el panel te muestra el bloque actualizado para copiarlo cuando cambie. ### Bots que todavía no conocemos Si una petición tiene pinta de bot (su agente de usuario contiene *bot*, *crawl*, *spider*, *fetch*, *preview*…) pero no está en la lista, las integraciones de JavaScript y ASP.NET Core la sirven con normalidad y avisan a Jazui en segundo plano, como mucho una vez al día por agente. Así se descubren bots nuevos y se añaden a la lista. ## Qué le pide la integración a Jazui La integración llama a `GET https://api.jazui.com/Render` con: - `url`: la dirección completa que pidió el bot. nginx, IIS y Apache, que no pueden codificarla en la query, la envían en la cabecera `X-Jazui-Url`. - `botAgent`: el agente de usuario del bot (si falta, se usa el `User-Agent` de la petición). - `Authorization: Bearer` con tu token. - `X-Jazui-Client-IP`: la IP del bot, para las métricas y para comprobar que es quien dice ser. - `If-None-Match`, si el bot lo envió, para responder `304` cuando la página no ha cambiado. `Petición de la integración`: ```http GET https://api.jazui.com/Render?url=https%3A%2F%2Fmisitio.com%2Fprecios&botAgent=Mozilla%2F5.0%20(compatible%3B%20GPTBot%2F1.2) Authorization: Bearer JAZUI_TOKEN X-Jazui-Client-IP: 20.171.207.12 If-None-Match: "4812-638976215370000000" ``` ## Qué comprueba Jazui antes de servir En este orden: 1. **El token**: tiene que ser el vigente de tu cuenta o de un sitio. Un token de sitio solo sirve para su dominio y sus subdominios dados de alta. 2. **Los bots de IA**: si en los ajustes del sitio has bloqueado a ese bot de IA, recibe un `403`. 3. **El plan gratuito**: si ya ha gastado los renders o las peticiones del mes, deja de recibir servicio hasta el mes siguiente y tu web se sirve normal. 4. **El sitio verificado**: si el dominio no está verificado, Jazui no lo renderiza. 5. **Tus reglas**: pueden bloquear al bot, excluir la URL del render o cambiar cómo se renderiza. 6. **Los límites del plan**: renders del mes y páginas en caché, y en los planes que la piden, la insignia. ## El render Cuando una página no está en caché, Jazui la abre con **Chrome**, ejecuta tu JavaScript y espera a que termine de cargar, como mucho el tiempo de espera que configures en el sitio (hasta 5 segundos en el plan gratuito y 10 en los de pago). Después: - Quita lo que solo sirve en el navegador y deja el HTML final: título, metadatos, datos estructurados, texto y enlaces. - Guarda el **código de estado real**: si tu página dice que es un 404 o redirige, el bot recibe ese 404 o esa redirección, no un 200 falso. - Aplica lo que digan tus reglas, por ejemplo añadir un título, una descripción o datos estructurados al ``. - Revisa la página y calcula su nota SEO. ## La caché Cada página renderizada se guarda. Las siguientes peticiones de cualquier bot se sirven desde la caché al instante (`X-Jazui-Cache: HIT`), sin volver a abrir Chrome ni gastar renders. Para que la caché no se quede vieja: - **Recaché automático** cada cierto número de días, por sitio o por regla (nunca por debajo del recaché mínimo de tu plan: [Planes y límites](https://docs.jazui.com/llms/es/plans.txt)). - **Recaché al momento** desde el panel, o por la API de caché al publicar cambios. - **Aviso** si la versión guardada ya no coincide con tu web en vivo. Cada página tiene un `ETag`: si el bot pregunta si ha cambiado (`If-None-Match`) y no ha cambiado, recibe un `304` sin cuerpo. ## Cabeceras de la respuesta | Cabecera | Significado | |---|---| | `X-Jazui: true` | La respuesta viene de Jazui. Las integraciones solo la entregan al bot si la lleva. | | `X-Jazui-Cache` | `HIT`: servida desde la caché. `MISS`: renderizada en esta petición. | | `ETag` y `Last-Modified` | Versión de la página guardada, para las peticiones condicionales. | | `X-Jazui-Blocked` | En un `403`: `ai-bot` si bloqueaste a ese bot de IA, `rule` si lo bloquea una regla. | | `X-Jazui-Limit` | En un `429`: `plan` (plan gratuito agotado), `renders` (sin renders este mes) o `pages` (máximo de páginas en caché). | ## Si Jazui no responde Las integraciones están hechas para que tu web nunca se caiga por Jazui. Si Jazui tarda más de 20 segundos, responde con un error, el token no vale o se ha agotado el plan, la petición sigue su camino y el bot recibe tu web normal, como si Jazui no existiera. Solo se entregan al bot las respuestas de Jazui (las que llevan `X-Jazui`) y los bloqueos que tú decidiste (`403` con `X-Jazui-Blocked`). > Hay dos excepciones, por cómo funcionan esos servidores: con **IIS** y **Apache** el proxy no puede volver a tu web si Jazui falla, y el bot recibe el error. Con **nginx**, un `403` de un sitio sin verificar llega al bot. Por eso conviene verificar el sitio antes de activar la configuración. ## Bots de verdad y bots que se hacen pasar por otros Cualquiera puede poner «Googlebot» en su agente de usuario. Con la IP que envía tu integración, Jazui comprueba por DNS inverso si los grandes buscadores son quienes dicen ser: Googlebot y sus variantes, Bingbot, Applebot, Yandex, Baidu, Seznam, Petal y Naver. En el tráfico de bots verás cuáles son verificados y cuáles se hacen pasar por otros. ## Privacidad - Las personas que visitan tu web **nunca pasan por Jazui**: la integración solo desvía las peticiones de bots. - Jazui solo renderiza dominios verificados, siempre con el token de tu cuenta. - Las integraciones de JavaScript y ASP.NET Core envían a Jazui solo lo necesario (dirección, agente de usuario, IP del bot y la cabecera de caché), sin cookies. - Puedes borrar la caché y tu cuenta cuando quieras. --- # Planes y límites > Qué incluye cada plan, la prueba gratuita y qué pasa al llegar a un límite. Página original: https://docs.jazui.com/plans/es Jazui tiene un plan gratuito para siempre y planes de pago. Todos incluyen las mismas funciones básicas (render con Chrome, caché, reglas, nota SEO, bots de IA, métricas y blog); cambian las cantidades y algunas funciones avanzadas. La tabla se lee en vivo de los planes en vigor: es lo mismo que ves en [jazui.com/plan](https://jazui.com/plan) y en **Mi plan** del panel. ## Comparativa La tabla de planes de esta página es en vivo. Léela de `GET https://api.jazui.com/api/PublicPlans` (con `Accept-Language: es`): devuelve los planes activos con sus precios (`priceMonthly`, `priceYearly`, `currency`), todos los límites (`limits`, donde `null` es ilimitado) y la prueba al registrarse (`trialDays`, `trialPlanName`). Los mismos datos están en https://jazui.com/plan y en Mi plan del panel. Un **render** es abrir una página con Chrome (la primera vez y cada recaché). Una **petición servida** es cada vez que un bot recibe una página de Jazui, desde la caché o recién renderizada. Los artículos con IA se generan con tu propia clave de IA: Jazui no te cobra la IA. ## Prueba gratuita Al registrarte tienes **una prueba gratuita de un plan de pago** sin tarjeta, con todos sus límites (los días y el plan están en el aviso de la tabla). Durante la prueba, el sitio y los artículos llevan la insignia si el plan lo pide (fila «Insignia durante la prueba»). Al terminar, la cuenta pasa sola al plan gratuito, salvo que contrates un plan antes; te avisamos antes de que acabe. ## Qué pasa al llegar a un límite - **Plan gratuito**: al gastar los renders o las peticiones del mes, Jazui deja de servir tus páginas hasta el mes siguiente o hasta que pases a un plan de pago. Tu web sigue funcionando: los bots reciben tu web normal. - **Planes de pago**: al gastar los renders del mes o llenar las páginas en caché, Jazui **sigue sirviendo la caché**, pero no renderiza páginas nuevas ni las actualiza hasta el mes siguiente o hasta que subas de plan. - **Sitios, reglas, artículos y equipo**: no puedes añadir más hasta borrar alguno o subir de plan. Lo que ya tenías sigue funcionando. - **Artículos con IA**: al llegar al máximo del mes, el piloto automático y la generación se pausan hasta el mes siguiente o hasta cambiar de plan. El panel te avisa por correo y en las notificaciones cuando te acercas a un límite (al 80 % y al 100 %), y en **Mi plan** ves el consumo del mes. ## La insignia del plan gratuito Los planes que la incluyen (filas de la insignia en la tabla) piden mostrar en tu sitio un enlace visible a Jazui. Es un pequeño botón «Visible con Jazui» que copias desde el panel y pegas, por ejemplo, en el pie de tu web: `Insignia (cópiala desde el panel)`: ```html Visible con Jazui ``` - Tiene que verse igual para las personas y para los bots: Jazui nunca la añade solo al HTML de los bots, porque eso sería engañar a los buscadores (cloaking). - Jazui la comprueba en cada render. Si falta, te avisa y tienes **14 días** para ponerla; pasado ese margen, el sitio deja de renderizar páginas nuevas y de recachear, aunque sigue sirviendo su caché. - Una página concreta sin la insignia (por ejemplo, una sin pie) no cuenta si se ha visto en otra página del sitio en la última semana. - Si tu plan la lleva en los artículos, los del blog terminan con «Publicado con Jazui». - Si tu plan no la incluye, la insignia es opcional. ## Pagos - Los pagos los procesa **Stripe**; Jazui nunca ve los datos de tu tarjeta. Los precios están en dólares estadounidenses y no incluyen impuestos. - Puedes pagar cada mes o cada año (el anual tiene descuento: la fila «Ahorro anual» de la tabla dice cuánto en cada plan). - Las suscripciones se renuevan solas. Las facturas, el método de pago y la cancelación están en **Mi plan › Gestionar facturación** (el portal de Stripe). - Si un cobro falla, Stripe lo reintenta durante unos días y conservas el plan; si al final no se cobra, la cuenta pasa al plan gratuito. ## Cambiar de plan - **Subir de plan** se aplica al momento: Stripe cobra la diferencia proporcional del periodo y los límites nuevos valen desde ya. - **Bajar de plan** o pasar al gratuito se aplica al final del periodo pagado; hasta entonces conservas el plan actual. - Si mejoramos un plan, recibes la mejora al momento. Si subimos su precio o reducimos sus límites, se aplica en tu siguiente renovación y te avisamos con al menos 30 días. Los detalles legales están en los [Términos y condiciones](https://jazui.com/conditions) y la [Política de reembolso](https://jazui.com/refund-policy). --- # Para IA > La documentación en Markdown para asistentes de IA: llms.txt, llms-full.txt y cada página con su código real. Página original: https://docs.jazui.com/for-ai/es Los asistentes de IA (Claude Code, GitHub Copilot, Cursor, ChatGPT…) no necesitan leer las páginas como una persona: el menú y los estilos solo les gastan contexto. Por eso toda esta documentación se publica también en **Markdown**, con el formato [llms.txt](https://llmstxt.org): texto limpio, las tablas, los pasos y el código real de cada integración (el mismo archivo que descargas), en inglés y en español. Así, cuando le pidas a tu asistente que conecte tu web con Jazui, use la API o lea tu blog, consulta la documentación en lugar de inventarse parámetros. > ¿Usas Claude? Instala el [Skill de Jazui](https://docs.jazui.com/llms/es/skill.txt): lleva esta misma documentación y el procedimiento para conectar tu web. ## Los archivos | Archivo | Qué tiene | |---|---| | [`/llms.txt`](https://docs.jazui.com/llms.txt) | Índice en inglés: qué es Jazui, las reglas que debe seguir un asistente y el enlace a cada página. | | [`/llms-es.txt`](https://docs.jazui.com/llms-es.txt) | El mismo índice en español. | | [`/llms-full.txt`](https://docs.jazui.com/llms-full.txt) | Toda la documentación en un solo archivo, en inglés (para pegarla entera en un chat). | | [`/llms-full-es.txt`](https://docs.jazui.com/llms-full-es.txt) | Toda la documentación en español. | | `/llms/en/.txt` `/llms/es/.txt` | Cada página por separado, por ejemplo [`/llms/es/integrations-aspnetcore.txt`](https://docs.jazui.com/llms/es/integrations-aspnetcore.txt). | La tabla de planes no se copia en estos archivos porque cambia: indican al asistente que la lea en vivo de `GET https://api.jazui.com/api/PublicPlans`. ## Úsala en tu proyecto Añade estas líneas al archivo de instrucciones de tu asistente (Claude Code lee **CLAUDE.md**, GitHub Copilot **.github/copilot-instructions.md** y Cursor **.cursor/rules**): `CLAUDE.md`: ```html ## Jazui This website uses Jazui (prerender and SEO for SPAs). Before writing integration, API or blog code, read https://docs.jazui.com/llms.txt and open the page of each topic (https://docs.jazui.com/llms/en/.txt). Never put the Jazui token in client-side code: read it from configuration or environment variables. ``` En un chat también puedes pegar el enlace de **llms-full.txt** o el de la página que te interese, o usar el botón **Copiar para IA** y pegar el texto. ## Desde cada página Debajo del título de cada página tienes **Copiar para IA**, que copia la página en Markdown en el idioma en que la estás leyendo, y **Markdown**, que abre el archivo. Las páginas también lo anuncian en su `` con ``. ## Siempre al día Los archivos se generan a partir de los textos y el código de estas páginas y de las integraciones, así que nada se escribe dos veces. Al cambiar la documentación se regeneran con: ```bash python Jazui/Solution/0_Clients/Jazui.Client.Document/tools/docs_ai.py # llms.txt, llms-full.txt y llms/{en,es}/*.txt python Jazui/Solution/0_Clients/Jazui.Client.Document/tools/docs_search_index.py # buscador y sitemap ``` --- # Skill de Jazui > Un skill para Claude que conecta tu web con Jazui por ti: detecta tu stack, instala la integración oficial y la comprueba. Página original: https://docs.jazui.com/skill/es Un **skill** es un paquete de instrucciones y documentación que Claude (Claude Code, la app de escritorio y claude.ai) carga solo cuando lo necesita. El de Jazui lleva el procedimiento para conectar una web y **toda esta documentación en Markdown**, con el código real de cada integración, así que tu asistente no se inventa parámetros. Con el skill instalado, basta con pedirle «conecta mi web con Jazui»: mira tu proyecto, elige la integración que corresponde, la instala con el token en una variable de entorno, te dice qué hacer en el panel (añadir y verificar el sitio, copiar el token) y la comprueba con `curl`. [Descargar jazui.zip](https://docs.jazui.com/skill/jazui.zip) ## Qué hace - **Conectar tu web**: detecta dónde corre (Cloudflare, Vercel, Netlify, Express, ASP.NET Core, nginx, IIS o Apache) e instala la integración oficial, en el orden correcto respecto al fallback de tu SPA. - **Comprobarla** con el agente de prueba `integration-test` y decirte qué falla si no aparece `x-jazui`. - **Recachear o purgar** páginas al desplegar con la API de caché, por ejemplo en tu pipeline de CI. - **Recibir webhooks** verificando la firma `X-Jazui-Signature`. - **Mostrar tu blog** en tu web con la API del blog. > El skill nunca escribe el token en el código del navegador ni lo sube al repositorio, y lee los precios y límites en vivo en lugar de suponerlos. Lo que se hace en el panel (cuenta, verificación, token) lo haces tú: el skill te dice dónde. ## Instalarlo en Claude Code Descomprime el zip en `~/.claude/skills/` para usarlo en todos tus proyectos, o en la carpeta `.claude/skills/` de un proyecto para compartirlo con tu equipo a través del repositorio. Queda en `skills/jazui/SKILL.md`. Desde la terminal: `macOS / Linux`: ```bash # Para todos tus proyectos (o .claude/skills dentro de un proyecto) mkdir -p ~/.claude/skills curl -L https://docs.jazui.com/skill/jazui.zip -o /tmp/jazui.zip unzip -o /tmp/jazui.zip -d ~/.claude/skills ``` `Windows (PowerShell)`: ```powershell # Para todos tus proyectos (o .claude\skills dentro de un proyecto) Invoke-WebRequest https://docs.jazui.com/skill/jazui.zip -OutFile $env:TEMP\jazui.zip Expand-Archive $env:TEMP\jazui.zip "$HOME\.claude\skills" -Force ``` Abre una sesión nueva de Claude Code y escribe `/jazui` para usarlo directamente, o simplemente pide lo que necesites: se activa solo cuando hablas de Jazui o de prerender. ## Instalarlo en claude.ai y la app de escritorio Descarga **jazui.zip** y súbelo tal cual (sin descomprimir) en la sección **Skills** de la configuración de Claude. Necesitas tener activada la ejecución de código, que es donde corren los skills. ## Qué pedirle - «Conecta esta web con Jazui.» - «Mi Blazor WebAssembly está en IIS: añade la regla de Jazui al web.config de publicación.» - «Comprueba si Jazui está sirviendo mi portada a los bots.» - «Recachea el sitio al final del despliegue de GitHub Actions.» - «Muestra los últimos 5 artículos de mi blog de Jazui en la portada.» ## Actualizarlo El skill se genera a partir de esta documentación, así que cada versión trae las páginas al día. Para actualizarlo, vuelve a descargarlo y reemplaza la carpeta `jazui` (los comandos de arriba ya la sobrescriben). ¿Usas otro asistente, como GitHub Copilot o Cursor? Usa la documentación de [Para IA](https://docs.jazui.com/llms/es/for-ai.txt). --- # Vista general > Qué integración elegir, qué tienen en común y cómo comprobar que funcionan. Página original: https://docs.jazui.com/integrations/es Una integración es el trozo de código o de configuración que se pone delante de tu web, en tu CDN o en tu servidor, y decide qué peticiones son de bots. Esas se las pide a Jazui; el resto sigue a tu web sin cambios. No tienes que tocar tu aplicación. ## Cuál elegir [**Cloudflare Workers**Si tu dominio está en Cloudflare, con cualquier hosting. No se toca el servidor.](https://docs.jazui.com/llms/es/integrations-cloudflare.txt) [**Vercel / Next.js**Middleware en el edge de Vercel.](https://docs.jazui.com/llms/es/integrations-vercel.txt) [**Netlify**Edge Function de Netlify.](https://docs.jazui.com/llms/es/integrations-netlify.txt) [**Node.js / Express**Middleware para tu servidor Node.](https://docs.jazui.com/llms/es/integrations-express.txt) [**ASP.NET Core**Paquete NuGet para Blazor, Angular o React servidos por .NET.](https://docs.jazui.com/llms/es/integrations-aspnetcore.txt) [**nginx**Configuración con proxy a Jazui.](https://docs.jazui.com/llms/es/integrations-nginx.txt) [**IIS**web.config con URL Rewrite y ARR.](https://docs.jazui.com/llms/es/integrations-iis.txt) [**Apache**.htaccess con mod_rewrite y mod_proxy.](https://docs.jazui.com/llms/es/integrations-apache.txt) Si tienes varias opciones, la mejor suele ser la que está más cerca del usuario: Cloudflare, Vercel o Netlify, y si no, la de tu framework (Express o ASP.NET Core) antes que la del servidor web. Las de código vuelven siempre a tu web si Jazui falla y se actualizan solas. ## Diferencias entre ellas | | Cloudflare, Vercel, Netlify, Express | ASP.NET Core | nginx | IIS y Apache | |---|---|---|---|---| | Lista de bots | Se descarga sola cada día | Se descarga sola cada día | En la configuración | En la configuración | | Avisa de bots desconocidos | Sí | Sí | No | No | | Si Jazui falla | Sirve tu web | Sirve tu web | Sirve tu web (salvo un 403) | El bot recibe el error | | Tiempo máximo de espera | 20 s | 20 s (configurable) | 20 s | El del proxy del servidor | | Envía a Jazui | Solo lo necesario | Solo lo necesario | Las cabeceras del bot, sin cookies | Las cabeceras del bot, sin cookies | ## Lo que hacen todas 1. **Deciden si la petición es de un bot**: `GET` o `HEAD`, agente de usuario en la lista de bots (o `_escaped_fragment_` en la dirección), ruta que no es un archivo y sin la cabecera `X-Jazui`. 2. **Se la piden a Jazui**: `GET https://api.jazui.com/Render` con la dirección completa, el agente de usuario, tu token en `Authorization: Bearer`, la IP del bot en `X-Jazui-Client-IP` y el `If-None-Match` del bot. 3. **Entregan al bot la respuesta de Jazui** si lleva `X-Jazui: true` (200, 404, redirecciones, 304…), con su `Content-Type`, `ETag`, `Last-Modified` y `X-Jazui-Cache`; o el `403` con `X-Jazui-Blocked` si bloqueaste al bot. 4. **En cualquier otro caso sirven tu web**: Jazui tarda, falla, el token no vale o el plan está agotado. El detalle está en [Cómo funciona](https://docs.jazui.com/llms/es/how-it-works.txt). ## La lista de bots Jazui mantiene una lista de más de 230 bots (buscadores, IA, redes sociales y herramientas SEO) y la publica sin necesidad de token: ```bash curl https://api.jazui.com/v2/bots curl https://api.jazui.com/v2/bots.txt ``` `/v2/bots` devuelve un JSON con la versión, la fecha y los fragmentos; `/v2/bots.txt`, un fragmento por línea. Las integraciones de código la descargan al arrancar y cada 24 horas; si no pueden, siguen con la lista que llevan dentro. Para nginx, IIS y Apache hay que copiarla a mano cuando cambia: se explica en la página de cada uno. ## Lo que nunca pasa por Jazui Las rutas que terminan en estas extensiones las sirve siempre tu web, venga quien venga: `js mjs css less map json xml rss txt webmanifest ico png jpg jpeg gif webp avif svg bmp tif tiff psd ai woff woff2 ttf otf eot mp3 m4a wav ogg mp4 m4v webm avi mov mpg mpeg wmv flv swf pdf doc docx xls xlsx ppt pptx csv zip gz rar 7z iso torrent exe dmg apk wasm dll dat blat` Tampoco pasan las peticiones `POST`, `PUT`… ni las que traen la cabecera `X-Jazui` (las del propio Jazui al renderizar). ## Qué token usar Cualquier integración funciona con el **token de la cuenta** (Inicio › Integración y ajustes) o con el **token del sitio** (Sitios web › Token del sitio). Si tienes varios sitios, usa el del sitio en cada uno: si uno se filtra, lo cambias sin tocar los demás. Nunca uses el PUBLIC Token en una integración. ## Comprobar cualquier integración Con `curl` y el agente de usuario de pruebas `integration-test`, que todas las integraciones tratan como bot: ```bash # Como un bot: debe llevar x-jazui: true curl -I -A "integration-test" https://misitio.com/ # Como una persona: tu web de siempre, sin x-jazui curl -I https://misitio.com/ # Un archivo estático nunca pasa por Jazui curl -I -A "integration-test" https://misitio.com/favicon.ico ``` También puedes usar la herramienta **Inspección de URLs** de Google Search Console o el depurador de vistas previas de Facebook o LinkedIn para ver la página como la reciben ellos. --- # Cloudflare Workers > Un Worker delante de tu web: no tocas el servidor y funciona con cualquier hosting. Página original: https://docs.jazui.com/integrations/cloudflare/es Si tu dominio usa Cloudflare, un Worker en la ruta de tu sitio recibe todas las peticiones antes que tu servidor. Las de bots se las pide a Jazui; el resto las manda a tu servidor con `fetch(request)`, como si el Worker no estuviera. Es la integración más sencilla y la que menos latencia añade. ## Antes de empezar - El dominio en Cloudflare con el proxy activado (nube naranja). - Tu sitio verificado en Jazui y tu token. - El plan gratuito de Workers es suficiente para la mayoría de sitios. ## Opción 1: desde el panel de Cloudflare 1. En Cloudflare, ve a **Workers y Pages › Crear › Crear Worker**, ponle de nombre `jazui` y pulsa **Implementar**. 2. Pulsa **Editar código**, borra lo que haya y pega este archivo completo (es la lógica común más el Worker, en uno solo): `worker.js` (https://docs.jazui.com/example_config/cloudflare_script.js): ```javascript // Generado por 3_Integrations/Sync-Integrations.ps1 (js/core.js + js/cloudflare-worker.js): no editar aquí. // Núcleo común de las integraciones JavaScript de Jazui (Cloudflare Workers, Netlify, Vercel y Express). // Solo usa la API Fetch estándar: funciona en Node 18+, Deno y los runtimes de borde. const DEFAULT_API = 'https://api.jazui.com'; const DEFAULT_TIMEOUT_MS = 20000; // Bots a los que se sirve el HTML renderizado (bots.txt; lo rellena Sync-Integrations.ps1) const BOTS = [ // BOTS:BEGIN 'googlebot', 'google-inspectiontool', 'storebot-google', 'adsbot-google', 'bingbot', 'bingpreview', 'msnbot', 'adidxbot', 'yandex', 'baiduspider', 'duckduckbot', 'slurp', 'applebot', 'seznambot', 'qwantbot', 'qwantify', 'petalbot', 'sogou', 'naver', 'exabot', 'yetibot', '360spider', 'soso', 'gptbot', 'chatgpt', 'oai-searchbot', 'claudebot', 'claude-user', 'claude-searchbot', 'anthropic-ai', 'perplexity', 'google-cloudvertexbot', 'bytespider', 'ccbot', 'amazonbot', 'meta-externalagent', 'cohere-ai', 'mistralai-user', 'youbot', 'duckassistbot', 'yibot', 'qwenbot', 'tongyibot', 'erniebot', 'yiyanbot', 'doubaobot', 'tiktokspider', 'deepseekbot', 'pangubot', 'manus-user', 'kimi-agent', 'kimi-searchbot', 'kimi-user', 'kimibot', 'moonshotbot', 'hunyuan', 'chatglm-spider', 'agi agent', 'ai2bot', 'ai2bot-deepresearcheval', 'ai2bot-dolma', 'aihitbot', 'amazon-kendra', 'amazon-qbusiness', 'amazonbuyforme', 'amzn-searchbot', 'amzn-user', 'bedrockbot', 'novaact', 'andibot', 'claude-web', 'apifybot', 'apifywebsitecontentcrawler', 'atlassian-bot', 'awario', 'bigsur.ai', 'brightbot', 'buddybot', 'cartai agentic commerce', 'terracotta', 'cledarabot', 'cloudflare-autorag', 'cloudflarebrowserrenderingcrawler', 'cohere-training-data-crawler', 'cragcrawler', 'crawlspace', 'datenbank crawler', 'diffbot', 'diffbot-user', 'anomura', 'echobot bot', 'echoboxbot', 'elevenlabsbot', 'elevenlabscrawler', 'exasearchbot', 'exauser', 'factset_spyderbot', 'firecrawlagent', 'geisthaus-pagefetcher', 'cloudvertexbot', 'gemini-deep-research', 'google-agent', 'google-firebase', 'google-notebooklm', 'googleagent-mariner', 'googleagent-urlcontext', 'googleother', 'googleother-image', 'googleother-video', 'notebooklm', 'iaskbot', 'iaskspider', 'imagesiftbot', 'img2dataset', 'isscyberriskcrawler', 'kagi-fetcher', 'keenablebot', 'klaviyoaibot', 'laiondownloader', 'linguee bot', 'linkupbot', 'aiwebindex', 'meta-externalfetcher', 'meta-webindexer', 'facebookbot', 'azureai-searchbot', 'mistralai-index', 'mistralai-training', 'mozilla-tabstack', 'nagetbot', 'netestate imprint crawler', 'icc-crawler', 'oai-adsbot', 'panscient', 'panscient.com', 'shapbot', 'payhawk invoice fetching agent', 'phindbot', 'poggio-citations', 'poseidon research crawler', 'qualifiedbot', 'wpbot', 'querit-searchbot', 'queritbot', 'quillbot', 'quillbot.com', 'reflectionbot', 'cotoyogi', 'sbintuitionsbot', 'scrybot', 'sidetrade indexer bot', 'tavilybot', 'agenttimes', 'thinkbot', 'timpibot', 'twinagent', 'velenpublicwebcrawler', 'wardbot', 'omgili', 'omgilibot', 'grok bot', 'grokappandroid', 'grokbot', 'addsearchbot', 'agentdatabot', 'aranet-searchbot', 'bixelbot', 'bravebot', 'channel3bot', 'friendlycrawler', 'henkbot', 'imagespider', 'kangaroo bot', 'kunatocrawler', 'laion-huggingface-processor', 'linerbot', 'mycentralaiscraperbot', 'terra cotta', 'wrtnbot', 'zanistabot', 'facebookexternalhit', 'facebot', 'twitterbot', 'linkedinbot', 'slackbot', 'whatsapp', 'telegrambot', 'discordbot', 'pinterest', 'redditbot', 'skypeuripreview', 'embedly', 'quora link preview', 'showyoubot', 'outbrain', 'developers.google.com/+/web/snippet', 'vkshare', 'tumblr', 'bitlybot', 'nuzzel', 'flipboard', 'snapchat', 'viber', 'mattermost', 'bitrix link preview', 'xing-contenttabreceiver', 'ahrefsbot', 'semrushbot', 'mj12bot', 'majestic-12', 'dotbot', 'rogerbot', 'screaming frog', 'sitebulb', 'serpstatbot', 'seokicks', 'siteimprove', 'lighthouse', 'google page speed', 'gtmetrix', 'pingdom', 'uptimerobot', 'uptimebot', 'w3c_validator', 'w3c-checklink', 'archive.org_bot', 'paperlibot', 'dataprovider.com', 'grapeshotcrawler', 'zoominfobot', 'feedly', 'newsblur', 'inoreader', 'integration-test', // BOTS:END ]; // Archivos que nunca se renderizan (extensions.txt) const STATIC_EXTENSIONS = [ // EXTENSIONS:BEGIN 'js', 'mjs', 'css', 'less', 'map', 'json', 'xml', 'rss', 'txt', 'webmanifest', 'ico', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'svg', 'bmp', 'tif', 'tiff', 'psd', 'ai', 'woff', 'woff2', 'ttf', 'otf', 'eot', 'mp3', 'm4a', 'wav', 'ogg', 'mp4', 'm4v', 'webm', 'avi', 'mov', 'mpg', 'mpeg', 'wmv', 'flv', 'swf', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'csv', 'zip', 'gz', 'rar', '7z', 'iso', 'torrent', 'exe', 'dmg', 'apk', 'wasm', 'dll', 'dat', 'blat', // EXTENSIONS:END ]; const escapeRegex = (text) => text.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&'); // Patrón de la lista incluida; refreshBots lo sustituye por el del catálogo de Jazui (GET /v2/bots) const BOT_PATTERN = new RegExp(BOTS.map(escapeRegex).join('|'), 'i'); const STATIC_PATTERN = new RegExp(`\\.(${STATIC_EXTENSIONS.join('|')})$`, 'i'); let botPattern = BOT_PATTERN; let botsVersion = 'incluida'; let botsCheckedAt = 0; let botsLoading = null; const BOTS_REFRESH_MS = 24 * 60 * 60 * 1000; const BOTS_RETRY_MS = 60 * 60 * 1000; /** Patrón de bots en uso: el del catálogo si ya se descargó, si no el de la lista incluida. */ const currentBotPattern = () => botPattern; /** Versión de la lista en uso ('incluida' hasta la primera descarga). */ const currentBotsVersion = () => botsVersion; /** * Descarga la lista de bots del catálogo de Jazui (GET /v2/bots) si toca: al arrancar y cada 24 h; si falla se * sigue con la que hay y se reintenta en una hora. Nunca falla ni retrasa la petición: devuelve la promesa para * waitUntil (Cloudflare, Vercel, Netlify) o null si no toca. Se desactiva con remoteBots: false. */ function refreshBots({ api = DEFAULT_API, remoteBots = true } = {}) { const now = Date.now(); if (!remoteBots || botsLoading || now - botsCheckedAt < BOTS_REFRESH_MS) return null; botsCheckedAt = now; botsLoading = fetch(`${(api || DEFAULT_API).replace(/\/+$/, '')}/v2/bots`, { signal: AbortSignal.timeout(5000) }) .then((response) => (response.ok ? response.json() : null)) .then((data) => { if (data && Array.isArray(data.bots) && data.bots.length > 0) { botPattern = new RegExp(data.bots.map(escapeRegex).join('|'), 'i'); botsVersion = data.version || botsVersion; } else { botsCheckedAt = now - BOTS_REFRESH_MS + BOTS_RETRY_MS; } }, () => { botsCheckedAt = now - BOTS_REFRESH_MS + BOTS_RETRY_MS; }) .finally(() => { botsLoading = null; }); return botsLoading; } // Cabeceras de Jazui que se devuelven al bot const PASS_HEADERS = ['content-type', 'location', 'etag', 'last-modified', 'x-jazui', 'x-jazui-cache']; /** * Si la petición hay que pasarla por Jazui: GET/HEAD de un bot conocido (o con _escaped_fragment_, el esquema * antiguo de Google para las SPA) a una página, no a un archivo. */ function shouldRender({ method, url, userAgent, headers }) { if (method !== 'GET' && method !== 'HEAD') return false; if (headers && headers.get('x-jazui')) return false; // nunca a sí mismo const { pathname, search } = new URL(url); if (STATIC_PATTERN.test(pathname)) return false; return (!!userAgent && botPattern.test(userAgent)) || search.includes('_escaped_fragment_'); } /** * Pide a Jazui el HTML de la URL. Devuelve la Response para el bot o null si hay que servir la página normal * (Jazui no respondió, se pasó del límite, el sitio no está verificado…): el bot nunca ve un error de Jazui. * Solo un 403 marcado con X-Jazui-Blocked (regla o bot de IA bloqueado) llega al bot. */ async function fetchRendered({ url, method = 'GET', userAgent, clientIp, ifNoneMatch, token, api = DEFAULT_API, siteUrl, timeoutMs = DEFAULT_TIMEOUT_MS }) { if (!token) return null; let target = url; if (siteUrl) { // Dominio público del sitio cuando el servidor lo ve con otro (proxy, entorno de pruebas) const original = new URL(url); target = new URL(original.pathname + original.search, siteUrl).toString(); } const query = new URLSearchParams({ url: target, botAgent: userAgent || '' }); const headers = { Authorization: `Bearer ${token}` }; if (clientIp) headers['X-Jazui-Client-IP'] = clientIp; if (ifNoneMatch) headers['If-None-Match'] = ifNoneMatch; let response; try { response = await fetch(`${(api || DEFAULT_API).replace(/\/+$/, '')}/Render?${query}`, { headers, redirect: 'manual', signal: AbortSignal.timeout(timeoutMs), }); } catch { return null; } const blocked = response.status === 403 && response.headers.get('x-jazui-blocked'); if (!response.headers.get('x-jazui') && !blocked) { response.body?.cancel(); return null; } const out = new Headers(); for (const name of PASS_HEADERS) { const value = response.headers.get(name); if (value) out.set(name, value); } if (blocked) out.set('x-jazui-blocked', blocked); const empty = method === 'HEAD' || response.status === 304; if (empty) response.body?.cancel(); return new Response(empty ? null : response.body, { status: response.status, headers: out }); } // User-agents con pinta de bot: si no están en BOTS se avisa a Jazui sin enviarle la página (el mismo patrón que la API) const UNKNOWN_BOT_PATTERN = /bot|crawl|spider|scrap|fetch|headless|preview|archiv|index/i; const MAX_REPORTED = 1000; const reported = new Set(); let reportedDay = ''; /** * Avisa a Jazui (POST /Render/Seen) de un user-agent con pinta de bot que no está en la lista, una vez al día por * user-agent y proceso, para que se pueda añadir al catálogo. No gasta renders y nunca falla: devuelve la promesa para * waitUntil (Cloudflare, Vercel) o null si no hay que avisar. */ function reportUnknownBot({ method, url, userAgent, token, api = DEFAULT_API, reportUnknownBots = true }) { if (!reportUnknownBots || !token || !userAgent || (method !== 'GET' && method !== 'HEAD')) return null; if (STATIC_PATTERN.test(new URL(url).pathname)) return null; if (!UNKNOWN_BOT_PATTERN.test(userAgent) || botPattern.test(userAgent)) return null; const today = new Date().toISOString().slice(0, 10); if (reportedDay !== today) { reported.clear(); reportedDay = today; } if (reported.size >= MAX_REPORTED || reported.has(userAgent)) return null; reported.add(userAgent); return fetch(`${(api || DEFAULT_API).replace(/\/+$/, '')}/Render/Seen`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ userAgent, url }), signal: AbortSignal.timeout(5000), }).then((response) => { response.body?.cancel(); }, () => {}); } /** * Atajo para los runtimes con Request estándar: la Response de Jazui o null. Con options.waitUntil (Cloudflare, * Vercel) la descarga de la lista de bots y el aviso de bots desconocidos siguen aunque la respuesta ya se haya enviado. */ function renderRequest(request, options, clientIp) { const refresh = refreshBots(options); if (refresh && options.waitUntil) options.waitUntil(refresh); const userAgent = request.headers.get('user-agent') || ''; if (!shouldRender({ method: request.method, url: request.url, userAgent, headers: request.headers })) { const report = reportUnknownBot({ ...options, method: request.method, url: request.url, userAgent }); if (report && options.waitUntil) options.waitUntil(report); return null; } return fetchRendered({ ...options, url: request.url, method: request.method, userAgent, clientIp, ifNoneMatch: request.headers.get('if-none-match'), }); } // Cloudflare Worker delante del sitio: los bots reciben el HTML de Jazui y el resto va al origen. // Variables del Worker: JAZUI_TOKEN (secreto) y, opcionales, JAZUI_API y JAZUI_SITE_URL. export default { async fetch(request, env, ctx) { const rendered = await renderRequest(request, { token: env.JAZUI_TOKEN, api: env.JAZUI_API, siteUrl: env.JAZUI_SITE_URL, waitUntil: (promise) => ctx.waitUntil(promise), }, request.headers.get('cf-connecting-ip')); return rendered ?? fetch(request); }, }; ``` 1. Pulsa **Implementar**. 2. En **Configuración › Variables y secretos**, añade `JAZUI_TOKEN` de tipo **Secreto** con tu token. 3. En **Configuración › Dominios y rutas › Añadir › Ruta**, elige tu zona y escribe `misitio.com/*` (y `www.misitio.com/*` si usas `www`). ## Opción 2: con Wrangler Si prefieres tenerlo en un repositorio, descarga `cloudflare-worker.js` y `core.js` (los dos están más abajo), ponlos juntos con este `wrangler.toml`, guarda el token como secreto y publica: ```bash mi-worker/ ├── wrangler.toml ├── cloudflare-worker.js └── core.js ``` `wrangler.toml`: ```bash name = "jazui" main = "cloudflare-worker.js" compatibility_date = "2025-01-01" routes = [{ pattern = "misitio.com/*", zone_name = "misitio.com" }] ``` ```bash npx wrangler secret put JAZUI_TOKEN npx wrangler deploy ``` ## Variables de entorno | Variable | Obligatoria | Para qué | |---|---|---| | `JAZUI_TOKEN` | Sí | Tu token de la cuenta o del sitio. Guárdalo como **secreto**, nunca en el código. | | `JAZUI_API` | No | Dirección de la API. Por defecto `https://api.jazui.com`. | | `JAZUI_SITE_URL` | No | Tu dominio público (por ejemplo `https://misitio.com`) si la plataforma ve otro, como una dirección de vista previa: Jazui renderiza la página con ese dominio. | Sin `JAZUI_TOKEN` la integración no hace nada y toda la web se sirve normal. ## Qué hace El Worker llama a la lógica común con tu token y la IP del bot, que Cloudflare da en `CF-Connecting-IP`. Si Jazui devuelve la página, la entrega; si no, sigue con `fetch(request)` hacia tu servidor. La lista de bots y los avisos de bots desconocidos se hacen en segundo plano con `ctx.waitUntil`, sin retrasar la respuesta. `cloudflare-worker.js` (https://docs.jazui.com/example_config/cloudflare-worker.js): ```javascript // Cloudflare Worker delante del sitio: los bots reciben el HTML de Jazui y el resto va al origen. // Variables del Worker: JAZUI_TOKEN (secreto) y, opcionales, JAZUI_API y JAZUI_SITE_URL. import { renderRequest } from './core.js'; export default { async fetch(request, env, ctx) { const rendered = await renderRequest(request, { token: env.JAZUI_TOKEN, api: env.JAZUI_API, siteUrl: env.JAZUI_SITE_URL, waitUntil: (promise) => ctx.waitUntil(promise), }, request.headers.get('cf-connecting-ip')); return rendered ?? fetch(request); }, }; ``` `core.js` (https://docs.jazui.com/example_config/core.js): ```javascript // Núcleo común de las integraciones JavaScript de Jazui (Cloudflare Workers, Netlify, Vercel y Express). // Solo usa la API Fetch estándar: funciona en Node 18+, Deno y los runtimes de borde. export const DEFAULT_API = 'https://api.jazui.com'; export const DEFAULT_TIMEOUT_MS = 20000; // Bots a los que se sirve el HTML renderizado (bots.txt; lo rellena Sync-Integrations.ps1) export const BOTS = [ // BOTS:BEGIN 'googlebot', 'google-inspectiontool', 'storebot-google', 'adsbot-google', 'bingbot', 'bingpreview', 'msnbot', 'adidxbot', 'yandex', 'baiduspider', 'duckduckbot', 'slurp', 'applebot', 'seznambot', 'qwantbot', 'qwantify', 'petalbot', 'sogou', 'naver', 'exabot', 'yetibot', '360spider', 'soso', 'gptbot', 'chatgpt', 'oai-searchbot', 'claudebot', 'claude-user', 'claude-searchbot', 'anthropic-ai', 'perplexity', 'google-cloudvertexbot', 'bytespider', 'ccbot', 'amazonbot', 'meta-externalagent', 'cohere-ai', 'mistralai-user', 'youbot', 'duckassistbot', 'yibot', 'qwenbot', 'tongyibot', 'erniebot', 'yiyanbot', 'doubaobot', 'tiktokspider', 'deepseekbot', 'pangubot', 'manus-user', 'kimi-agent', 'kimi-searchbot', 'kimi-user', 'kimibot', 'moonshotbot', 'hunyuan', 'chatglm-spider', 'agi agent', 'ai2bot', 'ai2bot-deepresearcheval', 'ai2bot-dolma', 'aihitbot', 'amazon-kendra', 'amazon-qbusiness', 'amazonbuyforme', 'amzn-searchbot', 'amzn-user', 'bedrockbot', 'novaact', 'andibot', 'claude-web', 'apifybot', 'apifywebsitecontentcrawler', 'atlassian-bot', 'awario', 'bigsur.ai', 'brightbot', 'buddybot', 'cartai agentic commerce', 'terracotta', 'cledarabot', 'cloudflare-autorag', 'cloudflarebrowserrenderingcrawler', 'cohere-training-data-crawler', 'cragcrawler', 'crawlspace', 'datenbank crawler', 'diffbot', 'diffbot-user', 'anomura', 'echobot bot', 'echoboxbot', 'elevenlabsbot', 'elevenlabscrawler', 'exasearchbot', 'exauser', 'factset_spyderbot', 'firecrawlagent', 'geisthaus-pagefetcher', 'cloudvertexbot', 'gemini-deep-research', 'google-agent', 'google-firebase', 'google-notebooklm', 'googleagent-mariner', 'googleagent-urlcontext', 'googleother', 'googleother-image', 'googleother-video', 'notebooklm', 'iaskbot', 'iaskspider', 'imagesiftbot', 'img2dataset', 'isscyberriskcrawler', 'kagi-fetcher', 'keenablebot', 'klaviyoaibot', 'laiondownloader', 'linguee bot', 'linkupbot', 'aiwebindex', 'meta-externalfetcher', 'meta-webindexer', 'facebookbot', 'azureai-searchbot', 'mistralai-index', 'mistralai-training', 'mozilla-tabstack', 'nagetbot', 'netestate imprint crawler', 'icc-crawler', 'oai-adsbot', 'panscient', 'panscient.com', 'shapbot', 'payhawk invoice fetching agent', 'phindbot', 'poggio-citations', 'poseidon research crawler', 'qualifiedbot', 'wpbot', 'querit-searchbot', 'queritbot', 'quillbot', 'quillbot.com', 'reflectionbot', 'cotoyogi', 'sbintuitionsbot', 'scrybot', 'sidetrade indexer bot', 'tavilybot', 'agenttimes', 'thinkbot', 'timpibot', 'twinagent', 'velenpublicwebcrawler', 'wardbot', 'omgili', 'omgilibot', 'grok bot', 'grokappandroid', 'grokbot', 'addsearchbot', 'agentdatabot', 'aranet-searchbot', 'bixelbot', 'bravebot', 'channel3bot', 'friendlycrawler', 'henkbot', 'imagespider', 'kangaroo bot', 'kunatocrawler', 'laion-huggingface-processor', 'linerbot', 'mycentralaiscraperbot', 'terra cotta', 'wrtnbot', 'zanistabot', 'facebookexternalhit', 'facebot', 'twitterbot', 'linkedinbot', 'slackbot', 'whatsapp', 'telegrambot', 'discordbot', 'pinterest', 'redditbot', 'skypeuripreview', 'embedly', 'quora link preview', 'showyoubot', 'outbrain', 'developers.google.com/+/web/snippet', 'vkshare', 'tumblr', 'bitlybot', 'nuzzel', 'flipboard', 'snapchat', 'viber', 'mattermost', 'bitrix link preview', 'xing-contenttabreceiver', 'ahrefsbot', 'semrushbot', 'mj12bot', 'majestic-12', 'dotbot', 'rogerbot', 'screaming frog', 'sitebulb', 'serpstatbot', 'seokicks', 'siteimprove', 'lighthouse', 'google page speed', 'gtmetrix', 'pingdom', 'uptimerobot', 'uptimebot', 'w3c_validator', 'w3c-checklink', 'archive.org_bot', 'paperlibot', 'dataprovider.com', 'grapeshotcrawler', 'zoominfobot', 'feedly', 'newsblur', 'inoreader', 'integration-test', // BOTS:END ]; // Archivos que nunca se renderizan (extensions.txt) export const STATIC_EXTENSIONS = [ // EXTENSIONS:BEGIN 'js', 'mjs', 'css', 'less', 'map', 'json', 'xml', 'rss', 'txt', 'webmanifest', 'ico', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'svg', 'bmp', 'tif', 'tiff', 'psd', 'ai', 'woff', 'woff2', 'ttf', 'otf', 'eot', 'mp3', 'm4a', 'wav', 'ogg', 'mp4', 'm4v', 'webm', 'avi', 'mov', 'mpg', 'mpeg', 'wmv', 'flv', 'swf', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'csv', 'zip', 'gz', 'rar', '7z', 'iso', 'torrent', 'exe', 'dmg', 'apk', 'wasm', 'dll', 'dat', 'blat', // EXTENSIONS:END ]; const escapeRegex = (text) => text.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&'); // Patrón de la lista incluida; refreshBots lo sustituye por el del catálogo de Jazui (GET /v2/bots) export const BOT_PATTERN = new RegExp(BOTS.map(escapeRegex).join('|'), 'i'); export const STATIC_PATTERN = new RegExp(`\\.(${STATIC_EXTENSIONS.join('|')})$`, 'i'); let botPattern = BOT_PATTERN; let botsVersion = 'incluida'; let botsCheckedAt = 0; let botsLoading = null; const BOTS_REFRESH_MS = 24 * 60 * 60 * 1000; const BOTS_RETRY_MS = 60 * 60 * 1000; /** Patrón de bots en uso: el del catálogo si ya se descargó, si no el de la lista incluida. */ export const currentBotPattern = () => botPattern; /** Versión de la lista en uso ('incluida' hasta la primera descarga). */ export const currentBotsVersion = () => botsVersion; /** * Descarga la lista de bots del catálogo de Jazui (GET /v2/bots) si toca: al arrancar y cada 24 h; si falla se * sigue con la que hay y se reintenta en una hora. Nunca falla ni retrasa la petición: devuelve la promesa para * waitUntil (Cloudflare, Vercel, Netlify) o null si no toca. Se desactiva con remoteBots: false. */ export function refreshBots({ api = DEFAULT_API, remoteBots = true } = {}) { const now = Date.now(); if (!remoteBots || botsLoading || now - botsCheckedAt < BOTS_REFRESH_MS) return null; botsCheckedAt = now; botsLoading = fetch(`${(api || DEFAULT_API).replace(/\/+$/, '')}/v2/bots`, { signal: AbortSignal.timeout(5000) }) .then((response) => (response.ok ? response.json() : null)) .then((data) => { if (data && Array.isArray(data.bots) && data.bots.length > 0) { botPattern = new RegExp(data.bots.map(escapeRegex).join('|'), 'i'); botsVersion = data.version || botsVersion; } else { botsCheckedAt = now - BOTS_REFRESH_MS + BOTS_RETRY_MS; } }, () => { botsCheckedAt = now - BOTS_REFRESH_MS + BOTS_RETRY_MS; }) .finally(() => { botsLoading = null; }); return botsLoading; } // Cabeceras de Jazui que se devuelven al bot const PASS_HEADERS = ['content-type', 'location', 'etag', 'last-modified', 'x-jazui', 'x-jazui-cache']; /** * Si la petición hay que pasarla por Jazui: GET/HEAD de un bot conocido (o con _escaped_fragment_, el esquema * antiguo de Google para las SPA) a una página, no a un archivo. */ export function shouldRender({ method, url, userAgent, headers }) { if (method !== 'GET' && method !== 'HEAD') return false; if (headers && headers.get('x-jazui')) return false; // nunca a sí mismo const { pathname, search } = new URL(url); if (STATIC_PATTERN.test(pathname)) return false; return (!!userAgent && botPattern.test(userAgent)) || search.includes('_escaped_fragment_'); } /** * Pide a Jazui el HTML de la URL. Devuelve la Response para el bot o null si hay que servir la página normal * (Jazui no respondió, se pasó del límite, el sitio no está verificado…): el bot nunca ve un error de Jazui. * Solo un 403 marcado con X-Jazui-Blocked (regla o bot de IA bloqueado) llega al bot. */ export async function fetchRendered({ url, method = 'GET', userAgent, clientIp, ifNoneMatch, token, api = DEFAULT_API, siteUrl, timeoutMs = DEFAULT_TIMEOUT_MS }) { if (!token) return null; let target = url; if (siteUrl) { // Dominio público del sitio cuando el servidor lo ve con otro (proxy, entorno de pruebas) const original = new URL(url); target = new URL(original.pathname + original.search, siteUrl).toString(); } const query = new URLSearchParams({ url: target, botAgent: userAgent || '' }); const headers = { Authorization: `Bearer ${token}` }; if (clientIp) headers['X-Jazui-Client-IP'] = clientIp; if (ifNoneMatch) headers['If-None-Match'] = ifNoneMatch; let response; try { response = await fetch(`${(api || DEFAULT_API).replace(/\/+$/, '')}/Render?${query}`, { headers, redirect: 'manual', signal: AbortSignal.timeout(timeoutMs), }); } catch { return null; } const blocked = response.status === 403 && response.headers.get('x-jazui-blocked'); if (!response.headers.get('x-jazui') && !blocked) { response.body?.cancel(); return null; } const out = new Headers(); for (const name of PASS_HEADERS) { const value = response.headers.get(name); if (value) out.set(name, value); } if (blocked) out.set('x-jazui-blocked', blocked); const empty = method === 'HEAD' || response.status === 304; if (empty) response.body?.cancel(); return new Response(empty ? null : response.body, { status: response.status, headers: out }); } // User-agents con pinta de bot: si no están en BOTS se avisa a Jazui sin enviarle la página (el mismo patrón que la API) export const UNKNOWN_BOT_PATTERN = /bot|crawl|spider|scrap|fetch|headless|preview|archiv|index/i; const MAX_REPORTED = 1000; const reported = new Set(); let reportedDay = ''; /** * Avisa a Jazui (POST /Render/Seen) de un user-agent con pinta de bot que no está en la lista, una vez al día por * user-agent y proceso, para que se pueda añadir al catálogo. No gasta renders y nunca falla: devuelve la promesa para * waitUntil (Cloudflare, Vercel) o null si no hay que avisar. */ export function reportUnknownBot({ method, url, userAgent, token, api = DEFAULT_API, reportUnknownBots = true }) { if (!reportUnknownBots || !token || !userAgent || (method !== 'GET' && method !== 'HEAD')) return null; if (STATIC_PATTERN.test(new URL(url).pathname)) return null; if (!UNKNOWN_BOT_PATTERN.test(userAgent) || botPattern.test(userAgent)) return null; const today = new Date().toISOString().slice(0, 10); if (reportedDay !== today) { reported.clear(); reportedDay = today; } if (reported.size >= MAX_REPORTED || reported.has(userAgent)) return null; reported.add(userAgent); return fetch(`${(api || DEFAULT_API).replace(/\/+$/, '')}/Render/Seen`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ userAgent, url }), signal: AbortSignal.timeout(5000), }).then((response) => { response.body?.cancel(); }, () => {}); } /** * Atajo para los runtimes con Request estándar: la Response de Jazui o null. Con options.waitUntil (Cloudflare, * Vercel) la descarga de la lista de bots y el aviso de bots desconocidos siguen aunque la respuesta ya se haya enviado. */ export function renderRequest(request, options, clientIp) { const refresh = refreshBots(options); if (refresh && options.waitUntil) options.waitUntil(refresh); const userAgent = request.headers.get('user-agent') || ''; if (!shouldRender({ method: request.method, url: request.url, userAgent, headers: request.headers })) { const report = reportUnknownBot({ ...options, method: request.method, url: request.url, userAgent }); if (report && options.waitUntil) options.waitUntil(report); return null; } return fetchRendered({ ...options, url: request.url, method: request.method, userAgent, clientIp, ifNoneMatch: request.headers.get('if-none-match'), }); } ``` ##

Pide tu portada como si fueras un bot, con el agente de usuario de pruebas integration-test:

curl -I -A "integration-test" https://misitio.com/

La respuesta debe llevar x-jazui: true y x-jazui-cache: MISS la primera vez y HIT después. Sin agente de bot (curl -I https://misitio.com/) debe llegar tu web de siempre, sin x-jazui. En el panel, la petición aparece en Tráfico de bots y la página en Páginas en caché.

Pide tu portada como si fueras un bot, con el agente de usuario de pruebas `integration-test`: ``` curl -I -A "integration-test" https://misitio.com/ ``` La respuesta debe llevar `x-jazui: true` y `x-jazui-cache: MISS` la primera vez y `HIT` después. Sin agente de bot (`curl -I https://misitio.com/`) debe llegar tu web de siempre, sin `x-jazui`. En el panel, la petición aparece en **Tráfico de bots** y la página en **Páginas en caché**. ## Si algo no funciona - **No aparece `x-jazui`**: comprueba que la ruta del Worker cubre la dirección que pruebas (con y sin `www`) y que `JAZUI_TOKEN` está guardado como secreto en ese Worker. - **El registro DNS no tiene la nube naranja**: sin el proxy de Cloudflare, el Worker no recibe las peticiones. - **Tienes otro Worker en la misma ruta**: solo se ejecuta uno por ruta; combina la lógica en uno o separa las rutas. - En **Workers › jazui › Registros** puedes ver en vivo cada petición y si hubo errores. --- # Vercel / Next.js > Middleware en el edge de Vercel, para Next.js o cualquier sitio estático en Vercel. Página original: https://docs.jazui.com/integrations/vercel/es Vercel ejecuta `middleware.js` antes de cada petición. El de Jazui pide a Jazui las páginas para bots y deja pasar el resto: si devuelve `undefined`, la petición sigue su camino normal. ## Antes de empezar - Un proyecto en Vercel (Next.js, Vite, Angular, React…). - Tu sitio verificado en Jazui y tu token. ## Instalación Descarga estos dos archivos y ponlos en la raíz del proyecto (en Next.js, junto a `app/` o `pages/`; si usas `src/`, dentro de `src/`). El primero se tiene que llamar `middleware.js`: ```bash mi-proyecto/ ├── app/ (o pages/, o tu SPA) ├── middleware.js ← vercel-middleware.js └── core.js ``` `middleware.js` (https://docs.jazui.com/example_config/vercel-middleware.js): ```javascript // Middleware de Vercel (también de Next.js): copia este archivo como middleware.js en la raíz del proyecto, con // core.js al lado, y define JAZUI_TOKEN en las variables de entorno. Sin respuesta, la petición sigue su camino. import { renderRequest } from './core.js'; export default async function middleware(request, context) { const ip = (request.headers.get('x-forwarded-for') || '').split(',')[0].trim() || request.headers.get('x-real-ip'); const rendered = await renderRequest(request, { token: process.env.JAZUI_TOKEN, api: process.env.JAZUI_API, siteUrl: process.env.JAZUI_SITE_URL, waitUntil: context?.waitUntil?.bind(context), }, ip); return rendered ?? undefined; } export const config = { matcher: '/((?!api/|_next/|_vercel/).*)' }; ``` `core.js` (https://docs.jazui.com/example_config/core.js): ```javascript // Núcleo común de las integraciones JavaScript de Jazui (Cloudflare Workers, Netlify, Vercel y Express). // Solo usa la API Fetch estándar: funciona en Node 18+, Deno y los runtimes de borde. export const DEFAULT_API = 'https://api.jazui.com'; export const DEFAULT_TIMEOUT_MS = 20000; // Bots a los que se sirve el HTML renderizado (bots.txt; lo rellena Sync-Integrations.ps1) export const BOTS = [ // BOTS:BEGIN 'googlebot', 'google-inspectiontool', 'storebot-google', 'adsbot-google', 'bingbot', 'bingpreview', 'msnbot', 'adidxbot', 'yandex', 'baiduspider', 'duckduckbot', 'slurp', 'applebot', 'seznambot', 'qwantbot', 'qwantify', 'petalbot', 'sogou', 'naver', 'exabot', 'yetibot', '360spider', 'soso', 'gptbot', 'chatgpt', 'oai-searchbot', 'claudebot', 'claude-user', 'claude-searchbot', 'anthropic-ai', 'perplexity', 'google-cloudvertexbot', 'bytespider', 'ccbot', 'amazonbot', 'meta-externalagent', 'cohere-ai', 'mistralai-user', 'youbot', 'duckassistbot', 'yibot', 'qwenbot', 'tongyibot', 'erniebot', 'yiyanbot', 'doubaobot', 'tiktokspider', 'deepseekbot', 'pangubot', 'manus-user', 'kimi-agent', 'kimi-searchbot', 'kimi-user', 'kimibot', 'moonshotbot', 'hunyuan', 'chatglm-spider', 'agi agent', 'ai2bot', 'ai2bot-deepresearcheval', 'ai2bot-dolma', 'aihitbot', 'amazon-kendra', 'amazon-qbusiness', 'amazonbuyforme', 'amzn-searchbot', 'amzn-user', 'bedrockbot', 'novaact', 'andibot', 'claude-web', 'apifybot', 'apifywebsitecontentcrawler', 'atlassian-bot', 'awario', 'bigsur.ai', 'brightbot', 'buddybot', 'cartai agentic commerce', 'terracotta', 'cledarabot', 'cloudflare-autorag', 'cloudflarebrowserrenderingcrawler', 'cohere-training-data-crawler', 'cragcrawler', 'crawlspace', 'datenbank crawler', 'diffbot', 'diffbot-user', 'anomura', 'echobot bot', 'echoboxbot', 'elevenlabsbot', 'elevenlabscrawler', 'exasearchbot', 'exauser', 'factset_spyderbot', 'firecrawlagent', 'geisthaus-pagefetcher', 'cloudvertexbot', 'gemini-deep-research', 'google-agent', 'google-firebase', 'google-notebooklm', 'googleagent-mariner', 'googleagent-urlcontext', 'googleother', 'googleother-image', 'googleother-video', 'notebooklm', 'iaskbot', 'iaskspider', 'imagesiftbot', 'img2dataset', 'isscyberriskcrawler', 'kagi-fetcher', 'keenablebot', 'klaviyoaibot', 'laiondownloader', 'linguee bot', 'linkupbot', 'aiwebindex', 'meta-externalfetcher', 'meta-webindexer', 'facebookbot', 'azureai-searchbot', 'mistralai-index', 'mistralai-training', 'mozilla-tabstack', 'nagetbot', 'netestate imprint crawler', 'icc-crawler', 'oai-adsbot', 'panscient', 'panscient.com', 'shapbot', 'payhawk invoice fetching agent', 'phindbot', 'poggio-citations', 'poseidon research crawler', 'qualifiedbot', 'wpbot', 'querit-searchbot', 'queritbot', 'quillbot', 'quillbot.com', 'reflectionbot', 'cotoyogi', 'sbintuitionsbot', 'scrybot', 'sidetrade indexer bot', 'tavilybot', 'agenttimes', 'thinkbot', 'timpibot', 'twinagent', 'velenpublicwebcrawler', 'wardbot', 'omgili', 'omgilibot', 'grok bot', 'grokappandroid', 'grokbot', 'addsearchbot', 'agentdatabot', 'aranet-searchbot', 'bixelbot', 'bravebot', 'channel3bot', 'friendlycrawler', 'henkbot', 'imagespider', 'kangaroo bot', 'kunatocrawler', 'laion-huggingface-processor', 'linerbot', 'mycentralaiscraperbot', 'terra cotta', 'wrtnbot', 'zanistabot', 'facebookexternalhit', 'facebot', 'twitterbot', 'linkedinbot', 'slackbot', 'whatsapp', 'telegrambot', 'discordbot', 'pinterest', 'redditbot', 'skypeuripreview', 'embedly', 'quora link preview', 'showyoubot', 'outbrain', 'developers.google.com/+/web/snippet', 'vkshare', 'tumblr', 'bitlybot', 'nuzzel', 'flipboard', 'snapchat', 'viber', 'mattermost', 'bitrix link preview', 'xing-contenttabreceiver', 'ahrefsbot', 'semrushbot', 'mj12bot', 'majestic-12', 'dotbot', 'rogerbot', 'screaming frog', 'sitebulb', 'serpstatbot', 'seokicks', 'siteimprove', 'lighthouse', 'google page speed', 'gtmetrix', 'pingdom', 'uptimerobot', 'uptimebot', 'w3c_validator', 'w3c-checklink', 'archive.org_bot', 'paperlibot', 'dataprovider.com', 'grapeshotcrawler', 'zoominfobot', 'feedly', 'newsblur', 'inoreader', 'integration-test', // BOTS:END ]; // Archivos que nunca se renderizan (extensions.txt) export const STATIC_EXTENSIONS = [ // EXTENSIONS:BEGIN 'js', 'mjs', 'css', 'less', 'map', 'json', 'xml', 'rss', 'txt', 'webmanifest', 'ico', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'svg', 'bmp', 'tif', 'tiff', 'psd', 'ai', 'woff', 'woff2', 'ttf', 'otf', 'eot', 'mp3', 'm4a', 'wav', 'ogg', 'mp4', 'm4v', 'webm', 'avi', 'mov', 'mpg', 'mpeg', 'wmv', 'flv', 'swf', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'csv', 'zip', 'gz', 'rar', '7z', 'iso', 'torrent', 'exe', 'dmg', 'apk', 'wasm', 'dll', 'dat', 'blat', // EXTENSIONS:END ]; const escapeRegex = (text) => text.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&'); // Patrón de la lista incluida; refreshBots lo sustituye por el del catálogo de Jazui (GET /v2/bots) export const BOT_PATTERN = new RegExp(BOTS.map(escapeRegex).join('|'), 'i'); export const STATIC_PATTERN = new RegExp(`\\.(${STATIC_EXTENSIONS.join('|')})$`, 'i'); let botPattern = BOT_PATTERN; let botsVersion = 'incluida'; let botsCheckedAt = 0; let botsLoading = null; const BOTS_REFRESH_MS = 24 * 60 * 60 * 1000; const BOTS_RETRY_MS = 60 * 60 * 1000; /** Patrón de bots en uso: el del catálogo si ya se descargó, si no el de la lista incluida. */ export const currentBotPattern = () => botPattern; /** Versión de la lista en uso ('incluida' hasta la primera descarga). */ export const currentBotsVersion = () => botsVersion; /** * Descarga la lista de bots del catálogo de Jazui (GET /v2/bots) si toca: al arrancar y cada 24 h; si falla se * sigue con la que hay y se reintenta en una hora. Nunca falla ni retrasa la petición: devuelve la promesa para * waitUntil (Cloudflare, Vercel, Netlify) o null si no toca. Se desactiva con remoteBots: false. */ export function refreshBots({ api = DEFAULT_API, remoteBots = true } = {}) { const now = Date.now(); if (!remoteBots || botsLoading || now - botsCheckedAt < BOTS_REFRESH_MS) return null; botsCheckedAt = now; botsLoading = fetch(`${(api || DEFAULT_API).replace(/\/+$/, '')}/v2/bots`, { signal: AbortSignal.timeout(5000) }) .then((response) => (response.ok ? response.json() : null)) .then((data) => { if (data && Array.isArray(data.bots) && data.bots.length > 0) { botPattern = new RegExp(data.bots.map(escapeRegex).join('|'), 'i'); botsVersion = data.version || botsVersion; } else { botsCheckedAt = now - BOTS_REFRESH_MS + BOTS_RETRY_MS; } }, () => { botsCheckedAt = now - BOTS_REFRESH_MS + BOTS_RETRY_MS; }) .finally(() => { botsLoading = null; }); return botsLoading; } // Cabeceras de Jazui que se devuelven al bot const PASS_HEADERS = ['content-type', 'location', 'etag', 'last-modified', 'x-jazui', 'x-jazui-cache']; /** * Si la petición hay que pasarla por Jazui: GET/HEAD de un bot conocido (o con _escaped_fragment_, el esquema * antiguo de Google para las SPA) a una página, no a un archivo. */ export function shouldRender({ method, url, userAgent, headers }) { if (method !== 'GET' && method !== 'HEAD') return false; if (headers && headers.get('x-jazui')) return false; // nunca a sí mismo const { pathname, search } = new URL(url); if (STATIC_PATTERN.test(pathname)) return false; return (!!userAgent && botPattern.test(userAgent)) || search.includes('_escaped_fragment_'); } /** * Pide a Jazui el HTML de la URL. Devuelve la Response para el bot o null si hay que servir la página normal * (Jazui no respondió, se pasó del límite, el sitio no está verificado…): el bot nunca ve un error de Jazui. * Solo un 403 marcado con X-Jazui-Blocked (regla o bot de IA bloqueado) llega al bot. */ export async function fetchRendered({ url, method = 'GET', userAgent, clientIp, ifNoneMatch, token, api = DEFAULT_API, siteUrl, timeoutMs = DEFAULT_TIMEOUT_MS }) { if (!token) return null; let target = url; if (siteUrl) { // Dominio público del sitio cuando el servidor lo ve con otro (proxy, entorno de pruebas) const original = new URL(url); target = new URL(original.pathname + original.search, siteUrl).toString(); } const query = new URLSearchParams({ url: target, botAgent: userAgent || '' }); const headers = { Authorization: `Bearer ${token}` }; if (clientIp) headers['X-Jazui-Client-IP'] = clientIp; if (ifNoneMatch) headers['If-None-Match'] = ifNoneMatch; let response; try { response = await fetch(`${(api || DEFAULT_API).replace(/\/+$/, '')}/Render?${query}`, { headers, redirect: 'manual', signal: AbortSignal.timeout(timeoutMs), }); } catch { return null; } const blocked = response.status === 403 && response.headers.get('x-jazui-blocked'); if (!response.headers.get('x-jazui') && !blocked) { response.body?.cancel(); return null; } const out = new Headers(); for (const name of PASS_HEADERS) { const value = response.headers.get(name); if (value) out.set(name, value); } if (blocked) out.set('x-jazui-blocked', blocked); const empty = method === 'HEAD' || response.status === 304; if (empty) response.body?.cancel(); return new Response(empty ? null : response.body, { status: response.status, headers: out }); } // User-agents con pinta de bot: si no están en BOTS se avisa a Jazui sin enviarle la página (el mismo patrón que la API) export const UNKNOWN_BOT_PATTERN = /bot|crawl|spider|scrap|fetch|headless|preview|archiv|index/i; const MAX_REPORTED = 1000; const reported = new Set(); let reportedDay = ''; /** * Avisa a Jazui (POST /Render/Seen) de un user-agent con pinta de bot que no está en la lista, una vez al día por * user-agent y proceso, para que se pueda añadir al catálogo. No gasta renders y nunca falla: devuelve la promesa para * waitUntil (Cloudflare, Vercel) o null si no hay que avisar. */ export function reportUnknownBot({ method, url, userAgent, token, api = DEFAULT_API, reportUnknownBots = true }) { if (!reportUnknownBots || !token || !userAgent || (method !== 'GET' && method !== 'HEAD')) return null; if (STATIC_PATTERN.test(new URL(url).pathname)) return null; if (!UNKNOWN_BOT_PATTERN.test(userAgent) || botPattern.test(userAgent)) return null; const today = new Date().toISOString().slice(0, 10); if (reportedDay !== today) { reported.clear(); reportedDay = today; } if (reported.size >= MAX_REPORTED || reported.has(userAgent)) return null; reported.add(userAgent); return fetch(`${(api || DEFAULT_API).replace(/\/+$/, '')}/Render/Seen`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ userAgent, url }), signal: AbortSignal.timeout(5000), }).then((response) => { response.body?.cancel(); }, () => {}); } /** * Atajo para los runtimes con Request estándar: la Response de Jazui o null. Con options.waitUntil (Cloudflare, * Vercel) la descarga de la lista de bots y el aviso de bots desconocidos siguen aunque la respuesta ya se haya enviado. */ export function renderRequest(request, options, clientIp) { const refresh = refreshBots(options); if (refresh && options.waitUntil) options.waitUntil(refresh); const userAgent = request.headers.get('user-agent') || ''; if (!shouldRender({ method: request.method, url: request.url, userAgent, headers: request.headers })) { const report = reportUnknownBot({ ...options, method: request.method, url: request.url, userAgent }); if (report && options.waitUntil) options.waitUntil(report); return null; } return fetchRendered({ ...options, url: request.url, method: request.method, userAgent, clientIp, ifNoneMatch: request.headers.get('if-none-match'), }); } ``` El `matcher` deja fuera `/api/`, `/_next/` y `/_vercel/`. Si ya tenías un `middleware.js`, llama primero a la función de Jazui y, si devuelve una respuesta, devuélvela; si no, sigue con tu lógica. ## Variables de entorno | Variable | Obligatoria | Para qué | |---|---|---| | `JAZUI_TOKEN` | Sí | Tu token de la cuenta o del sitio. Guárdalo como **secreto**, nunca en el código. | | `JAZUI_API` | No | Dirección de la API. Por defecto `https://api.jazui.com`. | | `JAZUI_SITE_URL` | No | Tu dominio público (por ejemplo `https://misitio.com`) si la plataforma ve otro, como una dirección de vista previa: Jazui renderiza la página con ese dominio. | Sin `JAZUI_TOKEN` la integración no hace nada y toda la web se sirve normal. ```bash vercel env add JAZUI_TOKEN production ``` ## Qué hace Toma la IP del bot de `x-forwarded-for` (o `x-real-ip`), pide la página a Jazui y la devuelve si llega. La lista de bots y los avisos se hacen en segundo plano con `waitUntil`. Tras añadir la variable, vuelve a desplegar para que la use. ##

Pide tu portada como si fueras un bot, con el agente de usuario de pruebas integration-test:

curl -I -A "integration-test" https://misitio.com/

La respuesta debe llevar x-jazui: true y x-jazui-cache: MISS la primera vez y HIT después. Sin agente de bot (curl -I https://misitio.com/) debe llegar tu web de siempre, sin x-jazui. En el panel, la petición aparece en Tráfico de bots y la página en Páginas en caché.

Pide tu portada como si fueras un bot, con el agente de usuario de pruebas `integration-test`: ``` curl -I -A "integration-test" https://misitio.com/ ``` La respuesta debe llevar `x-jazui: true` y `x-jazui-cache: MISS` la primera vez y `HIT` después. Sin agente de bot (`curl -I https://misitio.com/`) debe llegar tu web de siempre, sin `x-jazui`. En el panel, la petición aparece en **Tráfico de bots** y la página en **Páginas en caché**. ## Si algo no funciona - **No aparece `x-jazui`**: el archivo tiene que llamarse `middleware.js` y estar en la raíz (o en `src/`); comprueba en el registro del despliegue que Vercel lo detecta como Middleware. - **La variable no llega**: añádela al entorno **Production** (y Preview si pruebas ahí) y vuelve a desplegar. - **Pruebas en una dirección de vista previa**: define `JAZUI_SITE_URL` con tu dominio verificado, porque `*.vercel.app` no está verificado. --- # Netlify > Una Edge Function que atiende a los bots antes de servir tu sitio de Netlify. Página original: https://docs.jazui.com/integrations/netlify/es Las Edge Functions de Netlify se ejecutan antes de servir cada página. La de Jazui pide a Jazui las páginas para bots y, para el resto, llama a `context.next()` para que Netlify sirva tu sitio como siempre. ## Antes de empezar - Un sitio en Netlify. - Tu sitio verificado en Jazui y tu token. ## Instalación Crea la carpeta `netlify/edge-functions/jazui/` y pon dentro estos dos archivos. El primero tiene que llamarse como la carpeta, `jazui.js`, para que Netlify lo trate como la función y a `core.js` como un módulo suyo: ```bash mi-sitio/ └── netlify/ └── edge-functions/ └── jazui/ ├── jazui.js ← netlify-edge.js └── core.js ``` `netlify/edge-functions/jazui/jazui.js` (https://docs.jazui.com/example_config/netlify-edge.js): ```javascript // Edge Function de Netlify: copia este archivo y core.js en netlify/edge-functions/ y define JAZUI_TOKEN en las // variables de entorno del sitio. import { renderRequest } from './core.js'; export default async (request, context) => { const rendered = await renderRequest(request, { token: Netlify.env.get('JAZUI_TOKEN'), api: Netlify.env.get('JAZUI_API'), siteUrl: Netlify.env.get('JAZUI_SITE_URL'), waitUntil: context.waitUntil?.bind(context), }, context.ip); return rendered ?? context.next(); }; export const config = { path: '/*', excludedPath: ['/.netlify/*', '/api/*'] }; ``` `netlify/edge-functions/jazui/core.js` (https://docs.jazui.com/example_config/core.js): ```javascript // Núcleo común de las integraciones JavaScript de Jazui (Cloudflare Workers, Netlify, Vercel y Express). // Solo usa la API Fetch estándar: funciona en Node 18+, Deno y los runtimes de borde. export const DEFAULT_API = 'https://api.jazui.com'; export const DEFAULT_TIMEOUT_MS = 20000; // Bots a los que se sirve el HTML renderizado (bots.txt; lo rellena Sync-Integrations.ps1) export const BOTS = [ // BOTS:BEGIN 'googlebot', 'google-inspectiontool', 'storebot-google', 'adsbot-google', 'bingbot', 'bingpreview', 'msnbot', 'adidxbot', 'yandex', 'baiduspider', 'duckduckbot', 'slurp', 'applebot', 'seznambot', 'qwantbot', 'qwantify', 'petalbot', 'sogou', 'naver', 'exabot', 'yetibot', '360spider', 'soso', 'gptbot', 'chatgpt', 'oai-searchbot', 'claudebot', 'claude-user', 'claude-searchbot', 'anthropic-ai', 'perplexity', 'google-cloudvertexbot', 'bytespider', 'ccbot', 'amazonbot', 'meta-externalagent', 'cohere-ai', 'mistralai-user', 'youbot', 'duckassistbot', 'yibot', 'qwenbot', 'tongyibot', 'erniebot', 'yiyanbot', 'doubaobot', 'tiktokspider', 'deepseekbot', 'pangubot', 'manus-user', 'kimi-agent', 'kimi-searchbot', 'kimi-user', 'kimibot', 'moonshotbot', 'hunyuan', 'chatglm-spider', 'agi agent', 'ai2bot', 'ai2bot-deepresearcheval', 'ai2bot-dolma', 'aihitbot', 'amazon-kendra', 'amazon-qbusiness', 'amazonbuyforme', 'amzn-searchbot', 'amzn-user', 'bedrockbot', 'novaact', 'andibot', 'claude-web', 'apifybot', 'apifywebsitecontentcrawler', 'atlassian-bot', 'awario', 'bigsur.ai', 'brightbot', 'buddybot', 'cartai agentic commerce', 'terracotta', 'cledarabot', 'cloudflare-autorag', 'cloudflarebrowserrenderingcrawler', 'cohere-training-data-crawler', 'cragcrawler', 'crawlspace', 'datenbank crawler', 'diffbot', 'diffbot-user', 'anomura', 'echobot bot', 'echoboxbot', 'elevenlabsbot', 'elevenlabscrawler', 'exasearchbot', 'exauser', 'factset_spyderbot', 'firecrawlagent', 'geisthaus-pagefetcher', 'cloudvertexbot', 'gemini-deep-research', 'google-agent', 'google-firebase', 'google-notebooklm', 'googleagent-mariner', 'googleagent-urlcontext', 'googleother', 'googleother-image', 'googleother-video', 'notebooklm', 'iaskbot', 'iaskspider', 'imagesiftbot', 'img2dataset', 'isscyberriskcrawler', 'kagi-fetcher', 'keenablebot', 'klaviyoaibot', 'laiondownloader', 'linguee bot', 'linkupbot', 'aiwebindex', 'meta-externalfetcher', 'meta-webindexer', 'facebookbot', 'azureai-searchbot', 'mistralai-index', 'mistralai-training', 'mozilla-tabstack', 'nagetbot', 'netestate imprint crawler', 'icc-crawler', 'oai-adsbot', 'panscient', 'panscient.com', 'shapbot', 'payhawk invoice fetching agent', 'phindbot', 'poggio-citations', 'poseidon research crawler', 'qualifiedbot', 'wpbot', 'querit-searchbot', 'queritbot', 'quillbot', 'quillbot.com', 'reflectionbot', 'cotoyogi', 'sbintuitionsbot', 'scrybot', 'sidetrade indexer bot', 'tavilybot', 'agenttimes', 'thinkbot', 'timpibot', 'twinagent', 'velenpublicwebcrawler', 'wardbot', 'omgili', 'omgilibot', 'grok bot', 'grokappandroid', 'grokbot', 'addsearchbot', 'agentdatabot', 'aranet-searchbot', 'bixelbot', 'bravebot', 'channel3bot', 'friendlycrawler', 'henkbot', 'imagespider', 'kangaroo bot', 'kunatocrawler', 'laion-huggingface-processor', 'linerbot', 'mycentralaiscraperbot', 'terra cotta', 'wrtnbot', 'zanistabot', 'facebookexternalhit', 'facebot', 'twitterbot', 'linkedinbot', 'slackbot', 'whatsapp', 'telegrambot', 'discordbot', 'pinterest', 'redditbot', 'skypeuripreview', 'embedly', 'quora link preview', 'showyoubot', 'outbrain', 'developers.google.com/+/web/snippet', 'vkshare', 'tumblr', 'bitlybot', 'nuzzel', 'flipboard', 'snapchat', 'viber', 'mattermost', 'bitrix link preview', 'xing-contenttabreceiver', 'ahrefsbot', 'semrushbot', 'mj12bot', 'majestic-12', 'dotbot', 'rogerbot', 'screaming frog', 'sitebulb', 'serpstatbot', 'seokicks', 'siteimprove', 'lighthouse', 'google page speed', 'gtmetrix', 'pingdom', 'uptimerobot', 'uptimebot', 'w3c_validator', 'w3c-checklink', 'archive.org_bot', 'paperlibot', 'dataprovider.com', 'grapeshotcrawler', 'zoominfobot', 'feedly', 'newsblur', 'inoreader', 'integration-test', // BOTS:END ]; // Archivos que nunca se renderizan (extensions.txt) export const STATIC_EXTENSIONS = [ // EXTENSIONS:BEGIN 'js', 'mjs', 'css', 'less', 'map', 'json', 'xml', 'rss', 'txt', 'webmanifest', 'ico', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'svg', 'bmp', 'tif', 'tiff', 'psd', 'ai', 'woff', 'woff2', 'ttf', 'otf', 'eot', 'mp3', 'm4a', 'wav', 'ogg', 'mp4', 'm4v', 'webm', 'avi', 'mov', 'mpg', 'mpeg', 'wmv', 'flv', 'swf', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'csv', 'zip', 'gz', 'rar', '7z', 'iso', 'torrent', 'exe', 'dmg', 'apk', 'wasm', 'dll', 'dat', 'blat', // EXTENSIONS:END ]; const escapeRegex = (text) => text.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&'); // Patrón de la lista incluida; refreshBots lo sustituye por el del catálogo de Jazui (GET /v2/bots) export const BOT_PATTERN = new RegExp(BOTS.map(escapeRegex).join('|'), 'i'); export const STATIC_PATTERN = new RegExp(`\\.(${STATIC_EXTENSIONS.join('|')})$`, 'i'); let botPattern = BOT_PATTERN; let botsVersion = 'incluida'; let botsCheckedAt = 0; let botsLoading = null; const BOTS_REFRESH_MS = 24 * 60 * 60 * 1000; const BOTS_RETRY_MS = 60 * 60 * 1000; /** Patrón de bots en uso: el del catálogo si ya se descargó, si no el de la lista incluida. */ export const currentBotPattern = () => botPattern; /** Versión de la lista en uso ('incluida' hasta la primera descarga). */ export const currentBotsVersion = () => botsVersion; /** * Descarga la lista de bots del catálogo de Jazui (GET /v2/bots) si toca: al arrancar y cada 24 h; si falla se * sigue con la que hay y se reintenta en una hora. Nunca falla ni retrasa la petición: devuelve la promesa para * waitUntil (Cloudflare, Vercel, Netlify) o null si no toca. Se desactiva con remoteBots: false. */ export function refreshBots({ api = DEFAULT_API, remoteBots = true } = {}) { const now = Date.now(); if (!remoteBots || botsLoading || now - botsCheckedAt < BOTS_REFRESH_MS) return null; botsCheckedAt = now; botsLoading = fetch(`${(api || DEFAULT_API).replace(/\/+$/, '')}/v2/bots`, { signal: AbortSignal.timeout(5000) }) .then((response) => (response.ok ? response.json() : null)) .then((data) => { if (data && Array.isArray(data.bots) && data.bots.length > 0) { botPattern = new RegExp(data.bots.map(escapeRegex).join('|'), 'i'); botsVersion = data.version || botsVersion; } else { botsCheckedAt = now - BOTS_REFRESH_MS + BOTS_RETRY_MS; } }, () => { botsCheckedAt = now - BOTS_REFRESH_MS + BOTS_RETRY_MS; }) .finally(() => { botsLoading = null; }); return botsLoading; } // Cabeceras de Jazui que se devuelven al bot const PASS_HEADERS = ['content-type', 'location', 'etag', 'last-modified', 'x-jazui', 'x-jazui-cache']; /** * Si la petición hay que pasarla por Jazui: GET/HEAD de un bot conocido (o con _escaped_fragment_, el esquema * antiguo de Google para las SPA) a una página, no a un archivo. */ export function shouldRender({ method, url, userAgent, headers }) { if (method !== 'GET' && method !== 'HEAD') return false; if (headers && headers.get('x-jazui')) return false; // nunca a sí mismo const { pathname, search } = new URL(url); if (STATIC_PATTERN.test(pathname)) return false; return (!!userAgent && botPattern.test(userAgent)) || search.includes('_escaped_fragment_'); } /** * Pide a Jazui el HTML de la URL. Devuelve la Response para el bot o null si hay que servir la página normal * (Jazui no respondió, se pasó del límite, el sitio no está verificado…): el bot nunca ve un error de Jazui. * Solo un 403 marcado con X-Jazui-Blocked (regla o bot de IA bloqueado) llega al bot. */ export async function fetchRendered({ url, method = 'GET', userAgent, clientIp, ifNoneMatch, token, api = DEFAULT_API, siteUrl, timeoutMs = DEFAULT_TIMEOUT_MS }) { if (!token) return null; let target = url; if (siteUrl) { // Dominio público del sitio cuando el servidor lo ve con otro (proxy, entorno de pruebas) const original = new URL(url); target = new URL(original.pathname + original.search, siteUrl).toString(); } const query = new URLSearchParams({ url: target, botAgent: userAgent || '' }); const headers = { Authorization: `Bearer ${token}` }; if (clientIp) headers['X-Jazui-Client-IP'] = clientIp; if (ifNoneMatch) headers['If-None-Match'] = ifNoneMatch; let response; try { response = await fetch(`${(api || DEFAULT_API).replace(/\/+$/, '')}/Render?${query}`, { headers, redirect: 'manual', signal: AbortSignal.timeout(timeoutMs), }); } catch { return null; } const blocked = response.status === 403 && response.headers.get('x-jazui-blocked'); if (!response.headers.get('x-jazui') && !blocked) { response.body?.cancel(); return null; } const out = new Headers(); for (const name of PASS_HEADERS) { const value = response.headers.get(name); if (value) out.set(name, value); } if (blocked) out.set('x-jazui-blocked', blocked); const empty = method === 'HEAD' || response.status === 304; if (empty) response.body?.cancel(); return new Response(empty ? null : response.body, { status: response.status, headers: out }); } // User-agents con pinta de bot: si no están en BOTS se avisa a Jazui sin enviarle la página (el mismo patrón que la API) export const UNKNOWN_BOT_PATTERN = /bot|crawl|spider|scrap|fetch|headless|preview|archiv|index/i; const MAX_REPORTED = 1000; const reported = new Set(); let reportedDay = ''; /** * Avisa a Jazui (POST /Render/Seen) de un user-agent con pinta de bot que no está en la lista, una vez al día por * user-agent y proceso, para que se pueda añadir al catálogo. No gasta renders y nunca falla: devuelve la promesa para * waitUntil (Cloudflare, Vercel) o null si no hay que avisar. */ export function reportUnknownBot({ method, url, userAgent, token, api = DEFAULT_API, reportUnknownBots = true }) { if (!reportUnknownBots || !token || !userAgent || (method !== 'GET' && method !== 'HEAD')) return null; if (STATIC_PATTERN.test(new URL(url).pathname)) return null; if (!UNKNOWN_BOT_PATTERN.test(userAgent) || botPattern.test(userAgent)) return null; const today = new Date().toISOString().slice(0, 10); if (reportedDay !== today) { reported.clear(); reportedDay = today; } if (reported.size >= MAX_REPORTED || reported.has(userAgent)) return null; reported.add(userAgent); return fetch(`${(api || DEFAULT_API).replace(/\/+$/, '')}/Render/Seen`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ userAgent, url }), signal: AbortSignal.timeout(5000), }).then((response) => { response.body?.cancel(); }, () => {}); } /** * Atajo para los runtimes con Request estándar: la Response de Jazui o null. Con options.waitUntil (Cloudflare, * Vercel) la descarga de la lista de bots y el aviso de bots desconocidos siguen aunque la respuesta ya se haya enviado. */ export function renderRequest(request, options, clientIp) { const refresh = refreshBots(options); if (refresh && options.waitUntil) options.waitUntil(refresh); const userAgent = request.headers.get('user-agent') || ''; if (!shouldRender({ method: request.method, url: request.url, userAgent, headers: request.headers })) { const report = reportUnknownBot({ ...options, method: request.method, url: request.url, userAgent }); if (report && options.waitUntil) options.waitUntil(report); return null; } return fetchRendered({ ...options, url: request.url, method: request.method, userAgent, clientIp, ifNoneMatch: request.headers.get('if-none-match'), }); } ``` ## Variables de entorno | Variable | Obligatoria | Para qué | |---|---|---| | `JAZUI_TOKEN` | Sí | Tu token de la cuenta o del sitio. Guárdalo como **secreto**, nunca en el código. | | `JAZUI_API` | No | Dirección de la API. Por defecto `https://api.jazui.com`. | | `JAZUI_SITE_URL` | No | Tu dominio público (por ejemplo `https://misitio.com`) si la plataforma ve otro, como una dirección de vista previa: Jazui renderiza la página con ese dominio. | Sin `JAZUI_TOKEN` la integración no hace nada y toda la web se sirve normal. ```bash netlify env:set JAZUI_TOKEN "tu-token" netlify deploy --prod ``` ## Qué hace La función se aplica a todas las rutas salvo `/.netlify/*` y `/api/*` (lo dice su `config`; puedes añadir más exclusiones en `excludedPath`). La IP del bot la da Netlify en `context.ip`, y la lista de bots y los avisos van en segundo plano con `waitUntil`. ##

Pide tu portada como si fueras un bot, con el agente de usuario de pruebas integration-test:

curl -I -A "integration-test" https://misitio.com/

La respuesta debe llevar x-jazui: true y x-jazui-cache: MISS la primera vez y HIT después. Sin agente de bot (curl -I https://misitio.com/) debe llegar tu web de siempre, sin x-jazui. En el panel, la petición aparece en Tráfico de bots y la página en Páginas en caché.

Pide tu portada como si fueras un bot, con el agente de usuario de pruebas `integration-test`: ``` curl -I -A "integration-test" https://misitio.com/ ``` La respuesta debe llevar `x-jazui: true` y `x-jazui-cache: MISS` la primera vez y `HIT` después. Sin agente de bot (`curl -I https://misitio.com/`) debe llegar tu web de siempre, sin `x-jazui`. En el panel, la petición aparece en **Tráfico de bots** y la página en **Páginas en caché**. ## Si algo no funciona - **No aparece `x-jazui`**: en el registro del despliegue debe salir la Edge Function `jazui`. Si no, revisa la carpeta y el nombre del archivo. - **La variable no llega**: defínela en **Site configuration › Environment variables** con el ámbito de las funciones y vuelve a desplegar. - **Direcciones de vista previa** (`*.netlify.app`): define `JAZUI_SITE_URL` con tu dominio verificado. --- # Node.js / Express > Un middleware para tu servidor Node.js con Express. Página original: https://docs.jazui.com/integrations/express/es Si tu SPA la sirve un servidor Node con Express, el middleware de Jazui va antes de los archivos estáticos: pide a Jazui las páginas para bots y, para el resto, llama a `next()`. ## Antes de empezar - Node.js 18.17 o superior (usa `fetch` nativo) y Express 4 o 5. - El proyecto como módulos ES (`"type": "module"` en `package.json`). - Tu sitio verificado en Jazui y tu token. ## Instalación Descarga estos dos archivos y ponlos juntos en una carpeta de tu proyecto, por ejemplo `jazui/`: `jazui/express.js` (https://docs.jazui.com/example_config/express.js): ```javascript // Middleware de Express: sirve a los bots el HTML de Jazui y deja pasar el resto de peticiones. // import { jazui } from 'jazui-integrations/express'; // app.set('trust proxy', true); // si hay un proxy o CDN delante, para que req.ip sea la del bot // app.use(jazui({ token: process.env.JAZUI_TOKEN })); import { shouldRender, fetchRendered, reportUnknownBot, refreshBots } from './core.js'; export function jazui(options = {}) { return async (req, res, next) => { refreshBots(options); // lista de bots del catálogo, en segundo plano y cada 24 h const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`; const userAgent = req.get('user-agent') || ''; if (!shouldRender({ method: req.method, url, userAgent, headers: { get: (name) => req.get(name) } })) { reportUnknownBot({ ...options, method: req.method, url, userAgent }); // aviso en segundo plano return next(); } try { const rendered = await fetchRendered({ ...options, url, method: req.method, userAgent, clientIp: req.ip, ifNoneMatch: req.get('if-none-match'), }); if (!rendered) return next(); res.status(rendered.status); rendered.headers.forEach((value, name) => res.setHeader(name, value)); res.end(rendered.body ? Buffer.from(await rendered.arrayBuffer()) : undefined); } catch { next(); } }; } ``` `jazui/core.js` (https://docs.jazui.com/example_config/core.js): ```javascript // Núcleo común de las integraciones JavaScript de Jazui (Cloudflare Workers, Netlify, Vercel y Express). // Solo usa la API Fetch estándar: funciona en Node 18+, Deno y los runtimes de borde. export const DEFAULT_API = 'https://api.jazui.com'; export const DEFAULT_TIMEOUT_MS = 20000; // Bots a los que se sirve el HTML renderizado (bots.txt; lo rellena Sync-Integrations.ps1) export const BOTS = [ // BOTS:BEGIN 'googlebot', 'google-inspectiontool', 'storebot-google', 'adsbot-google', 'bingbot', 'bingpreview', 'msnbot', 'adidxbot', 'yandex', 'baiduspider', 'duckduckbot', 'slurp', 'applebot', 'seznambot', 'qwantbot', 'qwantify', 'petalbot', 'sogou', 'naver', 'exabot', 'yetibot', '360spider', 'soso', 'gptbot', 'chatgpt', 'oai-searchbot', 'claudebot', 'claude-user', 'claude-searchbot', 'anthropic-ai', 'perplexity', 'google-cloudvertexbot', 'bytespider', 'ccbot', 'amazonbot', 'meta-externalagent', 'cohere-ai', 'mistralai-user', 'youbot', 'duckassistbot', 'yibot', 'qwenbot', 'tongyibot', 'erniebot', 'yiyanbot', 'doubaobot', 'tiktokspider', 'deepseekbot', 'pangubot', 'manus-user', 'kimi-agent', 'kimi-searchbot', 'kimi-user', 'kimibot', 'moonshotbot', 'hunyuan', 'chatglm-spider', 'agi agent', 'ai2bot', 'ai2bot-deepresearcheval', 'ai2bot-dolma', 'aihitbot', 'amazon-kendra', 'amazon-qbusiness', 'amazonbuyforme', 'amzn-searchbot', 'amzn-user', 'bedrockbot', 'novaact', 'andibot', 'claude-web', 'apifybot', 'apifywebsitecontentcrawler', 'atlassian-bot', 'awario', 'bigsur.ai', 'brightbot', 'buddybot', 'cartai agentic commerce', 'terracotta', 'cledarabot', 'cloudflare-autorag', 'cloudflarebrowserrenderingcrawler', 'cohere-training-data-crawler', 'cragcrawler', 'crawlspace', 'datenbank crawler', 'diffbot', 'diffbot-user', 'anomura', 'echobot bot', 'echoboxbot', 'elevenlabsbot', 'elevenlabscrawler', 'exasearchbot', 'exauser', 'factset_spyderbot', 'firecrawlagent', 'geisthaus-pagefetcher', 'cloudvertexbot', 'gemini-deep-research', 'google-agent', 'google-firebase', 'google-notebooklm', 'googleagent-mariner', 'googleagent-urlcontext', 'googleother', 'googleother-image', 'googleother-video', 'notebooklm', 'iaskbot', 'iaskspider', 'imagesiftbot', 'img2dataset', 'isscyberriskcrawler', 'kagi-fetcher', 'keenablebot', 'klaviyoaibot', 'laiondownloader', 'linguee bot', 'linkupbot', 'aiwebindex', 'meta-externalfetcher', 'meta-webindexer', 'facebookbot', 'azureai-searchbot', 'mistralai-index', 'mistralai-training', 'mozilla-tabstack', 'nagetbot', 'netestate imprint crawler', 'icc-crawler', 'oai-adsbot', 'panscient', 'panscient.com', 'shapbot', 'payhawk invoice fetching agent', 'phindbot', 'poggio-citations', 'poseidon research crawler', 'qualifiedbot', 'wpbot', 'querit-searchbot', 'queritbot', 'quillbot', 'quillbot.com', 'reflectionbot', 'cotoyogi', 'sbintuitionsbot', 'scrybot', 'sidetrade indexer bot', 'tavilybot', 'agenttimes', 'thinkbot', 'timpibot', 'twinagent', 'velenpublicwebcrawler', 'wardbot', 'omgili', 'omgilibot', 'grok bot', 'grokappandroid', 'grokbot', 'addsearchbot', 'agentdatabot', 'aranet-searchbot', 'bixelbot', 'bravebot', 'channel3bot', 'friendlycrawler', 'henkbot', 'imagespider', 'kangaroo bot', 'kunatocrawler', 'laion-huggingface-processor', 'linerbot', 'mycentralaiscraperbot', 'terra cotta', 'wrtnbot', 'zanistabot', 'facebookexternalhit', 'facebot', 'twitterbot', 'linkedinbot', 'slackbot', 'whatsapp', 'telegrambot', 'discordbot', 'pinterest', 'redditbot', 'skypeuripreview', 'embedly', 'quora link preview', 'showyoubot', 'outbrain', 'developers.google.com/+/web/snippet', 'vkshare', 'tumblr', 'bitlybot', 'nuzzel', 'flipboard', 'snapchat', 'viber', 'mattermost', 'bitrix link preview', 'xing-contenttabreceiver', 'ahrefsbot', 'semrushbot', 'mj12bot', 'majestic-12', 'dotbot', 'rogerbot', 'screaming frog', 'sitebulb', 'serpstatbot', 'seokicks', 'siteimprove', 'lighthouse', 'google page speed', 'gtmetrix', 'pingdom', 'uptimerobot', 'uptimebot', 'w3c_validator', 'w3c-checklink', 'archive.org_bot', 'paperlibot', 'dataprovider.com', 'grapeshotcrawler', 'zoominfobot', 'feedly', 'newsblur', 'inoreader', 'integration-test', // BOTS:END ]; // Archivos que nunca se renderizan (extensions.txt) export const STATIC_EXTENSIONS = [ // EXTENSIONS:BEGIN 'js', 'mjs', 'css', 'less', 'map', 'json', 'xml', 'rss', 'txt', 'webmanifest', 'ico', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'svg', 'bmp', 'tif', 'tiff', 'psd', 'ai', 'woff', 'woff2', 'ttf', 'otf', 'eot', 'mp3', 'm4a', 'wav', 'ogg', 'mp4', 'm4v', 'webm', 'avi', 'mov', 'mpg', 'mpeg', 'wmv', 'flv', 'swf', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'csv', 'zip', 'gz', 'rar', '7z', 'iso', 'torrent', 'exe', 'dmg', 'apk', 'wasm', 'dll', 'dat', 'blat', // EXTENSIONS:END ]; const escapeRegex = (text) => text.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&'); // Patrón de la lista incluida; refreshBots lo sustituye por el del catálogo de Jazui (GET /v2/bots) export const BOT_PATTERN = new RegExp(BOTS.map(escapeRegex).join('|'), 'i'); export const STATIC_PATTERN = new RegExp(`\\.(${STATIC_EXTENSIONS.join('|')})$`, 'i'); let botPattern = BOT_PATTERN; let botsVersion = 'incluida'; let botsCheckedAt = 0; let botsLoading = null; const BOTS_REFRESH_MS = 24 * 60 * 60 * 1000; const BOTS_RETRY_MS = 60 * 60 * 1000; /** Patrón de bots en uso: el del catálogo si ya se descargó, si no el de la lista incluida. */ export const currentBotPattern = () => botPattern; /** Versión de la lista en uso ('incluida' hasta la primera descarga). */ export const currentBotsVersion = () => botsVersion; /** * Descarga la lista de bots del catálogo de Jazui (GET /v2/bots) si toca: al arrancar y cada 24 h; si falla se * sigue con la que hay y se reintenta en una hora. Nunca falla ni retrasa la petición: devuelve la promesa para * waitUntil (Cloudflare, Vercel, Netlify) o null si no toca. Se desactiva con remoteBots: false. */ export function refreshBots({ api = DEFAULT_API, remoteBots = true } = {}) { const now = Date.now(); if (!remoteBots || botsLoading || now - botsCheckedAt < BOTS_REFRESH_MS) return null; botsCheckedAt = now; botsLoading = fetch(`${(api || DEFAULT_API).replace(/\/+$/, '')}/v2/bots`, { signal: AbortSignal.timeout(5000) }) .then((response) => (response.ok ? response.json() : null)) .then((data) => { if (data && Array.isArray(data.bots) && data.bots.length > 0) { botPattern = new RegExp(data.bots.map(escapeRegex).join('|'), 'i'); botsVersion = data.version || botsVersion; } else { botsCheckedAt = now - BOTS_REFRESH_MS + BOTS_RETRY_MS; } }, () => { botsCheckedAt = now - BOTS_REFRESH_MS + BOTS_RETRY_MS; }) .finally(() => { botsLoading = null; }); return botsLoading; } // Cabeceras de Jazui que se devuelven al bot const PASS_HEADERS = ['content-type', 'location', 'etag', 'last-modified', 'x-jazui', 'x-jazui-cache']; /** * Si la petición hay que pasarla por Jazui: GET/HEAD de un bot conocido (o con _escaped_fragment_, el esquema * antiguo de Google para las SPA) a una página, no a un archivo. */ export function shouldRender({ method, url, userAgent, headers }) { if (method !== 'GET' && method !== 'HEAD') return false; if (headers && headers.get('x-jazui')) return false; // nunca a sí mismo const { pathname, search } = new URL(url); if (STATIC_PATTERN.test(pathname)) return false; return (!!userAgent && botPattern.test(userAgent)) || search.includes('_escaped_fragment_'); } /** * Pide a Jazui el HTML de la URL. Devuelve la Response para el bot o null si hay que servir la página normal * (Jazui no respondió, se pasó del límite, el sitio no está verificado…): el bot nunca ve un error de Jazui. * Solo un 403 marcado con X-Jazui-Blocked (regla o bot de IA bloqueado) llega al bot. */ export async function fetchRendered({ url, method = 'GET', userAgent, clientIp, ifNoneMatch, token, api = DEFAULT_API, siteUrl, timeoutMs = DEFAULT_TIMEOUT_MS }) { if (!token) return null; let target = url; if (siteUrl) { // Dominio público del sitio cuando el servidor lo ve con otro (proxy, entorno de pruebas) const original = new URL(url); target = new URL(original.pathname + original.search, siteUrl).toString(); } const query = new URLSearchParams({ url: target, botAgent: userAgent || '' }); const headers = { Authorization: `Bearer ${token}` }; if (clientIp) headers['X-Jazui-Client-IP'] = clientIp; if (ifNoneMatch) headers['If-None-Match'] = ifNoneMatch; let response; try { response = await fetch(`${(api || DEFAULT_API).replace(/\/+$/, '')}/Render?${query}`, { headers, redirect: 'manual', signal: AbortSignal.timeout(timeoutMs), }); } catch { return null; } const blocked = response.status === 403 && response.headers.get('x-jazui-blocked'); if (!response.headers.get('x-jazui') && !blocked) { response.body?.cancel(); return null; } const out = new Headers(); for (const name of PASS_HEADERS) { const value = response.headers.get(name); if (value) out.set(name, value); } if (blocked) out.set('x-jazui-blocked', blocked); const empty = method === 'HEAD' || response.status === 304; if (empty) response.body?.cancel(); return new Response(empty ? null : response.body, { status: response.status, headers: out }); } // User-agents con pinta de bot: si no están en BOTS se avisa a Jazui sin enviarle la página (el mismo patrón que la API) export const UNKNOWN_BOT_PATTERN = /bot|crawl|spider|scrap|fetch|headless|preview|archiv|index/i; const MAX_REPORTED = 1000; const reported = new Set(); let reportedDay = ''; /** * Avisa a Jazui (POST /Render/Seen) de un user-agent con pinta de bot que no está en la lista, una vez al día por * user-agent y proceso, para que se pueda añadir al catálogo. No gasta renders y nunca falla: devuelve la promesa para * waitUntil (Cloudflare, Vercel) o null si no hay que avisar. */ export function reportUnknownBot({ method, url, userAgent, token, api = DEFAULT_API, reportUnknownBots = true }) { if (!reportUnknownBots || !token || !userAgent || (method !== 'GET' && method !== 'HEAD')) return null; if (STATIC_PATTERN.test(new URL(url).pathname)) return null; if (!UNKNOWN_BOT_PATTERN.test(userAgent) || botPattern.test(userAgent)) return null; const today = new Date().toISOString().slice(0, 10); if (reportedDay !== today) { reported.clear(); reportedDay = today; } if (reported.size >= MAX_REPORTED || reported.has(userAgent)) return null; reported.add(userAgent); return fetch(`${(api || DEFAULT_API).replace(/\/+$/, '')}/Render/Seen`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ userAgent, url }), signal: AbortSignal.timeout(5000), }).then((response) => { response.body?.cancel(); }, () => {}); } /** * Atajo para los runtimes con Request estándar: la Response de Jazui o null. Con options.waitUntil (Cloudflare, * Vercel) la descarga de la lista de bots y el aviso de bots desconocidos siguen aunque la respuesta ya se haya enviado. */ export function renderRequest(request, options, clientIp) { const refresh = refreshBots(options); if (refresh && options.waitUntil) options.waitUntil(refresh); const userAgent = request.headers.get('user-agent') || ''; if (!shouldRender({ method: request.method, url: request.url, userAgent, headers: request.headers })) { const report = reportUnknownBot({ ...options, method: request.method, url: request.url, userAgent }); if (report && options.waitUntil) options.waitUntil(report); return null; } return fetchRendered({ ...options, url: request.url, method: request.method, userAgent, clientIp, ifNoneMatch: request.headers.get('if-none-match'), }); } ``` ## Usarlo Añádelo antes de servir los archivos y la SPA. Si delante hay un proxy o un CDN, activa `trust proxy` para que `req.ip` sea la IP del bot y no la del proxy: `server.js`: ```javascript import express from 'express'; import { jazui } from './jazui/express.js'; const app = express(); app.set('trust proxy', true); // con un proxy o CDN delante, para que req.ip sea la del bot app.use(jazui({ token: process.env.JAZUI_TOKEN })); // antes de los estáticos y de la SPA app.use(express.static('dist')); app.get('*', (req, res) => res.sendFile('index.html', { root: 'dist' })); app.listen(3000); ``` ## Opciones | Opción | Por defecto | Para qué | |---|---|---| | `token` | — | Tu token. Sin él, el middleware no hace nada. | | `api` | `https://api.jazui.com` | Dirección de la API. | | `siteUrl` | — | Tu dominio público si el servidor ve otro (detrás de un proxy, en pruebas). | | `timeoutMs` | 20000 | Milisegundos de espera; pasado ese tiempo se sirve la página normal. | | `remoteBots` | `true` | Descarga la lista de bots al arrancar y cada 24 h. | | `reportUnknownBots` | `true` | Avisa a Jazui de agentes con pinta de bot que no están en la lista. | ```javascript app.use(jazui({ token: process.env.JAZUI_TOKEN, // obligatorio api: 'https://api.jazui.com', // dirección de la API siteUrl: 'https://misitio.com', // dominio público si el servidor ve otro timeoutMs: 20000, // pasado este tiempo, la página normal remoteBots: true, // descargar la lista de bots cada día reportUnknownBots: true, // avisar de bots que no están en la lista })); ``` ## Qué hace Construye la dirección con `req.protocol`, el host y `req.originalUrl`, decide si es un bot y, si lo es, pide la página a Jazui y copia al bot el código, las cabeceras y el cuerpo. Ante cualquier error llama a `next()`, así que tu web nunca se queda sin servir. ##

Pide tu portada como si fueras un bot, con el agente de usuario de pruebas integration-test:

curl -I -A "integration-test" https://misitio.com/

La respuesta debe llevar x-jazui: true y x-jazui-cache: MISS la primera vez y HIT después. Sin agente de bot (curl -I https://misitio.com/) debe llegar tu web de siempre, sin x-jazui. En el panel, la petición aparece en Tráfico de bots y la página en Páginas en caché.

Pide tu portada como si fueras un bot, con el agente de usuario de pruebas `integration-test`: ``` curl -I -A "integration-test" https://misitio.com/ ``` La respuesta debe llevar `x-jazui: true` y `x-jazui-cache: MISS` la primera vez y `HIT` después. Sin agente de bot (`curl -I https://misitio.com/`) debe llegar tu web de siempre, sin `x-jazui`. En el panel, la petición aparece en **Tráfico de bots** y la página en **Páginas en caché**. ## Si algo no funciona - **No aparece `x-jazui`**: el middleware debe ir antes de `express.static` y de la ruta que sirve `index.html`. - **Las métricas muestran la IP del proxy**: activa `app.set('trust proxy', true)`. - **Detrás de un proxy, `req.protocol` es `http`**: con `trust proxy` Express usa `X-Forwarded-Proto`; si no, define `siteUrl`. --- # ASP.NET Core > Paquete NuGet con un middleware para aplicaciones ASP.NET Core (.NET 8 o superior). Página original: https://docs.jazui.com/integrations/aspnetcore/es Si tu SPA (Blazor WebAssembly, Angular, React, Vue…) la sirve una aplicación ASP.NET Core, `Jazui.AspNetCore` añade un middleware que atiende a los bots antes de los archivos estáticos y del *fallback* a `index.html`. Es la opción recomendada también en IIS cuando la aplicación es de .NET. ## Antes de empezar - .NET 8 o superior. - Tu sitio verificado en Jazui y tu token. - El origen de paquetes de AstrolUI (`https://nuget.astrolui.com/v3/index.json`) añadido a NuGet. ## Instalación `Jazui.AspNetCore` no está en nuget.org: se publica en el **repositorio de paquetes NuGet de AstrolUI**, `https://nuget.astrolui.com/v3/index.json`. Añade ese origen una vez (en Visual Studio, con la línea de comandos o en un `nuget.config`) y después instala el paquete como cualquier otro. El paquete lleva el logo de Jazui y su versión sigue la de .NET con la que se publica (por ejemplo `10.0.13.1`); funciona en aplicaciones de .NET 8 o superior. ### En Visual Studio 1. Abre **Herramientas › Opciones › Administrador de paquetes NuGet › Orígenes de paquetes**. 2. Pulsa **+**, escribe **Nombre**: `AstrolUI` y **Origen**: `https://nuget.astrolui.com/v3/index.json`, pulsa **Actualizar** y después **Aceptar**. 3. Con el botón derecho sobre tu proyecto, elige **Administrar paquetes NuGet…**. 4. Arriba a la derecha, en **Origen del paquete**, elige `AstrolUI` (o **Todo**). 5. En **Examinar**, busca `Jazui.AspNetCore`, elige la última versión y pulsa **Instalar**. > Si tu solución tiene un `nuget.config` con ``, Visual Studio solo usa los orígenes de ese archivo: añade AstrolUI ahí (abajo tienes el ejemplo). ### Con la línea de comandos ```bash dotnet nuget add source https://nuget.astrolui.com/v3/index.json --name AstrolUI dotnet add package Jazui.AspNetCore ``` ### Con nuget.config Para que todo el equipo y tu integración continua lo tengan sin configurar nada, pon un `nuget.config` junto a tu solución. Si ya usas la **asignación de orígenes** (`packageSourceMapping`), añade el patrón `Jazui.*` al origen AstrolUI; si no, basta con `packageSources`. `nuget.config`: ```xml ``` ## Configuración Registra el servicio y añade el middleware **antes** de `UseStaticFiles` y del `MapFallbackToFile`: `Program.cs`: ```csharp var builder = WebApplication.CreateBuilder(args); builder.Services.AddJazui(builder.Configuration); var app = builder.Build(); app.UseForwardedHeaders(); // solo si hay un proxy o CDN delante: la IP y el esquema del bot app.UseJazui(); // antes de los archivos estáticos y del fallback de la SPA app.UseStaticFiles(); app.MapFallbackToFile("index.html"); app.Run(); ``` `appsettings.json`: ```json { "Jazui": { "Token": "", "SiteUrl": "" } } ``` No pongas el token en `appsettings.json`: en producción usa la variable de entorno `Jazui__Token` (o los secretos de usuario en desarrollo). `UseForwardedHeaders` solo hace falta si hay un proxy o CDN delante, para que Jazui reciba la IP real del bot y el esquema `https`. ## Opciones Todas se leen de la sección `Jazui` de la configuración y se pueden cambiar también en código: | Opción | Por defecto | Para qué | |---|---|---| | `Token` | — | Token de la cuenta o del sitio. Sin token, el middleware no hace nada. | | `ApiUrl` | `https://api.jazui.com` | Dirección de la API. | | `SiteUrl` | — | Dominio público cuando la aplicación ve otro (proxy, pruebas). | | `Timeout` | 20 s | Pasado este tiempo se sirve la página normal. | | `ExcludedPaths` | `/api`, `/_framework`, `/_content`, `/_blazor` | Rutas que nunca pasan por Jazui (por segmento: `/api` cubre `/api/x` pero no `/apis`). | | `ExtraBots` | — | Fragmentos de agente de usuario que se suman a la lista de bots. | | `RemoteBots` | `true` | Descarga la lista de bots de Jazui al arrancar y cada `BotsRefreshInterval`. | | `BotsRefreshInterval` | 24 h | Cada cuánto se vuelve a descargar la lista. | | `ReportUnknownBots` | `true` | Avisa a Jazui de los agentes con pinta de bot que no están en la lista (sin enviar la página). | `Program.cs`: ```csharp builder.Services.AddJazui(builder.Configuration, options => { options.Timeout = TimeSpan.FromSeconds(10); options.ExcludedPaths = ["/api", "/_framework", "/_content", "/_blazor", "/admin"]; options.ExtraBots = ["mibotinterno"]; options.BotsRefreshInterval = TimeSpan.FromHours(12); }); ``` ## Qué hace Para cada petición que es de un bot, llama a la API con la dirección (o `SiteUrl` más la ruta), el agente de usuario, la IP del bot (de `Connection.RemoteIpAddress`) y el `If-None-Match`. Si Jazui responde con `X-Jazui` o con un bloqueo, copia el código, las cabeceras y el cuerpo; si tarda o falla, lo apunta en el registro como aviso y sigue con el resto de la aplicación. La versión de la lista de bots en uso está en `JazuiMiddleware.BotsVersion`. ##

Pide tu portada como si fueras un bot, con el agente de usuario de pruebas integration-test:

curl -I -A "integration-test" https://misitio.com/

La respuesta debe llevar x-jazui: true y x-jazui-cache: MISS la primera vez y HIT después. Sin agente de bot (curl -I https://misitio.com/) debe llegar tu web de siempre, sin x-jazui. En el panel, la petición aparece en Tráfico de bots y la página en Páginas en caché.

Pide tu portada como si fueras un bot, con el agente de usuario de pruebas `integration-test`: ``` curl -I -A "integration-test" https://misitio.com/ ``` La respuesta debe llevar `x-jazui: true` y `x-jazui-cache: MISS` la primera vez y `HIT` después. Sin agente de bot (`curl -I https://misitio.com/`) debe llegar tu web de siempre, sin `x-jazui`. En el panel, la petición aparece en **Tráfico de bots** y la página en **Páginas en caché**. ## Si algo no funciona - **No se encuentra `Jazui.AspNetCore`**: falta el origen AstrolUI, o tu `nuget.config` tiene `` o una asignación de orígenes sin el patrón `Jazui.*`. - **No aparece `x-jazui`**: `UseJazui()` tiene que ir antes de `UseStaticFiles()` y de `MapFallbackToFile`; revisa también que el token llega (variable `Jazui__Token`). - **Detrás de IIS o de un proxy, la URL sale con `http`**: configura `UseForwardedHeaders` o define `SiteUrl`. - **Los avisos del registro** («Jazui no respondió…») te dicen por qué se sirvió la página normal. --- # nginx > Configuración de nginx que manda a los bots a Jazui con un proxy interno. Página original: https://docs.jazui.com/integrations/nginx/es nginx decide con dos `map` si la petición es de un bot y si es un archivo estático. Las de bots se reescriben a una ubicación interna que hace de proxy hacia `https://api.jazui.com/Render`; si Jazui falla, se sirve tu `index.html`. ## Antes de empezar - nginx con soporte SSL para el proxy (el habitual). - Tu sitio **verificado antes de activar la configuración**: con nginx, el `403` de un sitio sin verificar llega al bot. - Tu token. ## Instalación Este es el archivo completo. Los dos `map` van en el bloque `http` (por ejemplo en `/etc/nginx/conf.d/jazui.conf`) y el `server` sustituye o se integra en el de tu sitio: `jazui.conf` (https://docs.jazui.com/example_config/nginx.txt): ```nginx # Jazui para nginx: los bots reciben el HTML renderizado de la SPA; el resto de visitas, la SPA normal. # Sustituye [JAZUI_TOKEN] por el token de render (Sitios web → Token del sitio) y adapta server_name y root. # Si Jazui no responde, se pasa del límite o falla, se sirve la SPA (el bot nunca ve el error). # Bots (bots.txt; las listas las rellena Sync-Integrations.ps1) map $http_user_agent $jazui_bot { default 0; # BOTS:BEGIN "~*(googlebot|google-inspectiontool|storebot-google|adsbot-google|bingbot|bingpreview|msnbot|adidxbot|yandex|baiduspider|duckduckbot|slurp|applebot|seznambot|qwantbot|qwantify|petalbot|sogou|naver|exabot|yetibot|360spider|soso|gptbot|chatgpt|oai-searchbot|claudebot|claude-user|claude-searchbot|anthropic-ai|perplexity|google-cloudvertexbot|bytespider|ccbot|amazonbot|meta-externalagent|cohere-ai|mistralai-user|youbot|duckassistbot|yibot|qwenbot|tongyibot|erniebot|yiyanbot|doubaobot|tiktokspider|deepseekbot|pangubot|manus-user|kimi-agent|kimi-searchbot|kimi-user|kimibot|moonshotbot|hunyuan|chatglm-spider|agi agent|ai2bot|ai2bot-deepresearcheval|ai2bot-dolma|aihitbot|amazon-kendra|amazon-qbusiness|amazonbuyforme|amzn-searchbot|amzn-user|bedrockbot|novaact|andibot|claude-web|apifybot|apifywebsitecontentcrawler|atlassian-bot|awario|bigsur\.ai|brightbot|buddybot|cartai agentic commerce|terracotta|cledarabot|cloudflare-autorag|cloudflarebrowserrenderingcrawler|cohere-training-data-crawler|cragcrawler|crawlspace|datenbank crawler|diffbot|diffbot-user|anomura|echobot bot|echoboxbot|elevenlabsbot|elevenlabscrawler|exasearchbot|exauser|factset_spyderbot|firecrawlagent|geisthaus-pagefetcher|cloudvertexbot|gemini-deep-research|google-agent|google-firebase|google-notebooklm|googleagent-mariner|googleagent-urlcontext|googleother|googleother-image|googleother-video|notebooklm|iaskbot|iaskspider|imagesiftbot|img2dataset|isscyberriskcrawler|kagi-fetcher|keenablebot|klaviyoaibot|laiondownloader|linguee bot|linkupbot|aiwebindex|meta-externalfetcher|meta-webindexer|facebookbot|azureai-searchbot|mistralai-index|mistralai-training|mozilla-tabstack|nagetbot|netestate imprint crawler|icc-crawler|oai-adsbot|panscient|panscient\.com|shapbot|payhawk invoice fetching agent|phindbot|poggio-citations|poseidon research crawler|qualifiedbot|wpbot|querit-searchbot|queritbot|quillbot|quillbot\.com|reflectionbot|cotoyogi|sbintuitionsbot|scrybot|sidetrade indexer bot|tavilybot|agenttimes|thinkbot|timpibot|twinagent|velenpublicwebcrawler|wardbot|omgili|omgilibot|grok bot|grokappandroid|grokbot|addsearchbot|agentdatabot|aranet-searchbot|bixelbot|bravebot|channel3bot|friendlycrawler|henkbot|imagespider|kangaroo bot|kunatocrawler|laion-huggingface-processor|linerbot|mycentralaiscraperbot|terra cotta|wrtnbot|zanistabot|facebookexternalhit|facebot|twitterbot|linkedinbot|slackbot|whatsapp|telegrambot|discordbot|pinterest|redditbot|skypeuripreview|embedly|quora link preview|showyoubot|outbrain|developers\.google\.com\/\+\/web\/snippet|vkshare|tumblr|bitlybot|nuzzel|flipboard|snapchat|viber|mattermost|bitrix link preview|xing-contenttabreceiver|ahrefsbot|semrushbot|mj12bot|majestic-12|dotbot|rogerbot|screaming frog|sitebulb|serpstatbot|seokicks|siteimprove|lighthouse|google page speed|gtmetrix|pingdom|uptimerobot|uptimebot|w3c_validator|w3c-checklink|archive\.org_bot|paperlibot|dataprovider\.com|grapeshotcrawler|zoominfobot|feedly|newsblur|inoreader|integration-test)" 1; # BOTS:END } # Archivos que nunca se renderizan (extensions.txt) map $uri $jazui_static { default 0; # EXTENSIONS:BEGIN "~*\.(js|mjs|css|less|map|json|xml|rss|txt|webmanifest|ico|png|jpg|jpeg|gif|webp|avif|svg|bmp|tif|tiff|psd|ai|woff|woff2|ttf|otf|eot|mp3|m4a|wav|ogg|mp4|m4v|webm|avi|mov|mpg|mpeg|wmv|flv|swf|pdf|doc|docx|xls|xlsx|ppt|pptx|csv|zip|gz|rar|7z|iso|torrent|exe|dmg|apk|wasm|dll|dat|blat)$" 1; # EXTENSIONS:END } server { listen 443 ssl; server_name misitio.com; root /var/www/misitio; location / { set $jazui $jazui_bot; # Esquema antiguo de Google para las SPA, con cualquier user-agent if ($args ~ "_escaped_fragment_") { set $jazui 1; } if ($jazui_static) { set $jazui 0; } if ($http_x_jazui) { set $jazui 0; } if ($request_method !~ ^(GET|HEAD)$) { set $jazui 0; } # El "?" final quita la query: la URL completa viaja en X-Jazui-Url if ($jazui = 1) { rewrite ^ /__jazui? last; } try_files $uri $uri/ /index.html; } location = /__jazui { internal; proxy_http_version 1.1; proxy_set_header Host api.jazui.com; proxy_set_header Authorization "Bearer [JAZUI_TOKEN]"; proxy_set_header X-Jazui-Url $scheme://$host$request_uri; # Detrás de un CDN o balanceador usa su cabecera ($http_cf_connecting_ip, $http_x_forwarded_for…) proxy_set_header X-Jazui-Client-IP $remote_addr; # Las cookies del visitante no le hacen falta a Jazui: no se envían proxy_set_header Cookie ""; proxy_ssl_server_name on; proxy_connect_timeout 5s; proxy_read_timeout 20s; # Errores de Jazui → la SPA. 404, 410, 3xx y 403 (bot bloqueado) llegan al bot tal cual. proxy_intercept_errors on; error_page 400 401 429 500 502 503 504 = @spa; proxy_pass https://api.jazui.com/Render; } location @spa { try_files /index.html =404; } } ``` Cambia `[JAZUI_TOKEN]` por tu token, y `server_name` y `root` por los de tu sitio. Si ya tienes un bloque `location /`, añade las líneas de `set` e `if` al principio y copia las ubicaciones `/__jazui` y `@spa`. Después comprueba y recarga: ```bash sudo nginx -t && sudo nginx -s reload ``` ## Qué hace - `$jazui_bot` vale 1 si el agente de usuario está en la lista; `$jazui_static`, si la ruta es un archivo. - En `location /` se combinan con `_escaped_fragment_`, la cabecera `X-Jazui` y el método: si hay que renderizar, se reescribe a `/__jazui`. - `/__jazui` es interna: envía `Authorization`, `X-Jazui-Url` (la dirección completa) y `X-Jazui-Client-IP`, con 5 s para conectar y 20 s para responder. - Con `proxy_intercept_errors`, los códigos 400, 401, 429 y 5xx de Jazui van a `@spa`, que sirve tu `index.html`. ## Mantener al día la lista de bots Esta configuración lleva la lista de bots escrita dentro, entre las marcas `BOTS:BEGIN` y `BOTS:END`, porque el servidor no puede descargarla solo. Cuando Jazui añade bots nuevos: 1. En el panel, ve a **Sitios web** y pulsa **Token del sitio** en cualquiera de tus sitios. 2. En **Bots en nginx, IIS y Apache**, elige la pestaña de tu servidor y copia el bloque. Verás cuántos bots tiene y la fecha de la lista. 3. Sustituye en tu configuración todo lo que hay entre `BOTS:BEGIN` y `BOTS:END` por ese bloque y recarga el servidor. También puedes descargar la lista en texto con `GET https://api.jazui.com/v2/bots.txt` o los bloques ya hechos con `GET https://api.jazui.com/v2/bots/config`. ##

Pide tu portada como si fueras un bot, con el agente de usuario de pruebas integration-test:

curl -I -A "integration-test" https://misitio.com/

La respuesta debe llevar x-jazui: true y x-jazui-cache: MISS la primera vez y HIT después. Sin agente de bot (curl -I https://misitio.com/) debe llegar tu web de siempre, sin x-jazui. En el panel, la petición aparece en Tráfico de bots y la página en Páginas en caché.

Pide tu portada como si fueras un bot, con el agente de usuario de pruebas `integration-test`: ``` curl -I -A "integration-test" https://misitio.com/ ``` La respuesta debe llevar `x-jazui: true` y `x-jazui-cache: MISS` la primera vez y `HIT` después. Sin agente de bot (`curl -I https://misitio.com/`) debe llegar tu web de siempre, sin `x-jazui`. En el panel, la petición aparece en **Tráfico de bots** y la página en **Páginas en caché**. ## Si algo no funciona - **Detrás de Cloudflare u otro CDN**, `$remote_addr` es la IP del CDN: usa `$http_cf_connecting_ip` en `X-Jazui-Client-IP` o configura `real_ip_header`. - **`nginx -t` falla en `map`**: los `map` tienen que estar dentro de `http`, no de `server`. - **Error de SSL con la API**: deja `proxy_ssl_server_name on` y el `Host api.jazui.com`. - **Los bots reciben 403**: el sitio no está verificado o bloqueaste a ese bot de IA o con una regla (mira la cabecera `X-Jazui-Blocked`). --- # IIS > Regla de URL Rewrite con ARR en el web.config de tu sitio de IIS. Página original: https://docs.jazui.com/integrations/iis/es En IIS, una regla de **URL Rewrite** reconoce a los bots y, con **Application Request Routing** (ARR) como proxy, les sirve la página de Jazui. Si tu aplicación es ASP.NET Core, es más sencillo y robusto usar el [paquete de ASP.NET Core](https://docs.jazui.com/llms/es/integrations-aspnetcore.txt). ## Antes de empezar - Los módulos **URL Rewrite** y **Application Request Routing** instalados (desde la web de IIS de Microsoft). - Tu sitio **verificado antes de activar la regla**: con ARR, si Jazui responde con error, el bot recibe ese error. - Tu token. ## Instalación Abre una consola como administrador y ejecuta estos dos comandos. El primero activa el proxy de ARR; el segundo permite que la regla escriba las cabeceras que necesita Jazui: `cmd`: ```bash %windir%\system32\inetsrv\appcmd.exe set config -section:system.webServer/proxy /enabled:"True" /commit:apphost %windir%\system32\inetsrv\appcmd.exe set config -section:system.webServer/rewrite/allowedServerVariables /+"[name='HTTP_AUTHORIZATION']" /+"[name='HTTP_X_JAZUI_URL']" /+"[name='HTTP_X_JAZUI_CLIENT_IP']" /+"[name='HTTP_COOKIE']" /commit:apphost ``` Después pon este `web.config` en la carpeta de tu sitio (o copia sus dos reglas en el que ya tengas, la de Jazui antes que la de la SPA): `web.config` (https://docs.jazui.com/example_config/iis_web.xml): ```xml ``` Cambia `[JAZUI_TOKEN]` por tu token. La dirección que se envía a Jazui empieza por `https://`: si tu sitio va por `http`, cámbialo en `HTTP_X_JAZUI_URL`. ## Qué hace - La regla **Jazui** comprueba en un solo patrón el agente de usuario y `_escaped_fragment_`, el método, la cabecera `X-Jazui` y que la ruta no sea un archivo. - Escribe `Authorization`, `X-Jazui-Url` y `X-Jazui-Client-IP` como variables del servidor y reescribe la petición a `https://api.jazui.com/Render`, que ARR resuelve como proxy. - La regla **SPA** manda a `index.html` todo lo que no es un archivo ni una carpeta. ## Mantener al día la lista de bots Esta configuración lleva la lista de bots escrita dentro, entre las marcas `BOTS:BEGIN` y `BOTS:END`, porque el servidor no puede descargarla solo. Cuando Jazui añade bots nuevos: 1. En el panel, ve a **Sitios web** y pulsa **Token del sitio** en cualquiera de tus sitios. 2. En **Bots en nginx, IIS y Apache**, elige la pestaña de tu servidor y copia el bloque. Verás cuántos bots tiene y la fecha de la lista. 3. Sustituye en tu configuración todo lo que hay entre `BOTS:BEGIN` y `BOTS:END` por ese bloque y recarga el servidor. También puedes descargar la lista en texto con `GET https://api.jazui.com/v2/bots.txt` o los bloques ya hechos con `GET https://api.jazui.com/v2/bots/config`. ##

Pide tu portada como si fueras un bot, con el agente de usuario de pruebas integration-test:

curl -I -A "integration-test" https://misitio.com/

La respuesta debe llevar x-jazui: true y x-jazui-cache: MISS la primera vez y HIT después. Sin agente de bot (curl -I https://misitio.com/) debe llegar tu web de siempre, sin x-jazui. En el panel, la petición aparece en Tráfico de bots y la página en Páginas en caché.

Pide tu portada como si fueras un bot, con el agente de usuario de pruebas `integration-test`: ``` curl -I -A "integration-test" https://misitio.com/ ``` La respuesta debe llevar `x-jazui: true` y `x-jazui-cache: MISS` la primera vez y `HIT` después. Sin agente de bot (`curl -I https://misitio.com/`) debe llegar tu web de siempre, sin `x-jazui`. En el panel, la petición aparece en **Tráfico de bots** y la página en **Páginas en caché**. ## Si algo no funciona - **Error 500.50 o «variable de servidor no permitida»**: falta el segundo comando (las variables permitidas). - **Error 404 o 502.3 al pedir como bot**: el proxy de ARR no está activado (primer comando) o el servidor no puede salir a Internet por el puerto 443. - **Detrás de Cloudflare**, `{REMOTE_ADDR}` es la IP del CDN: usa `{HTTP_CF_CONNECTING_IP}` (o `{HTTP_X_FORWARDED_FOR}`) en `HTTP_X_JAZUI_CLIENT_IP`. - El tiempo máximo de espera es el del proxy de ARR (en la configuración del servidor, **Application Request Routing Cache › Server Proxy Settings › Time-out**); 20 segundos es un buen valor. --- # Apache > .htaccess para Apache con mod_rewrite y mod_proxy, también en muchos hostings compartidos. Página original: https://docs.jazui.com/integrations/apache/es Con `mod_rewrite`, Apache reconoce a los bots y, con la bandera `[P]` de `mod_proxy`, les sirve la página de Jazui. El resto de peticiones van a tus archivos o a `index.html`. ## Antes de empezar - Los módulos `rewrite`, `headers`, `proxy`, `proxy_http` y `ssl` activos, y `SSLProxyEngine On` en tu ``, con `AllowOverride All` para que se lea el `.htaccess`. En un hosting compartido, pregunta a tu proveedor si tiene `mod_proxy`. - Tu sitio **verificado antes de activar la configuración**: con el proxy de Apache, si Jazui responde con error, el bot recibe ese error. - Tu token. En un servidor propio (Debian, Ubuntu): ```bash sudo a2enmod rewrite headers proxy proxy_http ssl sudo systemctl restart apache2 ``` `VirtualHost`: ```apache ServerName misitio.com DocumentRoot /var/www/misitio SSLProxyEngine On AllowOverride All ``` ## Instalación Pon este `.htaccess` en la carpeta raíz de tu sitio (o sus reglas dentro de tu ``): `.htaccess` (https://docs.jazui.com/example_config/apache.xml): ```apache # Jazui para Apache (.htaccess o ): los bots reciben el HTML renderizado; el resto, la SPA. # Requiere mod_rewrite, mod_headers, mod_proxy, mod_proxy_http y SSLProxyEngine On en el VirtualHost. # Sustituye [JAZUI_TOKEN] por el token de render del sitio. Las listas de bots y extensiones las rellena # Sync-Integrations.ps1 (bots.txt, extensions.txt). RewriteEngine On # Nunca a sí mismo y solo GET/HEAD RewriteCond %{HTTP:X-Jazui} ^$ RewriteCond %{REQUEST_METHOD} ^(GET|HEAD)$ # Un bot de la lista o _escaped_fragment_ en la query # BOTS:BEGIN RewriteCond %{HTTP_USER_AGENT} (googlebot|google-inspectiontool|storebot-google|adsbot-google|bingbot|bingpreview|msnbot|adidxbot|yandex|baiduspider|duckduckbot|slurp|applebot|seznambot|qwantbot|qwantify|petalbot|sogou|naver|exabot|yetibot|360spider|soso|gptbot|chatgpt|oai-searchbot|claudebot|claude-user|claude-searchbot|anthropic-ai|perplexity|google-cloudvertexbot|bytespider|ccbot|amazonbot|meta-externalagent|cohere-ai|mistralai-user|youbot|duckassistbot|yibot|qwenbot|tongyibot|erniebot|yiyanbot|doubaobot|tiktokspider|deepseekbot|pangubot|manus-user|kimi-agent|kimi-searchbot|kimi-user|kimibot|moonshotbot|hunyuan|chatglm-spider|agi\ agent|ai2bot|ai2bot-deepresearcheval|ai2bot-dolma|aihitbot|amazon-kendra|amazon-qbusiness|amazonbuyforme|amzn-searchbot|amzn-user|bedrockbot|novaact|andibot|claude-web|apifybot|apifywebsitecontentcrawler|atlassian-bot|awario|bigsur\.ai|brightbot|buddybot|cartai\ agentic\ commerce|terracotta|cledarabot|cloudflare-autorag|cloudflarebrowserrenderingcrawler|cohere-training-data-crawler|cragcrawler|crawlspace|datenbank\ crawler|diffbot|diffbot-user|anomura|echobot\ bot|echoboxbot|elevenlabsbot|elevenlabscrawler|exasearchbot|exauser|factset_spyderbot|firecrawlagent|geisthaus-pagefetcher|cloudvertexbot|gemini-deep-research|google-agent|google-firebase|google-notebooklm|googleagent-mariner|googleagent-urlcontext|googleother|googleother-image|googleother-video|notebooklm|iaskbot|iaskspider|imagesiftbot|img2dataset|isscyberriskcrawler|kagi-fetcher|keenablebot|klaviyoaibot|laiondownloader|linguee\ bot|linkupbot|aiwebindex|meta-externalfetcher|meta-webindexer|facebookbot|azureai-searchbot|mistralai-index|mistralai-training|mozilla-tabstack|nagetbot|netestate\ imprint\ crawler|icc-crawler|oai-adsbot|panscient|panscient\.com|shapbot|payhawk\ invoice\ fetching\ agent|phindbot|poggio-citations|poseidon\ research\ crawler|qualifiedbot|wpbot|querit-searchbot|queritbot|quillbot|quillbot\.com|reflectionbot|cotoyogi|sbintuitionsbot|scrybot|sidetrade\ indexer\ bot|tavilybot|agenttimes|thinkbot|timpibot|twinagent|velenpublicwebcrawler|wardbot|omgili|omgilibot|grok\ bot|grokappandroid|grokbot|addsearchbot|agentdatabot|aranet-searchbot|bixelbot|bravebot|channel3bot|friendlycrawler|henkbot|imagespider|kangaroo\ bot|kunatocrawler|laion-huggingface-processor|linerbot|mycentralaiscraperbot|terra\ cotta|wrtnbot|zanistabot|facebookexternalhit|facebot|twitterbot|linkedinbot|slackbot|whatsapp|telegrambot|discordbot|pinterest|redditbot|skypeuripreview|embedly|quora\ link\ preview|showyoubot|outbrain|developers\.google\.com\/\+\/web\/snippet|vkshare|tumblr|bitlybot|nuzzel|flipboard|snapchat|viber|mattermost|bitrix\ link\ preview|xing-contenttabreceiver|ahrefsbot|semrushbot|mj12bot|majestic-12|dotbot|rogerbot|screaming\ frog|sitebulb|serpstatbot|seokicks|siteimprove|lighthouse|google\ page\ speed|gtmetrix|pingdom|uptimerobot|uptimebot|w3c_validator|w3c-checklink|archive\.org_bot|paperlibot|dataprovider\.com|grapeshotcrawler|zoominfobot|feedly|newsblur|inoreader|integration-test) [NC,OR] # BOTS:END RewriteCond %{QUERY_STRING} _escaped_fragment_ # Archivos que nunca se renderizan # EXTENSIONS:BEGIN RewriteCond %{REQUEST_URI} !\.(js|mjs|css|less|map|json|xml|rss|txt|webmanifest|ico|png|jpg|jpeg|gif|webp|avif|svg|bmp|tif|tiff|psd|ai|woff|woff2|ttf|otf|eot|mp3|m4a|wav|ogg|mp4|m4v|webm|avi|mov|mpg|mpeg|wmv|flv|swf|pdf|doc|docx|xls|xlsx|ppt|pptx|csv|zip|gz|rar|7z|iso|torrent|exe|dmg|apk|wasm|dll|dat|blat)$ [NC] # EXTENSIONS:END # Ruta y query tal cual las pidió el bot (la URL completa viaja en X-Jazui-Url) RewriteCond %{THE_REQUEST} ^\S+\s(\S+) RewriteRule ^ https://api.jazui.com/Render [P,L,QSD,E=JAZUI_URL:%{REQUEST_SCHEME}://%{HTTP_HOST}%1,E=JAZUI_IP:%{REMOTE_ADDR}] # Fallback de la SPA: todo lo que no es un archivo va a index.html RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^ index.html [L] RequestHeader set Authorization "Bearer [JAZUI_TOKEN]" env=JAZUI_URL RequestHeader set X-Jazui-Url "%{JAZUI_URL}e" env=JAZUI_URL # Detrás de un CDN usa su cabecera (CF-Connecting-IP, X-Forwarded-For) RequestHeader set X-Jazui-Client-IP "%{JAZUI_IP}e" env=JAZUI_URL # Las cookies del visitante no le hacen falta a Jazui: no se envían RequestHeader unset Cookie env=JAZUI_URL ``` Cambia `[JAZUI_TOKEN]` por tu token. Si ya tenías reglas para la SPA, deja las de Jazui antes. ## Qué hace - Las condiciones comprueban que no haya cabecera `X-Jazui`, el método, el agente de usuario (o `_escaped_fragment_`) y que la ruta no sea un archivo. - La regla hace de proxy hacia `https://api.jazui.com/Render` y guarda en variables la dirección completa y la IP del bot. - `mod_headers` añade `Authorization`, `X-Jazui-Url` y `X-Jazui-Client-IP` solo a esas peticiones. - Las demás van a tus archivos o a `index.html`. ## Mantener al día la lista de bots Esta configuración lleva la lista de bots escrita dentro, entre las marcas `BOTS:BEGIN` y `BOTS:END`, porque el servidor no puede descargarla solo. Cuando Jazui añade bots nuevos: 1. En el panel, ve a **Sitios web** y pulsa **Token del sitio** en cualquiera de tus sitios. 2. En **Bots en nginx, IIS y Apache**, elige la pestaña de tu servidor y copia el bloque. Verás cuántos bots tiene y la fecha de la lista. 3. Sustituye en tu configuración todo lo que hay entre `BOTS:BEGIN` y `BOTS:END` por ese bloque y recarga el servidor. También puedes descargar la lista en texto con `GET https://api.jazui.com/v2/bots.txt` o los bloques ya hechos con `GET https://api.jazui.com/v2/bots/config`. ##

Pide tu portada como si fueras un bot, con el agente de usuario de pruebas integration-test:

curl -I -A "integration-test" https://misitio.com/

La respuesta debe llevar x-jazui: true y x-jazui-cache: MISS la primera vez y HIT después. Sin agente de bot (curl -I https://misitio.com/) debe llegar tu web de siempre, sin x-jazui. En el panel, la petición aparece en Tráfico de bots y la página en Páginas en caché.

Pide tu portada como si fueras un bot, con el agente de usuario de pruebas `integration-test`: ``` curl -I -A "integration-test" https://misitio.com/ ``` La respuesta debe llevar `x-jazui: true` y `x-jazui-cache: MISS` la primera vez y `HIT` después. Sin agente de bot (`curl -I https://misitio.com/`) debe llegar tu web de siempre, sin `x-jazui`. En el panel, la petición aparece en **Tráfico de bots** y la página en **Páginas en caché**. ## Si algo no funciona - **Error 500** al pedir como bot: falta algún módulo o `SSLProxyEngine On`; el registro de errores de Apache dice cuál. - **No aparece `x-jazui`**: comprueba que el `.htaccess` se lee (`AllowOverride All`). - **Detrás de Cloudflare**, `REMOTE_ADDR` es la IP del CDN: usa `%{HTTP:CF-Connecting-IP}` o `mod_remoteip`. - El tiempo máximo de espera es el del proxy de Apache (`ProxyTimeout`, 60 s por defecto); puedes bajarlo a 20 s en tu ``. --- # Inicio y tokens > Lo primero que ves al entrar: el resumen, tu plan y los tokens para la integración. Página original: https://docs.jazui.com/panel/home/es ## Resumen El inicio resume los últimos 30 días de todos tus sitios: - **Peticiones de bots**, con cuántas son de bots de IA. - **Servidas desde caché**: el porcentaje que salió al instante y cuántas se renderizaron en ese momento. - **Páginas en caché** frente al límite de tu plan, y cuántas tienen nota baja. - **Nota SEO media** de tus sitios y cuántos están verificados. Debajo verás las peticiones por día (desde caché o renderizadas), tus sitios con su nota, los bots que más te visitan y la tarjeta **Tu plan** con las barras de consumo del mes: se ponen en aviso al 80 % y en rojo al 100 %. Si aún no tienes sitios, verás el botón **Añadir sitio**. Si trabajas en el equipo de otra persona, el inicio te dice en qué cuenta estás y tu rol en cada sitio. ## Integración y ajustes Solo el dueño de la cuenta ve esta sección: - **JAZUI Token**: el token de render de la cuenta, el que va en tu integración. Puedes mostrarlo, copiarlo o **Renovar**lo; al renovarlo, el anterior deja de valer en menos de un minuto. - **PUBLIC Token**: de solo lectura, para leer tu blog con la API pública desde el navegador. - **Configuración de caché**: cada cuántos días se vuelven a renderizar tus páginas (de 0 a 30; 0 = nunca). Es el valor por defecto de la cuenta: cada sitio y cada regla pueden cambiarlo, y nunca baja del mínimo de tu plan. - **Tiempo máximo de carga esperado**: cuánto espera Chrome a que tu página termine de cargar (de 1 a 10 segundos; 5 por defecto). Súbelo si a las páginas renderizadas les falta contenido. ## El menú | Sección | Qué hay | |---|---| | **Sitios** | Sitios web, Páginas en caché, Reglas y Mapas del sitio. | | **Análisis** | Tráfico de bots, Cómo te ve la IA y Search Console. | | **Contenido** | Blog (artículos y categorías), Escribir con IA y Redes sociales. | | **Cuenta** | Mi plan, IA (tu clave) y Equipo. | En la barra superior están el selector de color y modo, la campana de avisos, el selector de cuenta (si perteneces a algún equipo) y tu menú con **Perfil** y **Salir**. El menú lateral tiene buscador y el cambio de idioma. --- # Sitios web > Da de alta tus dominios, verifícalos y gestiona su token, su nota y sus enlaces. Página original: https://docs.jazui.com/panel/sites/es ## La lista de sitios Cada sitio muestra su nivel, si está **Verificado**, su **nota SEO** y cuántas páginas tiene en caché (y, si tu plan la exige, el estado de la insignia). Selecciona uno para usar los botones: | Botón | Para qué | Rol | |---|---|---| | Agregar sitio | Dar de alta un dominio. | Dueño | | Verificar | Demostrar que es tuyo. | Administración | | Ajustes de render | Los [ajustes del sitio](https://docs.jazui.com/llms/es/panel-site-settings.txt). | Administración | | Token del sitio | Su token propio y los bloques de bots para nginx, IIS y Apache. | Administración | | Puntuación SEO | La nota del sitio y qué corregir primero. | Todos | | Enlaces rotos | La revisión de enlaces. | Todos | ## Añadir un sitio Escribe el dominio (`misitio.com`) o pega cualquier dirección del sitio: Jazui se queda con el dominio. Tiene que ser un dominio público, y cuentan los sitios de tu plan. Al guardarlo se abre la verificación. Cada subdominio (`blog.misitio.com`) se da de alta como un sitio más y cuenta para el límite del plan. Si el dominio ya está verificado por DNS en tu cuenta, el subdominio hereda la verificación al darlo de alta; comparte las reglas del dominio, sus páginas en caché cuentan en el mismo límite de la cuenta y tiene sus propios ajustes, métricas y token. > Para quitar un sitio usa **Eliminar sitio** (más abajo). ## Verificación Hay dos métodos y basta con uno: un registro **DNS TXT** `jazui-verification=CÓDIGO` en el dominio raíz (botón **Comprobar DNS**) o la **etiqueta meta** `` en la portada (botón **Comprobar etiqueta**). El paso a paso está en [Primeros pasos](https://docs.jazui.com/llms/es/getting-started.txt). Hasta que el sitio esté verificado, Jazui no renderiza sus páginas (la API responde `403`) y tampoco funcionan su sitemap, robots.txt, llms.txt, alertas ni revisión de enlaces. ## Token del sitio Un token de render que solo sirve para este sitio y sus subdominios dados de alta. Úsalo en la integración del sitio en lugar del de la cuenta: si se filtra, lo regeneras o revocas sin tocar tus otros sitios. Botones: **Generar token**, **Copiar**, **Regenerar token** (el actual deja de valer en menos de un minuto) y **Revocar token** (el sitio vuelve a usar el de la cuenta). En la misma ventana, **Bots en nginx, IIS y Apache** te da el bloque de bots actualizado para cada servidor, con cuántos bots tiene y la fecha de la lista. ## Nota del sitio **Puntuación SEO** abre la nota del sitio: la media de la nota de sus páginas en caché, ponderada por las visitas de bots de los últimos 30 días (cada página pesa sus visitas más una), recalculada cada hora. Debajo, **Corrige primero** ordena los problemas de todas las páginas por los puntos que suben al corregirlos multiplicados por sus visitas, con las páginas afectadas. Así sabes qué arreglo tiene más impacto. ## Revisión de enlaces Jazui lee los enlaces y las imágenes de las páginas en caché del sitio (lo que ven los bots) y de las de su sitemap, y comprueba cada uno. La frecuencia y cuántas páginas se leen dependen del plan (fila «Revisión de enlaces» en [Planes y límites](https://docs.jazui.com/llms/es/plans.txt)), con hasta 2.000 enlaces por revisión. Con **Revisar ahora** puedes lanzar una a mano, como mucho una cada 10 minutos. El resultado separa **Rotos** y **Redirecciones**, con el código (o «Sin respuesta»), si el enlace es interno, externo o una imagen, si está en el sitemap y desde cuántas páginas se enlaza. Los enlaces externos que no se dejan comprobar (401, 403, 429…) no cuentan como rotos. Si aparecen enlaces rotos, recibes un aviso. ## Eliminar un sitio Elige el sitio y pulsa **Eliminar sitio** (solo el dueño de la cuenta). La ventana te dice cuántos artículos del blog y páginas en caché tiene, y qué subdominios heredan de él su verificación. Para confirmar, escribe el dominio del sitio. Se borra todo lo suyo: ajustes, token del sitio, reglas propias, blog (artículos con sus imágenes, categorías y etiquetas), métricas de bots y visitas desde IA, Search Console, enlaces rotos y alertas. Sus páginas en caché (con y sin `www`) se borran en segundo plano y dejan de contar en el límite de la cuenta; las de sus subdominios no se tocan, porque son otros sitios. No se puede deshacer. > Los subdominios que heredaban la verificación de este sitio pasan a **sin verificar** y dejan de renderizarse hasta que los verifiques por su cuenta. Si el sitio se vuelve a dar de alta, hay que verificarlo de nuevo y sus páginas se renderizan otra vez con la primera visita de un bot. ## Insignia del plan gratuito Si tu plan pide la insignia «Visible con Jazui», la lista de sitios muestra su estado (**Vista**, **Falta** con la fecha límite o **Sin comprobar**) y un aviso con el botón **Añadir la insignia**, que te da el código en español o inglés con su vista previa. Las reglas están en [Planes y límites](https://docs.jazui.com/llms/es/plans.txt). --- # Ajustes del sitio > Cómo se renderiza cada sitio, su blog, su robots.txt, sus bots de IA y sus alertas. Página original: https://docs.jazui.com/panel/site-settings/es En **Sitios web**, selecciona un sitio y pulsa **Ajustes de render**. Hay cinco pestañas y todo se guarda con **Guardar**. Lo que dejes vacío usa el valor de tu cuenta. ## Render | Ajuste | Qué hace | |---|---| | Tiempo máximo | Segundos que Chrome espera a que la página termine (1 a 10). Nunca pasa del máximo de tu plan. | | Tiempo de recaché (0-30 días) | Cada cuántos días se vuelven a renderizar las páginas de este sitio. | | Dispositivo | **Escritorio** (por defecto) o **Móvil**: cambia la pantalla y el navegador con los que se renderiza. | | Selector de página lista | Un selector CSS (por ejemplo `#app .content`): Jazui espera a que ese elemento exista antes de guardar el HTML. Útil si tu SPA carga datos después. | | User agent | El agente con el que Chrome pide tu página. Vacío = el de Chrome del dispositivo. | | Cabeceras extra | Una por línea, `Nombre: valor` (por ejemplo `X-Preview-Token: abc123`). Solo se envían a tu propio dominio. | ## Blog e IndexNow - **Ruta de los artículos del blog**: cómo son las direcciones de tus artículos en tu web, con `{slug}` (o `{id}`) y, si quieres, `{lang}`. Vacío = `/blog/{slug}`. Ejemplo: `/{lang}/articulos/{slug}`. Se usa para las URL de la API, el sitemap y los enlaces de redes. - **Ruta de las páginas de categoría**: con `{slug}` y `{lang}`. Vacía, las categorías no salen en el sitemap. - **Al publicar, actualizar o retirar un artículo**: **Volver a renderizar la página del artículo** (activado por defecto) y el [webhook](https://docs.jazui.com/llms/es/api-webhooks.txt) del blog, con su botón **Probar** y el **Secreto de la firma**. - **Avisar a IndexNow** (Bing, Yandex, Seznam…): desactivado por defecto. Al guardar se genera la clave: publica el archivo `https://misitio.com/{clave}.txt` y pulsa **Comprobar**. Verás la fecha y el resultado del último aviso. ## Sitemap y robots.txt Enlaces al **sitemap**, **robots.txt** y **llms.txt** que Jazui genera para el sitio (solo si está verificado). Puedes escribir: - **Reglas propias del robots.txt** (hasta 8.000 caracteres). Vacío = permitir todo. La línea `Sitemap:` se añade sola, y también los bots de IA que bloquees en robots.txt. - **Descripción para el llms.txt** (hasta 1.000): una o dos frases sobre tu sitio para los modelos de lenguaje. Cómo servirlos en tu dominio está en [Sitemap, robots y llms.txt](https://docs.jazui.com/llms/es/api-site-files.txt). Si prefieres subir tus propios sitemaps, en **Mapas del sitio** (menú Sitios) puedes guardar archivos XML que se validan al guardar y se publican en una dirección de Jazui. ## Bots de IA Qué puede hacer cada bot de IA con tu sitio. Se explica en detalle en [Bots de IA](https://docs.jazui.com/llms/es/panel-ai-bots.txt). ## Alertas Activa **Avisarme cuando algo vaya mal en este sitio** y Jazui comprobará cada hora (en sitios verificados): | Alerta | Umbral por defecto | Cuándo se mira | |---|---|---| | Errores de render en 24 h | 10 % | Con al menos 20 peticiones en 24 h. No se repite en 24 h. | | Caída de Googlebot frente a la semana anterior | 50 % | Con al menos 20 visitas la semana anterior. No se repite en 7 días. | | Bajada de la nota SEO en 7 días | 10 puntos | No se repite en 7 días. | Cada umbral va de 0 a 100; 0 desactiva esa alerta. Las alertas llegan **por email** (a tu correo o a otro que indiques), como aviso en el panel y, si tu plan incluye webhooks de alertas, al **webhook de alertas** si lo configuras. Con **Enviar alerta de prueba** lo compruebas, y en **Últimas alertas** ves el historial. --- # Páginas en caché > Las páginas que Jazui sirve a los bots: verlas, compararlas con tu web, recachearlas y su nota. Página original: https://docs.jazui.com/panel/cached-pages/es Las páginas se crean solas cuando un bot pide una dirección a través de tu integración. También puedes añadirlas a mano para tenerlas listas antes de que llegue el primer bot. ## La lista - **Renderizado**: Activo o Detenido (una página detenida no se vuelve a renderizar sola). - **Fecha de caché**, con el aviso **Desfasada** si la comparación detectó cambios en tu web. - **Última solicitud** de un bot y la **URL en caché**. - **Tiempo máximo** de espera de esa página. - **Puntuación SEO** de lo que reciben los bots (la del sitio original, al pasar el ratón). - **Google**: clics e impresiones de 28 días, e **Indexación**, si conectaste Search Console. - **Fuente** (la añadiste tú o llegó un bot) y el **Robot** que la pidió. ## Qué puedes hacer - **Agregar**: una URL con su tiempo máximo y si se renderiza sola. **Editar** además te enseña qué reglas se aplicaron en la última petición. - **Ver página en caché**: el HTML exacto que recibe un bot. - **SEO**: la [nota SEO](https://docs.jazui.com/llms/es/panel-seo.txt) de la página con todas sus comprobaciones. - **Comparar con el sitio en vivo**: renderiza ahora tu página sin tocar la caché (gasta un render) y te dice si la caché está **al día** o **desfasada**: parecido del texto, bloques nuevos o quitados, campos clave (código, título, descripción, canonical, robots, H1, idioma, Open Graph, tipos de JSON-LD, palabras, enlaces, imágenes), los cambios en el texto y dos capturas. Se considera desfasada si el parecido baja del 95 % o cambia el título, la descripción, la canonical, el robots, el H1 o el código. Desde ahí puedes **Recachear ahora**. - **Recachear**: vuelve a renderizar las páginas seleccionadas; te avisa al empezar y al terminar. - **Eliminar**: quita páginas de la caché (por lotes). ## Lo que conviene saber - Cada render y cada recaché (también los automáticos) cuentan como **render** del plan, y cada página nueva ocupa un hueco de **páginas en caché**. - Si tu web responde con un error de servidor (5xx) al recachear, se conserva la versión anterior. - Jazui quita siempre de las direcciones los parámetros de seguimiento (`utm_*`, `fbclid`, `gclid`, `dclid`, `gbraid`, `wbraid`, `msclkid`, `yclid`, `igshid`, `mc_cid`, `mc_eid`, `_ga`, `_gl`, `ref_src`): todas esas variantes comparten una sola página. Para otros parámetros, usa una regla **Ignorar parámetros**. - El **recaché automático** usa los días de la regla «Días de caché» que se aplique; si no hay, los del sitio, y si no, los de la cuenta. Nunca baja del mínimo de tu plan y se para si no quedan renders. - Para recachear desde tu despliegue o tu CMS, usa la [API de caché](https://docs.jazui.com/llms/es/api-cache.txt). --- # Reglas > Decide qué se renderiza y cómo, sin tocar tu código: excluir, bloquear, inyectar etiquetas, forzar códigos y más. Página original: https://docs.jazui.com/panel/rules/es Una regla tiene una o varias **condiciones** sobre la dirección o el bot, y una **acción**. Puede valer para todos tus sitios o para uno (con sus subdominios). Cuántas reglas puedes tener depende del plan ([Planes y límites](https://docs.jazui.com/llms/es/plans.txt)). ## Condiciones Comparan, sin distinguir mayúsculas, con la dirección completa salvo donde se indica: | Condición | Ejemplo | |---|---| | Igual, Diferente | `https://misitio.com/precios` | | Contiene, No contiene | `/admin` | | Comienza con, No comienza con | `https://misitio.com/blog` | | Finaliza con, No finaliza con | `.pdf` | | Coincide con patrón (*) | `/blog/*` o `/*.pdf` (`*` = cualquier texto, `?` = un carácter). Si empieza por `http` se compara con la URL completa; si no, con la ruta y la query. Tiene que cubrir todo el texto. | | Expresión regular | `/(admin\|account)/` | | Tiene parámetro de query | `preview`, `page=2` o `filter_*` | | El bot es | `GPTBot`, `Googlebot`, `facebookexternalhit` (se busca dentro del agente de usuario) | Con **Añadir condición** sumas hasta 9 más y eliges si deben cumplirse **todas (Y)** o **alguna (O)**. ## Acciones | Acción | Qué hace | |---|---| | **Excluir** | La dirección no se renderiza (la integración sirve tu web normal). | | **Solo incluir** | Si un sitio tiene reglas de este tipo, solo se renderizan las direcciones que cumplen alguna; el resto queda excluido. | | **Días de caché** | Cada cuántos días se recachea (0 a 365; 0 = nunca), por encima del sitio y la cuenta. | | **Ajustes de render** | Tiempo máximo, dispositivo y selector de página lista solo para esas direcciones. | | **Ignorar parámetros** | Quita parámetros de la dirección antes de buscarla y renderizarla, para que las variantes compartan una página: `sessionid, sort, filter_*`. | | **Quitar elementos** | Selectores CSS, uno por línea (hasta 20), que se quitan del HTML antes de guardarlo: banners de cookies, chats, ventanas emergentes. | | **Inyectar en ** | Etiquetas `meta`, `link`, `title` o JSON-LD que se añaden. Sustituyen a las que ya tenga la página con el mismo `name` o `property`, y a su title o canonical. | | **Forzar código** | El bot recibe 200, 301, 302, 404 o 410 en lugar del código de la página (con 301 o 302, «Redirigir a»). Se guarda al renderizar: recachea las páginas que ya tenías. | | **Bloquear** | El bot recibe un `403`. Combínalo con «El bot es» para bloquear, por ejemplo, a CCBot. | ## En qué orden se aplican 1. Primero las **Solo incluir** del sitio: si hay alguna, la dirección tiene que cumplir al menos una o queda excluida. 2. Después el resto, de **menor a mayor prioridad** (0 a 9999, 100 por defecto); a igual prioridad, por orden de creación. 3. **Excluir** y **Bloquear** terminan la evaluación. **Ignorar parámetros** cambia la dirección para las reglas siguientes. **Días de caché**, **Ajustes de render** y **Forzar código** se quedan con el valor de la primera regla que los fija. **Quitar elementos** e **Inyectar** se suman. 4. Una regla con **Detener aquí** corta: las siguientes no se aplican. ## Plantillas y ejemplos Al crear una regla puedes partir de una plantilla: - **Excluir /admin y /account**: patrón `/admin*` o `/account*`. - **Ignorar parámetros de orden y vista**: `sort, order, orderby, view`. - **Quitar banner de cookies**: `#onetrust-banner-sdk`, `#CybotCookiebotDialog`, `#cookie-notice`, `.cc-window`, `.cookie-banner`. - **Bloquear bots de IA**: GPTBot, ChatGPT-User, OAI-SearchBot, ClaudeBot, anthropic-ai, CCBot, PerplexityBot, Bytespider, Amazonbot y meta-externalagent. Un ejemplo de **Inyectar en ** para una ficha de producto (condición «Coincide con patrón» `/productos/zapatilla-trail`): `Inyectar en `: ```html Zapatillas de trail | Mi tienda ``` ## Probar una dirección Con **Probar URL** (también dentro del formulario) aplicas tus reglas a una dirección, con un agente de bot opcional, sin guardar nada: verás si **se renderiza**, queda **excluida** o **bloqueada**, la dirección con la que se cachea, el código, los días, la espera, el dispositivo, el selector, lo que se quita y lo que se inyecta, y cada regla marcada como «Se aplica», «No se cumple» o «No se evalúa». La regla que estás editando se prueba aunque no la hayas guardado. --- # Nota SEO > Cómo se calcula la nota de cada página y qué revisa cada comprobación. Página original: https://docs.jazui.com/panel/seo/es Cada página en caché tiene dos notas de 0 a 100: **Lo que reciben los bots** (el HTML que sirve Jazui, con tus reglas aplicadas) y **Sitio original** (la página como la pinta el navegador, antes de las reglas). La diferencia te dice cuánto mejora Jazui tu página. Se audita en cada render; las comprobaciones que necesitan red se hacen en segundo plano y las que miran todo el sitio, cada hora. En la ventana de la nota tienes **Corrige primero** (cada problema con los puntos que sube al arreglarlo), una pestaña por categoría, la **Vista previa** en Google y al compartir, y el **Histórico** de cambios. ## Cálculo Cada comprobación sale **Bien** (vale todo su peso), **Aviso** (la mitad) o **Error** (nada). Cada categoría puntúa de 0 a 100 y la nota es la media ponderada: | Categoría | Peso | |---|---| | Metadatos | 25 | | Contenido | 20 | | Redes sociales | 15 | | Datos estructurados, Enlaces, Imágenes y Técnico | 10 cada una | ## Comprobaciones | Categoría | Comprobación (peso) | Qué pide | |---|---|---| | Metadatos | Título de la página (3) | Existe, es único y tiene 30–60 caracteres. | | Meta descripción (3) | 70–160 caracteres. | | | URL canónica (2) | Una sola, absoluta y apuntando a la propia página. | | | Idioma (1) y Viewport (1) | `lang` en ``; viewport con `width=device-width`. | | | Meta robots (3) | `noindex` es error; `nofollow`, aviso. | | | Título y descripción únicos en el sitio (2 + 2) | Sin repetir en otras páginas. | | | Destino de la canónica (2) | Responde 200, sin cadenas ni bucles. | | | Contenido | Contenido pintado (3) | Al menos 50 caracteres tras el render. | | Encabezado H1 (3) | Uno solo y con texto. | | | Orden de encabezados (1) | Sin saltos de nivel. | | | Cantidad de texto (2) | 300 palabras o más. | | | Proporción de texto (1) | Al menos un 10 % del HTML. | | | Redes sociales | og:title (2) y og:description (2) | Presentes. | | og:image (3) | Absoluta y, si declara medidas, de al menos 1200×630. | | | og:url (1) | Presente. | | | Tarjeta de X (1) | `twitter:card`. | | | Imagen al compartir (2) | Se descarga, no es SVG y mide al menos 1200×630. | | | Datos estructurados | Datos estructurados (2) | Hay JSON-LD. | | JSON-LD válido (3) | Se puede leer. | | | Tipos de datos estructurados (3) | Tipos reconocidos con sus campos obligatorios. | | | Enlaces | Enlaces internos (3) | La página enlaza a otras del sitio. | | Texto de los enlaces (2) | Sin «clic aquí» ni enlaces vacíos. | | | nofollow interno (1) | Sin `nofollow` en enlaces internos. | | | Enlaces rotos (2) | Los internos en caché no están rotos ni redirigen. | | | Página enlazada (2) | Otra página la enlaza (no es huérfana). | | | Imágenes | Texto alternativo (3) | Más de la mitad sin `alt` es error. | | Medidas de las imágenes (2) | `width` y `height`. | | | Formato (1) | WebP, AVIF o SVG. | | | Técnico | Código HTTP (3) | 200. | | Tiempo de render (1) | Hasta 3 s bien; hasta 8 s aviso. | | | Tamaño del HTML (1) | Más de 1 MB aviso; más de 5 MB error. | | | hreflang (1) y hreflang recíproco (1) | Alternativas de idioma correctas y que se enlazan entre sí. | | | robots.txt (3) y Sitemap (1) | robots.txt deja pasar la página y existe un sitemap. | | ## Cómo subir la nota sin tocar tu código - Falta el título, la descripción, Open Graph o los datos estructurados: una regla **Inyectar en **, o pide a [tu IA que los proponga](https://docs.jazui.com/llms/es/ai-suggestions.txt) y aplícalos como regla con un clic. - La página tarda o se guarda incompleta: sube el **tiempo máximo** o usa un **selector de página lista**. - Banners o chats que ensucian el contenido: una regla **Quitar elementos**. - Lo que dependa de tu HTML (H1, textos alternativos, enlaces internos) conviene corregirlo en tu web. --- # Bots de IA > Decide qué bots de IA pueden leer tu sitio, entrenar con él o citarte. Página original: https://docs.jazui.com/panel/ai-bots/es No todos los bots de IA hacen lo mismo. Jazui los clasifica por su **uso**: - **Entrenamiento**: recogen contenido para entrenar modelos (por ejemplo GPTBot, CCBot o Google-Extended). - **Buscador**: indexan para las respuestas de un buscador con IA (por ejemplo OAI-SearchBot o PerplexityBot). - **A petición del usuario**: leen una página porque alguien se lo ha pedido al asistente (por ejemplo ChatGPT-User o Claude-User). Bloquear a los de entrenamiento es una decisión de cada uno; bloquear a los de búsqueda o a los que responden a usuarios hace que la IA no pueda citarte. ## Qué puedes hacer con cada uno En **Ajustes del sitio › Bots de IA** está la lista completa, con su empresa, su uso y sus peticiones y rechazos de los últimos 30 días (primero los que te han visitado). Para cada uno: | Política | Efecto | |---|---| | **Permitir** (por defecto) | Recibe tus páginas renderizadas como cualquier bot. | | **Bloquear** | Recibe un `403` de Jazui (`X-Jazui-Blocked: ai-bot`). Solo para los bots que pasan por tu integración. | | **Bloquear y robots.txt** | Además añade `Disallow: /` para ese bot en el robots.txt que genera Jazui, para que los que respetan robots.txt ni lo intenten. | Hay acciones por lotes, incluida **Bloquear los de entrenamiento**. Algunos (como Google-Extended o Applebot-Extended) no visitan páginas: solo existen en robots.txt, así que para ellos solo hay «Bloquear y robots.txt». ## llms.txt y Markdown para IA Para que la IA te entienda mejor, Jazui publica para cada sitio verificado un `llms.txt` (índice de tu sitio para modelos de lenguaje) y una versión en **Markdown** de cada artículo del blog, enlazada desde su ``. La descripción del llms.txt se escribe en **Ajustes del sitio › Sitemap y robots.txt**. Cómo publicarlos en tu dominio está en [Sitemap, robots y llms.txt](https://docs.jazui.com/llms/es/api-site-files.txt). ## Medir En [Tráfico de bots](https://docs.jazui.com/llms/es/panel-traffic.txt) ves cuántas peticiones hacen los bots de IA, y en la pestaña **Visitas desde IA**, cuántas personas llegan a tu web desde las respuestas de ChatGPT, Perplexity, Gemini y otros asistentes. Con [Cómo te ve la IA](https://docs.jazui.com/llms/es/panel-ai-scan.txt) compruebas qué lee un bot de IA de cualquier página. --- # Tráfico de bots > Quién te visita, qué recibe, qué páginas se rastrean, las visitas desde la IA y tu consumo. Página original: https://docs.jazui.com/panel/traffic/es Elige un sitio (o todos) y el periodo: 7, 30 o 90 días, hasta el historial de métricas de tu plan ([Planes y límites](https://docs.jazui.com/llms/es/plans.txt)); los periodos que no incluye aparecen desactivados. Los días anteriores se resumen cada hora; hoy se calcula al momento. Las peticiones se guardan 90 días. ## Tráfico - **Indicadores**: peticiones, porcentaje desde la caché, respuesta media, render medio, rechazadas y errores, porcentaje de bots de IA, y **verificadas / falsas** (con la IP que envía tu integración, Jazui comprueba Googlebot, Bingbot, Applebot y otros buscadores). - **Gráficos**: peticiones por día y tipo de bot (buscadores, IA, redes sociales, herramientas SEO, otros), tiempos medios, rechazadas y errores por día, reparto por tipo y bots de IA. - **Bots**: cada bot con su tipo, peticiones, desde la caché, verificadas y falsas. ## Páginas y SEO - **Evolución de la nota SEO** de cada sitio, un punto al día. - **Páginas más rastreadas**: peticiones, de buscadores y de IA, última visita y nota. - **Páginas que Googlebot no ha visitado**: páginas en caché con 200 sin ninguna visita de Googlebot en el periodo. Enlázalas desde otras páginas y revisa que estén en el sitemap. ## Visitas desde IA Personas que llegan a tu web desde respuestas de ChatGPT, Perplexity, Gemini, Copilot, Claude, Meta AI, Le Chat, Grok, DeepSeek y otros (más de 20 asistentes). Las cuenta un pequeño script en tu web, sin cookies ni datos personales, que solo actúa cuando la visita viene de un asistente (por la página de origen o por `utm_source`). Verás las visitas por día, por asistente y las páginas a las que llegan. El script de cada sitio está en esta pestaña, listo para copiar en tu ``. ## Consumo Solo el dueño. Lo que suma el contador mensual de la cuenta: peticiones de bots, renders y recachés (también los automáticos) y lo que se hace desde el panel, con la previsión a fin de mes, la comparación con el mes anterior, los últimos 12 meses y los bots que más consumen. Los límites y el uso frente a ellos están en **Mi plan**. --- # Search Console > Clics, impresiones e indexación de Google junto a los datos de Jazui. Página original: https://docs.jazui.com/panel/search-console/es Conecta cada sitio con su propiedad de Google Search Console para ver sus clics, impresiones, posición e indexación sin salir del panel. Jazui solo pide permiso de **lectura**. Incluido en los planes con Google Search Console ([Planes y límites](https://docs.jazui.com/llms/es/plans.txt)); conectarlo requiere el rol Administración en el sitio. ## Conectar 1. En **Search Console**, pulsa **Conectar con Google** en el sitio y entra con la cuenta de Google que tiene acceso a la propiedad. 2. Jazui elige la propiedad que coincide con el sitio; puedes **Cambiar**la. Si la cuenta no tiene ninguna, verás «Sin propiedad». 3. Los datos se piden a Google en la hora siguiente; con **Actualizar ahora** no esperas. **Desconectar** quita la conexión, retira el permiso en Google y borra los datos leídos. ## El informe - Periodo de 7, 28 (por defecto) o 90 días, o 16 meses. - **Clics**, **impresiones** y **posición media**, con la variación frente al periodo anterior, y el gráfico por día. - **Páginas con más clics**: clics, impresiones, CTR y posición. - **Estado de indexación**: Jazui consulta en Google tus páginas en caché poco a poco (hasta 1.500 al día) y las repite cada 7 días, o al momento con **Inspeccionar ahora**. Verás si están indexadas, la canónica que eligió Google, la cobertura y el último rastreo. Los clics, las impresiones y la indexación también aparecen en **Páginas en caché**. Cómo trata Jazui estos datos está en su política de privacidad. --- # Cómo te ve la IA > Comprueba qué lee un bot de IA de cualquier página, tuya o de otros, y cómo mejorarla. Página original: https://docs.jazui.com/panel/ai-scan/es El escáner pide la página dos veces: como un bot de IA que no ejecuta JavaScript y como una persona con el navegador. Compara las dos, revisa lo básico que lee la IA y da una nota de 0 a 100. Sirve para tus páginas y para las de la competencia. Es lo mismo que el escáner de la web de Jazui, con más detalle. Tienes **10 escaneos al día**; el resultado de una dirección vale 24 horas para todos y repetirlo no gasta. No consume renders del plan. ## Qué revisa | Comprobación | Puntos | |---|---| | Contenido visible sin JavaScript (proporcional al texto que ve la IA) | 30 | | robots.txt deja entrar a los bots de IA que buscan o responden | 13 | | Título (10–70 caracteres) | 10 | | Datos estructurados (JSON-LD) | 10 | | Meta descripción (50–170 caracteres) | 8 | | Encabezado principal (H1) | 8 | | Respuesta 200 | 5 | | llms.txt | 5 | | URL canónica | 4 | | Open Graph | 4 | | Idioma (lang) | 3 | Si el servidor o su firewall rechaza al bot de IA (401, 403, 429…) mientras la página carga bien en el navegador, el escáner lo marca como **bloqueado** y lo explica: puede que tu CDN bloquee los bots de IA. En ese caso revisa el título, la descripción, etc. sobre la página real. ## El resultado - La nota y qué parte del texto de la página ve la IA. - El código HTTP y el tiempo de respuesta; «Ya usa Jazui» si la página la sirve Jazui. - **Bots de IA en tu robots.txt**: cuáles pueden entrar y para qué los usa cada empresa. - **Lo que ve la IA** frente a **lo que ve una persona**, con sus palabras. - Tus escaneos anteriores. Debajo, **Arreglos sugeridos por tu IA** te propone título, descripción y datos estructurados: se explica en [Sugerencias de arreglos](https://docs.jazui.com/llms/es/ai-suggestions.txt). --- # Avisos e informe semanal > Los avisos del panel, las alertas de tus sitios y el informe semanal por email. Página original: https://docs.jazui.com/panel/notifications/es ## La campana Todo lo que Jazui te avisa llega al momento a la campana de la barra superior (algunos avisos también por email). En la ventana de avisos puedes verlos por tipo (Plan, Consumo, Alerta, Enlaces, Sistema), abrir su detalle, ir a la sección, marcarlos como leídos o importantes y eliminarlos. Los antiguos se borran solos: los leídos a los 30 días y los no leídos a los 60 (los importantes, a los 90). ## Qué se avisa - Consumo al 80 % y al 100 % de renders o peticiones (también por email). - Cambios de plan: prueba, versiones, planes retirados y pagos. - Alertas de tus sitios y enlaces rotos. - La insignia que falta. - Recachés manuales en marcha y terminados. - Artículos escritos con IA, terminados o con error, y fallos al difundir en redes. - Equipo: invitaciones, salidas y bajas. ## Alertas de los sitios Se activan por sitio en [Ajustes del sitio › Alertas](https://docs.jazui.com/llms/es/panel-site-settings.txt): errores de render, caída de Googlebot y bajada de la nota SEO, por email, aviso y webhook. ## Informe semanal En **Tráfico de bots › Informe semanal**, activa **Recibir el informe semanal por email** (desactivado por defecto). Cada lunes a las 7:00 UTC recibirás el tráfico de bots de la semana frente a la anterior, tus sitios con su nota SEO, las alertas, lo que más conviene corregir y el consumo del mes. Puedes ver cómo es el de esta semana y **Enviártelo ahora**. --- # Equipo > Invita a otras personas a tus sitios con un rol en cada uno. Página original: https://docs.jazui.com/panel/team/es Los datos y el plan siguen siendo tuyos; solo tú gestionas el equipo, los sitios y la facturación. Cuántas personas caben depende del plan, contándote a ti ([Planes y límites](https://docs.jazui.com/llms/es/plans.txt)). Todo lo que hace el equipo cuenta para tu plan (renders, escaneos y artículos con IA) y usa tus conexiones de IA. ## Invitar En **Equipo**, pulsa **Invitar**, escribe el correo y elige un rol para cada sitio. La invitación caduca a los 7 días; puedes reenviarla o anularla. Si la persona no tiene cuenta, la crea desde el enlace y la invitación se acepta sola; si la tiene, la acepta al entrar. Puedes **Editar acceso** o **Quitar del equipo** cuando quieras. Si a ti te invitan, verás la invitación en el inicio y en **Equipo**; al aceptarla, cambias de cuenta con el selector de la barra superior. Puedes **Salir del equipo** en cualquier momento. ## Qué puede hacer cada rol El rol se da por sitio y cada uno incluye lo del anterior. | Función | Lectura | Edición | Administración | Dueño | |---|---|---|---|---| | Ver inicio, páginas en caché, SEO, métricas, reglas y blog | ✓ | ✓ | ✓ | ✓ | | Escanear con «Cómo te ve la IA» y ver Search Console | ✓ | ✓ | ✓ | ✓ | | Recachear, añadir y editar páginas y reglas | — | ✓ | ✓ | ✓ | | Escribir, editar y publicar artículos; escribir con IA y piloto automático | — | ✓ | ✓ | ✓ | | Pedir sugerencias a la IA y aplicarlas; difundir en redes | — | ✓ | ✓ | ✓ | | Token, verificación, alertas, webhooks, robots, sitemaps y bots de IA | — | — | ✓ | ✓ | | Cuentas de redes del sitio y conectar Search Console | — | — | ✓ | ✓ | | Conexiones de IA (el equipo las usa) | — | — | — | ✓ | | Equipo, sitios nuevos, mapas del sitio, plan y facturación | — | — | — | ✓ | --- # Cuenta y Mi plan > Tu perfil, la seguridad de tu cuenta, Mi plan y cómo eliminar la cuenta. Página original: https://docs.jazui.com/panel/account/es ## Perfil Desde tu menú de la barra superior, **Perfil**: foto (JPG, PNG o WebP, hasta 5 MB), nombre, teléfono e idioma (el del navegador, español o inglés: el panel, los emails y los avisos llegan en él). ## Seguridad - **Cambiar la contraseña** (al menos 10 caracteres). Si entraste con Google, puedes **Poner contraseña** para entrar también con tu email. - **Verificación en dos pasos**: al entrar, además de la contraseña, te pediremos un código que llega a tu email (también al entrar con Google). - Tras 5 intentos fallidos la cuenta se bloquea 15 minutos. - ¿Olvidaste la contraseña? En la pantalla de acceso recibes un enlace por email para restablecerla. ## Mi plan - **Plan actual**: nombre, si es prueba, mensual o anual, fecha de renovación y los cambios programados (fin de la prueba, bajada, cancelación…), con **Mantener mi plan** si te arrepientes. - **Tu uso** del mes: renders, peticiones, páginas en caché, sitios, reglas, artículos y artículos con IA. Los renders y las peticiones se reinician el día 1 de cada mes. - **Planes**: mensual o anual (con el ahorro), y **Contratar**, **Cambiar a este plan** o **Pasar a Gratis**. Se paga en el formulario seguro de Stripe. - **Gestionar facturación**: el portal de Stripe para tu tarjeta, tus facturas y la cancelación. **Actualizar desde Stripe** vuelve a leer tu suscripción. - **Facturas**: número, importe, estado, y verla o descargarla en PDF. Precios, límites y cambios de plan: [Planes y límites](https://docs.jazui.com/llms/es/plans.txt). ## Eliminar la cuenta En **Perfil › Eliminar cuenta** verás un resumen de lo que se pierde: plan, sitios, páginas en caché, reglas, blog, acceso con Google y equipos. Se confirma con tu contraseña o, si entras con Google, con Google o con un código por email. - Si tienes un plan de pago, se cancela en Stripe, sin más cobros ni reembolso del periodo en curso. - Sales de los equipos de otros (se avisa al dueño), y tu equipo pierde el acceso a tus sitios. - Se desconectan Search Console y el acceso con Google, y se borran tus sitios, la caché y el blog. Puedes volver a registrarte después con el mismo email. > No se puede deshacer. --- # Blog > Escribe en el blog de cada sitio desde Jazui y muéstralo en tu web con tu diseño. Página original: https://docs.jazui.com/blog/es El blog vive en Jazui y tu web lo lee con la [API del blog](https://docs.jazui.com/llms/es/api-blog.txt). Cada artículo pertenece a un sitio y a un idioma; así cada web solo muestra sus artículos aunque gestiones varias. Jazui genera el SEO de cada artículo (título, descripción, Open Graph, datos estructurados, hreflang), su versión en Markdown para la IA, el sitemap, el RSS y el llms.txt, y puede avisar a buscadores y redes al publicar. Cuántos artículos puedes tener depende del plan ([Planes y límites](https://docs.jazui.com/llms/es/plans.txt)). Si tu plan lleva la insignia en los artículos, terminan con «Publicado con Jazui». ## Escribir un artículo 1. En **Blog › Artículos**, pulsa **Agregar**: elige el **sitio** (sin sitio no sale en la API), el **idioma**, el **título**, el **slug** (se genera del título; es la parte final de la dirección y es único por sitio e idioma), el autor, el resumen y las etiquetas (separadas por comas; las nuevas se crean solas). 2. En **Contenido**, escribe en Markdown con el editor. El **Repositorio de imágenes** guarda las imágenes del artículo: sube una y **Copiar Markdown** te da el código para pegarla. La pestaña **SEO** revisa el texto mientras escribes, y **Vista previa** lo muestra como quedará. 3. Sube la **Portada** y asigna las **Categorías**. 4. En **Metadatos**, ajusta el título y la descripción para buscadores (hasta 60 y 160 caracteres), las palabras clave, la imagen para redes (vacía = la portada), la canónica (vacía = la dirección del artículo) y si debe tener `noindex`. El panel **SEO en vivo** te da la nota, la vista previa y las etiquetas del `` que se generan. 5. Cambia el **Estado** a **Publicado**. ## Estados | Estado | Qué significa | |---|---| | **Borrador** | No sale en la API mientras lo preparas. | | **Programado** | Se publica solo en la fecha y hora que elijas (hora de tu navegador). | | **Publicado** | Sale en la API desde ahora. | | **Archivado** | Deja de salir sin borrarlo. | Al publicar, actualizar o retirar un artículo, según los ajustes del sitio, Jazui vuelve a renderizar su página, llama a tu webhook, avisa a IndexNow y lo difunde en tus redes. ## Traducciones, versiones y categorías - **Traducciones**: en «Traducción de», enlaza el artículo con su versión en otro idioma del mismo sitio. Jazui genera las etiquetas `hreflang` y la API devuelve las traducciones de cada artículo. - **Historial**: cada vez que guardas el contenido se guarda una versión. Puedes **Restaurar** cualquiera; la actual queda en el historial. - **Categorías** (Blog › Categorías): por sitio e idioma, con slug, orden, icono, descripción y categoría padre para formar un árbol. Para que salgan en el sitemap, configura la ruta de las páginas de categoría en los ajustes del sitio. - **Dirección de los artículos**: la defines en [Ajustes del sitio › Blog e IndexNow](https://docs.jazui.com/llms/es/panel-site-settings.txt) (por defecto `/blog/{slug}`). ## Mostrarlo en tu web Con el PUBLIC Token, tu web pide los artículos a la API y los pinta con tu diseño. Si tu web es una SPA, la integración de Jazui hace que los bots vean el blog completo. Ejemplos y todos los campos en [API del blog](https://docs.jazui.com/llms/es/api-blog.txt). --- # IA con tu clave > Las funciones de IA de Jazui usan tus propias cuentas de IA: tú eliges proveedor y modelo, y pagas directamente el uso. Página original: https://docs.jazui.com/ai/es Jazui no cobra la IA ni pone límites a tu proveedor: guardas una o varias **conexiones** con su clave y cada proveedor te cobra a ti lo que uses. Las usan las [sugerencias de arreglos](https://docs.jazui.com/llms/es/ai-suggestions.txt) y [Escribir con IA](https://docs.jazui.com/llms/es/ai-writer.txt). Se configuran en **IA (tu clave)**, en el menú Cuenta; solo el dueño las cambia y su equipo las usa. ## Proveedores | Proveedor | Modelo propuesto | Dónde se saca la clave | |---|---|---| | Anthropic (Claude) | `claude-opus-5-5` | console.anthropic.com › API Keys | | OpenAI | `gpt-4.1-mini` | platform.openai.com › API keys | | Google Gemini | `gemini-2.5-flash` | aistudio.google.com › Get API key | | Mistral | `mistral-small-latest` | console.mistral.ai › API Keys | | DeepSeek | `deepseek-chat` | platform.deepseek.com › API keys | | Otra API compatible con OpenAI | El que indiques | Tu proveedor (por ejemplo OpenRouter: `https://openrouter.ai/api/v1`); la dirección debe ser https y terminar en `/v1`, y la clave es opcional. | El modelo se puede cambiar libremente en cada conexión. ## Conexiones 1. Pulsa **Añadir conexión**: ponle un nombre, elige el proveedor y el modelo, y pega la clave. 2. Pulsa **Probar conexión** para comprobar que responde. 3. Ordénalas con **Subir** y **Bajar**: la primera es la **predeterminada**. Con el **Modo automático** (activado por defecto), si la primera conexión no responde bien se prueba la siguiente, en tu orden. Desactivado, solo se usa la predeterminada. Al pedir algo también puedes elegir una conexión concreta. Puedes tener hasta 10 y desactivar las que no quieras usar. > La clave se guarda cifrada y nunca se vuelve a mostrar (solo sus últimos caracteres). Solo se envía a tu proveedor, con lo necesario para cada petición, y los datos que se envían los trata tu proveedor según sus condiciones. --- # Escribir con IA > Tu IA propone temas a partir de tu sitio y escribe artículos completos, a mano o en piloto automático. Página original: https://docs.jazui.com/ai/writer/es En **Escribir con IA** (menú Contenido), elige el sitio. Necesitas al menos una [conexión de IA](https://docs.jazui.com/llms/es/ai.txt). Verás cuántos artículos con IA llevas este mes frente al límite mensual de tu plan ([Planes y límites](https://docs.jazui.com/llms/es/plans.txt)). Proponer temas no gasta; cada artículo escrito sí (y cuenta también como artículo del blog). ## Temas propuestos 1. Elige el idioma, la conexión de IA (si tienes varias) y, si quieres, un **enfoque** («guías para principiantes, comparativas, novedades…»). 2. Pulsa **Proponer temas**. La IA lee tu portada, otras páginas en caché, tus artículos y, si lo conectaste, Search Console, y propone 6 temas con título, palabra clave, intención de búsqueda y esquema. 3. Marca los que quieras y pulsa **Escribir**. Se escriben en segundo plano: puedes salir de la página y te avisaremos. Cada artículo es un **borrador** de 800 a 1.300 palabras, con preguntas frecuentes, enlaces solo a páginas de tu sitio, título y descripción para buscadores, resumen y etiquetas. Revísalo en Blog › Artículos y publícalo cuando quieras. ## Piloto automático Por sitio, activa **Escribir artículos de forma periódica** y elige la **frecuencia** (cada día, cada 2, 3, 7, 14 o 30 días; 7 por defecto), el idioma, la conexión y el enfoque. Cada vez, la IA elige un tema nuevo para el sitio y escribe el artículo. - Por defecto quedan como **borradores** para que los revises. Con **Publicar sin revisar** se publican en cuanto están escritos: la IA puede equivocarse y tú eres responsable de lo que se publica. - Verás la próxima fecha y el último artículo (o el motivo si falló). Si llegas al límite del mes, el piloto espera sin llamar a la IA. Debajo, **Artículos en curso y recientes** muestra cada uno: en cola, escribiendo, borrador listo, publicado o con error (con **Reintentar**). --- # Sugerencias de arreglos > Tu IA propone título, descripción y datos estructurados para una página, y los aplicas con un clic. Página original: https://docs.jazui.com/ai/suggestions/es Después de escanear una página en [Cómo te ve la IA](https://docs.jazui.com/llms/es/panel-ai-scan.txt), en **Arreglos sugeridos por tu IA** pulsa **Sugerir arreglos con IA** (puedes elegir la conexión). Tu IA lee lo que dice la página, sin inventar, y propone: - **Título** (hasta 60 caracteres) y **meta descripción** (hasta 160). - **Datos estructurados** (JSON-LD) adecuados al tipo de página. - **Para el código del sitio**: el encabezado principal (H1) y consejos que conviene aplicar en tu web. Todo es editable y cada parte tiene su casilla para decidir qué aplicas. ## Aplicarlas - **Aplicar como regla** (páginas de tus sitios, rol Edición): crea una regla **Inyectar en ** solo para esa página, llamada «IA: /ruta». Lo marcado sustituye a lo que tenga la página al servirla a los bots, y el título y la descripción se ponen también en Open Graph. Cuenta para el límite de reglas. Recachea la página para verlo. - **Copiar etiquetas**: para páginas que no son de tus sitios, o si prefieres pegarlas tú en el código. - **Pedir otras**: vuelve a preguntar. Lo ideal es pasar también los cambios al código de tu web; la regla los aplica para los bots mientras tanto. --- # Redes sociales > Anuncia cada artículo en Bluesky, Mastodon, Telegram y Discord, solo o a mano. Página original: https://docs.jazui.com/social/es En **Redes sociales** (menú Contenido), elige el sitio y añade sus **canales** (hasta 10). Cada artículo que publiques se anunciará solo en ellos, y también puedes difundir a mano cualquier artículo publicado. Las credenciales se guardan cifradas. Los canales los gestiona el rol Administración; con Edición se puede difundir. ## Conectar cada red | Red | Qué necesitas | Cómo conseguirlo | |---|---|---| | Bluesky | Usuario (`minegocio.bsky.social`), contraseña de aplicación y, opcional, el servidor | Bluesky › Ajustes › Privacidad y seguridad › Contraseñas de aplicación. No uses tu contraseña normal. | | Mastodon | Servidor (`https://mastodon.social`) y token de acceso | Preferencias › Desarrollo › Nueva aplicación, con el permiso `write:statuses`; copia «Tu token de acceso». | | Telegram | Canal o grupo (`@micanal`) y token del bot | Crea un bot con @BotFather, añádelo como administrador del canal o grupo y pega su token. | | Discord | URL del webhook | Ajustes del canal › Integraciones › Webhooks › Nuevo webhook › Copiar URL. | Con **Probar conexión** compruebas que funciona. Cada canal puede publicar **automáticamente** los artículos nuevos o quedar solo para difundir a mano. ## El texto Cada canal tiene una **plantilla** con `{title}`, `{summary}`, `{url}` y `{tags}` (las etiquetas como `#etiquetas`). La de por defecto es: ```bash {title} {summary} {url} {tags} ``` En Bluesky va sin etiquetas. Si el texto no cabe en la red (Bluesky 300 caracteres, Mastodon 500, Discord 2.000, Telegram 4.096), se recorta primero el resumen, luego se quitan las etiquetas y por último se recorta el título; la dirección va siempre entera. ## Difundir y el historial - **Difundir un artículo**: elige un artículo publicado, escribe un texto propio si quieres (vacío = la plantilla de cada canal), mira la vista previa por canal y pulsa **Publicar ahora**. También desde Blog › Artículos › **Difundir**. - **Publicaciones**: cada envío con su canal, si fue automático o a mano, su estado y el enlace a la publicación. - Un artículo no se repite en el mismo canal. Los fallos pasajeros se reintentan a los 2 y a los 15 minutos; si fallan del todo, te avisamos y puedes **Reintentar**. Además, la API del blog devuelve con cada artículo los enlaces para que tus lectores lo compartan en X, Facebook, LinkedIn, Bluesky, WhatsApp, Telegram, Reddit o por email. --- # Autenticación y errores > Tokens, límites de velocidad, errores e idioma de todas las APIs de Jazui. Página original: https://docs.jazui.com/api/es Todas las APIs de Jazui están en `https://api.jazui.com`, usan HTTPS y responden en JSON (salvo `/Render`, que devuelve HTML, y los archivos públicos del sitio). Hay cuatro grupos: | API | Para qué | Token | |---|---|---| | [Render](https://docs.jazui.com/llms/es/api-render.txt) | La que usan las integraciones para pedir páginas. | Render (cuenta o sitio) | | [Caché](https://docs.jazui.com/llms/es/api-cache.txt) | Recachear o borrar páginas desde tu despliegue o tu CMS. | Render (cuenta o sitio) | | [Blog v2](https://docs.jazui.com/llms/es/api-blog.txt) | Leer artículos, categorías y etiquetas para pintarlos en tu web. | Público o render | | [Archivos del sitio](https://docs.jazui.com/llms/es/api-site-files.txt) | Sitemap, robots.txt, llms.txt, Markdown para IA y visitas desde IA. | Ninguno | Además, Jazui te avisa con [webhooks](https://docs.jazui.com/llms/es/api-webhooks.txt) cuando publicas un artículo o salta una alerta. ## Tokens Los tokens se envían en la cabecera `Authorization` con el esquema `Bearer`. No caducan: siguen valiendo hasta que los renuevas o los revocas. | Token | Dónde está | Sirve para | Secreto | |---|---|---|---| | **JAZUI Token** (render de la cuenta) | Inicio › Integración y ajustes | Render, caché y blog, para todos tus sitios. | Sí | | **Token del sitio** | Sitios web › Token del sitio | Render, caché y blog, solo de ese sitio y sus subdominios dados de alta. | Sí | | **PUBLIC Token** | Inicio › Integración y ajustes | Solo leer el blog. Se puede poner en el navegador. | No | ```bash curl https://api.jazui.com/v2/cache/tasks \ -H "Authorization: Bearer $JAZUI_TOKEN" ``` - Al **renovar** un token desde el panel, el anterior deja de valer en menos de un minuto: cambia el nuevo en tus integraciones y despliegues. - Un token de sitio usado con otro dominio recibe un `403`. - El PUBLIC Token no sirve para Render ni para la caché: recibe un `403` sin cuerpo. - Si la cuenta se bloquea o se elimina, sus tokens dejan de valer. ## Límites de velocidad | Qué | Límite | Se cuenta por | |---|---|---| | Render, caché y blog con token | 120 peticiones por minuto | Cuenta: todos sus tokens comparten el mismo límite | | Feeds, archivos del sitio y lista de bots (sin token) | 120 peticiones por minuto | IP | Al superarlo se recibe un `429` sin cuerpo. Es un límite por minuto: espera unos segundos y vuelve a intentarlo. Las integraciones lo tratan como un fallo y sirven tu web normal. No confundas este `429` con el de los límites del plan, que lleva la cabecera `X-Jazui-Limit`. ## Errores | Código | Cuándo | Cuerpo | |---|---|---| | `400` | Parámetros o JSON no válidos | `{code, message}` o ProblemDetails | | `401` | Sin token, token renovado o revocado | Vacío, con `WWW-Authenticate: Bearer` | | `403` | El token no sirve para esa API o ese sitio, sitio sin verificar, función no incluida en el plan o bloqueo | Vacío o `{code, message}` | | `404` | No existe o no está publicado | `{code, message}` o vacío | | `429` | Límite de velocidad, de tareas o del plan | Vacío o `{code, message}` | | `5xx` | Error de Jazui o de tu web al renderizar | ProblemDetails o texto, con `Retry-After` cuando conviene reintentar | Las APIs de caché y del blog explican el error con un código estable que puedes usar en tu código y un mensaje legible: `Error de la API de caché o del blog`: ```json { "code": "plan", "message": "Tu plan no incluye la API de caché." } ``` `ProblemDetails`: ```json { "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { "$": ["The JSON value could not be converted."] }, "traceId": "00-6f3c…-01" } ``` ## Idioma de los mensajes Los mensajes de error y los textos que devuelve la API salen en español por defecto. Para recibirlos en inglés, envía `Accept-Language: en` o añade `?culture=en` a la dirección. Los códigos (`code`) no cambian con el idioma. ## Sin token Algunas direcciones son públicas: la lista de bots que usan las integraciones, el estado del servicio, los feeds del blog y los archivos de cada sitio verificado. ```bash # Lista de bots que usan las integraciones (JSON y texto) curl https://api.jazui.com/v2/bots curl https://api.jazui.com/v2/bots.txt # Bloques listos para nginx, IIS y Apache curl https://api.jazui.com/v2/bots/config # Estado del servicio curl https://api.jazui.com/health ``` --- # Render > La API que usan las integraciones para pedir a Jazui la página de un bot. Página original: https://docs.jazui.com/api/render/es Normalmente no la llamas tú: lo hace tu integración. Conocerla sirve para depurar, para construir una integración propia o para comprobar qué recibe un bot de una página concreta. **Cada llamada cuenta como una petición servida del plan**, y si la página no estaba en caché, también como un render. ## Petición `GET https://api.jazui.com/Render` | Parámetro o cabecera | Obligatorio | Descripción | |---|---|---| | `url` | Sí* | Dirección completa de la página, codificada. *Si no la puedes poner en la query, envíala en la cabecera `X-Jazui-Url`. | | `botAgent` | No | Agente de usuario del bot. Si falta, se usa el `User-Agent` de la petición. | | `Authorization` | Sí | `Bearer` con el token de la cuenta o del sitio. | | `X-Jazui-Client-IP` | No | IP del bot. Si llega una lista separada por comas, se usa la primera. Sirve para las métricas y para verificar a los buscadores. | | `If-None-Match` | No | El `ETag` que tiene el bot, para recibir `304` si no ha cambiado. | ```bash curl -i "https://api.jazui.com/Render?url=https%3A%2F%2Fmisitio.com%2Fprecios&botAgent=integration-test" \ -H "Authorization: Bearer $JAZUI_TOKEN" \ -H "X-Jazui-Client-IP: 66.249.66.1" ``` ## Respuesta El HTML de la página con su **código real**: `200`, `404` si tu página es un 404, o la redirección (`301`, `302`…) con su `Location` y sin cuerpo. `Respuesta correcta`: ```http HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 X-Jazui: true X-Jazui-Cache: HIT ETag: "4812-638976215370000000" Last-Modified: Mon, 02 Nov 2026 18:22:17 GMT Precios | Mi sitio… ``` ## Otros códigos | Código | Cabecera | Motivo | |---|---|---| | `400` | — | Falta `url`, no es válida, o una regla excluye esa dirección del render. | | `401` | — | Sin token o token no vigente. | | `403` | — | Token de otro sitio, o el sitio no está verificado. | | `403` | `X-Jazui-Blocked: ai-bot` | Bloqueaste a ese bot de IA en los ajustes del sitio. | | `403` | `X-Jazui-Blocked: rule` | Una regla bloquea a ese bot. | | `429` | `X-Jazui-Limit: plan` | El plan gratuito ha gastado sus renders o peticiones del mes. | | `429` | `X-Jazui-Limit: renders` | No quedan renders este mes para una página nueva, o falta la insignia obligatoria pasado el margen. | | `429` | `X-Jazui-Limit: pages` | Se ha llegado al máximo de páginas en caché. | | `429` | — | Más de 120 peticiones por minuto. | | `502` | `Retry-After: 30` | Tu web respondió con un error de servidor al renderizar. | | `503` | `Retry-After: 30` | El render no terminó a tiempo. | ## Peticiones condicionales Cada página tiene un `ETag`. Si envías el que tienes en `If-None-Match` y la página no ha cambiado, recibes `304 Not Modified` sin cuerpo (también con `X-Jazui: true`). Así los bots que guardan las páginas no las descargan de nuevo. ```bash curl -i "https://api.jazui.com/Render?url=https%3A%2F%2Fmisitio.com%2F&botAgent=integration-test" \ -H "Authorization: Bearer $JAZUI_TOKEN" \ -H 'If-None-Match: "4812-638976215370000000"' # HTTP/1.1 304 Not Modified (sin cuerpo) ``` ## Avisar de un bot desconocido Las integraciones de código avisan con `POST /Render/Seen` cuando ven un agente de usuario con pinta de bot que no está en la lista. Responde siempre `204`; se cuenta una vez al día por agente, con un máximo de 500 distintos al día por cuenta. Si haces tu propia integración, puedes usarlo igual. ```bash curl -X POST https://api.jazui.com/Render/Seen \ -H "Authorization: Bearer $JAZUI_TOKEN" \ -H "Content-Type: application/json" \ -d '{"userAgent":"NuevoBot/1.0 (+https://nuevobot.ai)","url":"https://misitio.com/"}' # 204 No Content ``` ## Tu propia integración Si tu plataforma no está en la lista, puedes hacer una siguiendo el contrato de [Integraciones](https://docs.jazui.com/llms/es/integrations.txt): decide si es un bot con la lista de `/v2/bots`, llama a `/Render` con un tiempo máximo de espera (20 s), entrega la respuesta solo si lleva `X-Jazui` o es un `403` con `X-Jazui-Blocked`, y en cualquier otro caso sirve tu web. No reenvíes las cookies ni otras cabeceras del bot. --- # API de caché > Recachea o borra páginas cuando publicas cambios, desde tu despliegue, tu CMS o un script. Página original: https://docs.jazui.com/api/cache/es Cuando cambias tu web, las páginas en caché siguen mostrando la versión anterior hasta el siguiente recaché automático. Con esta API le dices a Jazui que vuelva a renderizarlas (o que las quite) al momento. Las peticiones crean una **tarea** que se procesa en segundo plano y cuyo estado puedes consultar. > Incluida en los planes con **API de caché** ([Planes y límites](https://docs.jazui.com/llms/es/plans.txt)). Usa el token de render de la cuenta o el de un sitio (con el del sitio, solo puedes tocar ese sitio). ## Direcciones | Método | Ruta | Qué hace | |---|---|---| | `POST` | `/v2/cache/recache` | Vuelve a renderizar las páginas. Las que no estaban en caché se renderizan por primera vez. | | `POST` | `/v2/cache/purge` | Quita las páginas de la caché (el siguiente bot provocará un render nuevo). | | `GET` | `/v2/cache/tasks/{id}` | Estado de una tarea. | | `GET` | `/v2/cache/tasks?limit=20` | Últimas tareas (de 1 a 100). Con un token de sitio, solo las de ese sitio. | ## Qué páginas El cuerpo lleva **exactamente una** de estas tres opciones: - `urls`: una lista de hasta 1.000 direcciones completas (se normalizan y se quitan las repetidas). - `prefix`: todas las páginas en caché que empiezan por esa dirección (también la dirección sin la barra final). Útil para una sección: `/blog/`, `/productos/`… - `site`: todas las páginas en caché de un sitio. Con `prefix` y `site` se procesan como mucho 10.000 páginas por tarea. `urls`: ```json { "urls": ["https://misitio.com/", "https://misitio.com/precios"] } ``` `prefix`: ```json { "prefix": "https://misitio.com/blog/" } ``` `site`: ```json { "site": "misitio.com" } ``` ## Ejemplo ```bash curl -X POST https://api.jazui.com/v2/cache/recache \ -H "Authorization: Bearer $JAZUI_TOKEN" \ -H "Content-Type: application/json" \ -d '{"urls":["https://misitio.com/","https://misitio.com/precios"]}' ``` `202 Accepted`: ```json { "id": "01a1842d-5d0b-4d6e-9a7f-2c3b4d5e6f70", "action": "Recache", "scope": "Urls", "target": null, "state": "Queued", "total": 2, "processed": 0, "failed": 0, "createdAt": "2026-11-03T20:22:17Z", "startedAt": null, "finishedAt": null, "error": null, "statusUrl": "https://api.jazui.com/v2/cache/tasks/01a1842d-5d0b-4d6e-9a7f-2c3b4d5e6f70" } ``` ```bash curl https://api.jazui.com/v2/cache/tasks/01a1842d-5d0b-4d6e-9a7f-2c3b4d5e6f70 \ -H "Authorization: Bearer $JAZUI_TOKEN" ``` ```json { "id": "01a1842d-5d0b-4d6e-9a7f-2c3b4d5e6f70", "action": "Recache", "scope": "Urls", "state": "Done", "total": 2, "processed": 2, "failed": 0, "createdAt": "2026-11-03T20:22:17Z", "startedAt": "2026-11-03T20:22:18Z", "finishedAt": "2026-11-03T20:22:31Z", "error": null } ``` ## La tarea | Campo | Descripción | |---|---| | `action` | `Recache` o `Purge`. | | `scope` y `target` | `Urls`, `Prefix` o `Site`, con el prefijo o el sitio. | | `state` | `Queued` (en cola), `Running`, `Done` o `Failed` (con el motivo en `error`). | | `total`, `processed`, `failed` | Páginas en total, hechas y con error. El progreso se guarda cada 10 páginas. | | `statusUrl` | Dirección para consultar la tarea (también en la cabecera `Location`). | Las tareas se procesan de una en una, por orden de llegada. Recachear **gasta renders** del plan como cualquier otro render; si se acaban, esas páginas cuentan como fallidas. ## Errores | Código | `code` | Motivo | |---|---|---| | `400` | `invalid_request` | No llega exactamente una opción, más de 1.000 direcciones o alguna no es absoluta. | | `403` | `plan` | Tu plan no incluye la API de caché. | | `403` | `forbidden` | El dominio no es de tu cuenta (o del sitio del token), o el sitio no está verificado. | | `429` | `too_many_tasks` | Ya tienes 10 tareas en cola o en marcha: espera a que terminen. | ## Al desplegar Lo habitual es recachear el sitio entero (o la sección que cambió) al final de tu despliegue. Guarda el token como secreto de tu CI: `GitHub Actions`: ```bash # .github/workflows/deploy.yml (después de publicar la web) - name: Recachear en Jazui run: | curl -fsS -X POST https://api.jazui.com/v2/cache/recache \ -H "Authorization: Bearer ${{ secrets.JAZUI_TOKEN }}" \ -H "Content-Type: application/json" \ -d '{"site":"misitio.com"}' ``` --- # Webhooks > Jazui avisa a tu servidor cuando publicas un artículo o salta una alerta de un sitio. Página original: https://docs.jazui.com/api/webhooks/es Un webhook es una dirección de tu servidor a la que Jazui hace un `POST` con un JSON cuando pasa algo. Hay dos por sitio: el del **blog** (artículos publicados, actualizados o retirados) y el de **alertas** (errores de render, caída de Googlebot, bajada de la nota SEO). Los dos se firman con el mismo secreto del sitio. ## Configurarlos En el panel, abre los ajustes del sitio: - **Blog e IndexNow**: dirección del webhook del blog. Con **Volver a renderizar la página del artículo** (activado por defecto), Jazui además actualiza la página del artículo en la caché. - **Alertas**: dirección del webhook de alertas (si tu plan incluye webhooks de alertas). La dirección tiene que ser pública y con `https` o `http`. Al guardar el primer webhook se genera el **secreto** del sitio (32 caracteres), que verás en los ajustes. Con **Probar** (blog) recibes un evento `ping`, y con **Enviar alerta de prueba** (alertas), una alerta de tipo `Test`. ## Cómo llegan Un `POST` con `Content-Type: application/json` y estas cabeceras: - `X-Jazui-Event`: el tipo (`Published`, `Updated`, `Unpublished`, `alert` o `ping`). - `X-Jazui-Delivery`: identificador del envío, el mismo en los reintentos (úsalo para no procesar dos veces el mismo). - `X-Jazui-Signature`: `sha256=` seguido del HMAC-SHA256 del cuerpo con tu secreto, en hexadecimal en minúsculas. ```http POST https://misitio.com/webhooks/jazui Content-Type: application/json X-Jazui-Event: Published X-Jazui-Delivery: 3f0c9a3e-7a51-4f43-9c1e-0b8d2f6a4e11 X-Jazui-Signature: sha256=5d41402abc4b2a76b9719d911017c592ae8b6e1f0a2c4d3b5e7f9a1c3d5e7f90 ``` ## Eventos del blog `event` vale `article.published`, `article.updated` o `article.unpublished`. El mismo artículo con el mismo evento no se envía dos veces en 30 segundos. Úsalo para regenerar tu web estática, avisar a tu equipo o publicar en otros sitios. ```json { "event": "article.published", "site": "misitio.com", "occurredAt": "2026-11-03T00:47:24Z", "article": { "id": "7c1d0e2f-…", "slug": "como-funciona-el-prerender", "url": "https://misitio.com/blog/como-funciona-el-prerender", "language": "es", "title": "Cómo funciona el prerender" } } ``` ## Alertas `alert.type` vale `RenderErrors` (errores de render en 24 h), `GoogleDrop` (caída de las visitas de Googlebot frente a la semana anterior), `SeoDrop` (bajada de la nota SEO en 7 días) o `Test`. `value` es lo medido y `threshold` el umbral que configuraste. ```json { "event": "site.alert", "site": "misitio.com", "occurredAt": "2026-11-03T09:00:00Z", "alert": { "type": "SeoDrop", "message": "La nota SEO media de misitio.com ha bajado 12 puntos en 7 días.", "value": 12, "threshold": 10, "detail": "90 → 78" } } ``` ## Prueba El botón de prueba del webhook del blog envía: ```json { "event": "ping", "site": "misitio.com", "occurredAt": "2026-11-03T09:00:00Z" } ``` ## Comprobar la firma Calcula el HMAC-SHA256 del **cuerpo tal como llega** (sin volver a serializarlo) con tu secreto y compáralo con la cabecera en tiempo constante. Si no coincide, responde `401` y no lo proceses. `Node.js (Express)`: ```javascript import crypto from 'node:crypto'; import express from 'express'; const app = express(); app.post('/webhooks/jazui', express.raw({ type: 'application/json' }), (req, res) => { const expected = 'sha256=' + crypto.createHmac('sha256', process.env.JAZUI_WEBHOOK_SECRET) .update(req.body).digest('hex'); const received = req.get('X-Jazui-Signature') || ''; if (expected.length !== received.length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) { return res.sendStatus(401); } const event = JSON.parse(req.body.toString('utf8')); // event.event: article.published, article.updated, article.unpublished, site.alert o ping res.sendStatus(204); }); ``` `C# (ASP.NET Core)`: ```csharp app.MapPost("/webhooks/jazui", async (HttpRequest request, IConfiguration config) => { using var reader = new StreamReader(request.Body, Encoding.UTF8); var body = await reader.ReadToEndAsync(); var key = Encoding.UTF8.GetBytes(config["Jazui:WebhookSecret"]!); var expected = "sha256=" + Convert.ToHexString(HMACSHA256.HashData(key, Encoding.UTF8.GetBytes(body))).ToLowerInvariant(); var received = request.Headers["X-Jazui-Signature"].ToString(); if (!CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(received))) return Results.Unauthorized(); using var json = JsonDocument.Parse(body); var type = json.RootElement.GetProperty("event").GetString(); return Results.NoContent(); }); ``` ## Respuestas y reintentos - Cualquier respuesta por debajo de `500` se da por entregada: responde `2xx` en cuanto lo recibas y haz el trabajo después. - Con un `5xx` o sin respuesta, Jazui lo reintenta a los 2 y a los 10 segundos (tres intentos en total). - Cada intento espera como mucho 15 segundos y sigue hasta 3 redirecciones. --- # API del blog > Lee los artículos, categorías y etiquetas de tu blog en Jazui para mostrarlos en tu web. Página original: https://docs.jazui.com/api/blog/es Escribes en el blog desde el panel de Jazui y lo muestras en tu web con esta API, con tu propio diseño. Cada artículo trae su HTML, su versión en Markdown, el `` completo para SEO (título, descripción, canonical, hreflang, Open Graph y datos estructurados), el índice, las traducciones y los enlaces para compartir. Usa el **PUBLIC Token**: es de solo lectura y se puede poner en el navegador. La API admite peticiones desde cualquier dominio (CORS), solo de lectura. ## Direcciones | Ruta | Parámetros | Devuelve | |---|---|---| | `GET /v2/blog/articles` | `site`, `lang`, `category`, `tag`, `q`, `page` (1), `pageSize` (10, máx. 50) | Página de resúmenes, del más reciente al más antiguo. | | `GET /v2/blog/articles/{slug}` | `site`, `lang`, `format` | Un artículo completo. | | `GET /v2/blog/articles/by-id/{id}` | `format` | Un artículo por su identificador. | | `GET /v2/blog/articles/{slug}/related` | `site`, `lang`, `count` (4, máx. 12) | Artículos relacionados. | | `GET /v2/blog/categories` | `site`, `lang` | Categorías, también las que aún no tienen artículos. | | `GET /v2/blog/tags` | `site` | Etiquetas con artículos. | | `GET /v2/blog/feed/{site}/rss` | `lang` | RSS 2.0, sin token. | | `GET /v2/blog/feed/{site}/atom` | `lang` | Atom 1.0, sin token. | `site` es el dominio del sitio (`misitio.com`; se aceptan también `https://`, mayúsculas y rutas, que se quitan). Si gestionas varios sitios, ponlo siempre: así cada web solo ve sus artículos. Solo se devuelven artículos **publicados** o programados cuya fecha ya ha pasado. ## Listado `category` incluye también las subcategorías; `q` busca en el título, el resumen, las etiquetas y el contenido. ```bash curl -H "Authorization: Bearer $JAZUI_PUBLIC_TOKEN" \ "https://api.jazui.com/v2/blog/articles?site=misitio.com&lang=es&category=guias&page=1&pageSize=10" ``` ```json { "items": [ { "id": "7c1d0e2f-…", "slug": "como-funciona-el-prerender", "url": "https://misitio.com/blog/como-funciona-el-prerender", "site": "misitio.com", "language": "es", "title": "Cómo funciona el prerender", "summary": "Qué ven los bots de una SPA y cómo servirles el HTML completo.", "author": "Ana Pérez", "coverUrl": "https://api.jazui.com/Upload/Blog/…/portada.webp", "publishedDate": "2026-11-02T10:00:00Z", "updatedDate": "2026-11-03T08:15:00Z", "readingMinutes": 6, "categories": [{ "slug": "guias", "name": "Guías" }], "tags": [{ "slug": "seo", "name": "SEO" }] } ], "page": 1, "pageSize": 10, "totalItems": 24, "totalPages": 3 } ``` ## Un artículo `format` decide qué contenido viene: `html` (por defecto), `md` (solo Markdown) o `both`. `markdownUrl` viene siempre: es la versión Markdown pública del artículo, pensada para la IA. ```bash curl -H "Authorization: Bearer $JAZUI_PUBLIC_TOKEN" \ "https://api.jazui.com/v2/blog/articles/como-funciona-el-prerender?site=misitio.com&lang=es&format=both" ``` ```json { "id": "7c1d0e2f-…", "slug": "como-funciona-el-prerender", "url": "https://misitio.com/blog/como-funciona-el-prerender", "title": "Cómo funciona el prerender", "html": "

Qué ven los bots

…

", "markdown": "## Qué ven los bots\n\n…", "markdownUrl": "https://api.jazui.com/v2/sites/misitio.com/articles/es/como-funciona-el-prerender.md", "head": "Cómo funciona el prerender", "wordCount": 1240, "tableOfContents": [{ "level": 2, "id": "que-ven-los-bots", "text": "Qué ven los bots" }], "translations": [{ "language": "en", "slug": "how-prerendering-works", "url": "https://misitio.com/blog/how-prerendering-works" }], "metaTitle": "Cómo funciona el prerender | Mi sitio", "metaDescription": "Qué ven los bots de una SPA y cómo servirles el HTML completo.", "canonical": "https://misitio.com/blog/como-funciona-el-prerender", "socialImage": "https://api.jazui.com/Upload/Blog/…/portada.webp", "noIndex": false, "share": [{ "network": "linkedin", "name": "LinkedIn", "url": "https://www.linkedin.com/sharing/share-offsite/?url=…" }], "shareHtml": "
…
" } ``` - `head` trae las etiquetas para el `` de tu página del artículo: inclúyelas tal cual para que buscadores, redes e IA lo entiendan. - `tableOfContents` son los títulos `h2` y `h3` con su `id`, para un índice con enlaces. - `translations` lleva el mismo artículo en otros idiomas. - Si tu plan lleva la insignia en los artículos, el HTML termina con «Publicado con Jazui». ## Categorías y etiquetas Las categorías vienen ordenadas por su orden y su nombre, con `parentSlug` para las subcategorías y `articleCount`. Las etiquetas solo aparecen si tienen artículos. `GET /v2/blog/categories`: ```json [ { "slug": "guias", "name": "Guías", "description": "Paso a paso para sacar partido a tu web.", "icon": null, "language": "es", "site": "misitio.com", "order": 1, "parentSlug": null, "articleCount": 12 } ] ``` ## RSS y Atom Los 20 artículos más recientes de un sitio verificado, con el contenido completo. No necesitan token. ```bash # Sin token: para lectores de RSS y agregadores curl https://api.jazui.com/v2/blog/feed/misitio.com/rss?lang=es curl https://api.jazui.com/v2/blog/feed/misitio.com/atom?lang=es ``` ## Desde el navegador Un ejemplo mínimo con `fetch`. Si tu web es una SPA, recuerda que los bots ven el blog gracias a tu integración de Jazui: `blog.js`: ```javascript const API = 'https://api.jazui.com/v2/blog'; const TOKEN = 'TU_PUBLIC_TOKEN'; // solo lectura: se puede usar en el navegador async function articulos(pagina = 1) { const url = `${API}/articles?site=misitio.com&lang=es&page=${pagina}&pageSize=9`; const response = await fetch(url, { headers: { Authorization: `Bearer ${TOKEN}` } }); if (!response.ok) throw new Error(`Blog: ${response.status}`); return response.json(); // { items, page, pageSize, totalItems, totalPages } } async function articulo(slug) { const url = `${API}/articles/${encodeURIComponent(slug)}?site=misitio.com&lang=es`; const response = await fetch(url, { headers: { Authorization: `Bearer ${TOKEN}` } }); if (response.status === 404) return null; const data = await response.json(); document.head.insertAdjacentHTML('beforeend', data.head); // título, metas y JSON-LD return data; } ``` ## Caché Cada respuesta lleva un `ETag`. Envíalo en `If-None-Match` y recibirás `304` sin cuerpo si nada ha cambiado. Los feeds se pueden guardar 15 minutos. ## Errores - `404 not_found`: el artículo no existe o no está publicado. - `400 ambiguous`: hay artículos con ese `slug` en varios sitios; añade `?site=`. --- # Sitemap, robots y llms.txt > Archivos públicos que Jazui genera para cada sitio verificado: sitemap, robots.txt, llms.txt y Markdown para IA. Página original: https://docs.jazui.com/api/site-files/es Jazui genera al momento, a partir de tu caché, tu blog y tus ajustes, los archivos que buscadores e IA buscan en tu web. Son públicos (sin token), solo existen para sitios verificados y se pueden guardar una hora. ## Direcciones | Ruta | Contenido | |---|---| | `/v2/sites/{sitio}/sitemap.xml` | Sitemap con fecha y alternativas por idioma. Con más de 50.000 direcciones es un índice que apunta a `sitemap-1.xml`, `sitemap-2.xml`… | | `/v2/sites/{sitio}/robots.txt` | Tu robots.txt, con los bots de IA que bloqueaste y la línea del sitemap. | | `/v2/sites/{sitio}/llms.txt` | Índice de tu sitio para modelos de lenguaje (formato llmstxt.org). | | `/v2/sites/{sitio}/llms-full.txt` | Lo mismo con el contenido completo de cada artículo. | | `/v2/sites/{sitio}/articles/{lang}/{slug}.md` | Un artículo en Markdown, limpio para la IA. | | `/v2/sites/{sitio}/visits.js` | Script que cuenta las visitas que llegan desde respuestas de IA. | ## Sitemap Incluye, sin repetir: los artículos publicados (con sus traducciones como `hreflang`), la portada del blog de cada idioma, las categorías con artículos (si configuraste su plantilla de dirección) y tus páginas en caché que responden `200`, sin redirecciones. Si prefieres subir tus propios sitemaps, puedes hacerlo en **Mapas del sitio**. `sitemap.xml`: ```xml https://misitio.com/blog/como-funciona-el-prerender 2026-11-03T08:15:00+00:00 https://misitio.com/precios 2026-11-01T12:00:00+00:00 ``` ## robots.txt Parte del robots.txt que escribas en los ajustes del sitio (o de `User-agent: *` y `Allow: /`), añade un grupo con los bots de IA que bloqueaste en robots.txt y la línea `Sitemap:` si no la tienes. `robots.txt`: ```bash User-agent: * Allow: / # AI bots blocked in Jazui User-agent: CCBot User-agent: Bytespider Disallow: / Sitemap: https://api.jazui.com/v2/sites/misitio.com/sitemap.xml ``` ## llms.txt Empieza con tu dominio y una descripción (la escribes en los ajustes del sitio), sigue con una sección por idioma con tus artículos (enlazados a su versión Markdown) y termina con hasta 1.000 de tus páginas en caché. `llms.txt`: ```bash # misitio.com > Zapatillas de trail y montaña con envío en 24 horas. ## Blog (es) - [Cómo funciona el prerender](https://api.jazui.com/v2/sites/misitio.com/articles/es/como-funciona-el-prerender.md): Qué ven los bots de una SPA y cómo servirles el HTML completo. (https://misitio.com/blog/como-funciona-el-prerender) ## Optional - [Precios](https://misitio.com/precios) ``` ## Servirlos en tu dominio Buscadores e IA buscan estos archivos en tu dominio (`misitio.com/robots.txt`). Para que los encuentren, sírvelos desde ahí haciendo de proxy hacia Jazui, por ejemplo: `Cloudflare Worker`: ```javascript // Antes de la lógica de Jazui en tu Worker: sirve los archivos del sitio desde Jazui const FILES = ['/sitemap.xml', '/robots.txt', '/llms.txt', '/llms-full.txt']; const url = new URL(request.url); if (FILES.includes(url.pathname)) { return fetch(`https://api.jazui.com/v2/sites/misitio.com${url.pathname}`); } ``` `nginx`: ```nginx location ~ ^/(sitemap(-\d+)?\.xml|robots\.txt|llms(-full)?\.txt)$ { proxy_set_header Host api.jazui.com; proxy_ssl_server_name on; proxy_pass https://api.jazui.com/v2/sites/misitio.com/$1; } ``` ## Visitas desde respuestas de IA Para saber cuántas personas llegan a tu web desde ChatGPT, Perplexity, Gemini, Copilot, Claude y otros asistentes, pega esta línea en el `` de tu web (el panel te la da ya con tu dominio en **Tráfico de bots › Visitas desde IA**). Solo actúa cuando la visita viene de un asistente, sin cookies ni datos personales: ```html ``` ## IndexNow Si activas IndexNow en los ajustes del sitio (**Blog e IndexNow**), Jazui avisa a Bing, Yandex y los demás buscadores que lo usan cada vez que publicas, actualizas o retiras un artículo. Para que acepten el aviso, publica en tu dominio el archivo `https://misitio.com/{clave}.txt` con la clave como único contenido (la clave está en los ajustes) y pulsa **Comprobar**. --- # Preguntas frecuentes > Respuestas rápidas a lo que más se pregunta sobre Jazui. Página original: https://docs.jazui.com/faq/es ### ¿Afecta Jazui a mis visitantes? No. Tu integración solo desvía las peticiones de bots; las personas siguen llegando a tu web como siempre. Si Jazui tarda o falla, los bots también reciben tu web normal (salvo con IIS y Apache, ver [Integraciones](https://docs.jazui.com/llms/es/integrations.txt)). ### ¿Es cloaking? ¿Me puede penalizar Google? No es cloaking: los bots reciben la misma página que ve una persona, ya renderizada. Google lo llama *renderizado dinámico*. Lo que sí sería cloaking es enseñar a los bots un contenido distinto; por eso Jazui no añade nada solo para ellos (ni siquiera la insignia) salvo lo que tú pongas con reglas, que debe corresponder a la página. ### Google ya ejecuta JavaScript, ¿para qué lo necesito? Google lo hace en una segunda pasada que puede tardar, y depende de que tu JavaScript cargue bien. Además, los bots de IA y las redes sociales no ejecutan JavaScript. Pruébalo con el escáner de [jazui.com](https://jazui.com). ### ¿Funciona con mi framework? Con cualquier web que funcione en un navegador: Angular, React, Vue, Svelte, Blazor, Next.js, Nuxt… Jazui la abre con Chrome como una persona. Lo que importa es dónde está alojada, para elegir la integración. ### ¿Cuánto tarda en responder a un bot? Desde la caché, unos cientos de milisegundos. La primera vez que se pide una página hay que renderizarla (lo que tarde tu web, hasta el máximo que configures). ### ¿Qué pasa cuando cambio mi web? Las páginas se recachean solas cada cierto número de días. Para que se actualicen al momento, recachéalas desde el panel o con la [API de caché](https://docs.jazui.com/llms/es/api-cache.txt) al desplegar. ### ¿Cuenta como petición cualquier visita? Solo las de bots que tu integración envía a Jazui. Las de personas y los archivos (imágenes, CSS, JavaScript…) no pasan por Jazui ni cuentan. ### ¿Puedo impedir que la IA entrene con mi contenido? Sí: en [Bots de IA](https://docs.jazui.com/llms/es/panel-ai-bots.txt) bloqueas los bots de entrenamiento (en Jazui y en tu robots.txt) y dejas pasar a los que te citan en sus respuestas. ### ¿Cuánto cuesta la IA del blog? Jazui no cobra la IA: usas tu propia clave y tu proveedor te cobra el uso. El plan solo limita cuántos artículos con IA se escriben al mes. ### ¿Puedo usar Jazui con varios sitios o clientes? Sí. Cada sitio tiene sus ajustes, su blog, sus reglas y, si quieres, su propio token; y con [Equipo](https://docs.jazui.com/llms/es/panel-team.txt) das acceso a cada persona solo a los sitios que quieras. ### ¿Qué datos guarda Jazui? Las páginas renderizadas, los datos técnicos de las peticiones de bots (90 días) y lo que configuras. Los detalles están en la [política de privacidad](https://jazui.com/privacy-policy). ### ¿Cómo cancelo? En **Mi plan › Gestionar facturación**. Mantienes el plan hasta el final del periodo pagado y después pasas al gratuito.