@anubhav-choubey/web-for-agents 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/LazyyVenom/Web-For-Agents/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/LazyyVenom/Web-For-Agents/actions/workflows/ci.yml)
4
4
 
5
- SEO and LLM-readiness helpers for React + Vite. At runtime it manages `<head>` metadata (title, description, canonical, Open Graph, Twitter) and emits typed JSON-LD structured data. At build time a Vite plugin prerenders every route to static HTML, writes a markdown version of each page, generates `llms.txt` and `llms-full.txt`, an `agents.json` map of the actions agents can take on each page, a `sitemap.xml` built from the discovered routes, and a `robots.txt` with an explicit policy for AI crawlers, so you decide which bots may train on your content and which may use it for search.
5
+ SEO and LLM-readiness helpers for React + Vite. At runtime it manages `<head>` metadata (title, description, canonical, Open Graph, Twitter) and emits typed JSON-LD. At build time a Vite plugin prerenders every route to static HTML and writes a markdown version of each page, `llms.txt`, `llms-full.txt`, an `agents.json` map of the actions agents can take, a `sitemap.xml`, and a `robots.txt` with an explicit AI-crawler policy, so you decide which bots may train on your content and which may use it for search.
6
6
 
7
7
  ## Install
8
8
 
@@ -10,31 +10,10 @@ SEO and LLM-readiness helpers for React + Vite. At runtime it manages `<head>` m
10
10
  npm i @anubhav-choubey/web-for-agents
11
11
  ```
12
12
 
13
- Requires Node 20.19 or later; Node 22.13 or later is recommended for builds, because prerendering is better isolated there (see [Security](#security)). The runtime components and hooks need `react >=18` and `react-dom >=18`, and the Vite plugin needs `vite >=5`; CI tests it against Vite 5, 6, 7 and 8. All three are optional peers, so the CLI works without them.
13
+ Requires Node 20.19 or later; Node 22.13 or later is recommended for builds, because prerendering is better isolated there (see [Security](https://github.com/LazyyVenom/Web-For-Agents/blob/master/docs/security.md)). The runtime needs `react >=18` and `react-dom >=18`, and the Vite plugin needs `vite >=5` (tested on Vite 5 to 8). All three are optional peers, so the CLI works without them.
14
14
 
15
15
  ## Quick start
16
16
 
17
- ```tsx
18
- import { SeoProvider, Seo, JsonLd, schema } from "@anubhav-choubey/web-for-agents";
19
-
20
- export function Root({ children }: { children: React.ReactNode }) {
21
- return (
22
- <SeoProvider defaults={{ siteName: "My Site", siteUrl: "https://example.com", titleTemplate: "%s | My Site" }}>
23
- {children}
24
- </SeoProvider>
25
- );
26
- }
27
-
28
- export function Post() {
29
- return (
30
- <>
31
- <Seo title="Hello world" description="My first post" canonical="/blog/hello" type="article" />
32
- <JsonLd data={schema.article({ headline: "Hello world", datePublished: "2026-10-28" })} />
33
- </>
34
- );
35
- }
36
- ```
37
-
38
17
  ```ts
39
18
  // vite.config.ts
40
19
  import { defineConfig } from "vite";
@@ -53,181 +32,49 @@ export default defineConfig({
53
32
  });
54
33
  ```
55
34
 
56
- `webForAgents()` returns an array of plugins (the build plugin and the dev-only overlay); put it in `plugins` as shown, and Vite flattens it.
57
-
58
- `robots.ai` is required: the plugin throws if you do not state a policy for both `training` and `search`. Files you place in `public/` (for example your own `robots.txt`) are never overwritten; the plugin logs a warning and skips generation.
59
-
60
- Use one `SeoProvider` per document. It owns the shared defaults and the base head tags; nested or multiple providers will overwrite each other.
61
-
62
- ## Prerendering
63
-
64
- After `vite build`, the plugin serves your `dist` locally, starts at `/` plus any `routes`, follows same-origin links (crawl), and writes one file per route:
65
-
66
- - `dist/<route>/index.html`: the rendered page with its `<head>` (title, Open Graph, JSON-LD) and a `data-wfa-prerendered` attribute on `<html>`. Your client bundle is kept, so the app still boots.
67
- - `dist/<route>.md`: the markdown version (`dist/index.md` for `/`).
68
- - `dist/200.html`: your original SPA shell (the built `index.html` before `/` is prerendered over it), for host fallbacks. See [SPA fallback](#spa-fallback).
69
- - `llms.txt`, `llms-full.txt`, `agents.json`, `sitemap.xml`, `robots.txt`.
70
-
71
- Non-page links (other origins, `mailto:`, files with an extension, `exclude`d routes) are not followed. A path whose last segment contains a dot (`/v1.2`, `/files/report.final`) is treated as a file and not crawled; list such pages in `routes` to prerender them. Routes marked `<Seo noindex>` are written but left out of the sitemap and `llms.txt`. Nothing is written until every route has rendered: if any route fails, the build rejects with one error listing each failing route and renderer.
72
-
73
- The default renderer is [happy-dom](https://github.com/capricorn86/happy-dom), which needs no browser. Pages that need a real browser can use your installed Chrome or Edge:
74
-
75
- ```sh
76
- npm i -D playwright-core
77
- ```
78
-
79
- No browser is ever downloaded; the plugin launches the Chrome/Edge already on your machine (or `prerender.executablePath`). Use `renderer: "chrome"` for everything, `routeRenderers` for specific routes, or `fallback: "chrome"` to retry only routes that fail under happy-dom.
80
-
81
- ```ts
82
- webForAgents({
83
- siteUrl: "https://example.com",
84
- robots: { ai: { training: "block", search: "allow" } },
85
- prerender: {
86
- fallback: "chrome",
87
- routeRenderers: [{ match: "/charts/*", renderer: "chrome" }],
88
- },
89
- });
90
- ```
91
-
92
- happy-dom renders in a separate, locked-down Node.js process, and pages can only load your build's own files unless you allow other origins with `prerender.allowOrigins`. See [Security](#security) for what that means for your pages.
93
-
94
- A synchronous infinite loop in a page cannot be interrupted inside happy-dom. The render process is killed `prerender.timeoutMs` plus 10 seconds after the route started, which fails that route and any others rendering in the same process. Render such pages with the Chrome renderer (`routeRenderers` or `renderer: "chrome"`), which enforces the timeout per page.
95
-
96
- Pressing Ctrl-C during prerendering reports `prerender interrupted` rather than a list of failed routes.
97
-
98
- happy-dom 21 fails to load a module when a regex literal follows a character outside its narrow list of punctuation, such as `return`, `!`, `typeof`, `-`, whitespace or `)`. This is common in minified code, e.g. `return/^[a-z]+:/i.test(e)`. When any route may render under happy-dom, the local prerender server works around it by wrapping each regex literal in the JS it serves in parentheses. With mixed renderers, Chrome gets the same rewritten JS, which is harmless. Your `dist` files are not changed.
99
-
100
- Set `prerender: false` to skip all of this; `markdown`, `llms` and `agentMap` then turn off too, with a warning if you set them.
101
-
102
- ### Readiness
103
-
104
- A route is captured once the page has loaded, no `fetch`/XHR is in flight, and the DOM has been quiet for a moment (`prerender.idleMs`). If you load data some other way (timers, a custom client, a Suspense boundary), tell the renderer to wait:
105
-
106
35
  ```tsx
107
- import { holdPrerender } from "@anubhav-choubey/web-for-agents";
108
-
109
- useEffect(() => {
110
- const release = holdPrerender();
111
- load().then(setData).finally(release);
112
- }, []);
113
- ```
114
-
115
- `holdPrerender()` returns an idempotent release function. It sets `window.__WFA_READY__`, which only the prerenderer reads, so it is harmless in a normal browser session. A route that is not ready after `prerender.timeoutMs` (default 15000) fails the build.
116
-
117
- Pages that never go idle (polling requests, a long-lived `fetch` stream, a DOM that keeps changing) time out the same way. Hold and release around the work the page actually needs, so the renderer captures it once that is done, or raise `prerender.timeoutMs` for slow routes.
118
-
119
- ### SPA fallback
120
-
121
- After prerendering, `dist/index.html` is the rendered `/` page. If your host serves `index.html` for unknown URLs, deep links that were not prerendered would get the home page's HTML: it flashes before the app boots, and `hydrateApp` would hydrate the wrong markup. Point the fallback at `dist/200.html` instead, which is the untouched shell. If `public/200.html` exists, it is used as is and not generated.
122
-
123
- Netlify, `public/_redirects`:
124
-
125
- ```
126
- /* /200.html 200
127
- ```
128
-
129
- Vercel, `vercel.json`:
130
-
131
- ```json
132
- { "rewrites": [{ "source": "/(.*)", "destination": "/200.html" }] }
133
- ```
134
-
135
- Vercel serves existing files before applying rewrites, so prerendered pages are still served directly.
136
-
137
- Cloudflare Pages: when the output has no top-level `404.html` it already serves the SPA shell, but that is `index.html`. Add `public/_redirects` with the same rule as Netlify:
138
-
139
- ```
140
- /* /200.html 200
141
- ```
142
-
143
- nginx (`$uri/` serves `<route>/index.html` through the default `index` directive):
36
+ import { SeoProvider, Seo, JsonLd, schema } from "@anubhav-choubey/web-for-agents";
144
37
 
145
- ```nginx
146
- location / {
147
- try_files $uri $uri/ /200.html;
38
+ export function Root({ children }: { children: React.ReactNode }) {
39
+ return (
40
+ <SeoProvider defaults={{ siteName: "My Site", siteUrl: "https://example.com", titleTemplate: "%s | My Site" }}>
41
+ {children}
42
+ </SeoProvider>
43
+ );
148
44
  }
149
- ```
150
45
 
151
- Surge and other hosts that look for `200.html` pick it up with no configuration.
152
-
153
- ### Hydration
154
-
155
- By default the prerendered HTML is replaced when the app boots (`createRoot`), so the page is correct without JavaScript and head tags are not duplicated. To hydrate instead, use `hydrateApp`; it hydrates when the page was prerendered and falls back to a normal render otherwise (for example in `vite dev`):
156
-
157
- ```tsx
158
- import { hydrateApp } from "@anubhav-choubey/web-for-agents";
159
-
160
- hydrateApp(document.getElementById("root")!, <App />);
161
- ```
162
-
163
- ## Content for agents
164
-
165
- The markdown for a page comes from its `<main>` element (change it with `markdown.contentSelector`). Mark a different region explicitly with `ContentRegion`, and keep boilerplate such as newsletter forms out with `ExcludeFromContent`:
166
-
167
- ```tsx
168
- import { ContentRegion, ExcludeFromContent } from "@anubhav-choubey/web-for-agents";
169
-
170
- <ContentRegion as="article">
171
- <h1>Pricing</h1>
172
- <ExcludeFromContent>Subscribe to our newsletter</ExcludeFromContent>
173
- </ContentRegion>
46
+ export function Post() {
47
+ return (
48
+ <>
49
+ <Seo title="Hello world" description="My first post" canonical="/blog/hello" type="article" />
50
+ <JsonLd data={schema.article({ headline: "Hello world", datePublished: "2026-10-28" })} />
51
+ </>
52
+ );
53
+ }
174
54
  ```
175
55
 
176
- Both accept `as` (default `div`) and pass other props to the element. If a page has neither a `ContentRegion` nor a match for `contentSelector`, the content is taken from `<body>` minus `header`, `nav`, `footer` and `aside`, and the build logs a warning naming those routes.
177
-
178
- ## Markdown URLs
179
-
180
- Every prerendered page gets a markdown twin: `/about` is served as `/about.md` and `/` as `/index.md`. The HTML advertises it with `<link rel="alternate" type="text/markdown" href="https://example.com/about.md">`. Links in the markdown are absolute.
56
+ `robots.ai` is required: the plugin throws if you do not state a policy for both `training` and `search`. Use one `SeoProvider` per document. More in [Head tags and JSON-LD](https://github.com/LazyyVenom/Web-For-Agents/blob/master/docs/seo.md).
181
57
 
182
- ## llms.txt and llms-full.txt
58
+ ## What the build produces
183
59
 
184
- `llms.txt` lists your pages with links to their `.md` versions (title and description come from the root page unless you set `llms.title` / `llms.description`). `llms-full.txt` concatenates every page's markdown, each prefixed with `Source: <url>`. `llms-full.txt` needs `markdown` enabled.
60
+ After `vite build`, the plugin crawls from `/` plus `routes` and writes:
185
61
 
186
- When a page has no `<h1>`, its markdown starts with a `# title` heading. That heading, and the titles and descriptions in `llms.txt` and `llms-full.txt`, are written on a single line, with `\`, `[`, `]` and `<` backslash-escaped, so they cannot close link text or open raw HTML (`Vec<T>` is written `Vec\<T>`). Page bodies are converted from HTML with their text escaped, so text that looks like HTML never becomes live HTML, and `<script>`, `<style>`, `<noscript>` and `<template>` elements are dropped.
62
+ | File | Contents |
63
+ | --- | --- |
64
+ | `dist/<route>/index.html` | Prerendered HTML with the full `<head>`; your client bundle still boots. |
65
+ | `dist/<route>.md` | Markdown version of the page (`dist/index.md` for `/`). |
66
+ | `dist/200.html` | The untouched SPA shell, for host fallbacks. |
67
+ | `llms.txt`, `llms-full.txt` | Page index linking to the `.md` files, and all pages' markdown in one file. |
68
+ | `agents.json` | Every page and the agent actions on it. |
69
+ | `sitemap.xml`, `robots.txt` | Sitemap of discovered routes, and your AI-crawler policy. |
187
70
 
188
- ## Blocked AI crawlers
71
+ Already have your own prerender script? Skip the plugin's prerender and run `npx web-for-agents generate dist --site-url https://example.com --ai-training block --ai-search allow` after it. It adds the same `.md`, `llms.txt`, `robots.txt`, `sitemap.xml` and `agents.json` files to your HTML. See [Using your own prerender](https://github.com/LazyyVenom/Web-For-Agents/blob/master/docs/prerendering.md#using-your-own-prerender).
189
72
 
190
- If `robots.ai.training` or `robots.ai.search` is `"block"` while `.md` files, `llms.txt`, `llms-full.txt` or `agents.json` are enabled, the build logs a warning: those files are still generated and publicly fetchable, so blocking bots in `robots.txt` does not hide them. Set `markdown: false`, `llms: false` and/or `agentMap: false` if you do not want them published.
73
+ Files you place in `public/` are never overwritten. Content regions, `holdPrerender`, `hydrateApp`, host fallback configs and memory limits: see [Prerendering](https://github.com/LazyyVenom/Web-For-Agents/blob/master/docs/prerendering.md).
191
74
 
192
75
  ## Agent actions
193
76
 
194
- Mark the things an agent can do on a page. Each marked action is described in `agents.json` at build time and, in browsers that support it, registered as a [WebMCP](https://webmachinelearning.github.io/webmcp/) tool at runtime.
195
-
196
- ### `AgentForm`
197
-
198
- A `<form>` that is also an action. Without a `handler`, the input schema is derived from the form's fields and the agent's input is filled in and submitted through the form itself (the form fallback), so your existing `onSubmit` and validation run as for a user:
199
-
200
- ```tsx
201
- import { AgentForm, agentParam } from "@anubhav-choubey/web-for-agents";
202
-
203
- <AgentForm agent={{ name: "contact", description: "Send us a message" }} onSubmit={send}>
204
- <input type="email" name="email" required {...agentParam("Your email address")} />
205
- <select name="topic">
206
- <option value="sales">Sales</option>
207
- <option value="support">Support</option>
208
- </select>
209
- <button type="submit">Send</button>
210
- </AgentForm>
211
- ```
212
-
213
- The derived schema uses each field's `name`, type (`email`, `url`, `number`, `checkbox`, ...), `required`, `min`/`max`, `minlength`/`maxlength` and `pattern`; `<select>` options and radio groups become an `enum`. Some fields are never exposed: they do not appear in the schema or in `agents.json`, and an agent cannot fill them, even through another field with the same name. These are:
214
-
215
- - disabled and `readonly` fields, and fields inside a `<fieldset disabled>` (except in its first `<legend>`);
216
- - fields without a `name`;
217
- - hidden, password and file inputs, and buttons;
218
- - fields hidden by markup: inside a `hidden`, `inert` or `aria-hidden="true"` element, a closed `<dialog>`, or the content of a closed `<details>`.
219
-
220
- Visibility is read from markup only, not CSS. One exception: a checkbox or radio with its own `hidden` attribute is still exposed when it has a visible, enabled `<label>` (the styled-label pattern). Options that are disabled, `hidden` or inside a disabled `<optgroup>` are left out of the `enum`.
221
-
222
- A form action without a `handler` has exactly one schema: the one derived from the form. It is what the tool advertises and what the input is checked against, using the form's fields at the moment the action runs. If you also pass `inputSchema`, it is ignored and a warning is logged once; add a `handler` if you want your own schema.
223
-
224
- Fields are re-read after every value the agent sets, so a field revealed by an earlier one (a "Company" box behind a "Business account" checkbox) is filled in the same call, and its value is checked when it appears. A key that names no field of the form is rejected before anything is written; a key whose field stays hidden is rejected without submitting. The advertised WebMCP schema is not rebuilt on every render: after you reveal fields (open a modal, switch a tab, move to a wizard step), call `refreshAgentActions({ force: true })`.
225
-
226
- A form action reports `{ status: "submitted", url, messages }`, where `messages` is the text of any `role="alert"`, `role="status"` or `aria-live` element in the form shortly after submit, or `{ status: "invalid", errors }` when the input or the browser's form validation fails. The form is submitted through its default button (its first submit or image button, including one outside the form linked by `form="..."`), so that button's `name`/`value`, `formaction` and `formmethod` apply. If that button is disabled or hidden by markup, or the form is hidden or inside a `<fieldset disabled>`, the agent gets `{ status: "error" }` and nothing is filled or submitted.
227
-
228
- ### `useAgentAction`
229
-
230
- For actions that are not a form, or when you want to run code instead of submitting one. Spread `hintProps` on the element and attach `ref`:
77
+ Mark what an agent can do on a page. Each action is listed in `agents.json` and, in browsers that support it, registered as a [WebMCP](https://webmachinelearning.github.io/webmcp/) tool.
231
78
 
232
79
  ```tsx
233
80
  import { useAgentAction } from "@anubhav-choubey/web-for-agents";
@@ -247,363 +94,75 @@ function DeleteAccount() {
247
94
  }
248
95
  ```
249
96
 
250
- With a `handler`, the agent's input is validated against `inputSchema` (an object JSON Schema, empty when omitted) and passed to it. Nested objects and array items are checked the same way, with errors reported by path (`addr.zip`, `items[0].id`); keys not listed in `properties` are rejected unless that object sets `additionalProperties: true`. A string longer than 10,000 characters is rejected, without running the regex, when its schema has a `pattern` or `format: "email"`; this keeps slow polynomial patterns fast, but does not make exponential ones such as `(a+)+$` safe, so avoid those. The handler's return value is reported as `{ status: "ok", result }`. Without a `handler`, `ref` decides: a `<form>` uses the form fallback above, any other element is clicked (`{ status: "clicked" }`), unless it is disabled (`disabled`, `aria-disabled="true"` or inside a `<fieldset disabled>`) or hidden by markup (as above), which reports `{ status: "error" }` instead. An action with neither a handler nor an attached element is not exposed. If a handler throws, the error is caught and reported to the agent as `{ status: "error", message }`.
251
-
252
- `hintProps` puts `data-agent-action`, `data-agent-description`, `aria-description` and, for destructive actions, `data-agent-destructive="true"` on the element, so the hints are also in the prerendered HTML. `agentParam(description)` adds `data-agent-param` to a form field; it is used as that field's description in the schema (otherwise its `<label>`, `aria-label` or `placeholder`).
253
-
254
- `listAgentActions()` and `executeAgentAction(name, input)` give you the same registry directly. `refreshAgentActions()` re-checks the registry; pass `{ force: true }` to also re-describe actions that are already registered with WebMCP and re-register any whose schema changed.
255
-
256
- ### Names and duplicates
257
-
258
- Names must be 1 to 128 characters of `A-Z a-z 0-9 _ - .`; an invalid name throws. Names are per page: if two mounted components use the same name, only the first mounted one is exposed (in WebMCP and in `agents.json`) and a warning is logged. Give each a unique name, or use one action with an id input.
259
-
260
- ### Destructive actions
261
-
262
- `destructive: true` asks the user before the action runs, with `window.confirm` by default (pass `confirm` for your own dialog, `confirmMessage` for the text). The checks that would make the action fail (invalid input, a disabled or hidden submit button, a hidden form or element, keys that name no field) run first, so the user is never asked to approve an action that cannot run.
263
-
264
- The default text names the action, states how many fields the agent provided, and lists every field on its own line as `path: value` (nested values as `address.zip` or `items[0].id`). Fields declared in the schema come first, in schema order, and every declared field the agent provided is shown; for a form action, every key the agent sends is listed. Each name and value is cut to `max(24, 1000 / declared fields)` characters with a "…(truncated)" marker. Keys outside the schema follow, capped at about 1,000 characters in total, with a note of how many were omitted. Unusual keys and all values are JSON-encoded, and bidi, line-separator, invisible and control characters are shown as `\uXXXX` escapes, so the input cannot reorder, hide or add lines.
265
-
266
- If the user declines, the agent gets `{ status: "cancelled", reason: "user-declined" }` and nothing runs. It also sets the WebMCP `consequentialHint`; `readOnly: true` sets `readOnlyHint`.
267
-
268
- ### WebMCP
269
-
270
- WebMCP is feature-detected: when `document.modelContext` (or the older `navigator.modelContext`) exists, each exposed action is registered with `registerTool`; otherwise nothing is registered and nothing breaks. Nothing branches on the user agent. Tools are unregistered when their component unmounts, so the tool list follows what is on screen. The advertised `inputSchema` is captured when the tool is registered: a tool is registered again when its name, title, description, flags, `inputSchema` or handler presence changes, but not when a form's fields change. Execution never relies on that snapshot; it always validates against the current schema (for a form, its current fields). A failed registration is logged as a warning, does not affect the page, and is retried the next time the action list is updated. If `modelContext` appears after the page loads (for example, injected by an extension), actions are registered on the next mount, unmount or `refreshAgentActions()` call.
271
-
272
- ### agents.json
273
-
274
- After prerendering, the plugin writes `agents.json` with every listed page and the actions found on it, and links it from `llms.txt` in an `## Agents` section. Pages are sorted by path and actions by name; pages without actions are listed with `"actions": []`. Titles and descriptions are the same as in `llms.txt`:
275
-
276
- ```json
277
- {
278
- "version": 1,
279
- "generator": "@anubhav-choubey/web-for-agents",
280
- "site": { "url": "https://example.com", "name": "My Site" },
281
- "llms": "https://example.com/llms.txt",
282
- "webmcp": { "api": "document.modelContext", "spec": "https://webmachinelearning.github.io/webmcp/" },
283
- "pages": [
284
- {
285
- "path": "/contact",
286
- "url": "https://example.com/contact",
287
- "title": "Contact",
288
- "description": "Get in touch",
289
- "markdown": "https://example.com/contact.md",
290
- "actions": [
291
- {
292
- "name": "contact",
293
- "description": "Send us a message",
294
- "inputSchema": {
295
- "type": "object",
296
- "properties": {
297
- "email": { "type": "string", "format": "email", "description": "Your email address" },
298
- "topic": { "type": "string", "enum": ["sales", "support"], "description": "Options: sales (Sales), support (Support)" }
299
- },
300
- "additionalProperties": false,
301
- "required": ["email"]
302
- },
303
- "destructive": false,
304
- "readOnly": false,
305
- "kind": "form"
306
- }
307
- ]
308
- }
309
- ]
310
- }
311
- ```
312
-
313
- `kind` is `"handler"`, `"form"` or `"click"`. `llms` is omitted when `llms: false`, and `markdown` when `markdown: false`.
314
-
315
- Use `agentMap: { fileName: "ai.json" }` to change the file name (a plain `.json` name, no directories), or `agentMap: false` to turn the file and the `llms.txt` section off; WebMCP registration at runtime is unaffected. The CLI audit looks for the default `agents.json` unless you pass `--agent-map`. A `public/agents.json` of your own is never overwritten. The agent map needs prerender.
316
-
317
- ## Hooks and components
318
-
319
- `<Seo {...props} />` is a component wrapper around `useSeo(props)`. Call the hook directly when you prefer hooks or need to compute props in a component:
97
+ `AgentForm` turns a form into an action; its schema comes from the fields, and the agent submits through your own `onSubmit`:
320
98
 
321
99
  ```tsx
322
- import { useSeo } from "@anubhav-choubey/web-for-agents";
100
+ import { AgentForm, agentParam } from "@anubhav-choubey/web-for-agents";
323
101
 
324
- export function Post({ title }: { title: string }) {
325
- useSeo({ title, canonical: "/blog/hello", type: "article" });
326
- return null;
327
- }
102
+ <AgentForm agent={{ name: "contact", description: "Send us a message" }} onSubmit={send}>
103
+ <input type="email" name="email" required {...agentParam("Your email address")} />
104
+ <button type="submit">Send</button>
105
+ </AgentForm>
328
106
  ```
329
107
 
330
- Nested `Seo`/`useSeo` calls merge, with the deeper one winning per tag.
331
-
332
- ### Props
333
-
334
- `SeoProvider` `defaults`:
335
-
336
- | Field | Description |
337
- | --- | --- |
338
- | `siteName` | `og:site_name`, and the title when a page sets none. |
339
- | `siteUrl` | Absolute site URL. Paths in `canonical` and `image` resolve against it; enables the default canonical. |
340
- | `titleTemplate` | Title format, `%s` is replaced by the page title, e.g. `"%s \| My Site"`. |
341
- | `description` | Default description. |
342
- | `image` | Default Open Graph and Twitter image. |
343
- | `twitterSite` | `twitter:site` handle. |
344
- | `locale` | `og:locale`. |
345
-
346
- `Seo` / `useSeo`:
347
-
348
- | Prop | Description |
349
- | --- | --- |
350
- | `title` | Page title, formatted with `titleTemplate`. Also used for `og:title` and `twitter:title`. |
351
- | `description` | Meta, Open Graph and Twitter description. |
352
- | `canonical` | Absolute URL or path, or `false` for none. See [Canonical URL](#canonical-url). |
353
- | `image` | Open Graph and Twitter image; a large Twitter card is used when set. |
354
- | `type` | `og:type`: `"website"` (default), `"article"` or `"product"`. |
355
- | `noindex` | Emits `robots: noindex, nofollow`; prerendered but left out of the sitemap and `llms.txt`. |
356
- | `meta` | Extra `{ name, content }` or `{ property, content }` tags. |
357
-
358
- ### Canonical URL
359
-
360
- `canonical` takes an absolute URL or a path. A path, with or without a leading slash, resolves under `defaults.siteUrl`, including its path, the same way sitemap URLs do: with `siteUrl: "https://example.com/sub"`, `canonical="/about"` and `canonical="about"` both become `https://example.com/sub/about`.
361
-
362
- When no `<Seo>` passes one and `SeoProvider` has `defaults.siteUrl`, the head defaults the canonical link and `og:url` to the page's public URL. The current pathname comes from `document.location`, has the query and hash removed and the `siteUrl` path stripped, and loses any trailing slash except at the root. So `/sub/about/` gives `https://example.com/sub/about` and `/sub/` gives `https://example.com/sub/`. Prerendered pages carry this canonical in their HTML.
363
-
364
- The pathname is read in effects, never during render, and the head is rebuilt on `popstate`, on the Navigation API's `navigatesuccess` where it exists, and otherwise on `history.pushState` and `replaceState`. These listeners are installed when the first `<Seo>` mounts and removed when the last one unmounts, so a layout `<Seo>` stays correct across client-side navigation.
365
-
366
- The default is the lowest-priority layer, so an explicit `canonical` on any `<Seo>` in the tree wins over it, including when a deeper `<Seo>` omits `canonical`. Among explicit values the deepest `<Seo>` wins. `canonical={false}` emits neither the canonical link nor `og:url`, and a deeper `false` removes an ancestor's explicit canonical. Without `siteUrl`, no canonical is emitted.
367
-
368
- ## Schema builders
369
-
370
- `schema` returns typed JSON-LD nodes for `<JsonLd data={...} />`:
371
-
372
- - `schema.article`
373
- - `schema.product`
374
- - `schema.faqPage`
375
- - `schema.breadcrumbList`
376
- - `schema.organization`
377
- - `schema.webSite`
378
-
379
- ## Plugin options
380
-
381
- | Option | Type | Description |
382
- | --- | --- | --- |
383
- | `siteUrl` | `string` | Required. Absolute http(s) URL of the deployed site. |
384
- | `routes` | `string[]` | Extra routes to prerender, for pages no link reaches. `/` is always included. |
385
- | `exclude` | `Array<string \| RegExp>` | Routes to leave out. A string is an exact path, or a prefix when it ends in `/*`. A leading slash is optional. |
386
- | `robots.ai.training` | `"allow" \| "block"` | Required. Policy for AI training crawlers. |
387
- | `robots.ai.search` | `"allow" \| "block"` | Required. Policy for AI search and user-agent fetchers. |
388
- | `robots.ai.overrides` | `Record<string, "allow" \| "block">` | Per-bot override, matched case-insensitively. |
389
- | `robots.disallow` | `string[]` | Extra `Disallow` paths for all user agents. |
390
- | `sitemap` | `boolean` | Generate `sitemap.xml` and the `Sitemap:` line. Default `true`. |
391
- | `prerender` | `object \| false` | Prerender routes after the build. `false` disables prerender, markdown, llms and the agent map. |
392
- | `prerender.crawl` | `boolean` | Follow same-origin links from `/` and `routes`. Default `true`. |
393
- | `prerender.renderer` | `"happy-dom" \| "chrome" \| "msedge"` | Default renderer. Default `"happy-dom"`. |
394
- | `prerender.fallback` | `"chrome" \| "msedge" \| false` | Retry routes that fail in this browser. Default `false`. |
395
- | `prerender.routeRenderers` | `Array<{ match: string \| RegExp; renderer }>` | Per-route renderer; `match` uses `exclude` semantics, first match wins. Default `[]`. |
396
- | `prerender.executablePath` | `string` | Browser binary to launch instead of the installed Chrome/Edge channel. |
397
- | `prerender.timeoutMs` | `number` | Per-route readiness timeout. Default `15000`. |
398
- | `prerender.concurrency` | `number` | Routes rendered at once. Default `8` when only happy-dom renders, `4` when `renderer` or any `routeRenderers` entry is `"chrome"` or `"msedge"` (`fallback` does not count). |
399
- | `prerender.maxRoutes` | `number` | Stop discovering after this many routes (a warning is logged). Default `500`. |
400
- | `prerender.allowOrigins` | `string[]` | http(s) origins (`"https://cdn.example.com"`) pages may load from while prerendering. Every other request outside your build is blocked and fails like a network error. Default `[]`. |
401
- | `prerender.idleMs` | `number` | How long, in ms, a page must be quiet (no DOM changes, no requests in flight) to count as rendered. Default `50` for happy-dom once every timer and request has settled, else `200`; always `200` for Chrome. |
402
- | `prerender.isolation` | `{ codeGeneration?: boolean }` | The happy-dom render process. `codeGeneration: true` lets code build functions from strings with Node's own `Function`, which also lets page code escape happy-dom; prefer rendering such routes with Chrome. The page's own `eval` and `new Function` work either way. Default `{ codeGeneration: false }`. |
403
- | `markdown` | `{ contentSelector?: string } \| false` | Write `.md` files. `false` disables them and `llms-full.txt`. Default `{ contentSelector: "main" }`. |
404
- | `llms` | `{ title?: string; description?: string } \| false` | Write `llms.txt` and `llms-full.txt`. Default on. |
405
- | `agentMap` | `boolean \| { fileName?: string }` | Write `agents.json` and link it from `llms.txt`. Needs prerender. Default `{ fileName: "agents.json" }`. |
406
- | `overlay` | `boolean \| { shortcut?: string; contentSelector?: string }` | Dev overlay, injected by `vite` (serve) only, under any `base` (e.g. `"/sub/"`), and skipped when Vitest runs. `false` disables it. Default on, shortcut `"alt+shift+a"`, `contentSelector` from `markdown.contentSelector` (else `"main"`). |
407
-
408
- ## AI crawlers
409
-
410
- Training bots (`robots.ai.training`):
411
-
412
- - `GPTBot`
413
- - `ClaudeBot`
414
- - `CCBot`
415
- - `Google-Extended`
416
- - `Applebot-Extended`
417
- - `Meta-ExternalAgent`
418
- - `Bytespider`
419
- - `cohere-training-data-crawler`
420
-
421
- Search bots (`robots.ai.search`):
422
-
423
- - `OAI-SearchBot`
424
- - `ChatGPT-User`
425
- - `Claude-SearchBot`
426
- - `Claude-User`
427
- - `PerplexityBot`
428
- - `Perplexity-User`
429
-
430
- The list is exported as `AI_BOTS` from `@anubhav-choubey/web-for-agents/vite`.
108
+ Agents can never reach hidden, password, file, disabled or `readonly` fields, or fields hidden by markup. Full API, confirm dialog and field rules: [Agent actions](https://github.com/LazyyVenom/Web-For-Agents/blob/master/docs/agent-actions.md).
431
109
 
432
110
  ## CLI
433
111
 
434
- Audit any URL for AI-agent readiness. Nothing is installed in the target site.
112
+ Audit any URL for AI-agent readiness, with nothing installed in the target site:
435
113
 
436
114
  ```sh
437
- npx @anubhav-choubey/web-for-agents audit example.com
438
- npx @anubhav-choubey/web-for-agents audit https://example.com/docs --json --min-score 80
115
+ npx @anubhav-choubey/web-for-agents audit https://example.com --min-score 80
439
116
  ```
440
117
 
441
- Flags:
442
-
443
- - `--json` prints the report as one JSON document on stdout. Page data is kept as the site sent it (see [Security](#cli-audit)).
444
- - `--min-score <0-100>` exits with code 1 when the score is lower.
445
- - `--timeout <ms>` sets the per-request timeout (default 10000).
446
- - `--agent-map <path>` sets where the agent map lives. A file name (`map.json`) is looked for in the same places as `agents.json`. A path starting with `/` or a full URL is fetched as given.
447
- - `--no-private-redirects` refuses redirects from a public host to a private, loopback or link-local address. By default they are followed with a warning.
448
- - `-h`, `--help` prints usage.
449
-
450
- A URL without a scheme gets `http://` for `localhost`, `127.0.0.1` and `[::1]`, and `https://` for every other host.
451
-
452
- `robots.txt` is read from the site root. The sitemap URL comes from its `Sitemap:` line, or `/sitemap.xml` if there is none. `llms.txt` and `agents.json` are looked for at the root and in each parent directory of the page and of its markdown alternate link. For example, auditing `https://example.com/sub/` also tries `/sub/llms.txt` and `/sub/agents.json`, which covers sites deployed under a subpath. If more than one copy exists, the deepest one is used. A missing file's message lists every URL tried.
453
-
454
- Checks and weights (total 100):
455
-
456
- | Check | Weight |
457
- | --- | --- |
458
- | Content without JavaScript | 25 |
459
- | JSON-LD | 12 |
460
- | robots.txt | 10 |
461
- | Title | 8 |
462
- | Meta description | 8 |
463
- | Sitemap | 8 |
464
- | Open Graph | 7 |
465
- | llms.txt | 7 |
466
- | Markdown copy | 6 |
467
- | Canonical URL | 5 |
468
- | Agent map | 4 |
469
-
470
- A passing check earns its full weight, a warning half, a failure none. Each non-passing check prints a `Fix:` line.
471
-
472
- Exit codes: `0` ok, `1` score below `--min-score`, `2` usage error or fatal error (unreachable page, non-2xx status, not HTML, timeout).
473
-
474
- The programmatic API is available from `@anubhav-choubey/web-for-agents/cli` (`audit`, `runCli`, `formatText`, and the `AuditOptions`, `AuditReport`, `CheckResult`, `CheckId` and `CheckStatus` types).
475
-
476
- ### Auditing a local preview
477
-
478
- You can audit a build locally with `vite preview`:
479
-
480
- ```sh
481
- npx vite build && npx vite preview --port 4173
482
- npx @anubhav-choubey/web-for-agents audit localhost:4173
483
- ```
484
-
485
- The build writes absolute URLs from `siteUrl` into the canonical link, the sitemap, the markdown alternate link and the `Sitemap:` line in `robots.txt`. If `siteUrl` is your production origin, those URLs point away from the preview. The audit does not fetch them, so a local run never calls production. Instead it warns and names the other origin. For example:
486
-
487
- ```
488
- WARN Canonical URL 2.5/5 Canonical https://example.com/ points to https://example.com (different origin than the audited page)
489
- Fix: Audit the deployed site, or build with siteUrl set to http://localhost:4173 to audit this preview.
490
- ```
491
-
492
- To get a clean local score, build with `siteUrl` set to the preview origin, for example `siteUrl: process.env.SITE_URL ?? "https://example.com"` with `SITE_URL=http://localhost:4173 npx vite build`. Otherwise, audit the deployed site.
493
-
494
- ## Security
495
-
496
- ### Prerendering
497
-
498
- Prerendering runs your app's JavaScript at build time, including anything it loads. happy-dom is **not a sandbox**: code in a happy-dom page can, in principle, escape to the Node.js process that runs it. Several layers sit between a prerendered page and your build machine:
499
-
500
- - **No outside network during the build.** Pages can only load your build's own files from the local prerender server on 127.0.0.1. Requests to any other origin (scripts, stylesheets, `fetch`/XHR, frames, WebSockets) are blocked and fail like a network error. A blocked request is not fatal by itself; if the page needs it to render, the build fails and names the blocked URL. Allow its origin with `prerender: { allowOrigins: ["https://api.example.com"] }`, and only allow origins you trust: their code runs during your build.
501
- - **Isolated renderer.** happy-dom renders in a separate Node.js process with a scrubbed environment: only `PATH`, `HOME`, the temp directory variables and `NODE_ENV` (plus the system variables Windows needs for sockets). CI tokens, API keys and other environment variables are not visible to page code.
502
- - **No code generation from strings.** The render process cannot build functions from strings with Node's own `Function`, the usual way out of happy-dom. The page's own `eval` and `new Function` keep working. If a route fails because something needed Node-level code generation, the error says so; render that route with Chrome (`prerender.routeRenderers` or `prerender.fallback`), or set `prerender: { isolation: { codeGeneration: true } }`. That option lets page code escape happy-dom again, so use it only for code you trust.
503
- - **Contained network, even after an escape.** Inside the render process, TCP, TLS, HTTP, DNS, `fetch` and WebSockets can only reach the prerender server and your `allowOrigins`; UDP and listening for connections are blocked. These wrappers are locked so code cannot remove them.
504
- - **Permission model.** On Node.js 22.13+ (or 23.5+), the render process also runs under Node's permission model: it can read only the renderer's own code and `node_modules`, and cannot write files, start processes or workers, or reach Node's internal bindings. Because Node cannot yet limit network access by host, this is what keeps the network wrappers above from being bypassed.
505
- - **Contained output.** The local server never serves files outside your build output, even through symlinks, and answers only requests addressed to 127.0.0.1. Routes whose file names would be too long are skipped with a warning, and if writing the output fails, the files already written are rolled back.
506
-
507
- **Older Node.js.** Without the permission model (Node.js before 22.13, or 23.x before 23.5), a build that renders with happy-dom logs a warning once. The layers above still apply, but code that escapes happy-dom could open raw sockets, start a child process (for example `curl`) or a worker, which loads without the network wrappers, and read or write any file the build user can access.
508
-
509
- **For untrusted scripts** (user-generated pages, third-party widgets you must allow), build on Node.js 22.13 or later, or use the Chrome renderer (`prerender: { renderer: "chrome" }`), which runs pages in Chrome's own sandbox with the same network blocking. In Chrome, `<link rel="preconnect">` and `dns-prefetch` can still resolve DNS and open connections without sending a request, and blocking WebSockets needs playwright-core 1.48 or later.
510
-
511
- **Classic scripts.** Under the permission model, happy-dom cannot make synchronous requests itself. To keep classic `<script src>` tags in your `index.html` working (no `async`, `defer` or `type="module"`), they are fetched from your build ahead of time and served from memory: up to 4 MiB per script and 10 MiB in total. A script over a limit, or one that could not be fetched within `prerender.timeoutMs`, is skipped with a warning naming it. Only same-origin scripts are preloaded. Under the permission model, any other synchronous load, such as a classic script the page inserts later, one from an allowed origin, or a synchronous XHR, is refused: the build fails if the page needs it, and otherwise warns once per request, listing the routes. Add `type="module"` or `defer` to the script, make the request asynchronous, or render the route with Chrome. Vite's own scripts are modules and are unaffected.
512
-
513
- ### Agent actions
514
-
515
- - Input is validated strictly against the action's schema before anything runs, including nested objects and arrays. Keys not in the schema are rejected unless `additionalProperties: true` is set. See [`useAgentAction`](#useagentaction).
516
- - Some form fields are never exposed: hidden, password and file inputs; disabled and `readonly` fields; and fields hidden by markup (`hidden`, `inert`, `aria-hidden="true"`, a closed `<dialog>` or `<details>`). A checkbox or radio hidden only by its own `hidden` attribute stays exposed through a visible, enabled label. See [`AgentForm`](#agentform).
517
- - Destructive actions ask the user first, and the default dialog shows every declared field, so the agent cannot hide a field by padding others. Actions that would fail are refused before the dialog. See [Destructive actions](#destructive-actions).
518
- - The WebMCP schema is captured when a tool is registered. After revealing fields, call `refreshAgentActions({ force: true })` so agents see the current form.
519
-
520
- ### CLI audit
521
-
522
- - The text report strips terminal control and bidi characters from everything the audited site supplies (titles, URLs, error text), collapses whitespace and shortens very long values, so a page cannot rewrite your terminal.
523
- - `--json` keeps those values exactly as the site sent them, with control and bidi characters written as `\u` escapes. The JSON is safe to print, but tools that render its fields should sanitize them.
524
- - If a redirect goes from a public host to a private, loopback or link-local address (for example `169.254.169.254`), the audit follows it and adds a `Warning:` line (`warnings` in JSON). Pass `--no-private-redirects` (or `audit(url, { blockPrivateRedirects: true })`) to refuse such redirects. The check resolves each host before the request, and the request resolves it again, so a DNS-rebinding host can still reach a private address: treat it as a guard against accidents, not a sandbox.
525
- - robots.txt is read up to 500 KiB (the minimum RFC 9309 requires), and patterns longer than 512 characters are ignored. When either limit is hit, the robots check says so in its message.
526
-
527
- ## Performance and memory
528
-
529
- Prerendering is fast. As approximate measurements on a laptop: the 12-page demo in `examples/demo` builds and prerenders in about 1.5 seconds, and a 500-route test site prerenders in under 10 seconds with the default settings.
530
-
531
- Every page being rendered is held in memory at once. These figures are approximate. A typical page takes tens of MB, but a heavy page (a large DOM, big data payloads) can peak at around 500 MB. Peak memory is roughly `prerender.concurrency` times the heaviest page, plus about 150 MB for the build and 100 MB for the render process; the Chrome renderer adds Chrome's own processes on top. On small CI runners or containers (1 to 2 GB), lower `prerender.concurrency`, for example to `2`. If a render runs out of memory, the build fails with a message suggesting exactly that, rather than crashing.
118
+ It scores 0 to 100 across content without JavaScript, JSON-LD, robots.txt, title, meta description, sitemap, Open Graph, llms.txt, the markdown copy, canonical URL and the agent map, and prints a `Fix:` line for each miss. Flags, weights and JSON output: [CLI](https://github.com/LazyyVenom/Web-For-Agents/blob/master/docs/cli.md).
532
119
 
533
120
  ## Dev overlay
534
121
 
535
- While `vite` (the dev server) runs, the plugin adds a small "WFA" button to every page. Click it or press `Alt+Shift+A` to open a panel showing what an agent sees on the current page:
536
-
537
- - title (and whether it came from `<title>` or the first `<h1>`), description, canonical, robots, Open Graph and Twitter tags
538
- - each JSON-LD block, with its `@type`s and any parse error
539
- - the markdown generated from the content region (the same code that writes the `.md` files), with a warning when there is no `<main>`
540
- - agent actions: registered handlers, `data-agent-action` markup and WebMCP declarative tools
541
-
542
- The panel lives in a shadow root, so it does not pick up or leak styles, and it refreshes as the page changes.
122
+ While `vite` (the dev server) runs, a "WFA" button on every page (or `Alt+Shift+A`) opens a panel showing what an agent sees: head tags, JSON-LD, the generated markdown and agent actions. It is never included in `vite build`. See [Dev overlay](https://github.com/LazyyVenom/Web-For-Agents/blob/master/docs/overlay.md).
543
123
 
544
- The overlay is dev-only. The plugin injects it only in serve mode, so `vite build` never includes it and no overlay code ships in production. It is also skipped when `VITEST` is set, so Vitest browser mode and test servers don't get a panel. It works with any Vite `base`: with `base: "/sub/"` the script loads from `/sub/@id/…`, and the panel strips the base from the URL to find the route.
545
-
546
- The markdown in the panel matches the generated `.md` file. Links resolve against `siteUrl` plus the route, and a page with no `<title>` or `<h1>` is titled with its route. When neither `[data-wfa-content]` nor your `contentSelector` matches, the panel names the selector it looked for.
124
+ ## Security
547
125
 
548
- ```ts
549
- webForAgents({
550
- siteUrl: "https://example.com",
551
- robots: { ai: { training: "block", search: "allow" } },
552
- overlay: { shortcut: "ctrl+shift+k", contentSelector: "#content" },
553
- });
554
- ```
126
+ - Prerendering runs your app's code at build time. happy-dom is **not a sandbox**; it renders in a separate, locked-down Node.js process with a scrubbed environment.
127
+ - During the build, pages can only load your build's own files unless you allow other origins with `prerender.allowOrigins`.
128
+ - Build on Node.js 22.13+ for the permission model, or use the Chrome renderer for untrusted scripts.
129
+ - Agent input is validated strictly against the action's schema, and destructive actions ask the user first.
130
+ - The CLI strips terminal control and bidi characters from site data in its text report.
555
131
 
556
- A shortcut is any of `alt`, `shift`, `ctrl`, `meta` plus one letter, digit or `/`; an invalid one throws when the config loads. Set `overlay: false` to turn it off.
132
+ Details: [Security](https://github.com/LazyyVenom/Web-For-Agents/blob/master/docs/security.md).
557
133
 
558
- Prefer the Vite plugin: it never injects the overlay into `vite build`, whatever the environment or `--mode`. If you mount it yourself, don't guard on `import.meta.env.DEV`, `PROD` or `MODE`: `DEV` is true whenever `NODE_ENV` is not `production`, and `vite build --mode development` still passes a `MODE === "development"` check, so both can ship the overlay. Guard on `import.meta.hot` instead. Only the dev server defines it, and Vite replaces it with a statically false value in builds, so the whole block (including the dynamic import) is removed:
134
+ ## Key options
559
135
 
560
- ```ts
561
- if (import.meta.hot) {
562
- import("@anubhav-choubey/web-for-agents/overlay").then((m) =>
563
- m.mountOverlay({ shortcut: "alt+shift+a", siteUrl: "https://example.com", base: import.meta.env.BASE_URL }),
564
- );
565
- }
566
- ```
567
-
568
- Pass `siteUrl` and `base` so the panel's markdown links match the build; both are optional.
569
-
570
- ## Advanced exports
571
-
572
- - `HeadTag` (main entry): the type of one managed `<head>` tag: a title, meta or link with its merge key.
573
- - `serializeJsonLd(data)` (main entry): the escaped JSON string `<JsonLd>` puts in its script, for rendering JSON-LD yourself.
574
- - `CONTENT_ATTR` (`"data-wfa-content"`, main entry): the attribute `ContentRegion` sets, for marking the content region in plain HTML.
575
- - `EXCLUDE_ATTR` (`"data-wfa-exclude"`, main entry): the attribute `ExcludeFromContent` sets.
576
- - `PRERENDERED_ATTR` (`"data-wfa-prerendered"`, main entry): set on `<html>` in prerendered pages.
577
- - `READY_GLOBAL` (`"__WFA_READY__"`, main entry): the window flag `holdPrerender()` sets to `false` while a hold is active.
578
- - `OVERLAY_HOST_ATTR` (`"data-wfa-overlay"`, `/overlay` entry): the attribute on the overlay's host element, for excluding it from your own DOM queries or tests.
136
+ | Option | Description |
137
+ | --- | --- |
138
+ | `siteUrl` | Required. Absolute http(s) URL of the deployed site. |
139
+ | `robots.ai.training` / `robots.ai.search` | Required. `"allow"` or `"block"` for AI training and AI search crawlers. |
140
+ | `routes` | Extra routes to prerender, for pages no link reaches. |
141
+ | `exclude` | Routes to leave out (exact path, `/*` prefix or `RegExp`). |
142
+ | `prerender` | Renderer, timeouts, concurrency, `allowOrigins`; `false` turns off prerender, markdown, llms and the agent map. |
143
+ | `markdown`, `llms`, `agentMap`, `sitemap`, `overlay` | Turn each output off with `false`, or configure it. |
144
+
145
+ All options and the AI crawler list: [Plugin options](https://github.com/LazyyVenom/Web-For-Agents/blob/master/docs/options.md).
146
+
147
+ ## Documentation
148
+
149
+ - [Plugin options](https://github.com/LazyyVenom/Web-For-Agents/blob/master/docs/options.md)
150
+ - [Head tags and JSON-LD](https://github.com/LazyyVenom/Web-For-Agents/blob/master/docs/seo.md)
151
+ - [Prerendering](https://github.com/LazyyVenom/Web-For-Agents/blob/master/docs/prerendering.md)
152
+ - [Agent actions](https://github.com/LazyyVenom/Web-For-Agents/blob/master/docs/agent-actions.md)
153
+ - [CLI](https://github.com/LazyyVenom/Web-For-Agents/blob/master/docs/cli.md)
154
+ - [Dev overlay](https://github.com/LazyyVenom/Web-For-Agents/blob/master/docs/overlay.md)
155
+ - [Security](https://github.com/LazyyVenom/Web-For-Agents/blob/master/docs/security.md)
156
+ - [Advanced exports](https://github.com/LazyyVenom/Web-For-Agents/blob/master/docs/advanced.md)
157
+ - [Changelog](https://github.com/LazyyVenom/Web-For-Agents/blob/master/CHANGELOG.md)
579
158
 
580
159
  ## Development
581
160
 
582
161
  ```sh
583
- npm ci
584
- npm run typecheck # tsc --noEmit
585
- npm test # unit, Vite and e2e tests (vitest)
586
- npm run build # tsup into dist/
587
- npm run test:pack # packs the package, installs the tarball into a fresh Vite app, builds it and audits it
588
- npm run demo:dev # demo site with the dev overlay
589
- npm run demo:build # prerender the demo into dist-demo/
162
+ npm ci && npm test && npm run build
590
163
  ```
591
164
 
592
- `npm run test:pack` is kept out of `npm test`: it builds `dist`, runs `npm pack`, and installs the tarball with `vite`, `react`, `react-dom` and `react-router` from the npm registry into a temp app copied from `test/fixtures/pack-app`, so it needs the network (or a warm npm cache). Set `PACK_VITE=5|6|7|8` to pick the app's Vite major (default 8).
593
-
594
- Tests that need a real browser use your installed Chrome. They are skipped when Chrome cannot be launched; set `WFA_CHROME=skip` to always skip them, or `WFA_CHROME=require` to fail instead of skipping (CI does this).
595
-
596
- To run the Vite tests against another Vite major, swap it in without touching the lockfile, then run them:
597
-
598
- ```sh
599
- node scripts/install-vite.mjs 5
600
- npx vitest run test/vite test/e2e test/cli/audit.e2e.test.ts
601
- npm ci # back to the locked versions
602
- ```
603
-
604
- CI runs one job on every push and pull request (Node 22: typecheck, tests, build, pack and the demo build). The full matrix (Node 20.19, 22 and 24, Vite 5 to 7, and the packed package against Vite 5 to 8) runs only when started manually with "Run workflow" on the Actions tab, for example before a release.
605
-
606
- The demo in `examples/demo` uses every feature; its README lists what each page shows.
165
+ Scripts, CI and testing against other Vite majors: [Development](https://github.com/LazyyVenom/Web-For-Agents/blob/master/docs/development.md).
607
166
 
608
167
  ## License
609
168