# Jazui documentation (full) --- # Getting started > From zero to your first page served to a bot in a few minutes. Source page: https://docs.jazui.com/getting-started/en Jazui sits between bots and your website: your server or CDN recognizes bots (Googlebot, GPTBot, WhatsApp…) and asks Jazui for the page, which opens it with a real browser, runs your JavaScript and returns the full HTML. People keep reaching your website as usual. To get started you need five things: an account, your site added, proof that it's yours, the token and the integration on your server. This page walks you through each step and how to check that everything works. ## 1. Create your account Go to [app.jazui.com/register](https://app.jazui.com/register) and sign up with your email and a password, or with your Google account. If you use your email, you'll get a code to confirm it. > **Free trial:** when you sign up you get **a trial of a paid plan without a card** (how many days and which plan, in [Plans and limits](https://docs.jazui.com/llms/en/plans.txt)). When it ends, the account moves to the free plan on its own unless you subscribe; you're not charged anything. ## 2. Add your site In the dashboard, go to **Websites** and click **Add**. Type the domain without `https://` or paths, for example `mysite.com`. The site is the center of everything in Jazui: its settings, rules, blog, metrics and AI bots belong to it. How many sites you can have depends on your plan: see [Plans and limits](https://docs.jazui.com/llms/en/plans.txt). ## 3. Prove the site is yours Jazui only renders verified domains, so nobody can use the service with other people's websites. In the site list click **Verify**: you'll see a code unique to your site and two ways to use it. One is enough. ### DNS TXT record (recommended) At your domain's DNS provider (Cloudflare, GoDaddy, Namecheap, your hosting…), create a **TXT** record on the root domain with the value `jazui-verification=` followed by your code: `DNS record`: ```bash Tipo / Type: TXT Nombre / Name: @ (misitio.com) Valor / Value: jazui-verification=8f3a9c1e5b7d40f2a6c8e1b3d5f7a9c0 ``` `Meta tag on the home page`: ```html ``` ### Meta tag If you can't change the DNS, add the tag above inside the `` of your site's home page. Jazui looks for it at `https://mysite.com/` and `https://www.mysite.com/`. It must be in the HTML the server returns, not added later with JavaScript. When it's in place, click **Check**. DNS changes can take a few minutes to propagate; if it fails, wait a little and try again. > **What about subdomains?** Jazui only renders hosts that are added and verified. Each subdomain (`blog`, `shop`…) is added as one more site and counts towards your plan's site limit (`www` doesn't: it's the same site). If the domain is verified by DNS in your account, the subdomain inherits the verification when you add it, with no further steps, and shows as "Verified by mysite.com". A subdomain's TXT record also works when published on its domain. With the meta tag only the exact host is verified, and its subdomains don't inherit it. ## 4. Copy your token The token is the key your integration uses to talk to Jazui. There are two kinds and you can use either: | Token | Where it is | What for | |---|---|---| | **Account token** (JAZUI Token) | Home › Integration and settings | Works for all your verified sites. | | **Site token** | Websites › Site token | Only works for that site and its added subdomains. If it leaks, you regenerate or revoke it without affecting the others. Recommended if you have several sites. | On Home you'll also see the **PUBLIC Token**: it's read-only and is used to read your blog from the browser (the public blog API). Don't use it in the integration. > Treat the token like a password: store it as a secret or environment variable and don't push it to a public repository. If you renew it, the old one stops working in less than a minute. ## 5. Connect the integration The integration is a small snippet that sits in front of your website and decides which requests come from bots. Pick the one that matches where your website runs: | Your website runs on… | Use | |---|---| | Any hosting behind Cloudflare | Cloudflare Worker (the simplest: you don't touch the server) | | Vercel or Next.js | Vercel middleware | | Netlify | Netlify Edge Function | | A Node server with Express | Express middleware | | ASP.NET Core (Blazor, Angular or React served by .NET) | Jazui.AspNetCore package | | A server with nginx | nginx configuration | | Windows with IIS | web.config with URL Rewrite and ARR | | Apache or shared hosting with .htaccess | Apache .htaccess | Each one has its own page with the full code and steps in the **Integrations** section of the menu. They all follow the same contract, explained in [How it works](https://docs.jazui.com/llms/en/how-it-works.txt). ## 6. Check that it works Every integration treats the user agent `integration-test` as a bot, so you can test with it. Request your home page with it: ```bash curl -I -A "integration-test" https://misitio.com/ ``` `Expected response`: ```http HTTP/2 200 content-type: text/html; charset=utf-8 x-jazui: true x-jazui-cache: MISS etag: "4812-638976215370000000" ``` If you see `x-jazui: true`, the page was served by Jazui. The first time `x-jazui-cache` is `MISS` (it was just rendered); repeat the request and you'll see `HIT`, served instantly from the cache. If the `x-jazui` header isn't there, the request went to your normal website. Check: - That the site is **verified**. - That the **token** is the right one and copied correctly (no spaces). - That the integration is **active** on the path you're testing (in Cloudflare, the Worker route; in Vercel and Netlify, the `matcher` or `path`). - That the path doesn't end in a file extension (`.js`, `.png`…): those never go through Jazui. In the dashboard, under **Cached pages**, you'll see the page that was just saved, and under **Bot traffic** the `integration-test` request. ## What's next? With the integration connected, Jazui already serves your pages to bots. From here you can: - See the **SEO score** of each page and fix what fails without touching your code, with **rules**. - Decide which **AI bots** can read you and publish your `robots.txt` and `llms.txt`. - Follow your **bot traffic** and the visits that come from AI answers. - Connect **Google Search Console**. - Write on the **blog**, with help from AI using your own key, and share each article on social networks. --- # How it works > What happens from the moment a bot asks for a page until it gets the full HTML. Source page: https://docs.jazui.com/how-it-works/en ## The problem it solves Websites built with Angular, React, Vue, Blazor or other JavaScript frameworks (SPAs) are built in the browser: the server returns almost empty HTML and JavaScript paints the content. People don't notice, but many bots do: - **AI bots** (GPTBot, ClaudeBot, PerplexityBot…) don't run JavaScript. They see an empty page and can't cite or recommend you. - **Social networks** (WhatsApp, LinkedIn, X, Facebook, Telegram…) build link previews from the HTML. Without a title, description and image, the link is shared without a card. - **Google** does run JavaScript, but in a second pass that can take days, costs more resources per page and depends on your JavaScript loading well. Jazui makes all of them get the same page a person sees, already rendered, without changing your application. ## The journey of a request 1. **A bot asks for a page** on your website, for example GPTBot asks for `https://mysite.com/pricing`. 2. **Your integration recognizes it** (on your CDN or server) by its user agent and, instead of serving your SPA, asks Jazui's API for it with your token. 3. **Jazui checks** the token, the plan, your rules and whether the site is verified. 4. **If the page is cached**, it returns it instantly. If not, it opens it with Chrome, waits for your JavaScript to finish, saves it and returns it. 5. **Your integration delivers** the full HTML to the bot, with the real status code (200, 404, redirect…). People don't go through Jazui: your integration only diverts bot requests. If Jazui is slow or fails, your integration serves your normal website. ## How the integration recognizes bots A request goes through Jazui only if all of these are true: - It's a `GET` or `HEAD`. - Its user agent contains one of the names in Jazui's bot list (case-insensitive), **or** the address has `_escaped_fragment_`. - The path does **not** end in a file extension: `.js`, `.css`, images, fonts, videos, documents, archives, `.json`, `.xml`, `.txt`… Your website always serves those files. - The request does **not** carry the `X-Jazui` header. This prevents loops: when Jazui opens your page with Chrome, it does so as a normal browser and your website serves it directly. ### The bot list It has more than 230 names: search engines (Googlebot, Bingbot, Yandex, Baidu, DuckDuckBot, Applebot…), AI bots (GPTBot, ChatGPT-User, OAI-SearchBot, ClaudeBot, PerplexityBot, CCBot and about 150 more), social networks and link previews (facebookexternalhit, Twitterbot, LinkedInBot, WhatsApp, TelegramBot, Discordbot, Slackbot…), SEO and monitoring tools (AhrefsBot, SemrushBot, Lighthouse, GTmetrix…) and others such as archive.org. The list is maintained by Jazui and **the JavaScript and ASP.NET Core integrations download it on their own every day** (`GET /v2/bots`); if they can't, they keep the one they ship with. In nginx, IIS and Apache the list lives in the configuration: the dashboard shows you the updated block to copy when it changes. ### Bots we don't know yet If a request looks like a bot (its user agent contains *bot*, *crawl*, *spider*, *fetch*, *preview*…) but isn't on the list, the JavaScript and ASP.NET Core integrations serve it normally and notify Jazui in the background, at most once a day per agent. That's how new bots are found and added to the list. ## What the integration asks Jazui The integration calls `GET https://api.jazui.com/Render` with: - `url`: the full address the bot asked for. nginx, IIS and Apache, which can't encode it in the query, send it in the `X-Jazui-Url` header. - `botAgent`: the bot's user agent (if missing, the request's `User-Agent` is used). - `Authorization: Bearer` with your token. - `X-Jazui-Client-IP`: the bot's IP, for metrics and to check it is who it claims to be. - `If-None-Match`, if the bot sent it, to answer `304` when the page hasn't changed. `Integration request`: ```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" ``` ## What Jazui checks before serving In this order: 1. **The token**: it must be the current one for your account or a site. A site token only works for its domain and its added subdomains. 2. **AI bots**: if you've blocked that AI bot in the site settings, it gets a `403`. 3. **The free plan**: if it has used up the month's renders or requests, it stops receiving service until next month and your website is served normally. 4. **Verified site**: if the domain isn't verified, Jazui doesn't render it. 5. **Your rules**: they can block the bot, exclude the URL from rendering or change how it's rendered. 6. **Plan limits**: renders for the month and cached pages, and on plans that require it, the badge. ## Rendering When a page isn't cached, Jazui opens it with **Chrome**, runs your JavaScript and waits for it to finish loading, at most the wait time you set for the site (up to 5 seconds on the free plan and 10 on paid plans). Then: - It removes what only matters in the browser and keeps the final HTML: title, metadata, structured data, text and links. - It keeps the **real status code**: if your page says it's a 404 or redirects, the bot gets that 404 or that redirect, not a fake 200. - It applies what your rules say, for example adding a title, description or structured data to the ``. - It reviews the page and calculates its SEO score. ## The cache Every rendered page is saved. Later requests from any bot are served from the cache instantly (`X-Jazui-Cache: HIT`), without opening Chrome again or using renders. To keep the cache fresh: - **Automatic recache** every so many days, per site or per rule (never below your plan's minimum recache: [Plans and limits](https://docs.jazui.com/llms/en/plans.txt)). - **Instant recache** from the dashboard, or through the cache API when you publish changes. - **Warning** if the saved version no longer matches your live website. Each page has an `ETag`: if the bot asks whether it changed (`If-None-Match`) and it hasn't, it gets a `304` with no body. ## Response headers | Header | Meaning | |---|---| | `X-Jazui: true` | The response comes from Jazui. Integrations only pass it to the bot if it has this header. | | `X-Jazui-Cache` | `HIT`: served from the cache. `MISS`: rendered on this request. | | `ETag` and `Last-Modified` | Version of the saved page, for conditional requests. | | `X-Jazui-Blocked` | On a `403`: `ai-bot` if you blocked that AI bot, `rule` if a rule blocks it. | | `X-Jazui-Limit` | On a `429`: `plan` (free plan used up), `renders` (no renders left this month) or `pages` (cached page limit reached). | ## If Jazui doesn't respond Integrations are built so your website never goes down because of Jazui. If Jazui takes more than 20 seconds, answers with an error, the token is invalid or the plan is used up, the request continues and the bot gets your normal website, as if Jazui didn't exist. Only Jazui's responses (those with `X-Jazui`) and the blocks you decided on (`403` with `X-Jazui-Blocked`) are passed to the bot. > There are two exceptions, because of how those servers work: with **IIS** and **Apache** the proxy can't fall back to your website if Jazui fails, and the bot gets the error. With **nginx**, a `403` for an unverified site reaches the bot. That's why you should verify the site before enabling the configuration. ## Real bots and bots pretending to be others Anyone can put «Googlebot» in their user agent. With the IP your integration sends, Jazui checks through reverse DNS whether the major search engines are who they claim to be: Googlebot and its variants, Bingbot, Applebot, Yandex, Baidu, Seznam, Petal and Naver. In your bot traffic you'll see which ones are verified and which ones are pretending. ## Privacy - People visiting your website **never go through Jazui**: the integration only diverts bot requests. - Jazui only renders verified domains, always with your account token. - The JavaScript and ASP.NET Core integrations send Jazui only what's needed (address, user agent, bot IP and the cache header), without cookies. - You can delete the cache and your account whenever you want. --- # Plans and limits > What each plan includes, the free trial and what happens when you reach a limit. Source page: https://docs.jazui.com/plans/en Jazui has a free-forever plan and paid plans. All include the same core features (Chrome rendering, cache, rules, SEO score, AI bots, metrics and blog); what changes are the amounts and some advanced features. The table is read live from the plans in force: it's the same as you see at [jazui.com/plan](https://jazui.com/plan) and in **My plan** in the dashboard. ## Comparison The plan table on this page is live. Read it from `GET https://api.jazui.com/api/PublicPlans` (send `Accept-Language: en`): it returns the active plans with their prices (`priceMonthly`, `priceYearly`, `currency`), every limit (`limits`, where `null` means unlimited) and the sign-up trial (`trialDays`, `trialPlanName`). The same data is at https://jazui.com/plan and in My plan in the dashboard. A **render** is opening a page with Chrome (the first time and on every recache). A **served request** is each time a bot gets a page from Jazui, from the cache or freshly rendered. AI articles are generated with your own AI key: Jazui doesn't charge you for AI. ## Free trial When you sign up you get **a free trial of a paid plan** without a card, with all its limits (the days and the plan are in the note above the table). During the trial, the site and articles carry the badge if the plan asks for it (the «Badge during the trial» row). When it ends, the account moves to the free plan on its own, unless you subscribe first; we'll let you know before it ends. ## What happens when you reach a limit - **Free plan**: when the month's renders or requests are used up, Jazui stops serving your pages until next month or until you move to a paid plan. Your website keeps working: bots get your normal website. - **Paid plans**: when the month's renders are used up or cached pages are full, Jazui **keeps serving the cache**, but doesn't render new pages or refresh them until next month or until you upgrade. - **Sites, rules, articles and team**: you can't add more until you delete some or upgrade. What you already have keeps working. - **AI articles**: when the monthly maximum is reached, autopilot and generation pause until next month or until you change plans. The dashboard warns you by email and in notifications when you're close to a limit (at 80% and 100%), and under **My plan** you see the month's usage. ## The free plan badge Plans that include it (the badge rows in the table) ask you to show a visible link to Jazui on your site. It's a small «Visible with Jazui» button you copy from the dashboard and paste, for example, in your website footer: `Badge (copy it from the dashboard)`: ```html Visible con Jazui ``` - It must look the same to people and bots: Jazui never adds it only to the bots' HTML, because that would deceive search engines (cloaking). - Jazui checks it on every render. If it's missing, you're notified and have **14 days** to add it; after that grace period, the site stops rendering new pages and recaching, although it keeps serving its cache. - A single page without the badge (for example, one with no footer) doesn't count if it was seen on another page of the site in the last week. - If your plan carries it on articles, blog articles end with «Published with Jazui». - If your plan doesn't include it, the badge is optional. ## Payments - Payments are processed by **Stripe**; Jazui never sees your card details. Prices are in US dollars and don't include taxes. - You can pay monthly or yearly (yearly is discounted: the «Yearly saving» row in the table shows how much for each plan). - Subscriptions renew automatically. Invoices, payment method and cancellation are in **My plan › Manage billing** (the Stripe portal). - If a charge fails, Stripe retries it for a few days and you keep your plan; if it's ultimately not collected, the account moves to the free plan. ## Changing plans - **Upgrading** applies right away: Stripe charges the prorated difference for the period and the new limits apply immediately. - **Downgrading** or moving to Free applies at the end of the paid period; until then you keep your current plan. - If we improve a plan, you get the improvement right away. If we raise its price or reduce its limits, it applies at your next renewal and we notify you at least 30 days ahead. The legal details are in the [Terms and Conditions](https://jazui.com/conditions) and the [Refund Policy](https://jazui.com/refund-policy). --- # For AI > The documentation in Markdown for AI assistants: llms.txt, llms-full.txt and every page with its real code. Source page: https://docs.jazui.com/for-ai/en AI assistants (Claude Code, GitHub Copilot, Cursor, ChatGPT…) don't need to read the pages like a person: the menu and styles only use up their context. That's why all this documentation is also published in **Markdown**, in the [llms.txt](https://llmstxt.org) format: clean text, the tables, the steps and the real code of each integration (the same file you download), in English and Spanish. That way, when you ask your assistant to connect your website to Jazui, use the API or read your blog, it checks the documentation instead of making up parameters. > Using Claude? Install the [Jazui Skill](https://docs.jazui.com/llms/en/skill.txt): it carries this same documentation and the procedure to connect your website. ## The files | File | What it has | |---|---| | [`/llms.txt`](https://docs.jazui.com/llms.txt) | Index in English: what Jazui is, the rules an assistant must follow and the link to every page. | | [`/llms-es.txt`](https://docs.jazui.com/llms-es.txt) | The same index in Spanish. | | [`/llms-full.txt`](https://docs.jazui.com/llms-full.txt) | All the documentation in one file, in English (to paste it whole into a chat). | | [`/llms-full-es.txt`](https://docs.jazui.com/llms-full-es.txt) | All the documentation in Spanish. | | `/llms/en/.txt` `/llms/es/.txt` | Each page on its own, for example [`/llms/en/integrations-aspnetcore.txt`](https://docs.jazui.com/llms/en/integrations-aspnetcore.txt). | The plan table isn't copied into these files because it changes: they tell the assistant to read it live from `GET https://api.jazui.com/api/PublicPlans`. ## Use it in your project Add these lines to your assistant's instructions file (Claude Code reads **CLAUDE.md**, GitHub Copilot **.github/copilot-instructions.md** and 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. ``` In a chat you can also paste the **llms-full.txt** link or the link of the page you need, or use the **Copy for AI** button and paste the text. ## From every page Under each page title you have **Copy for AI**, which copies the page in Markdown in the language you're reading it in, and **Markdown**, which opens the file. Pages also announce it in their `` with ``. ## Always up to date The files are generated from the texts and code of these pages and of the integrations, so nothing is written twice. After changing the documentation they are regenerated with: ```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 ``` --- # Jazui Skill > A skill for Claude that connects your website to Jazui for you: it detects your stack, installs the official integration and checks it. Source page: https://docs.jazui.com/skill/en A **skill** is a package of instructions and documentation that Claude (Claude Code, the desktop app and claude.ai) loads only when it needs it. Jazui's carries the procedure to connect a website and **all this documentation in Markdown**, with the real code of each integration, so your assistant doesn't make up parameters. With the skill installed, just ask it to “connect my website to Jazui”: it looks at your project, picks the right integration, installs it with the token in an environment variable, tells you what to do in the dashboard (add and verify the site, copy the token) and checks it with `curl`. [Download jazui.zip](https://docs.jazui.com/skill/jazui.zip) ## What it does - **Connect your website**: it detects where it runs (Cloudflare, Vercel, Netlify, Express, ASP.NET Core, nginx, IIS or Apache) and installs the official integration, in the right order relative to your SPA fallback. - **Check it** with the `integration-test` user agent and tell you what's wrong if `x-jazui` is missing. - **Recache or purge** pages on deployment with the Cache API, for example in your CI pipeline. - **Receive webhooks**, verifying the `X-Jazui-Signature` signature. - **Show your blog** on your website with the Blog API. > The skill never writes the token into browser code or commits it, and it reads prices and limits live instead of assuming them. What's done in the dashboard (account, verification, token) is up to you: the skill tells you where. ## Install it in Claude Code Unzip it into `~/.claude/skills/` to use it in all your projects, or into a project's `.claude/skills/` folder to share it with your team through the repository. It ends up in `skills/jazui/SKILL.md`. From the 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 ``` Open a new Claude Code session and type `/jazui` to use it directly, or just ask for what you need: it kicks in on its own when you talk about Jazui or prerendering. ## Install it in claude.ai and the desktop app Download **jazui.zip** and upload it as is (without unzipping) in the **Skills** section of Claude's settings. Code execution must be turned on, since that's where skills run. ## What to ask - “Connect this website to Jazui.” - “My Blazor WebAssembly app is on IIS: add Jazui's rule to the publish web.config.” - “Check whether Jazui is serving my home page to bots.” - “Recache the site at the end of the GitHub Actions deployment.” - “Show the latest 5 articles of my Jazui blog on the home page.” ## Updating it The skill is generated from this documentation, so each version brings the pages up to date. To update it, download it again and replace the `jazui` folder (the commands above already overwrite it). Using another assistant, such as GitHub Copilot or Cursor? Use the [For AI](https://docs.jazui.com/llms/en/for-ai.txt) documentation. --- # Overview > Which integration to choose, what they have in common and how to check they work. Source page: https://docs.jazui.com/integrations/en An integration is the piece of code or configuration that sits in front of your website, on your CDN or server, and decides which requests come from bots. It asks Jazui for those; everything else goes to your website unchanged. You don't have to touch your application. ## Which one to choose [**Cloudflare Workers**If your domain is on Cloudflare, with any hosting. No server changes.](https://docs.jazui.com/llms/en/integrations-cloudflare.txt) [**Vercel / Next.js**Middleware on Vercel's edge.](https://docs.jazui.com/llms/en/integrations-vercel.txt) [**Netlify**Netlify Edge Function.](https://docs.jazui.com/llms/en/integrations-netlify.txt) [**Node.js / Express**Middleware for your Node server.](https://docs.jazui.com/llms/en/integrations-express.txt) [**ASP.NET Core**NuGet package for Blazor, Angular or React served by .NET.](https://docs.jazui.com/llms/en/integrations-aspnetcore.txt) [**nginx**Configuration with a proxy to Jazui.](https://docs.jazui.com/llms/en/integrations-nginx.txt) [**IIS**web.config with URL Rewrite and ARR.](https://docs.jazui.com/llms/en/integrations-iis.txt) [**Apache**.htaccess with mod_rewrite and mod_proxy.](https://docs.jazui.com/llms/en/integrations-apache.txt) If you have several options, the best one is usually the one closest to the user: Cloudflare, Vercel or Netlify, otherwise your framework's (Express or ASP.NET Core) rather than the web server's. The code-based ones always fall back to your website if Jazui fails and update themselves. ## How they differ | | Cloudflare, Vercel, Netlify, Express | ASP.NET Core | nginx | IIS and Apache | |---|---|---|---|---| | Bot list | Downloaded daily on its own | Downloaded daily on its own | In the configuration | In the configuration | | Reports unknown bots | Yes | Yes | No | No | | If Jazui fails | Serves your website | Serves your website | Serves your website (except a 403) | The bot gets the error | | Maximum wait | 20 s | 20 s (configurable) | 20 s | The server proxy's | | Sends to Jazui | Only what's needed | Only what's needed | The bot's headers, without cookies | The bot's headers, without cookies | ## What they all do 1. **They decide whether the request is from a bot**: `GET` or `HEAD`, user agent in the bot list (or `_escaped_fragment_` in the address), a path that isn't a file and no `X-Jazui` header. 2. **They ask Jazui for it**: `GET https://api.jazui.com/Render` with the full address, the user agent, your token in `Authorization: Bearer`, the bot's IP in `X-Jazui-Client-IP` and the bot's `If-None-Match`. 3. **They pass Jazui's response to the bot** if it has `X-Jazui: true` (200, 404, redirects, 304…), with its `Content-Type`, `ETag`, `Last-Modified` and `X-Jazui-Cache`; or the `403` with `X-Jazui-Blocked` if you blocked the bot. 4. **In any other case they serve your website**: Jazui is slow, fails, the token is invalid or the plan is used up. The details are in [How it works](https://docs.jazui.com/llms/en/how-it-works.txt). ## The bot list Jazui maintains a list of more than 230 bots (search engines, AI, social networks and SEO tools) and publishes it without a token: ```bash curl https://api.jazui.com/v2/bots curl https://api.jazui.com/v2/bots.txt ``` `/v2/bots` returns JSON with the version, date and fragments; `/v2/bots.txt`, one fragment per line. The code-based integrations download it at startup and every 24 hours; if they can't, they keep the list they ship with. For nginx, IIS and Apache you copy it by hand when it changes: each page explains how. ## What never goes through Jazui Paths ending in these extensions are always served by your website, whoever asks: `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` `POST`, `PUT`… requests don't go through either, nor do requests carrying the `X-Jazui` header (Jazui's own while rendering). ## Which token to use Any integration works with the **account token** (Home › Integration and settings) or the **site token** (Websites › Site token). If you have several sites, use each site's own token: if one leaks, you change it without touching the others. Never use the PUBLIC Token in an integration. ## Checking any integration With `curl` and the test user agent `integration-test`, which every integration treats as a 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 ``` You can also use Google Search Console's **URL Inspection** tool or the Facebook or LinkedIn preview debuggers to see the page as they get it. --- # Cloudflare Workers > A Worker in front of your website: no server changes and it works with any hosting. Source page: https://docs.jazui.com/integrations/cloudflare/en If your domain uses Cloudflare, a Worker on your site's route gets every request before your server. It asks Jazui for bot requests and sends the rest to your server with `fetch(request)`, as if the Worker weren't there. It's the simplest integration and the one that adds the least latency. ## Before you start - The domain on Cloudflare with the proxy enabled (orange cloud). - Your site verified in Jazui and your token. - The Workers free plan is enough for most sites. ## Option 1: from the Cloudflare dashboard 1. In Cloudflare, go to **Workers & Pages › Create › Create Worker**, name it `jazui` and click **Deploy**. 2. Click **Edit code**, delete what's there and paste this whole file (the shared logic plus the Worker, in one): `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. Click **Deploy**. 2. Under **Settings › Variables and Secrets**, add `JAZUI_TOKEN` as a **Secret** with your token. 3. Under **Settings › Domains & Routes › Add › Route**, choose your zone and type `mysite.com/*` (and `www.mysite.com/*` if you use `www`). ## Option 2: with Wrangler If you prefer to keep it in a repository, download `cloudflare-worker.js` and `core.js` (both are below), put them together with this `wrangler.toml`, store the token as a secret and deploy: ```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 ``` ## Environment variables | Variable | Required | What for | |---|---|---| | `JAZUI_TOKEN` | Yes | Your account or site token. Store it as a **secret**, never in the code. | | `JAZUI_API` | No | API address. Defaults to `https://api.jazui.com`. | | `JAZUI_SITE_URL` | No | Your public domain (for example `https://mysite.com`) if the platform sees another one, such as a preview address: Jazui renders the page with that domain. | Without `JAZUI_TOKEN` the integration does nothing and the whole website is served normally. ## What it does The Worker calls the shared logic with your token and the bot's IP, which Cloudflare provides in `CF-Connecting-IP`. If Jazui returns the page, it delivers it; otherwise it continues with `fetch(request)` to your server. The bot list and unknown-bot reports run in the background with `ctx.waitUntil`, without delaying the response. `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'), }); } ``` ##

Request your home page as if you were a bot, with the test user agent integration-test:

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

The response must include x-jazui: true, with x-jazui-cache: MISS the first time and HIT afterwards. Without a bot user agent (curl -I https://mysite.com/) you should get your usual website, without x-jazui. In the dashboard, the request shows up in Bot traffic and the page in Cached pages.

Request your home page as if you were a bot, with the test user agent `integration-test`: ``` curl -I -A "integration-test" https://mysite.com/ ``` The response must include `x-jazui: true`, with `x-jazui-cache: MISS` the first time and `HIT` afterwards. Without a bot user agent (`curl -I https://mysite.com/`) you should get your usual website, without `x-jazui`. In the dashboard, the request shows up in **Bot traffic** and the page in **Cached pages**. ## If something doesn't work - **No `x-jazui`**: check that the Worker route covers the address you're testing (with and without `www`) and that `JAZUI_TOKEN` is stored as a secret on that Worker. - **The DNS record isn't proxied (orange cloud)**: without Cloudflare's proxy, the Worker doesn't get the requests. - **You have another Worker on the same route**: only one runs per route; combine the logic into one or split the routes. - Under **Workers › jazui › Logs** you can watch each request live and see any errors. --- # Vercel / Next.js > Middleware on Vercel's edge, for Next.js or any static site on Vercel. Source page: https://docs.jazui.com/integrations/vercel/en Vercel runs `middleware.js` before every request. Jazui's asks Jazui for pages for bots and lets everything else through: when it returns `undefined`, the request continues as usual. ## Before you start - A project on Vercel (Next.js, Vite, Angular, React…). - Your site verified in Jazui and your token. ## Installation Download these two files and put them at the project root (in Next.js, next to `app/` or `pages/`; if you use `src/`, inside `src/`). The first one must be named `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'), }); } ``` The `matcher` leaves out `/api/`, `/_next/` and `/_vercel/`. If you already had a `middleware.js`, call Jazui's function first and, if it returns a response, return it; otherwise continue with your logic. ## Environment variables | Variable | Required | What for | |---|---|---| | `JAZUI_TOKEN` | Yes | Your account or site token. Store it as a **secret**, never in the code. | | `JAZUI_API` | No | API address. Defaults to `https://api.jazui.com`. | | `JAZUI_SITE_URL` | No | Your public domain (for example `https://mysite.com`) if the platform sees another one, such as a preview address: Jazui renders the page with that domain. | Without `JAZUI_TOKEN` the integration does nothing and the whole website is served normally. ```bash vercel env add JAZUI_TOKEN production ``` ## What it does It takes the bot's IP from `x-forwarded-for` (or `x-real-ip`), asks Jazui for the page and returns it if it arrives. The bot list and reports run in the background with `waitUntil`. After adding the variable, redeploy so it's picked up. ##

Request your home page as if you were a bot, with the test user agent integration-test:

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

The response must include x-jazui: true, with x-jazui-cache: MISS the first time and HIT afterwards. Without a bot user agent (curl -I https://mysite.com/) you should get your usual website, without x-jazui. In the dashboard, the request shows up in Bot traffic and the page in Cached pages.

Request your home page as if you were a bot, with the test user agent `integration-test`: ``` curl -I -A "integration-test" https://mysite.com/ ``` The response must include `x-jazui: true`, with `x-jazui-cache: MISS` the first time and `HIT` afterwards. Without a bot user agent (`curl -I https://mysite.com/`) you should get your usual website, without `x-jazui`. In the dashboard, the request shows up in **Bot traffic** and the page in **Cached pages**. ## If something doesn't work - **No `x-jazui`**: the file must be named `middleware.js` and sit at the root (or in `src/`); check in the deployment log that Vercel detects it as Middleware. - **The variable doesn't arrive**: add it to the **Production** environment (and Preview if you test there) and redeploy. - **Testing on a preview address**: set `JAZUI_SITE_URL` to your verified domain, because `*.vercel.app` isn't verified. --- # Netlify > An Edge Function that handles bots before your Netlify site is served. Source page: https://docs.jazui.com/integrations/netlify/en Netlify Edge Functions run before each page is served. Jazui's asks Jazui for pages for bots and, for everything else, calls `context.next()` so Netlify serves your site as usual. ## Before you start - A site on Netlify. - Your site verified in Jazui and your token. ## Installation Create the folder `netlify/edge-functions/jazui/` and put these two files in it. The first one must be named after the folder, `jazui.js`, so Netlify treats it as the function and `core.js` as its module: ```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'), }); } ``` ## Environment variables | Variable | Required | What for | |---|---|---| | `JAZUI_TOKEN` | Yes | Your account or site token. Store it as a **secret**, never in the code. | | `JAZUI_API` | No | API address. Defaults to `https://api.jazui.com`. | | `JAZUI_SITE_URL` | No | Your public domain (for example `https://mysite.com`) if the platform sees another one, such as a preview address: Jazui renders the page with that domain. | Without `JAZUI_TOKEN` the integration does nothing and the whole website is served normally. ```bash netlify env:set JAZUI_TOKEN "tu-token" netlify deploy --prod ``` ## What it does The function applies to every path except `/.netlify/*` and `/api/*` (set in its `config`; you can add more exclusions in `excludedPath`). Netlify provides the bot's IP in `context.ip`, and the bot list and reports run in the background with `waitUntil`. ##

Request your home page as if you were a bot, with the test user agent integration-test:

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

The response must include x-jazui: true, with x-jazui-cache: MISS the first time and HIT afterwards. Without a bot user agent (curl -I https://mysite.com/) you should get your usual website, without x-jazui. In the dashboard, the request shows up in Bot traffic and the page in Cached pages.

Request your home page as if you were a bot, with the test user agent `integration-test`: ``` curl -I -A "integration-test" https://mysite.com/ ``` The response must include `x-jazui: true`, with `x-jazui-cache: MISS` the first time and `HIT` afterwards. Without a bot user agent (`curl -I https://mysite.com/`) you should get your usual website, without `x-jazui`. In the dashboard, the request shows up in **Bot traffic** and the page in **Cached pages**. ## If something doesn't work - **No `x-jazui`**: the deployment log should list the `jazui` Edge Function. If not, check the folder and the file name. - **The variable doesn't arrive**: set it under **Site configuration › Environment variables** with the functions scope and redeploy. - **Preview addresses** (`*.netlify.app`): set `JAZUI_SITE_URL` to your verified domain. --- # Node.js / Express > A middleware for your Node.js server with Express. Source page: https://docs.jazui.com/integrations/express/en If your SPA is served by a Node server with Express, Jazui's middleware goes before the static files: it asks Jazui for pages for bots and calls `next()` for everything else. ## Before you start - Node.js 18.17 or later (it uses native `fetch`) and Express 4 or 5. - The project as ES modules (`"type": "module"` in `package.json`). - Your site verified in Jazui and your token. ## Installation Download these two files and put them together in a folder of your project, for example `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'), }); } ``` ## Using it Add it before serving the files and the SPA. If there's a proxy or CDN in front, enable `trust proxy` so `req.ip` is the bot's IP rather than the proxy's: `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); ``` ## Options | Option | Default | What for | |---|---|---| | `token` | — | Your token. Without it, the middleware does nothing. | | `api` | `https://api.jazui.com` | API address. | | `siteUrl` | — | Your public domain if the server sees another one (behind a proxy, in tests). | | `timeoutMs` | 20000 | Milliseconds to wait; after that the normal page is served. | | `remoteBots` | `true` | Downloads the bot list at startup and every 24 h. | | `reportUnknownBots` | `true` | Tells Jazui about bot-like agents that aren't on the list. | ```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 })); ``` ## What it does It builds the address from `req.protocol`, the host and `req.originalUrl`, decides whether it's a bot and, if so, asks Jazui for the page and copies the status, headers and body to the bot. On any error it calls `next()`, so your website is never left unserved. ##

Request your home page as if you were a bot, with the test user agent integration-test:

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

The response must include x-jazui: true, with x-jazui-cache: MISS the first time and HIT afterwards. Without a bot user agent (curl -I https://mysite.com/) you should get your usual website, without x-jazui. In the dashboard, the request shows up in Bot traffic and the page in Cached pages.

Request your home page as if you were a bot, with the test user agent `integration-test`: ``` curl -I -A "integration-test" https://mysite.com/ ``` The response must include `x-jazui: true`, with `x-jazui-cache: MISS` the first time and `HIT` afterwards. Without a bot user agent (`curl -I https://mysite.com/`) you should get your usual website, without `x-jazui`. In the dashboard, the request shows up in **Bot traffic** and the page in **Cached pages**. ## If something doesn't work - **No `x-jazui`**: the middleware must come before `express.static` and the route that serves `index.html`. - **Metrics show the proxy's IP**: enable `app.set('trust proxy', true)`. - **Behind a proxy, `req.protocol` is `http`**: with `trust proxy` Express uses `X-Forwarded-Proto`; otherwise set `siteUrl`. --- # ASP.NET Core > NuGet package with a middleware for ASP.NET Core applications (.NET 8 or later). Source page: https://docs.jazui.com/integrations/aspnetcore/en If your SPA (Blazor WebAssembly, Angular, React, Vue…) is served by an ASP.NET Core application, `Jazui.AspNetCore` adds a middleware that handles bots before the static files and the `index.html` fallback. It's also the recommended option on IIS when the application is .NET. ## Before you start - .NET 8 or later. - Your site verified in Jazui and your token. - The AstrolUI package source (`https://nuget.astrolui.com/v3/index.json`) added to NuGet. ## Installation `Jazui.AspNetCore` isn't on nuget.org: it's published in the **AstrolUI NuGet package repository**, `https://nuget.astrolui.com/v3/index.json`. Add that source once (in Visual Studio, from the command line or in a `nuget.config`) and then install the package like any other. The package carries the Jazui logo and its version follows the .NET version it's published with (for example `10.0.13.1`); it works in .NET 8 or later applications. ### In Visual Studio 1. Open **Tools › Options › NuGet Package Manager › Package Sources**. 2. Click **+**, type **Name**: `AstrolUI` and **Source**: `https://nuget.astrolui.com/v3/index.json`, click **Update** and then **OK**. 3. Right-click your project and choose **Manage NuGet Packages…**. 4. Top right, under **Package source**, choose `AstrolUI` (or **All**). 5. In **Browse**, search for `Jazui.AspNetCore`, pick the latest version and click **Install**. > If your solution has a `nuget.config` with ``, Visual Studio only uses that file's sources: add AstrolUI there (example below). ### From the command line ```bash dotnet nuget add source https://nuget.astrolui.com/v3/index.json --name AstrolUI dotnet add package Jazui.AspNetCore ``` ### With nuget.config So the whole team and your CI get it without setting anything up, put a `nuget.config` next to your solution. If you already use **package source mapping** (`packageSourceMapping`), add the `Jazui.*` pattern to the AstrolUI source; otherwise `packageSources` is enough. `nuget.config`: ```xml ``` ## Setup Register the service and add the middleware **before** `UseStaticFiles` and `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": "" } } ``` Don't put the token in `appsettings.json`: in production use the `Jazui__Token` environment variable (or user secrets in development). `UseForwardedHeaders` is only needed if there's a proxy or CDN in front, so Jazui gets the bot's real IP and the `https` scheme. ## Options All are read from the `Jazui` configuration section and can also be changed in code: | Option | Default | What for | |---|---|---| | `Token` | — | Account or site token. Without a token, the middleware does nothing. | | `ApiUrl` | `https://api.jazui.com` | API address. | | `SiteUrl` | — | Public domain when the application sees another one (proxy, tests). | | `Timeout` | 20 s | After this time the normal page is served. | | `ExcludedPaths` | `/api`, `/_framework`, `/_content`, `/_blazor` | Paths that never go through Jazui (per segment: `/api` covers `/api/x` but not `/apis`). | | `ExtraBots` | — | User agent fragments added to the bot list. | | `RemoteBots` | `true` | Downloads Jazui's bot list at startup and every `BotsRefreshInterval`. | | `BotsRefreshInterval` | 24 h | How often the list is downloaded again. | | `ReportUnknownBots` | `true` | Tells Jazui about bot-like agents that aren't on the list (without sending the page). | `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); }); ``` ## What it does For each bot request, it calls the API with the address (or `SiteUrl` plus the path), the user agent, the bot's IP (from `Connection.RemoteIpAddress`) and the `If-None-Match`. If Jazui answers with `X-Jazui` or a block, it copies the status, headers and body; if it's slow or fails, it logs a warning and continues with the rest of the application. The bot list version in use is in `JazuiMiddleware.BotsVersion`. ##

Request your home page as if you were a bot, with the test user agent integration-test:

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

The response must include x-jazui: true, with x-jazui-cache: MISS the first time and HIT afterwards. Without a bot user agent (curl -I https://mysite.com/) you should get your usual website, without x-jazui. In the dashboard, the request shows up in Bot traffic and the page in Cached pages.

Request your home page as if you were a bot, with the test user agent `integration-test`: ``` curl -I -A "integration-test" https://mysite.com/ ``` The response must include `x-jazui: true`, with `x-jazui-cache: MISS` the first time and `HIT` afterwards. Without a bot user agent (`curl -I https://mysite.com/`) you should get your usual website, without `x-jazui`. In the dashboard, the request shows up in **Bot traffic** and the page in **Cached pages**. ## If something doesn't work - **`Jazui.AspNetCore` isn't found**: the AstrolUI source is missing, or your `nuget.config` has `` or a source mapping without the `Jazui.*` pattern. - **No `x-jazui`**: `UseJazui()` must come before `UseStaticFiles()` and `MapFallbackToFile`; also check that the token arrives (`Jazui__Token` variable). - **Behind IIS or a proxy, the URL comes out as `http`**: configure `UseForwardedHeaders` or set `SiteUrl`. - **Log warnings** tell you why the normal page was served. --- # nginx > nginx configuration that sends bots to Jazui through an internal proxy. Source page: https://docs.jazui.com/integrations/nginx/en nginx uses two `map` blocks to decide whether the request is from a bot and whether it's a static file. Bot requests are rewritten to an internal location that proxies to `https://api.jazui.com/Render`; if Jazui fails, your `index.html` is served. ## Before you start - nginx with SSL support for the proxy (the usual build). - Your site **verified before enabling the configuration**: with nginx, an unverified site's `403` reaches the bot. - Your token. ## Installation This is the full file. The two `map` blocks go in the `http` block (for example in `/etc/nginx/conf.d/jazui.conf`) and the `server` replaces or merges into your site's: `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; } } ``` Replace `[JAZUI_TOKEN]` with your token, and `server_name` and `root` with your site's. If you already have a `location /` block, add the `set` and `if` lines at the top and copy the `/__jazui` and `@spa` locations. Then check and reload: ```bash sudo nginx -t && sudo nginx -s reload ``` ## What it does - `$jazui_bot` is 1 if the user agent is on the list; `$jazui_static`, if the path is a file. - In `location /` they're combined with `_escaped_fragment_`, the `X-Jazui` header and the method: if it must be rendered, it's rewritten to `/__jazui`. - `/__jazui` is internal: it sends `Authorization`, `X-Jazui-Url` (the full address) and `X-Jazui-Client-IP`, with 5 s to connect and 20 s to respond. - With `proxy_intercept_errors`, Jazui's 400, 401, 429 and 5xx codes go to `@spa`, which serves your `index.html`. ## Keeping the bot list up to date This configuration has the bot list written inside it, between the `BOTS:BEGIN` and `BOTS:END` markers, because the server can't download it on its own. When Jazui adds new bots: 1. In the dashboard, go to **Websites** and click **Site token** on any of your sites. 2. Under **Bots in nginx, IIS and Apache**, choose your server's tab and copy the block. You'll see how many bots it has and the list date. 3. Replace everything between `BOTS:BEGIN` and `BOTS:END` in your configuration with that block and reload the server. You can also download the list as text with `GET https://api.jazui.com/v2/bots.txt` or the ready-made blocks with `GET https://api.jazui.com/v2/bots/config`. ##

Request your home page as if you were a bot, with the test user agent integration-test:

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

The response must include x-jazui: true, with x-jazui-cache: MISS the first time and HIT afterwards. Without a bot user agent (curl -I https://mysite.com/) you should get your usual website, without x-jazui. In the dashboard, the request shows up in Bot traffic and the page in Cached pages.

Request your home page as if you were a bot, with the test user agent `integration-test`: ``` curl -I -A "integration-test" https://mysite.com/ ``` The response must include `x-jazui: true`, with `x-jazui-cache: MISS` the first time and `HIT` afterwards. Without a bot user agent (`curl -I https://mysite.com/`) you should get your usual website, without `x-jazui`. In the dashboard, the request shows up in **Bot traffic** and the page in **Cached pages**. ## If something doesn't work - **Behind Cloudflare or another CDN**, `$remote_addr` is the CDN's IP: use `$http_cf_connecting_ip` in `X-Jazui-Client-IP` or configure `real_ip_header`. - **`nginx -t` fails on `map`**: the `map` blocks must be inside `http`, not `server`. - **SSL error with the API**: keep `proxy_ssl_server_name on` and `Host api.jazui.com`. - **Bots get 403**: the site isn't verified or you blocked that AI bot or with a rule (check the `X-Jazui-Blocked` header). --- # IIS > A URL Rewrite rule with ARR in your IIS site's web.config. Source page: https://docs.jazui.com/integrations/iis/en On IIS, a **URL Rewrite** rule recognizes bots and, with **Application Request Routing** (ARR) as a proxy, serves them Jazui's page. If your application is ASP.NET Core, it's simpler and more robust to use the [ASP.NET Core package](https://docs.jazui.com/llms/en/integrations-aspnetcore.txt). ## Before you start - The **URL Rewrite** and **Application Request Routing** modules installed (from Microsoft's IIS website). - Your site **verified before enabling the rule**: with ARR, if Jazui answers with an error, the bot gets that error. - Your token. ## Installation Open a console as administrator and run these two commands. The first enables ARR's proxy; the second lets the rule write the headers Jazui needs: `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 ``` Then put this `web.config` in your site's folder (or copy its two rules into your existing one, Jazui's before the SPA's): `web.config` (https://docs.jazui.com/example_config/iis_web.xml): ```xml ``` Replace `[JAZUI_TOKEN]` with your token. The address sent to Jazui starts with `https://`: if your site runs on `http`, change it in `HTTP_X_JAZUI_URL`. ## What it does - The **Jazui** rule checks the user agent and `_escaped_fragment_` in a single pattern, the method, the `X-Jazui` header and that the path isn't a file. - It writes `Authorization`, `X-Jazui-Url` and `X-Jazui-Client-IP` as server variables and rewrites the request to `https://api.jazui.com/Render`, which ARR resolves as a proxy. - The **SPA** rule sends everything that isn't a file or folder to `index.html`. ## Keeping the bot list up to date This configuration has the bot list written inside it, between the `BOTS:BEGIN` and `BOTS:END` markers, because the server can't download it on its own. When Jazui adds new bots: 1. In the dashboard, go to **Websites** and click **Site token** on any of your sites. 2. Under **Bots in nginx, IIS and Apache**, choose your server's tab and copy the block. You'll see how many bots it has and the list date. 3. Replace everything between `BOTS:BEGIN` and `BOTS:END` in your configuration with that block and reload the server. You can also download the list as text with `GET https://api.jazui.com/v2/bots.txt` or the ready-made blocks with `GET https://api.jazui.com/v2/bots/config`. ##

Request your home page as if you were a bot, with the test user agent integration-test:

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

The response must include x-jazui: true, with x-jazui-cache: MISS the first time and HIT afterwards. Without a bot user agent (curl -I https://mysite.com/) you should get your usual website, without x-jazui. In the dashboard, the request shows up in Bot traffic and the page in Cached pages.

Request your home page as if you were a bot, with the test user agent `integration-test`: ``` curl -I -A "integration-test" https://mysite.com/ ``` The response must include `x-jazui: true`, with `x-jazui-cache: MISS` the first time and `HIT` afterwards. Without a bot user agent (`curl -I https://mysite.com/`) you should get your usual website, without `x-jazui`. In the dashboard, the request shows up in **Bot traffic** and the page in **Cached pages**. ## If something doesn't work - **Error 500.50 or «server variable not allowed»**: the second command (allowed variables) is missing. - **404 or 502.3 when requesting as a bot**: ARR's proxy isn't enabled (first command) or the server can't reach the Internet on port 443. - **Behind Cloudflare**, `{REMOTE_ADDR}` is the CDN's IP: use `{HTTP_CF_CONNECTING_IP}` (or `{HTTP_X_FORWARDED_FOR}`) in `HTTP_X_JAZUI_CLIENT_IP`. - The maximum wait is ARR's proxy timeout (server settings, **Application Request Routing Cache › Server Proxy Settings › Time-out**); 20 seconds is a good value. --- # Apache > .htaccess for Apache with mod_rewrite and mod_proxy, also on many shared hostings. Source page: https://docs.jazui.com/integrations/apache/en With `mod_rewrite`, Apache recognizes bots and, with `mod_proxy`'s `[P]` flag, serves them Jazui's page. Everything else goes to your files or to `index.html`. ## Before you start - The `rewrite`, `headers`, `proxy`, `proxy_http` and `ssl` modules enabled, and `SSLProxyEngine On` in your ``, with `AllowOverride All` so the `.htaccess` is read. On shared hosting, ask your provider whether `mod_proxy` is available. - Your site **verified before enabling the configuration**: with Apache's proxy, if Jazui answers with an error, the bot gets that error. - Your token. On your own server (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 ``` ## Installation Put this `.htaccess` in your site's root folder (or its rules inside your ``): `.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 ``` Replace `[JAZUI_TOKEN]` with your token. If you already had SPA rules, keep Jazui's before them. ## What it does - The conditions check that there's no `X-Jazui` header, the method, the user agent (or `_escaped_fragment_`) and that the path isn't a file. - The rule proxies to `https://api.jazui.com/Render` and stores the full address and the bot's IP in variables. - `mod_headers` adds `Authorization`, `X-Jazui-Url` and `X-Jazui-Client-IP` only to those requests. - Everything else goes to your files or to `index.html`. ## Keeping the bot list up to date This configuration has the bot list written inside it, between the `BOTS:BEGIN` and `BOTS:END` markers, because the server can't download it on its own. When Jazui adds new bots: 1. In the dashboard, go to **Websites** and click **Site token** on any of your sites. 2. Under **Bots in nginx, IIS and Apache**, choose your server's tab and copy the block. You'll see how many bots it has and the list date. 3. Replace everything between `BOTS:BEGIN` and `BOTS:END` in your configuration with that block and reload the server. You can also download the list as text with `GET https://api.jazui.com/v2/bots.txt` or the ready-made blocks with `GET https://api.jazui.com/v2/bots/config`. ##

Request your home page as if you were a bot, with the test user agent integration-test:

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

The response must include x-jazui: true, with x-jazui-cache: MISS the first time and HIT afterwards. Without a bot user agent (curl -I https://mysite.com/) you should get your usual website, without x-jazui. In the dashboard, the request shows up in Bot traffic and the page in Cached pages.

Request your home page as if you were a bot, with the test user agent `integration-test`: ``` curl -I -A "integration-test" https://mysite.com/ ``` The response must include `x-jazui: true`, with `x-jazui-cache: MISS` the first time and `HIT` afterwards. Without a bot user agent (`curl -I https://mysite.com/`) you should get your usual website, without `x-jazui`. In the dashboard, the request shows up in **Bot traffic** and the page in **Cached pages**. ## If something doesn't work - **Error 500** when requesting as a bot: a module or `SSLProxyEngine On` is missing; Apache's error log says which. - **No `x-jazui`**: check that the `.htaccess` is read (`AllowOverride All`). - **Behind Cloudflare**, `REMOTE_ADDR` is the CDN's IP: use `%{HTTP:CF-Connecting-IP}` or `mod_remoteip`. - The maximum wait is Apache's proxy timeout (`ProxyTimeout`, 60 s by default); you can lower it to 20 s in your ``. --- # Home and tokens > The first thing you see when you sign in: the summary, your plan and the tokens for the integration. Source page: https://docs.jazui.com/panel/home/en ## Summary Home summarizes the last 30 days of all your sites: - **Bot requests**, with how many come from AI bots. - **Served from cache**: the share served instantly and how many were rendered on the spot. - **Cached pages** against your plan limit, and how many have a low score. - **Average SEO score** of your sites and how many are verified. Below you'll see requests per day (from cache or rendered), your sites with their score, the bots that visit you most and the **Your plan** card with the month's usage bars: they turn to warning at 80% and red at 100%. If you have no sites yet, you'll see the **Add site** button. If you work in someone else's team, home tells you which account you're in and your role on each site. ## Integration and settings Only the account owner sees this section: - **JAZUI Token**: the account render token, the one that goes in your integration. You can show it, copy it or **Renew** it; when renewed, the old one stops working within a minute. - **PUBLIC Token**: read-only, to read your blog with the public API from the browser. - **Cache settings**: how many days until your pages are rendered again (0 to 30; 0 = never). It's the account default: each site and rule can change it, and it never goes below your plan's minimum. - **Maximum expected load time**: how long Chrome waits for your page to finish loading (1 to 10 seconds; 5 by default). Raise it if rendered pages are missing content. ## The menu | Section | What's there | |---|---| | **Sites** | Websites, Cached pages, Rules and Sitemaps. | | **Analytics** | Bot traffic, How AI sees you and Search Console. | | **Content** | Blog (articles and categories), Write with AI and Social networks. | | **Account** | My plan, AI (your key) and Team. | The top bar has the color and mode picker, the notifications bell, the account switcher (if you belong to a team) and your menu with **Profile** and **Sign out**. The side menu has a search box and the language switch. --- # Websites > Add your domains, verify them and manage their token, score and links. Source page: https://docs.jazui.com/panel/sites/en ## The site list Each site shows its level, whether it's **Verified**, its **SEO score** and how many pages it has cached (and, if your plan requires it, the badge status). Select one to use the buttons: | Button | What for | Role | |---|---|---| | Add site | Add a domain. | Owner | | Verify | Prove it's yours. | Administration | | Render settings | The [site settings](https://docs.jazui.com/llms/en/panel-site-settings.txt). | Administration | | Site token | Its own token and the bot blocks for nginx, IIS and Apache. | Administration | | SEO score | The site score and what to fix first. | Everyone | | Broken links | The link check. | Everyone | ## Adding a site Type the domain (`mysite.com`) or paste any address on the site: Jazui keeps the domain. It must be a public domain, and it counts towards your plan's sites. Saving it opens verification. Each subdomain (`blog.mysite.com`) is added as one more site and counts towards your plan limit. If the domain is already verified by DNS in your account, the subdomain inherits the verification when you add it; it shares the domain's rules, its cached pages count towards the same account limit, and it has its own settings, metrics and token. > To remove a site, use **Delete website** (below). ## Verification There are two methods and one is enough: a **DNS TXT** record `jazui-verification=CODE` on the root domain (**Check DNS** button) or the **meta tag** `` on the home page (**Check tag** button). The step-by-step guide is in [Getting started](https://docs.jazui.com/llms/en/getting-started.txt). Until the site is verified, Jazui doesn't render its pages (the API answers `403`) and its sitemap, robots.txt, llms.txt, alerts and link check don't work either. ## Site token A render token that only works for this site and its added subdomains. Use it in the site's integration instead of the account token: if it leaks, you regenerate or revoke it without touching your other sites. Buttons: **Generate token**, **Copy**, **Regenerate token** (the current one stops working within a minute) and **Revoke token** (the site goes back to the account token). In the same window, **Bots in nginx, IIS and Apache** gives you the updated bot block for each server, with how many bots it has and the list date. ## Site score **SEO score** opens the site score: the average score of its cached pages, weighted by bot visits over the last 30 days (each page weighs its visits plus one), recalculated hourly. Below, **Fix first** sorts the problems of every page by the points gained when fixed multiplied by their visits, with the affected pages. That way you know which fix has the most impact. ## Link check Jazui reads the links and images of the site's cached pages (what bots see) and of its sitemap pages, and checks each one. How often and how many pages are read depends on the plan (the «Link check» row in [Plans and limits](https://docs.jazui.com/llms/en/plans.txt)), with up to 2,000 links per check. With **Check now** you can start one by hand, at most once every 10 minutes. The result separates **Broken** and **Redirects**, with the code (or «No response»), whether the link is internal, external or an image, whether it's in the sitemap and how many pages link to it. External links that can't be checked (401, 403, 429…) don't count as broken. If broken links show up, you get a notification. ## Delete a website Select the site and click **Delete website** (account owner only). The window tells you how many blog articles and cached pages it has, and which subdomains inherit its verification. To confirm, type the site's domain. Everything in it is deleted: settings, site token, its own rules, blog (articles with their images, categories and tags), bot metrics and AI visits, Search Console, broken links and alerts. Its cached pages (with and without `www`) are removed in the background and stop counting towards the account limit; those of its subdomains are kept, because they are other sites. This cannot be undone. > Subdomains that inherited this site's verification become **unverified** and stop being rendered until you verify them on their own. If the site is added again, it has to be verified again, and its pages are rendered again on the first bot visit. ## Free plan badge If your plan asks for the «Visible with Jazui» badge, the site list shows its status (**Seen**, **Missing** with the deadline, or **Not checked**) and a notice with the **Add the badge** button, which gives you the code in Spanish or English with a preview. The rules are in [Plans and limits](https://docs.jazui.com/llms/en/plans.txt). --- # Site settings > How each site is rendered, its blog, robots.txt, AI bots and alerts. Source page: https://docs.jazui.com/panel/site-settings/en In **Websites**, select a site and click **Render settings**. There are five tabs and everything is saved with **Save**. Anything left empty uses your account value. ## Render | Setting | What it does | |---|---| | Maximum time | Seconds Chrome waits for the page to finish (1 to 10). Never above your plan's maximum. | | Recache time (0-30 days) | How many days until this site's pages are rendered again. | | Device | **Desktop** (default) or **Mobile**: changes the screen and browser used to render. | | Page-ready selector | A CSS selector (for example `#app .content`): Jazui waits for that element to exist before saving the HTML. Useful if your SPA loads data later. | | User agent | The agent Chrome uses to request your page. Empty = the device's Chrome agent. | | Extra headers | One per line, `Name: value` (for example `X-Preview-Token: abc123`). Only sent to your own domain. | ## Blog and IndexNow - **Blog article path**: what your article addresses look like on your website, with `{slug}` (or `{id}`) and, optionally, `{lang}`. Empty = `/blog/{slug}`. Example: `/{lang}/articles/{slug}`. Used for the API URLs, the sitemap and social links. - **Category page path**: with `{slug}` and `{lang}`. Empty, categories don't appear in the sitemap. - **When publishing, updating or unpublishing an article**: **Render the article page again** (on by default) and the blog [webhook](https://docs.jazui.com/llms/en/api-webhooks.txt), with its **Test** button and the **Signing secret**. - **Notify IndexNow** (Bing, Yandex, Seznam…): off by default. Saving generates the key: publish the file `https://mysite.com/{key}.txt` and click **Check**. You'll see the date and result of the last notice. ## Sitemap and robots.txt Links to the **sitemap**, **robots.txt** and **llms.txt** Jazui generates for the site (only if verified). You can write: - **Custom robots.txt rules** (up to 8,000 characters). Empty = allow everything. The `Sitemap:` line is added automatically, as are the AI bots you block in robots.txt. - **Description for llms.txt** (up to 1,000): one or two sentences about your site for language models. How to serve them on your domain is in [Sitemap, robots and llms.txt](https://docs.jazui.com/llms/en/api-site-files.txt). If you'd rather upload your own sitemaps, **Sitemaps** (Sites menu) lets you store XML files that are validated on save and published at a Jazui address. ## AI bots What each AI bot can do with your site. Explained in detail in [AI bots](https://docs.jazui.com/llms/en/panel-ai-bots.txt). ## Alerts Enable **Notify me when something goes wrong on this site** and Jazui will check hourly (on verified sites): | Alert | Default threshold | When it's checked | |---|---|---| | Render errors in 24 h | 10% | With at least 20 requests in 24 h. Not repeated within 24 h. | | Googlebot drop versus the previous week | 50% | With at least 20 visits the previous week. Not repeated within 7 days. | | SEO score drop in 7 days | 10 points | Not repeated within 7 days. | Each threshold goes from 0 to 100; 0 turns that alert off. Alerts arrive **by email** (to your address or another one), as a dashboard notification and, if your plan includes alert webhooks, to the **alerts webhook** if you set it. **Send test alert** checks it, and **Latest alerts** shows the history. --- # Cached pages > The pages Jazui serves to bots: view them, compare them with your website, recache them and see their score. Source page: https://docs.jazui.com/panel/cached-pages/en Pages are created automatically when a bot requests an address through your integration. You can also add them by hand so they're ready before the first bot arrives. ## The list - **Rendering**: Active or Stopped (a stopped page isn't rendered again automatically). - **Cache date**, with the **Outdated** warning if the comparison found changes on your website. - A bot's **Last request** and the **Cached URL**. - That page's **Maximum time**. - **SEO score** of what bots get (the original site's on hover). - **Google**: clicks and impressions over 28 days, and **Indexing**, if you connected Search Console. - **Source** (you added it or a bot did) and the **Robot** that requested it. ## What you can do - **Add**: a URL with its maximum time and whether it renders automatically. **Edit** also shows which rules applied on the last request. - **View cached page**: the exact HTML a bot gets. - **SEO**: the page's [SEO score](https://docs.jazui.com/llms/en/panel-seo.txt) with every check. - **Compare with the live site**: renders your page now without touching the cache (it uses a render) and tells you whether the cache is **up to date** or **outdated**: text similarity, new or removed blocks, key fields (status, title, description, canonical, robots, H1, language, Open Graph, JSON-LD types, words, links, images), the text changes and two screenshots. It's outdated if similarity drops below 95% or the title, description, canonical, robots, H1 or status change. From there you can **Recache now**. - **Recache**: renders the selected pages again; it notifies you when it starts and finishes. - **Delete**: removes pages from the cache (in bulk). ## Good to know - Every render and recache (automatic ones too) counts as a plan **render**, and each new page takes a **cached pages** slot. - If your website answers with a server error (5xx) while recaching, the previous version is kept. - Jazui always strips tracking parameters from addresses (`utm_*`, `fbclid`, `gclid`, `dclid`, `gbraid`, `wbraid`, `msclkid`, `yclid`, `igshid`, `mc_cid`, `mc_eid`, `_ga`, `_gl`, `ref_src`): all those variants share one page. For other parameters, use an **Ignore parameters** rule. - **Automatic recache** uses the days of the applicable «Cache days» rule; otherwise the site's, otherwise the account's. It never goes below your plan's minimum and stops if no renders are left. - To recache from your deployment or CMS, use the [cache API](https://docs.jazui.com/llms/en/api-cache.txt). --- # Rules > Decide what gets rendered and how, without touching your code: exclude, block, inject tags, force status codes and more. Source page: https://docs.jazui.com/panel/rules/en A rule has one or more **conditions** on the address or the bot, and an **action**. It can apply to all your sites or one (with its subdomains). How many rules you can have depends on the plan ([Plans and limits](https://docs.jazui.com/llms/en/plans.txt)). ## Conditions They compare, case-insensitively, against the full address unless stated otherwise: | Condition | Example | |---|---| | Equals, Not equal | `https://mysite.com/pricing` | | Contains, Doesn't contain | `/admin` | | Starts with, Doesn't start with | `https://mysite.com/blog` | | Ends with, Doesn't end with | `.pdf` | | Matches pattern (*) | `/blog/*` or `/*.pdf` (`*` = any text, `?` = one character). If it starts with `http` it's compared with the full URL; otherwise with the path and query. It must cover the whole text. | | Regular expression | `/(admin\|account)/` | | Has query parameter | `preview`, `page=2` or `filter_*` | | Bot is | `GPTBot`, `Googlebot`, `facebookexternalhit` (searched within the user agent) | With **Add condition** you add up to 9 more and choose whether **all (AND)** or **any (OR)** must match. ## Actions | Action | What it does | |---|---| | **Exclude** | The address isn't rendered (the integration serves your normal website). | | **Only include** | If a site has rules of this kind, only addresses matching one of them are rendered; the rest are excluded. | | **Cache days** | How many days until recache (0 to 365; 0 = never), above the site and account. | | **Render settings** | Maximum time, device and page-ready selector just for those addresses. | | **Ignore parameters** | Removes parameters from the address before looking it up and rendering it, so variants share one page: `sessionid, sort, filter_*`. | | **Remove elements** | CSS selectors, one per line (up to 20), removed from the HTML before saving: cookie banners, chats, pop-ups. | | **Inject into ** | `meta`, `link`, `title` or JSON-LD tags to add. They replace those the page already has with the same `name` or `property`, and its title or canonical. | | **Force status** | The bot gets 200, 301, 302, 404 or 410 instead of the page's code (with 301 or 302, «Redirect to»). Applied when rendering: recache the pages you already had. | | **Block** | The bot gets a `403`. Combine it with «Bot is» to block, for example, CCBot. | ## Order of evaluation 1. First the site's **Only include** rules: if there are any, the address must match at least one or it's excluded. 2. Then the rest, from **lowest to highest priority** (0 to 9999, 100 by default); with equal priority, in creation order. 3. **Exclude** and **Block** end the evaluation. **Ignore parameters** changes the address for the following rules. **Cache days**, **Render settings** and **Force status** keep the value of the first rule that sets them. **Remove elements** and **Inject** add up. 4. A rule with **Stop here** cuts off: the following ones don't apply. ## Templates and examples When creating a rule you can start from a template: - **Exclude /admin and /account**: pattern `/admin*` or `/account*`. - **Ignore sort and view parameters**: `sort, order, orderby, view`. - **Remove cookie banner**: `#onetrust-banner-sdk`, `#CybotCookiebotDialog`, `#cookie-notice`, `.cc-window`, `.cookie-banner`. - **Block AI bots**: GPTBot, ChatGPT-User, OAI-SearchBot, ClaudeBot, anthropic-ai, CCBot, PerplexityBot, Bytespider, Amazonbot and meta-externalagent. An **Inject into ** example for a product page (condition «Matches pattern» `/products/trail-shoe`): `Inject into `: ```html Zapatillas de trail | Mi tienda ``` ## Testing an address With **Test URL** (also inside the form) you apply your rules to an address, with an optional bot agent, without saving anything: you'll see whether it's **rendered**, **excluded** or **blocked**, the address it's cached under, the status, days, wait, device, selector, what's removed and injected, and each rule marked «Applies», «Doesn't match» or «Not evaluated». The rule you're editing is tested even if unsaved. --- # SEO score > How each page's score is calculated and what each check looks at. Source page: https://docs.jazui.com/panel/seo/en Each cached page has two scores from 0 to 100: **What bots get** (the HTML Jazui serves, with your rules applied) and **Original site** (the page as the browser paints it, before rules). The difference tells you how much Jazui improves your page. It's audited on every render; checks that need the network run in the background and site-wide checks run hourly. The score window has **Fix first** (each problem with the points gained by fixing it), one tab per category, the Google and sharing **Preview**, and the change **History**. ## Calculation Each check comes out **Good** (full weight), **Warning** (half) or **Error** (nothing). Each category scores from 0 to 100 and the total is the weighted average: | Category | Weight | |---|---| | Metadata | 25 | | Content | 20 | | Social | 15 | | Structured data, Links, Images and Technical | 10 each | ## Checks | Category | Check (weight) | What it requires | |---|---|---| | Metadata | Page title (3) | Exists, is unique and has 30–60 characters. | | Meta description (3) | 70–160 characters. | | | Canonical URL (2) | Only one, absolute and pointing to the page itself. | | | Language (1) and Viewport (1) | `lang` on ``; viewport with `width=device-width`. | | | Meta robots (3) | `noindex` is an error; `nofollow`, a warning. | | | Unique title and description on the site (2 + 2) | Not repeated on other pages. | | | Canonical target (2) | Answers 200, no chains or loops. | | | Content | Painted content (3) | At least 50 characters after rendering. | | H1 heading (3) | Exactly one, with text. | | | Heading order (1) | No skipped levels. | | | Amount of text (2) | 300 words or more. | | | Text ratio (1) | At least 10% of the HTML. | | | Social | og:title (2) and og:description (2) | Present. | | og:image (3) | Absolute and, if it declares dimensions, at least 1200×630. | | | og:url (1) | Present. | | | X card (1) | `twitter:card`. | | | Share image (2) | Downloads, isn't SVG and is at least 1200×630. | | | Structured data | Structured data (2) | JSON-LD is present. | | Valid JSON-LD (3) | It can be parsed. | | | Structured data types (3) | Recognized types with their required fields. | | | Links | Internal links (3) | The page links to others on the site. | | Link text (2) | No «click here» or empty links. | | | Internal nofollow (1) | No `nofollow` on internal links. | | | Broken links (2) | Cached internal links aren't broken or redirecting. | | | Linked page (2) | Another page links to it (not an orphan). | | | Images | Alt text (3) | More than half without `alt` is an error. | | Image dimensions (2) | `width` and `height`. | | | Format (1) | WebP, AVIF or SVG. | | | Technical | HTTP status (3) | 200. | | Render time (1) | Up to 3 s good; up to 8 s warning. | | | HTML size (1) | Over 1 MB warning; over 5 MB error. | | | hreflang (1) and reciprocal hreflang (1) | Correct language alternates that link to each other. | | | robots.txt (3) and Sitemap (1) | robots.txt allows the page and a sitemap exists. | | ## Raising the score without touching your code - Missing title, description, Open Graph or structured data: an **Inject into ** rule, or ask [your AI to suggest them](https://docs.jazui.com/llms/en/ai-suggestions.txt) and apply them as a rule in one click. - The page is slow or saved incomplete: raise the **maximum time** or use a **page-ready selector**. - Banners or chats cluttering the content: a **Remove elements** rule. - Anything that depends on your HTML (H1, alt text, internal links) is best fixed on your website. --- # AI bots > Decide which AI bots can read your site, train on it or cite you. Source page: https://docs.jazui.com/panel/ai-bots/en Not all AI bots do the same thing. Jazui groups them by **use**: - **Training**: they collect content to train models (for example GPTBot, CCBot or Google-Extended). - **Search**: they index for an AI search engine's answers (for example OAI-SearchBot or PerplexityBot). - **User-triggered**: they read a page because someone asked the assistant (for example ChatGPT-User or Claude-User). Blocking training bots is your call; blocking search or user-triggered bots means AI can't cite you. ## What you can do with each **Site settings › AI bots** has the full list, with each bot's company, use, and requests and rejections over the last 30 days (those that visited you first). For each one: | Policy | Effect | |---|---| | **Allow** (default) | Gets your rendered pages like any bot. | | **Block** | Gets a `403` from Jazui (`X-Jazui-Blocked: ai-bot`). Only for bots that go through your integration. | | **Block and robots.txt** | Also adds `Disallow: /` for that bot to the robots.txt Jazui generates, so bots that respect robots.txt don't even try. | There are bulk actions, including **Block training bots**. Some (such as Google-Extended or Applebot-Extended) don't visit pages: they only exist in robots.txt, so they only offer «Block and robots.txt». ## llms.txt and Markdown for AI To help AI understand you better, Jazui publishes for each verified site an `llms.txt` (your site's index for language models) and a **Markdown** version of every blog article, linked from its ``. The llms.txt description is written in **Site settings › Sitemap and robots.txt**. How to publish them on your domain is in [Sitemap, robots and llms.txt](https://docs.jazui.com/llms/en/api-site-files.txt). ## Measuring In [Bot traffic](https://docs.jazui.com/llms/en/panel-traffic.txt) you see how many requests AI bots make, and in the **Visits from AI** tab, how many people reach your website from ChatGPT, Perplexity, Gemini and other assistants' answers. With [How AI sees you](https://docs.jazui.com/llms/en/panel-ai-scan.txt) you check what an AI bot reads from any page. --- # Bot traffic > Who visits you, what they get, which pages are crawled, visits from AI and your usage. Source page: https://docs.jazui.com/panel/traffic/en Pick a site (or all) and the period: 7, 30 or 90 days, up to your plan's metrics history ([Plans and limits](https://docs.jazui.com/llms/en/plans.txt)); periods it doesn't include are disabled. Previous days are summarized hourly; today is calculated on the fly. Requests are kept for 90 days. ## Traffic - **Indicators**: requests, share from cache, average response, average render, rejected and errors, share of AI bots, and **verified / fake** (with the IP your integration sends, Jazui checks Googlebot, Bingbot, Applebot and other search engines). - **Charts**: requests per day and bot type (search engines, AI, social, SEO tools, other), average times, rejected and errors per day, split by type, and AI bots. - **Bots**: each bot with its type, requests, from cache, verified and fake. ## Pages and SEO - **SEO score trend** for each site, one point per day. - **Most crawled pages**: requests, from search engines and AI, last visit and score. - **Pages Googlebot hasn't visited**: cached 200 pages with no Googlebot visit in the period. Link to them from other pages and make sure they're in the sitemap. ## Visits from AI People reaching your website from ChatGPT, Perplexity, Gemini, Copilot, Claude, Meta AI, Le Chat, Grok, DeepSeek and others (more than 20 assistants). They're counted by a small script on your website, without cookies or personal data, that only acts when the visit comes from an assistant (by referrer or `utm_source`). You'll see visits per day, per assistant and the landing pages. Each site's script is in this tab, ready to copy into your ``. ## Usage Owner only. What the account's monthly counter adds up: bot requests, renders and recaches (automatic ones too) and what's done from the dashboard, with the end-of-month forecast, comparison with last month, the last 12 months and the bots that consume most. Limits and usage against them are in **My plan**. --- # Search Console > Google clicks, impressions and indexing alongside Jazui's data. Source page: https://docs.jazui.com/panel/search-console/en Connect each site to its Google Search Console property to see clicks, impressions, position and indexing without leaving the dashboard. Jazui only asks for **read** permission. Included in plans with Google Search Console ([Plans and limits](https://docs.jazui.com/llms/en/plans.txt)); connecting requires the Administration role on the site. ## Connecting 1. In **Search Console**, click **Connect with Google** on the site and sign in with the Google account that has access to the property. 2. Jazui picks the property matching the site; you can **Change** it. If the account has none, you'll see «No property». 3. Data is requested from Google within the next hour; **Update now** skips the wait. **Disconnect** removes the connection, revokes the permission in Google and deletes the data read. ## The report - Period of 7, 28 (default) or 90 days, or 16 months. - **Clicks**, **impressions** and **average position**, with the change versus the previous period, and the daily chart. - **Top pages by clicks**: clicks, impressions, CTR and position. - **Indexing status**: Jazui checks your cached pages in Google gradually (up to 1,500 a day) and repeats every 7 days, or right away with **Inspect now**. You'll see whether they're indexed, the canonical Google chose, coverage and last crawl. Clicks, impressions and indexing also appear in **Cached pages**. How Jazui handles this data is in its privacy policy. --- # How AI sees you > Check what an AI bot reads from any page, yours or others', and how to improve it. Source page: https://docs.jazui.com/panel/ai-scan/en The scanner requests the page twice: as an AI bot that doesn't run JavaScript and as a person with a browser. It compares both, reviews the basics AI reads and gives a score from 0 to 100. It works for your pages and your competitors'. It's the same as the scanner on Jazui's website, with more detail. You get **10 scans a day**; a URL's result is valid for 24 hours for everyone and repeating it doesn't count. It doesn't use plan renders. ## What it checks | Check | Points | |---|---| | Content visible without JavaScript (proportional to the text AI sees) | 30 | | robots.txt lets in AI bots that search or answer | 13 | | Title (10–70 characters) | 10 | | Structured data (JSON-LD) | 10 | | Meta description (50–170 characters) | 8 | | Main heading (H1) | 8 | | 200 response | 5 | | llms.txt | 5 | | Canonical URL | 4 | | Open Graph | 4 | | Language (lang) | 3 | If the server or its firewall rejects the AI bot (401, 403, 429…) while the page loads fine in a browser, the scanner marks it as **blocked** and explains it: your CDN may be blocking AI bots. In that case it reviews the title, description, etc. on the real page. ## The result - The score and how much of the page's text AI sees. - The HTTP status and response time; «Already uses Jazui» if Jazui serves the page. - **AI bots in your robots.txt**: which can get in and what each company uses them for. - **What AI sees** versus **what a person sees**, with their word counts. - Your previous scans. Below, **Fixes suggested by your AI** proposes a title, description and structured data: see [Fix suggestions](https://docs.jazui.com/llms/en/ai-suggestions.txt). --- # Notifications and weekly report > Dashboard notifications, your sites' alerts and the weekly email report. Source page: https://docs.jazui.com/panel/notifications/en ## The bell Everything Jazui tells you reaches the bell in the top bar instantly (some notices also by email). In the notifications window you can view them by type (Plan, Usage, Alert, Links, System), open their details, go to the section, mark them as read or important and delete them. Old ones are deleted automatically: read ones after 30 days and unread ones after 60 (important ones, after 90). ## What gets notified - Usage at 80% and 100% of renders or requests (also by email). - Plan changes: trial, versions, retired plans and payments. - Your sites' alerts and broken links. - A missing badge. - Manual recaches started and finished. - AI-written articles, finished or failed, and social sharing failures. - Team: invitations, departures and removals. ## Site alerts Enabled per site in [Site settings › Alerts](https://docs.jazui.com/llms/en/panel-site-settings.txt): render errors, Googlebot drop and SEO score drop, by email, notification and webhook. ## Weekly report In **Bot traffic › Weekly report**, enable **Receive the weekly report by email** (off by default). Every Monday at 7:00 UTC you'll get the week's bot traffic versus the previous one, your sites with their SEO score, alerts, what's most worth fixing and the month's usage. You can preview this week's and **Send it to me now**. --- # Team > Invite other people to your sites with a role on each. Source page: https://docs.jazui.com/panel/team/en The data and plan remain yours; only you manage the team, sites and billing. How many people fit depends on the plan, counting yourself ([Plans and limits](https://docs.jazui.com/llms/en/plans.txt)). Everything the team does counts towards your plan (renders, scans and AI articles) and uses your AI connections. ## Inviting In **Team**, click **Invite**, type the email and choose a role for each site. The invitation expires after 7 days; you can resend or cancel it. If the person has no account, they create one from the link and the invitation is accepted automatically; if they do, they accept it when signing in. You can **Edit access** or **Remove from team** at any time. If you're invited, you'll see the invitation on home and in **Team**; once accepted, you switch accounts with the top bar selector. You can **Leave the team** at any time. ## What each role can do The role is given per site and each includes the previous one. | Feature | Read | Edit | Administration | Owner | |---|---|---|---|---| | View home, cached pages, SEO, metrics, rules and blog | ✓ | ✓ | ✓ | ✓ | | Scan with «How AI sees you» and view Search Console | ✓ | ✓ | ✓ | ✓ | | Recache, add and edit pages and rules | — | ✓ | ✓ | ✓ | | Write, edit and publish articles; write with AI and autopilot | — | ✓ | ✓ | ✓ | | Ask AI for suggestions and apply them; share on social | — | ✓ | ✓ | ✓ | | Token, verification, alerts, webhooks, robots, sitemaps and AI bots | — | — | ✓ | ✓ | | Site social accounts and connecting Search Console | — | — | ✓ | ✓ | | AI connections (the team uses them) | — | — | — | ✓ | | Team, new sites, sitemaps, plan and billing | — | — | — | ✓ | --- # Account and My plan > Your profile, account security, My plan and how to delete the account. Source page: https://docs.jazui.com/panel/account/en ## Profile From your top bar menu, **Profile**: photo (JPG, PNG or WebP, up to 5 MB), name, phone and language (browser, Spanish or English: the dashboard, emails and notifications use it). ## Security - **Change password** (at least 10 characters). If you signed in with Google, you can **Set a password** to also sign in with your email. - **Two-step verification**: when signing in, besides the password, we'll ask for a code sent to your email (also with Google sign-in). - After 5 failed attempts the account is locked for 15 minutes. - Forgot your password? The sign-in screen emails you a link to reset it. ## My plan - **Current plan**: name, whether it's trial, monthly or yearly, renewal date and scheduled changes (trial end, downgrade, cancellation…), with **Keep my plan** if you change your mind. - **Your usage** this month: renders, requests, cached pages, sites, rules, articles and AI articles. Renders and requests reset on the 1st of each month. - **Plans**: monthly or yearly (with the saving), and **Subscribe**, **Switch to this plan** or **Move to Free**. You pay in Stripe's secure form. - **Manage billing**: the Stripe portal for your card, invoices and cancellation. **Refresh from Stripe** re-reads your subscription. - **Invoices**: number, amount, status, and view or download as PDF. Prices, limits and plan changes: [Plans and limits](https://docs.jazui.com/llms/en/plans.txt). ## Deleting the account In **Profile › Delete account** you'll see a summary of what's lost: plan, sites, cached pages, rules, blog, Google sign-in and teams. It's confirmed with your password or, if you sign in with Google, with Google or an email code. - If you have a paid plan, it's cancelled in Stripe, with no further charges or refund of the current period. - You leave other people's teams (the owner is notified), and your team loses access to your sites. - Search Console and Google sign-in are disconnected, and your sites, cache and blog are deleted. You can sign up again later with the same email. > This can't be undone. --- # Blog > Write each site's blog from Jazui and show it on your website with your own design. Source page: https://docs.jazui.com/blog/en The blog lives in Jazui and your website reads it with the [blog API](https://docs.jazui.com/llms/en/api-blog.txt). Each article belongs to a site and a language; that way each website only shows its own articles even if you manage several. Jazui generates each article's SEO (title, description, Open Graph, structured data, hreflang), its Markdown version for AI, the sitemap, RSS and llms.txt, and can notify search engines and social networks on publishing. How many articles you can have depends on the plan ([Plans and limits](https://docs.jazui.com/llms/en/plans.txt)). If your plan carries the badge on articles, they end with «Published with Jazui». ## Writing an article 1. In **Blog › Articles**, click **Add**: choose the **site** (without one it isn't in the API), the **language**, the **title**, the **slug** (generated from the title; it's the last part of the address and unique per site and language), the author, summary and tags (comma-separated; new ones are created automatically). 2. In **Content**, write Markdown in the editor. The **Image repository** stores the article's images: upload one and **Copy Markdown** gives you the code to paste it. The **SEO** tab checks the text as you write, and **Preview** shows how it'll look. 3. Upload the **Cover** and assign **Categories**. 4. In **Metadata**, adjust the search title and description (up to 60 and 160 characters), keywords, social image (empty = the cover), canonical (empty = the article address) and whether it should be `noindex`. The **Live SEO** panel gives you the score, preview and generated `` tags. 5. Change the **Status** to **Published**. ## Statuses | Status | Meaning | |---|---| | **Draft** | Not in the API while you prepare it. | | **Scheduled** | Published automatically at the date and time you choose (your browser's time). | | **Published** | In the API from now on. | | **Archived** | No longer shown, without deleting it. | When you publish, update or unpublish an article, depending on the site settings, Jazui renders its page again, calls your webhook, notifies IndexNow and shares it on your social networks. ## Translations, versions and categories - **Translations**: in «Translation of», link the article to its version in another language on the same site. Jazui generates the `hreflang` tags and the API returns each article's translations. - **History**: each time you save the content a version is stored. You can **Restore** any of them; the current one stays in the history. - **Categories** (Blog › Categories): per site and language, with slug, order, icon, description and parent category to build a tree. For them to appear in the sitemap, set the category page path in the site settings. - **Article address**: defined in [Site settings › Blog and IndexNow](https://docs.jazui.com/llms/en/panel-site-settings.txt) (`/blog/{slug}` by default). ## Showing it on your website With the PUBLIC Token, your website asks the API for articles and paints them with your design. If your website is an SPA, the Jazui integration lets bots see the full blog. Examples and every field are in [Blog API](https://docs.jazui.com/llms/en/api-blog.txt). --- # AI with your key > Jazui's AI features use your own AI accounts: you choose the provider and model, and pay for usage directly. Source page: https://docs.jazui.com/ai/en Jazui doesn't charge for AI or limit your provider: you store one or more **connections** with their key and each provider bills you for what you use. They're used by [fix suggestions](https://docs.jazui.com/llms/en/ai-suggestions.txt) and [Write with AI](https://docs.jazui.com/llms/en/ai-writer.txt). They're set up in **AI (your key)**, in the Account menu; only the owner changes them and their team uses them. ## Providers | Provider | Suggested model | Where to get the key | |---|---|---| | 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 | | Another OpenAI-compatible API | The one you set | Your provider (for example OpenRouter: `https://openrouter.ai/api/v1`); the address must be https and end in `/v1`, and the key is optional. | The model can be freely changed on each connection. ## Connections 1. Click **Add connection**: give it a name, choose the provider and model, and paste the key. 2. Click **Test connection** to check it responds. 3. Order them with **Up** and **Down**: the first is the **default**. With **Automatic mode** (on by default), if the first connection doesn't respond well the next one is tried, in your order. Off, only the default is used. When asking for something you can also pick a specific connection. You can have up to 10 and disable those you don't want to use. > The key is stored encrypted and never shown again (only its last characters). It's only sent to your provider, with what each request needs, and your provider handles the data sent under its own terms. --- # Write with AI > Your AI proposes topics based on your site and writes full articles, on demand or on autopilot. Source page: https://docs.jazui.com/ai/writer/en In **Write with AI** (Content menu), choose the site. You need at least one [AI connection](https://docs.jazui.com/llms/en/ai.txt). You'll see how many AI articles you've written this month against your plan's monthly limit ([Plans and limits](https://docs.jazui.com/llms/en/plans.txt)). Proposing topics doesn't count; each written article does (and also counts as a blog article). ## Proposed topics 1. Choose the language, the AI connection (if you have several) and, optionally, a **focus** («beginner guides, comparisons, news…»). 2. Click **Propose topics**. The AI reads your home page, other cached pages, your articles and, if connected, Search Console, and proposes 6 topics with title, keyword, search intent and outline. 3. Tick the ones you want and click **Write**. They're written in the background: you can leave the page and we'll notify you. Each article is a **draft** of 800 to 1,300 words, with FAQs, links only to pages on your site, search title and description, summary and tags. Review it in Blog › Articles and publish it whenever you want. ## Autopilot Per site, enable **Write articles periodically** and choose the **frequency** (daily, every 2, 3, 7, 14 or 30 days; 7 by default), language, connection and focus. Each time, the AI picks a new topic for the site and writes the article. - By default they stay as **drafts** for you to review. With **Publish without review** they're published as soon as they're written: AI can make mistakes and you're responsible for what's published. - You'll see the next date and the last article (or the reason if it failed). If you reach the month's limit, autopilot waits without calling the AI. Below, **Articles in progress and recent** shows each one: queued, writing, draft ready, published or failed (with **Retry**). --- # Fix suggestions > Your AI proposes a title, description and structured data for a page, and you apply them in one click. Source page: https://docs.jazui.com/ai/suggestions/en After scanning a page in [How AI sees you](https://docs.jazui.com/llms/en/panel-ai-scan.txt), in **Fixes suggested by your AI** click **Suggest fixes with AI** (you can choose the connection). Your AI reads what the page says, without making things up, and proposes: - **Title** (up to 60 characters) and **meta description** (up to 160). - **Structured data** (JSON-LD) suited to the page type. - **For the site's code**: the main heading (H1) and tips worth applying on your website. Everything is editable and each part has its checkbox to decide what you apply. ## Applying them - **Apply as rule** (pages on your sites, Edit role): creates an **Inject into ** rule just for that page, named «AI: /path». What you ticked replaces what the page has when served to bots, and the title and description also go into Open Graph. It counts towards the rules limit. Recache the page to see it. - **Copy tags**: for pages that aren't on your sites, or if you'd rather paste them into the code yourself. - **Ask for others**: asks again. Ideally also move the changes into your website's code; the rule applies them for bots in the meantime. --- # Social networks > Announce each article on Bluesky, Mastodon, Telegram and Discord, automatically or by hand. Source page: https://docs.jazui.com/social/en In **Social networks** (Content menu), choose the site and add its **channels** (up to 10). Each article you publish will be announced on them automatically, and you can also share any published article by hand. Credentials are stored encrypted. Channels are managed by the Administration role; Edit can share. ## Connecting each network | Network | What you need | How to get it | |---|---|---| | Bluesky | Handle (`mybusiness.bsky.social`), app password and, optionally, the server | Bluesky › Settings › Privacy and security › App passwords. Don't use your normal password. | | Mastodon | Server (`https://mastodon.social`) and access token | Preferences › Development › New application, with the `write:statuses` scope; copy «Your access token». | | Telegram | Channel or group (`@mychannel`) and bot token | Create a bot with @BotFather, add it as an administrator of the channel or group and paste its token. | | Discord | Webhook URL | Channel settings › Integrations › Webhooks › New webhook › Copy URL. | **Test connection** checks it works. Each channel can publish new articles **automatically** or be kept for manual sharing only. ## The text Each channel has a **template** with `{title}`, `{summary}`, `{url}` and `{tags}` (tags as `#tags`). The default one is: ```bash {title} {summary} {url} {tags} ``` On Bluesky there are no tags. If the text doesn't fit the network (Bluesky 300 characters, Mastodon 500, Discord 2,000, Telegram 4,096), the summary is trimmed first, then tags are removed and finally the title is trimmed; the address is always kept whole. ## Sharing and history - **Share an article**: choose a published article, write your own text if you want (empty = each channel's template), check the per-channel preview and click **Publish now**. Also from Blog › Articles › **Share**. - **Posts**: each send with its channel, whether it was automatic or manual, its status and the link to the post. - An article isn't repeated on the same channel. Temporary failures are retried after 2 and 15 minutes; if they fail completely, we notify you and you can **Retry**. The blog API also returns with each article the links for your readers to share it on X, Facebook, LinkedIn, Bluesky, WhatsApp, Telegram, Reddit or by email. --- # Authentication and errors > Tokens, rate limits, errors and language for all of Jazui's APIs. Source page: https://docs.jazui.com/api/en All of Jazui's APIs live at `https://api.jazui.com`, use HTTPS and answer in JSON (except `/Render`, which returns HTML, and the public site files). There are four groups: | API | What for | Token | |---|---|---| | [Render](https://docs.jazui.com/llms/en/api-render.txt) | Used by integrations to ask for pages. | Render (account or site) | | [Cache](https://docs.jazui.com/llms/en/api-cache.txt) | Recache or purge pages from your deployment or CMS. | Render (account or site) | | [Blog v2](https://docs.jazui.com/llms/en/api-blog.txt) | Read articles, categories and tags to show them on your website. | Public or render | | [Site files](https://docs.jazui.com/llms/en/api-site-files.txt) | Sitemap, robots.txt, llms.txt, Markdown for AI and AI visits. | None | Jazui also notifies you with [webhooks](https://docs.jazui.com/llms/en/api-webhooks.txt) when you publish an article or an alert fires. ## Tokens Tokens go in the `Authorization` header with the `Bearer` scheme. They don't expire: they keep working until you renew or revoke them. | Token | Where it is | Used for | Secret | |---|---|---|---| | **JAZUI Token** (account render) | Home › Integration and settings | Render, cache and blog, for all your sites. | Yes | | **Site token** | Websites › Site token | Render, cache and blog, only for that site and its added subdomains. | Yes | | **PUBLIC Token** | Home › Integration and settings | Reading the blog only. Safe to put in the browser. | No | ```bash curl https://api.jazui.com/v2/cache/tasks \ -H "Authorization: Bearer $JAZUI_TOKEN" ``` - When you **renew** a token from the dashboard, the old one stops working within a minute: update your integrations and deployments with the new one. - A site token used with another domain gets a `403`. - The PUBLIC Token doesn't work for Render or cache: it gets a `403` with no body. - If the account is locked or deleted, its tokens stop working. ## Rate limits | What | Limit | Counted per | |---|---|---| | Render, cache and blog with a token | 120 requests per minute | Account: all its tokens share the same limit | | Feeds, site files and bot list (no token) | 120 requests per minute | IP | Going over it returns a `429` with no body. It's a per-minute limit: wait a few seconds and try again. Integrations treat it as a failure and serve your normal website. Don't confuse this `429` with the plan limits one, which carries the `X-Jazui-Limit` header. ## Errors | Code | When | Body | |---|---|---| | `400` | Invalid parameters or JSON | `{code, message}` or ProblemDetails | | `401` | No token, or a renewed or revoked token | Empty, with `WWW-Authenticate: Bearer` | | `403` | The token doesn't work for that API or site, unverified site, feature not in your plan, or a block | Empty or `{code, message}` | | `404` | Doesn't exist or isn't published | `{code, message}` or empty | | `429` | Rate, task or plan limit | Empty or `{code, message}` | | `5xx` | Jazui error or your website failing to render | ProblemDetails or text, with `Retry-After` when retrying makes sense | The cache and blog APIs explain the error with a stable code you can use in your code and a readable message: `Cache or blog API error`: ```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" } ``` ## Message language Error messages and texts returned by the API are in Spanish by default. To get them in English, send `Accept-Language: en` or add `?culture=en` to the address. Codes (`code`) don't change with the language. ## Without a token Some addresses are public: the bot list used by integrations, the service status, blog feeds and each verified site's files. ```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 > The API integrations use to ask Jazui for a bot's page. Source page: https://docs.jazui.com/api/render/en You don't usually call it yourself: your integration does. Knowing it helps you debug, build your own integration or check what a bot gets from a specific page. **Each call counts as a served request in your plan**, and if the page wasn't cached, also as a render. ## Request `GET https://api.jazui.com/Render` | Parameter or header | Required | Description | |---|---|---| | `url` | Yes* | Full page address, encoded. *If you can't put it in the query, send it in the `X-Jazui-Url` header. | | `botAgent` | No | The bot's user agent. If missing, the request's `User-Agent` is used. | | `Authorization` | Yes | `Bearer` with the account or site token. | | `X-Jazui-Client-IP` | No | The bot's IP. If a comma-separated list arrives, the first one is used. Used for metrics and to verify search engines. | | `If-None-Match` | No | The bot's `ETag`, to get a `304` if it hasn't changed. | ```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" ``` ## Response The page's HTML with its **real status code**: `200`, `404` if your page is a 404, or the redirect (`301`, `302`…) with its `Location` and no body. `Successful response`: ```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… ``` ## Other codes | Code | Header | Reason | |---|---|---| | `400` | — | `url` is missing or invalid, or a rule excludes that address from rendering. | | `401` | — | No token or token no longer valid. | | `403` | — | Token from another site, or the site isn't verified. | | `403` | `X-Jazui-Blocked: ai-bot` | You blocked that AI bot in the site settings. | | `403` | `X-Jazui-Blocked: rule` | A rule blocks that bot. | | `429` | `X-Jazui-Limit: plan` | The free plan has used up its renders or requests for the month. | | `429` | `X-Jazui-Limit: renders` | No renders left this month for a new page, or the required badge is missing past the grace period. | | `429` | `X-Jazui-Limit: pages` | The cached page limit has been reached. | | `429` | — | More than 120 requests per minute. | | `502` | `Retry-After: 30` | Your website answered with a server error while rendering. | | `503` | `Retry-After: 30` | Rendering didn't finish in time. | ## Conditional requests Each page has an `ETag`. If you send the one you have in `If-None-Match` and the page hasn't changed, you get `304 Not Modified` with no body (also with `X-Jazui: true`). That way bots that store pages don't download them again. ```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) ``` ## Reporting an unknown bot Code-based integrations report with `POST /Render/Seen` when they see a bot-like user agent that isn't on the list. It always answers `204`; each agent counts once a day, up to 500 different ones per account per day. If you build your own integration, you can use it the same way. ```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 ``` ## Your own integration If your platform isn't listed, you can build one following the contract in [Integrations](https://docs.jazui.com/llms/en/integrations.txt): decide whether it's a bot with the `/v2/bots` list, call `/Render` with a maximum wait (20 s), pass the response on only if it has `X-Jazui` or is a `403` with `X-Jazui-Blocked`, and serve your website in any other case. Don't forward the bot's cookies or other headers. --- # Cache API > Recache or purge pages when you publish changes, from your deployment, CMS or a script. Source page: https://docs.jazui.com/api/cache/en When you change your website, cached pages keep showing the previous version until the next automatic recache. With this API you tell Jazui to render them again (or remove them) right away. Requests create a **task** processed in the background, whose status you can check. > Included in plans with the **Cache API** ([Plans and limits](https://docs.jazui.com/llms/en/plans.txt)). Use the account render token or a site token (with a site token, you can only touch that site). ## Endpoints | Method | Path | What it does | |---|---|---| | `POST` | `/v2/cache/recache` | Renders the pages again. Pages that weren't cached are rendered for the first time. | | `POST` | `/v2/cache/purge` | Removes the pages from the cache (the next bot triggers a new render). | | `GET` | `/v2/cache/tasks/{id}` | Status of a task. | | `GET` | `/v2/cache/tasks?limit=20` | Latest tasks (1 to 100). With a site token, only that site's. | ## Which pages The body has **exactly one** of these three options: - `urls`: a list of up to 1,000 full addresses (normalized, duplicates removed). - `prefix`: every cached page that starts with that address (also the address without the trailing slash). Useful for a section: `/blog/`, `/products/`… - `site`: every cached page of a site. With `prefix` and `site`, at most 10,000 pages are processed per task. `urls`: ```json { "urls": ["https://misitio.com/", "https://misitio.com/precios"] } ``` `prefix`: ```json { "prefix": "https://misitio.com/blog/" } ``` `site`: ```json { "site": "misitio.com" } ``` ## Example ```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 } ``` ## The task | Field | Description | |---|---| | `action` | `Recache` or `Purge`. | | `scope` and `target` | `Urls`, `Prefix` or `Site`, with the prefix or site. | | `state` | `Queued`, `Running`, `Done` or `Failed` (with the reason in `error`). | | `total`, `processed`, `failed` | Total pages, done and failed. Progress is saved every 10 pages. | | `statusUrl` | Address to check the task (also in the `Location` header). | Tasks are processed one at a time, in arrival order. Recaching **uses renders** from your plan like any other render; if they run out, those pages count as failed. ## Errors | Code | `code` | Reason | |---|---|---| | `400` | `invalid_request` | Not exactly one option, more than 1,000 addresses, or one isn't absolute. | | `403` | `plan` | Your plan doesn't include the cache API. | | `403` | `forbidden` | The domain isn't in your account (or the token's site), or the site isn't verified. | | `429` | `too_many_tasks` | You already have 10 queued or running tasks: wait for them to finish. | ## On deployment The usual approach is to recache the whole site (or the section that changed) at the end of your deployment. Store the token as a CI secret: `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 notifies your server when you publish an article or a site alert fires. Source page: https://docs.jazui.com/api/webhooks/en A webhook is an address on your server where Jazui sends a `POST` with JSON when something happens. There are two per site: the **blog** one (articles published, updated or unpublished) and the **alerts** one (render errors, Googlebot drop, SEO score drop). Both are signed with the same site secret. ## Setting them up In the dashboard, open the site settings: - **Blog and IndexNow**: blog webhook address. With **Render the article page again** (on by default), Jazui also refreshes the article page in the cache. - **Alerts**: alerts webhook address (if your plan includes alert webhooks). The address must be public, over `https` or `http`. Saving the first webhook generates the site's **secret** (32 characters), shown in the settings. With **Test** (blog) you get a `ping` event, and with **Send test alert** (alerts), an alert of type `Test`. ## How they arrive A `POST` with `Content-Type: application/json` and these headers: - `X-Jazui-Event`: the type (`Published`, `Updated`, `Unpublished`, `alert` or `ping`). - `X-Jazui-Delivery`: delivery ID, the same on retries (use it to avoid processing the same one twice). - `X-Jazui-Signature`: `sha256=` followed by the HMAC-SHA256 of the body with your secret, in lowercase hex. ```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 ``` ## Blog events `event` is `article.published`, `article.updated` or `article.unpublished`. The same article with the same event isn't sent twice within 30 seconds. Use it to rebuild your static site, notify your team or publish elsewhere. ```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" } } ``` ## Alerts `alert.type` is `RenderErrors` (render errors in 24 h), `GoogleDrop` (Googlebot visits dropping versus the previous week), `SeoDrop` (SEO score dropping over 7 days) or `Test`. `value` is what was measured and `threshold` the threshold you set. ```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" } } ``` ## Test The blog webhook test button sends: ```json { "event": "ping", "site": "misitio.com", "occurredAt": "2026-11-03T09:00:00Z" } ``` ## Verifying the signature Compute the HMAC-SHA256 of the **body exactly as received** (without re-serializing it) with your secret and compare it with the header in constant time. If it doesn't match, answer `401` and don't process it. `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(); }); ``` ## Responses and retries - Any response below `500` counts as delivered: answer `2xx` as soon as you receive it and do the work afterwards. - With a `5xx` or no response, Jazui retries after 2 and 10 seconds (three attempts in total). - Each attempt waits at most 15 seconds and follows up to 3 redirects. --- # Blog API > Read the articles, categories and tags of your Jazui blog to show them on your website. Source page: https://docs.jazui.com/api/blog/en You write on the blog from Jazui's dashboard and show it on your website with this API, with your own design. Each article includes its HTML, its Markdown version, the full `` for SEO (title, description, canonical, hreflang, Open Graph and structured data), the table of contents, translations and share links. Use the **PUBLIC Token**: it's read-only and safe in the browser. The API accepts requests from any domain (CORS), read-only. ## Endpoints | Path | Parameters | Returns | |---|---|---| | `GET /v2/blog/articles` | `site`, `lang`, `category`, `tag`, `q`, `page` (1), `pageSize` (10, max 50) | A page of summaries, newest first. | | `GET /v2/blog/articles/{slug}` | `site`, `lang`, `format` | A full article. | | `GET /v2/blog/articles/by-id/{id}` | `format` | An article by its ID. | | `GET /v2/blog/articles/{slug}/related` | `site`, `lang`, `count` (4, max 12) | Related articles. | | `GET /v2/blog/categories` | `site`, `lang` | Categories, including those with no articles yet. | | `GET /v2/blog/tags` | `site` | Tags that have articles. | | `GET /v2/blog/feed/{site}/rss` | `lang` | RSS 2.0, no token. | | `GET /v2/blog/feed/{site}/atom` | `lang` | Atom 1.0, no token. | `site` is the site's domain (`mysite.com`; `https://`, uppercase and paths are also accepted and stripped). If you manage several sites, always send it: that way each website only sees its own articles. Only **published** articles, or scheduled ones whose date has passed, are returned. ## Listing `category` also includes subcategories; `q` searches the title, summary, tags and content. ```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 } ``` ## An article `format` decides which content comes back: `html` (default), `md` (Markdown only) or `both`. `markdownUrl` is always present: it's the article's public Markdown version, meant for AI. ```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` has the tags for the `` of your article page: include them as they are so search engines, social networks and AI understand it. - `tableOfContents` holds the `h2` and `h3` headings with their `id`, for a linked table of contents. - `translations` has the same article in other languages. - If your plan carries the badge on articles, the HTML ends with «Published with Jazui». ## Categories and tags Categories come sorted by their order and name, with `parentSlug` for subcategories and `articleCount`. Tags only appear if they have articles. `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 and Atom The 20 most recent articles of a verified site, with the full content. No token needed. ```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 ``` ## From the browser A minimal example with `fetch`. If your website is an SPA, remember bots see the blog thanks to your Jazui integration: `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; } ``` ## Caching Every response has an `ETag`. Send it in `If-None-Match` and you'll get `304` with no body if nothing changed. Feeds can be cached for 15 minutes. ## Errors - `404 not_found`: the article doesn't exist or isn't published. - `400 ambiguous`: articles with that `slug` exist on several sites; add `?site=`. --- # Sitemap, robots and llms.txt > Public files Jazui generates for each verified site: sitemap, robots.txt, llms.txt and Markdown for AI. Source page: https://docs.jazui.com/api/site-files/en Jazui generates on the fly, from your cache, blog and settings, the files search engines and AI look for on your website. They're public (no token), exist only for verified sites and can be cached for an hour. ## Addresses | Path | Content | |---|---| | `/v2/sites/{site}/sitemap.xml` | Sitemap with dates and language alternates. With more than 50,000 addresses it's an index pointing to `sitemap-1.xml`, `sitemap-2.xml`… | | `/v2/sites/{site}/robots.txt` | Your robots.txt, with the AI bots you blocked and the sitemap line. | | `/v2/sites/{site}/llms.txt` | Your site's index for language models (llmstxt.org format). | | `/v2/sites/{site}/llms-full.txt` | The same with each article's full content. | | `/v2/sites/{site}/articles/{lang}/{slug}.md` | An article in Markdown, clean for AI. | | `/v2/sites/{site}/visits.js` | Script that counts visits coming from AI answers. | ## Sitemap It includes, without duplicates: published articles (with their translations as `hreflang`), each language's blog index, categories with articles (if you set their address template) and your cached pages answering `200`, without redirects. If you'd rather upload your own sitemaps, you can do so under **Sitemaps**. `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 It starts from the robots.txt you write in the site settings (or `User-agent: *` and `Allow: /`), adds a group with the AI bots you blocked in robots.txt and the `Sitemap:` line if you don't have one. `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 It starts with your domain and a description (written in the site settings), continues with one section per language listing your articles (linked to their Markdown version) and ends with up to 1,000 of your cached pages. `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) ``` ## Serving them on your domain Search engines and AI look for these files on your domain (`mysite.com/robots.txt`). So they find them, serve them from there by proxying to Jazui, for example: `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; } ``` ## Visits from AI answers To know how many people reach your website from ChatGPT, Perplexity, Gemini, Copilot, Claude and other assistants, paste this line in your website's `` (the dashboard gives it to you with your domain under **Bot traffic › Visits from AI**). It only acts when the visit comes from an assistant, without cookies or personal data: ```html ``` ## IndexNow If you enable IndexNow in the site settings (**Blog and IndexNow**), Jazui notifies Bing, Yandex and the other search engines using it each time you publish, update or unpublish an article. For them to accept it, publish the file `https://mysite.com/{key}.txt` on your domain with the key as its only content (the key is in the settings) and click **Check**. --- # FAQ > Quick answers to the most common questions about Jazui. Source page: https://docs.jazui.com/faq/en ### Does Jazui affect my visitors? No. Your integration only diverts bot requests; people keep reaching your website as usual. If Jazui is slow or fails, bots also get your normal website (except with IIS and Apache, see [Integrations](https://docs.jazui.com/llms/en/integrations.txt)). ### Is it cloaking? Can Google penalize me? It isn't cloaking: bots get the same page a person sees, already rendered. Google calls it *dynamic rendering*. Cloaking would be showing bots different content; that's why Jazui doesn't add anything just for them (not even the badge) except what you add with rules, which must match the page. ### Google already runs JavaScript, why do I need this? Google does it in a second pass that can take a while, and depends on your JavaScript loading well. Also, AI bots and social networks don't run JavaScript. Try it with the scanner on [jazui.com](https://jazui.com). ### Does it work with my framework? With any website that works in a browser: Angular, React, Vue, Svelte, Blazor, Next.js, Nuxt… Jazui opens it with Chrome like a person. What matters is where it's hosted, to choose the integration. ### How fast does it answer a bot? From the cache, a few hundred milliseconds. The first time a page is requested it has to be rendered (as long as your website takes, up to the maximum you set). ### What happens when I change my website? Pages are recached automatically every so many days. To update them right away, recache them from the dashboard or with the [cache API](https://docs.jazui.com/llms/en/api-cache.txt) on deployment. ### Does every visit count as a request? Only bot visits your integration sends to Jazui. People's visits and files (images, CSS, JavaScript…) don't go through Jazui and don't count. ### Can I stop AI from training on my content? Yes: in [AI bots](https://docs.jazui.com/llms/en/panel-ai-bots.txt) you block training bots (in Jazui and in your robots.txt) and let through those that cite you in their answers. ### How much does the blog AI cost? Jazui doesn't charge for AI: you use your own key and your provider bills you. The plan only limits how many AI articles are written per month. ### Can I use Jazui with several sites or clients? Yes. Each site has its settings, blog, rules and, if you want, its own token; and with [Team](https://docs.jazui.com/llms/en/panel-team.txt) you give each person access only to the sites you choose. ### What data does Jazui store? The rendered pages, technical data from bot requests (90 days) and what you configure. Details are in the [privacy policy](https://jazui.com/privacy-policy). ### How do I cancel? In **My plan › Manage billing**. You keep the plan until the end of the paid period and then move to Free.