> ## 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: AI エージェントが読めるページのコピー

> querying.ai が公開したページを人が見るとおりにレンダリングし、画像の文字を alt テキストに書き込んでコピーを作ります。サイトの CDN はこのコピーを保管して ChatGPT などの AI エージェントに送り、人にはサイトをそのまま見せます。

AI エージェントはサーバーが送る HTML を読みます。スクリプトを実行しないため、スクリプトが描く価格、レビュー、仕様は届かず、画像の中の
文字は alt テキストからしか読みません。AI Rendering は公開したページを人が見るとおりにレンダリングし、スクリプトを外し、画像ごとに文字を
alt テキストに書き込みます。サイトの CDN はこのコピーをダウンロードして保管し、AI エージェントに返します。人、検索エンジン、その他の
リクエストはこれまでどおりサイトに届き、querying.ai はサイトへのリクエストの経路に入りません。

[GEO Score](/ja/research/geo-score.md) がこの差を測ります。`rendering.score` はコピーが含むページの割合です。CDN がコピーを送れば、受け取る
エージェントはその分を読みます。

## 仕組み

1. **サイトを接続します。** サイトキーは一度だけ表示されます。このキーはサイトのコピーを読むことしかできません。
2. **ページを公開します。** ページの URL を送ると、querying.ai がサイトの国からレンダリングしてコピーを保管し、`refreshDays` 日ごと
   (既定 28 日)に再レンダリングします。
3. **コピーを同期します。** CDN が 10 分ごとにサイトキーでコピー一覧を読み、新しいコピーをダウンロードし、一覧から消えたコピーを削除します。
4. **AI エージェントに返します。** AI エージェントが GET を送ると、CDN は保管したコピーで応答します。コピーのないページ、期限切れのコピー、
   エラーはオリジンに送ります。

## サイトの接続

```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…"
}
```

`key` を保管してください。このレスポンスにしかなく、CDN が送ります。`POST /v1/ai-rendering/sites/{id}/key` は新しいキーを返し、
古いキーをすぐに止めます。

| フィールド | 必須 | 説明 |
| - | - | - |
| `host` | はい | CDN が配信する公開ホスト名です(例: `www.example.com`)。ホストごとに一度接続し、アカウントあたり 20 件までです。 |
| `refreshDays` | いいえ | 公開したページを再レンダリングする間隔(日)で `1`–`28`、既定は `28` です。コピーのファイルは 30 日で期限切れになります。 |
| `country` | いいえ | ページをレンダリングする国で、`US`(既定)または `KR` です。GEO Score の読者の国と同じです。顧客がアクセスする国を選んでください。 |

あとから `PATCH /v1/ai-rendering/sites/{id}` で変更できます。

## ページの公開

```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" }'
```

ページをレンダリングしています:

```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"
}
```

レンダリングには 30 秒ほどかかります。`GET /v1/async/task/{taskId}` で追うか、`GET /v1/ai-rendering/sites/{id}` でページの
`status` を読みます:

* `ready`: コピーがコピー一覧にあります。`copy` がその HTML ファイルです。
* `rendering`: 最初のコピーを作っています。
* `failed`: 最初のレンダリングが失敗しました。`refreshAt` に再レンダリングします。
* `expired`: コピーが期限切れになり、次のレンダリングまで一覧から外れました。

サイトあたり 1,000 ページまでです。公開済みのページをもう一度送ると、今すぐ再レンダリングします。ページはパスとクエリで区別し、
`utm_*` や `gclid` などの計測パラメータは除きます。`DELETE /v1/ai-rendering/sites/{id}/pages/{pageId}` でページを取り下げます。

[GEO Score](/ja/research/geo-score.md) が作ったコピーを再レンダリングせずに公開するには、その task id を送ります:
`{ "taskId": "0b6c1f7e-…" }`。アカウントの完了した GEO Score で、サイトの国から測ったものである必要があります。

## CDN の設定

次を CDN のコードの隣に `ai-rendering.mjs` として保存します。`syncCopies` はストアをコピー一覧に合わせ、`copyFor` は AI エージェント
の GET にストアのコピーで応答します:

```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**

Worker はコピーを Workers KV に置き、cron で 10 分ごとに同期します。コピーは KV でコピーのファイルと同時に期限切れになります。

```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) }));
	},
};
```

Workers Free プランでは KV の書き込みが 1 日 1,000 回のため、大きなサイトは最初の同期に 1 日以上かかることがあります。

**ほかの CDN とサーバー**

同じモジュールに、そのプラットフォームのストアをつなぎます。ストアは `get(key)`、`list()`、`put(key, body, meta)`、
`delete(key)` を持つオブジェクトです。`list()` は保存したキーごとに保存時の `renderedAt` を返します。`syncCopies` は定期的に、
`copyFor` はページへのリクエストごとに実行します。URL だけでキャッシュする CDN では、エージェントをキャッシュキーに加えて、人に
コピーが届かないようにしてください。

次の同期が終わったあと、公開したページを ChatGPT が開くように確認します。コピーには `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
```

## コピー一覧

CDN は `X-Querying-Site-Key` にサイトキーを入れて `GET https://api.querying.ai/v1/ai-rendering/copies` を読みます:

```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"
    }
  ]
}
```

コピーが期限切れでない公開ページをすべて含みます。`copy` はキーなしでダウンロードできます。ページを再レンダリングすると
`renderedAt` が変わるので、`renderedAt` が新しいときだけダウンロードしてください。一覧にないページのコピーは削除します。一覧と
ダウンロードは無料です。

## エージェント

モジュールの `AGENTS` がコピーを受け取る相手を決めます。User-Agent のどこにあっても、大文字と小文字を区別せずトークンで照合します。

* **既定:** 人が質問しながらページに送るエージェント(`chatgpt-user`、`codex`、`claude-user`、`claude-code`、`perplexity-user`、
  `gemini-deep-research`、`duckassistbot`、`amzn-user`、`mistralai-user`)と、ChatGPT 検索のクローラー `oai-searchbot` です。
  ChatGPT 検索はこのクローラーが取得したページから答え、GEO Score の `score` がそのページを測ります。検索インデックスは OpenAI が
  再クロールしたあとに変わります。
* **必要なら追加:** `claude-searchbot`、`perplexitybot`。
* **入れない:** 学習用クローラー(GPTBot、ClaudeBot)と Web 検索のクローラー(Googlebot、Bingbot)。Googlebot はスクリプトを自分で実行します。

## 検索エンジンのルールを守る

ボットに人と違う内容を送るとクローキングです。Google は、ボット向けにレンダリングしたページでも内容が近ければクローキングとみなしません
([Dynamic rendering](https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering))。OpenAI はクローキングの
方針を公開していないため、コピーを Google の基準に合わせます:

* コピーは人のブラウザが描いたページです。querying.ai はスクリプトを外し、画像の中の文字だけをその画像の alt テキストに加えます。alt は
  画像の文字が入る場所です。
* コピーはそのページの URL でだけ送ります。
* コピーは最新に保たれます。querying.ai が `refreshDays` ごとに再レンダリングし、モジュールは期限切れのコピーを送りません。
* コピーを編集するなら、削除か修正だけにしてください。読み違えた文字の修正やポップアップの削除は問題ありません。人に見えない主張、
  価格、リンクを加えないでください。

## コピーの中身

コピーは、人のブラウザがページを最後までスクロールしたあとに描いた HTML です。スクリプトが読み込んだ文字、元の位置に入ったフレームの
ページがあり、構造化データ(JSON-LD)を除くスクリプトはすべて外され、大きな画像ごとに文字が alt テキストに入ります。リンクと画像の
アドレスは絶対アドレスのまま残り、ページと同じように読めます。コピーは最後のレンダリング時点のページです。価格や在庫の変更は、次の
レンダリング(最大 `refreshDays` 日後)に CDN の同期時間を足した分だけ遅れてエージェントに届きます。画像の文字は読んだとおりに入るため、
読み違えた文字もそのまま送られます。

## 課金

レンダリング 1 回で 5 クレジット、文字を読んだ画像ごとに 5 クレジット、アカウントが過去 90 日に読んだ画像(同じファイル、同じ読み方)は
1 枚 2 クレジットで、最大 12 枚です。送信時に 65 クレジットを確保し、完了時に使った分だけ差し引きます。失敗したレンダリングは
課金しません。更新も同じ規則でサイトのアカウントに課金し、クレジットが足りないアカウントは 1 時間後に再試行します。サイトの接続、
GEO Score のコピーの公開、コピー一覧、コピーのダウンロードは無料です。コピーは CDN が送るため、エージェントのトラフィックに querying.ai の
費用はかかりません。[クレジットと上限](/ja/concepts/credits-and-limits.md)を参照してください。

## エラー

| ステータス | コード | 原因 |
| - | - | - |
| `401` | `UNAUTHORIZED` | コピー一覧のリクエストにサイトキーがない、置き換えたキー、または削除したサイトのキーです。 |
| `402` | `INSUFFICIENT_CREDITS` | 残高がレンダリングの 65 クレジットの確保に足りません。 |
| `403` | `KEY_SCOPE_DENIED` | API キーがエンジンを制限しており、`GEO_SCORE` が含まれていません。 |
| `404` | `NOT_FOUND` | その id のサイト、ページ、GEO Score がアカウントにありません。 |
| `409` | `SITE_EXISTS`、`SITE_LIMIT`、`PAGE_LIMIT`、`TASK_NOT_COMPLETED` | 接続済みのホスト、アカウントのサイト 20 件、サイトのページ 1,000 件、完了していない GEO Score です。 |
| `410` | `COPY_EXPIRED` | GEO Score のコピーが期限切れです。URL で公開してください。 |
| `422` | `VALIDATION_ERROR`、`NO_RENDERED_COPY` | フィールドが不正、別ホストの URL、別の国で測った GEO Score、またはコピーを作れなかった GEO Score です。 |
| `429`、`503` | | プランの同時実行数かレンダリングのキューがいっぱいです。`Retry-After` のあとに再送してください。 |

エンドポイントは [API reference](/ja/api-reference/ai-rendering/create-site.md) にあります。


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