> ## Documentation Index
> Fetch the complete documentation index at: https://docs.querying.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# AI Rendering : des copies de vos pages que les agents IA peuvent lire

> querying.ai rend chaque page que vous publiez telle qu'une personne la voit et écrit le texte de ses images dans leur texte alternatif. Votre CDN conserve ces copies et les envoie à ChatGPT et aux autres agents IA, tandis que les personnes continuent de recevoir votre site.

Les agents IA lisent le HTML que votre serveur envoie. Ils n'exécutent aucun script : les prix, avis et caractéristiques
qu'un script dessine ne leur parviennent jamais, et ils ne lisent le texte d'une image que dans son texte alternatif.
AI Rendering rend chaque page que vous publiez telle qu'une personne la voit, retire les scripts et écrit le texte de chaque
image dans son texte alternatif. Votre CDN télécharge ces copies et répond avec elles aux agents IA. Les personnes, les
moteurs de recherche et toutes les autres requêtes atteignent votre site comme avant, et querying.ai n'est jamais sur le
chemin d'une requête vers votre site.

[GEO Score](/fr/research/geo-score.md) mesure l'écart : `rendering.score` est la part d'une page que contient une copie. Dès que
votre CDN envoie la copie, les agents qui la reçoivent en lisent autant.

## Fonctionnement

1. **Connectez votre site.** Vous recevez une clé de site une seule fois. Elle ne peut que lire les copies de votre site.
2. **Publiez des pages.** Envoyez l'URL d'une page. querying.ai la rend depuis le pays de votre site, conserve la copie et
   rend la page à nouveau tous les `refreshDays` jours (28 par défaut).
3. **Synchronisez les copies.** Toutes les 10 minutes, votre CDN lit la liste des copies avec la clé de site, télécharge les
   nouvelles copies et supprime celles qui ne sont plus listées.
4. **Répondez aux agents IA.** Quand un agent IA envoie un GET, votre CDN répond avec la copie stockée. Une page sans copie,
   une copie expirée et toute erreur vont à votre serveur d'origine.

## Connecter un site

```bash theme={"theme":"css-variables"}
curl -X POST https://api.querying.ai/v1/ai-rendering/sites \
  -H "Authorization: Bearer $QUERYING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "host": "www.example.com" }'
```

```json theme={"theme":"css-variables"}
{
  "success": true,
  "site": {
    "id": "5b0e9f52-6a3c-4f0e-8d0b-2c7e4a1f9d33",
    "host": "www.example.com",
    "refreshDays": 28,
    "country": "US",
    "keyPrefix": "rs_8f2c4a1b",
    "pages": 0,
    "createdAt": "2026-10-23T01:00:00.000Z",
    "updatedAt": "2026-10-23T01:00:00.000Z"
  },
  "key": "rs_8f2c4a1b…"
}
```

Conservez la `key` : seule cette réponse la contient, et votre CDN l'envoie. `POST /v1/ai-rendering/sites/{id}/key` en
délivre une nouvelle et arrête aussitôt l'ancienne.

| Champ | Obligatoire | Description |
| - | - | - |
| `host` | Oui | Le nom d'hôte public servi par votre CDN, par exemple `www.example.com`. Connectez chaque hôte une fois, jusqu'à 20 par compte. |
| `refreshDays` | Non | Jours entre deux rendus d'une page publiée, `1`–`28`, `28` par défaut. Le fichier d'une copie expire après 30 jours. |
| `country` | Non | D'où les pages sont rendues : `US` (par défaut) ou `KR`, comme le pays du lecteur de GEO Score. Choisissez le pays depuis lequel vos clients naviguent. |

Modifiez-les ensuite avec `PATCH /v1/ai-rendering/sites/{id}`.

## Publier une page

```bash theme={"theme":"css-variables"}
curl -X POST https://api.querying.ai/v1/ai-rendering/sites/5b0e9f52-6a3c-4f0e-8d0b-2c7e4a1f9d33/pages \
  -H "Authorization: Bearer $QUERYING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://www.example.com/products/sunscreen" }'
```

La page est en cours de rendu :

```json theme={"theme":"css-variables"}
{
  "success": true,
  "page": {
    "id": "a3d1c9e0-7b52-4e1f-9c84-0f6b2d5e7a10",
    "url": "https://www.example.com/products/sunscreen",
    "path": "/products/sunscreen",
    "status": "rendering",
    "copy": null,
    "taskId": null,
    "renderedAt": null,
    "expiresAt": null,
    "publishedAt": "2026-10-23T01:00:00.000Z",
    "refreshAt": "2026-10-23T07:00:00.000Z",
    "refreshTaskId": "7c4e2a90-1d3b-4f6a-8e25-9b0c1f3d4e57"
  },
  "taskId": "7c4e2a90-1d3b-4f6a-8e25-9b0c1f3d4e57"
}
```

Un rendu prend environ 30 secondes. Suivez-le avec `GET /v1/async/task/{taskId}`, ou lisez le `status` de la page dans
`GET /v1/ai-rendering/sites/{id}` :

* `ready` : la copie figure dans votre liste de copies. `copy` est son fichier HTML.
* `rendering` : la première copie est en cours de création.
* `failed` : le premier rendu a échoué. La page est rendue à nouveau à `refreshAt`.
* `expired` : la copie a expiré et a quitté la liste jusqu'au prochain rendu.

Un site contient jusqu'à 1 000 pages. Renvoyer une page publiée la rend tout de suite. Une page correspond à son chemin et à
sa requête, sans paramètres de suivi comme `utm_*` et `gclid`. `DELETE /v1/ai-rendering/sites/{id}/pages/{pageId}` retire une
page.

Pour publier la copie d'un [GEO Score](/fr/research/geo-score.md) sans nouveau rendu, envoyez son identifiant de tâche :
`{ "taskId": "0b6c1f7e-…" }`. Ce doit être un GEO Score terminé de votre compte, mesuré depuis le pays du site.

## Configurer votre CDN

Enregistrez ceci sous `ai-rendering.mjs` à côté du code de votre CDN. `syncCopies` aligne un stockage sur votre liste de
copies, et `copyFor` répond à partir de lui au GET d'un agent IA :

```js theme={"theme":"css-variables"}
export const COPIES_URL = "https://api.querying.ai/v1/ai-rendering/copies";

export const AGENTS = [
	"chatgpt-user",
	"codex",
	"claude-user",
	"claude-code",
	"perplexity-user",
	"gemini-deep-research",
	"duckassistbot",
	"amzn-user",
	"mistralai-user",
	"oai-searchbot",
];

export const DOWNLOADS_PER_SYNC = 40;

const EXPIRY_MARGIN_MS = 10 * 60_000;

const TRACKING_PARAM =
	/^(?:utm_[a-z0-9_]+|gclid|gbraid|wbraid|dclid|fbclid|msclkid|twclid|ttclid|srsltid|mc_cid|mc_eid|_ga|_gl)$/i;

export function pagePath(url) {
	const kept = [...url.searchParams].filter(([name]) => !TRACKING_PARAM.test(name));
	return (url.pathname || "/") + (kept.length ? "?" + new URLSearchParams(kept).toString() : "");
}

export async function copyKey(host, path) {
	const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(host.toLowerCase() + path));
	return "copy:" + [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
}

export function agentRequest(request, agents = AGENTS) {
	if (request.method !== "GET") return false;
	const userAgent = (request.headers.get("user-agent") ?? "").toLowerCase();
	return agents.some((token) => userAgent.includes(token));
}

export async function syncCopies(options) {
	const {
		siteKey,
		store,
		copiesUrl = COPIES_URL,
		downloads = DOWNLOADS_PER_SYNC,
		fetch: send = fetch,
		now = Date.now,
	} = options;
	if (!siteKey) throw new Error("AI Rendering: no site key");
	const answer = await send(copiesUrl, { headers: { "X-Querying-Site-Key": siteKey } });
	if (!answer.ok) throw new Error(`AI Rendering: the copy list answered ${answer.status}`);
	const { host, copies } = await answer.json();
	const stored = await store.list();
	const listed = new Set();
	const result = { listed: copies.length, downloaded: 0, failed: 0, waiting: 0, removed: 0 };
	for (const copy of copies) {
		const key = await copyKey(host, copy.path);
		listed.add(key);
		if (stored.get(key) === copy.renderedAt || Date.parse(copy.expiresAt) - now() < EXPIRY_MARGIN_MS) continue;
		if (result.downloaded + result.failed >= downloads) {
			result.waiting++;
			continue;
		}
		try {
			const file = await send(copy.copy);
			if (!file.ok) throw new Error(String(file.status));
			await store.put(key, await file.arrayBuffer(), { renderedAt: copy.renderedAt, expiresAt: copy.expiresAt });
			result.downloaded++;
		} catch {
			result.failed++;
		}
	}
	for (const key of stored.keys())
		if (!listed.has(key)) {
			await store.delete(key);
			result.removed++;
		}
	return result;
}

export async function copyFor(request, store, agents = AGENTS) {
	if (!agentRequest(request, agents)) return null;
	try {
		const url = new URL(request.url);
		const copy = await store.get(await copyKey(url.hostname, pagePath(url)));
		if (!copy || !(Date.parse(copy.expiresAt) > Date.now())) return null;
		return new Response(copy.body, {
			status: 200,
			headers: {
				"Content-Type": "text/html; charset=utf-8",
				"Cache-Control": "private, no-store",
				Vary: "User-Agent",
				"X-Querying-Rendering": "HIT",
				"X-Querying-Rendered-At": copy.renderedAt,
			},
		});
	} catch {
		return null;
	}
}
```

**Cloudflare Worker**

Le Worker conserve les copies dans Workers KV et les synchronise toutes les 10 minutes avec un cron trigger. Chaque copie
expire dans KV en même temps que son fichier.

```bash theme={"theme":"css-variables"}
npx wrangler kv namespace create AI_RENDERING
npx wrangler secret put QUERYING_SITE_KEY
```

`wrangler.toml` :

```toml theme={"theme":"css-variables"}
name = "ai-rendering"
main = "cloudflare-worker.mjs"
compatibility_date = "2026-10-01"
routes = [{ pattern = "www.example.com/*", zone_name = "example.com" }]
kv_namespaces = [{ binding = "AI_RENDERING", id = "<namespace id>" }]

[triggers]
crons = ["*/10 * * * *"]
```

`cloudflare-worker.mjs` :

```js theme={"theme":"css-variables"}
import { copyFor, syncCopies } from "./ai-rendering.mjs";

export function kvStore(kv) {
	return {
		async get(key) {
			const { value, metadata } = await kv.getWithMetadata(key, "arrayBuffer");
			return value && metadata ? { body: value, ...metadata } : null;
		},
		async list() {
			const stored = new Map();
			let cursor;
			do {
				const page = await kv.list({ prefix: "copy:", cursor });
				for (const key of page.keys) stored.set(key.name, key.metadata?.renderedAt);
				cursor = page.list_complete ? undefined : page.cursor;
			} while (cursor);
			return stored;
		},
		put: (key, body, meta) =>
			kv.put(key, body, { expiration: Math.floor(Date.parse(meta.expiresAt) / 1000), metadata: meta }),
		delete: (key) => kv.delete(key),
	};
}

export default {
	async fetch(request, env) {
		return (await copyFor(request, kvStore(env.AI_RENDERING))) ?? fetch(request);
	},
	async scheduled(_controller, env, ctx) {
		ctx.waitUntil(syncCopies({ siteKey: env.QUERYING_SITE_KEY, store: kvStore(env.AI_RENDERING) }));
	},
};
```

Avec l'offre Workers Free, KV accepte 1 000 écritures par jour : la première synchronisation d'un grand site peut
prendre plus d'une journée.

**Autres CDN et serveurs**

Utilisez le même module avec un stockage de votre plateforme : un objet doté de `get(key)`, `list()`,
`put(key, body, meta)` et `delete(key)`. `list()` renvoie chaque clé stockée avec le `renderedAt` enregistré avec elle.
Exécutez `syncCopies` selon un calendrier et `copyFor` à chaque requête de page. Si votre CDN met en cache par URL
seulement, ajoutez l'agent à la clé de cache pour qu'une personne ne reçoive jamais de copie.

Une fois la synchronisation suivante faite, vérifiez une page publiée comme ChatGPT l'ouvrirait. La copie porte
`X-Querying-Rendering: HIT` :

```bash theme={"theme":"css-variables"}
curl -sI -A "Mozilla/5.0 (compatible; ChatGPT-User/1.0; +https://openai.com/bot)" https://www.example.com/products/sunscreen
```

## La liste des copies

Votre CDN lit `GET https://api.querying.ai/v1/ai-rendering/copies` avec la clé de site dans `X-Querying-Site-Key` :

```json theme={"theme":"css-variables"}
{
  "success": true,
  "host": "www.example.com",
  "copies": [
    {
      "path": "/products/sunscreen",
      "url": "https://www.example.com/products/sunscreen",
      "copy": "https://api.querying.ai/v1/evidence/1792717200-9f3c2a7d1e4b.html",
      "renderedAt": "2026-10-23T01:00:31.000Z",
      "expiresAt": "2026-11-22T01:00:00.000Z"
    }
  ]
}
```

Elle liste chaque page publiée dont la copie n'a pas expiré. `copy` se télécharge sans clé. `renderedAt` change quand la page
est rendue à nouveau : ne téléchargez une copie que si son `renderedAt` est nouveau. Supprimez les copies stockées des pages
que la liste ne contient plus. La liste et les téléchargements sont gratuits.

## Agents

`AGENTS` dans le module choisit qui reçoit la copie, par un jeton présent n'importe où dans le User-Agent, sans tenir compte
de la casse.

* **Par défaut :** les agents qu'une personne envoie vers une page en posant une question (`chatgpt-user`, `codex`,
  `claude-user`, `claude-code`, `perplexity-user`, `gemini-deep-research`, `duckassistbot`, `amzn-user`, `mistralai-user`) et
  le robot de la recherche ChatGPT, `oai-searchbot`. La recherche ChatGPT répond à partir de la page que conserve son robot, ce
  que mesure le `score` de GEO Score. Son index change après qu'OpenAI a de nouveau exploré la page.
* **À ajouter si vous le souhaitez :** `claude-searchbot` et `perplexitybot`.
* **À laisser de côté :** les robots d'entraînement (GPTBot, ClaudeBot) et de recherche web (Googlebot, Bingbot). Googlebot
  exécute lui-même les scripts.

## Respecter les règles des moteurs de recherche

Envoyer aux robots un contenu différent de celui des personnes, c'est du cloaking. Google ne considère pas comme du cloaking
une page rendue pour les robots tant que le contenu est similaire
([Dynamic rendering](https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering)). OpenAI n'a
publié aucune politique de cloaking : tenez donc la copie à la règle de Google.

* La copie est la page dessinée par le navigateur d'une personne. querying.ai retire les scripts et n'ajoute que le texte des
  images, dans leur texte alternatif, la place du texte d'une image.
* Elle est servie à la propre URL de la page.
* Elle reste à jour. querying.ai la rend à nouveau tous les `refreshDays`, et le module cesse d'envoyer une copie expirée.
* Si vous modifiez une copie, contentez-vous de retirer ou de corriger : rectifier un caractère mal lu, supprimer une
  fenêtre surgissante. N'ajoutez ni affirmations, ni prix, ni liens que les personnes ne voient pas.

## Ce qu'est la copie

La copie est le HTML dessiné par le navigateur d'une personne après défilement jusqu'au bas de la page : le texte chargé par
les scripts, la page de chaque cadre à sa place, tous les scripts retirés sauf les données structurées (JSON-LD), et le texte
de chaque grande image ajouté à son texte alternatif. Les liens et les sources d'image restent absolus, si bien que la copie
se lit comme la page. C'est la page lors de son dernier rendu : un changement de prix ou de stock atteint les agents au rendu
suivant, au plus `refreshDays` plus tard, plus le délai de synchronisation de votre CDN. Le texte des images est recopié tel
qu'il a été lu : un caractère mal lu part tel quel.

## Facturation

Un rendu coûte 5 crédits, plus 5 par image dont le texte est lu et 2 par image que votre compte a lue au cours des 90 derniers
jours (le même fichier, lu de la même façon), dans la limite de 12 images. L'envoi réserve 65 crédits et la fin du rendu facture
ce qu'il a utilisé. Un rendu échoué n'est pas facturé. Une actualisation coûte la même chose et est facturée au compte du site ;
un compte sans crédits attend une heure avant que la page soit retentée. Connecter un site, publier la copie d'un GEO Score, la
liste des copies et le téléchargement des copies sont gratuits. Votre CDN envoie les copies : le trafic des agents ne coûte rien
chez querying.ai. Voir [Crédits et limites](/fr/concepts/credits-and-limits.md).

## Erreurs

| Statut | Code | Cause |
| - | - | - |
| `401` | `UNAUTHORIZED` | La liste des copies n'a reçu aucune clé de site, une clé remplacée ou la clé d'un site supprimé. |
| `402` | `INSUFFICIENT_CREDITS` | Le solde ne couvre pas la réserve de 65 crédits d'un rendu. |
| `403` | `KEY_SCOPE_DENIED` | La clé d'API est limitée à certains moteurs et `GEO_SCORE` n'en fait pas partie. |
| `404` | `NOT_FOUND` | Aucun site, aucune page ni aucun GEO Score de votre compte avec cet identifiant. |
| `409` | `SITE_EXISTS`, `SITE_LIMIT`, `PAGE_LIMIT`, `TASK_NOT_COMPLETED` | L'hôte est déjà connecté, le compte a 20 sites, le site a 1 000 pages, ou le GEO Score n'est pas terminé. |
| `410` | `COPY_EXPIRED` | La copie du GEO Score a expiré. Publiez la page par son URL. |
| `422` | `VALIDATION_ERROR`, `NO_RENDERED_COPY` | Un champ n'est pas valide, l'URL est sur un autre hôte, le GEO Score a été mesuré depuis un autre pays, ou il n'a produit aucune copie. |
| `429`, `503` | | La concurrence du forfait ou la file de rendu est pleine. Réessayez après `Retry-After`. |

Les points de terminaison figurent dans la [référence de l'API](/fr/api-reference/ai-rendering/create-site.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.