diff --git a/docs/README.md b/docs/README.md
index 642402b..7ad9b35 100755
--- a/docs/README.md
+++ b/docs/README.md
@@ -27,6 +27,12 @@ aeo.js provides native integrations for the following frameworks:
| Vite | [vite.md](./vite.md) | Plugin | ✅ Stable |
| Angular | [angular.md](./angular.md) | Post-build | ✅ Stable |
| Webpack | [webpack.md](./webpack.md) | Plugin | ✅ Stable |
+| **Vanilla / Static HTML** | [vanilla.md](./vanilla.md) | CLI | ✅ Stable |
+
+## Additional references
+
+- **[CLI Reference](./cli.md)** — every command (`init` · `generate` · `check` · `report`), every flag
+- **[Custom JSON-LD Recipes](./json-ld.md)** — copy-paste schemas for FAQ, HowTo, Product, Article, Recipe, Event, VideoObject, BreadcrumbList
## Quick Start
@@ -149,6 +155,18 @@ module.exports = {
[→ Full Webpack Guide](./webpack.md)
+### Vanilla JS / Static HTML
+No framework? Use the CLI directly on any static site or hand-rolled HTML.
+
+```bash
+npx aeo.js generate \
+ --url https://mysite.com \
+ --title "My Site" \
+ --out public
+```
+
+[→ Full Vanilla Guide](./vanilla.md) · [→ CLI Reference](./cli.md)
+
## What Gets Generated?
When you integrate aeo.js, it automatically generates:
diff --git a/docs/angular.md b/docs/angular.md
index bed46db..7e36e1a 100644
--- a/docs/angular.md
+++ b/docs/angular.md
@@ -273,8 +273,163 @@ The extractor strips scripts and boilerplate, prefers ``. If your Angular
```
+## Deployment
+
+The generated AEO files live in `dist//browser` (Angular 17+) or `dist/` (older versions) — deploy them with whatever you'd already use for static Angular hosting.
+
+### Vercel
+
+```jsonc
+// vercel.json
+{
+ "buildCommand": "npm run build",
+ "outputDirectory": "dist/my-app/browser"
+}
+```
+
+Vercel runs `npm run build`, which runs the `postbuild` hook, which generates the AEO files into the same output directory — all served as static assets automatically.
+
+### Netlify
+
+```toml
+# netlify.toml
+[build]
+ command = "npm run build"
+ publish = "dist/my-app/browser"
+```
+
+### Cloudflare Pages
+
+In the Cloudflare dashboard:
+- **Build command:** `npm run build`
+- **Build output directory:** `dist/my-app/browser`
+
+No `wrangler.toml` changes needed — Pages picks up the `postbuild` hook automatically.
+
+### Firebase Hosting
+
+```jsonc
+// firebase.json
+{
+ "hosting": {
+ "public": "dist/my-app/browser",
+ "ignore": ["firebase.json", "**/.*", "**/node_modules/**"]
+ }
+}
+```
+
+```bash
+npm run build
+firebase deploy
+```
+
+## Examples
+
+### Blog with prerendered routes
+
+```ts
+// scripts/aeo.mjs
+import { postBuild } from 'aeo.js/angular';
+
+await postBuild({
+ title: 'Tech Blog',
+ description: 'In-depth technical articles on Angular, RxJS, and the web platform',
+ url: 'https://techblog.dev',
+ schema: {
+ enabled: true,
+ organization: { name: 'Tech Blog', url: 'https://techblog.dev' },
+ defaultType: 'Article',
+ },
+});
+```
+
+Prerender every route in `angular.json`'s build target; `Article` becomes the default schema type for each prerendered page, which is what you want for content-heavy sites.
+
+### Documentation Site
+
+```ts
+// scripts/aeo.mjs
+import { postBuild } from 'aeo.js/angular';
+
+await postBuild({
+ title: 'My API Documentation',
+ description: 'REST + GraphQL API reference and integration guides',
+ url: 'https://docs.myapi.com',
+ schema: {
+ enabled: true,
+ organization: { name: 'My API', url: 'https://docs.myapi.com' },
+ },
+ // Doc sites often have hand-listed nav routes that aren't all in *.routes.ts
+ pages: [
+ { pathname: '/', title: 'Overview', description: 'Start here' },
+ { pathname: '/auth', title: 'Authentication' },
+ { pathname: '/api/users', title: 'Users API' },
+ { pathname: '/api/orders', title: 'Orders API' },
+ { pathname: '/guides/quick', title: 'Quick Start' },
+ ],
+});
+```
+
+### E-commerce SPA
+
+For an SPA that fetches product data at runtime and doesn't prerender, list the static marketing routes explicitly and let your product pages live in `pages` only for the sitemap:
+
+```ts
+import { postBuild } from 'aeo.js/angular';
+
+await postBuild({
+ title: 'My Store',
+ description: 'Quality products, fast shipping',
+ url: 'https://mystore.com',
+ robots: {
+ allow: ['/'],
+ disallow: ['/checkout', '/account', '/api'],
+ },
+ schema: {
+ enabled: true,
+ organization: { name: 'My Store', url: 'https://mystore.com' },
+ },
+ pages: [
+ { pathname: '/', title: 'Home' },
+ { pathname: '/products', title: 'All Products' },
+ { pathname: '/about', title: 'About' },
+ { pathname: '/contact', title: 'Contact' },
+ ],
+});
+```
+
+For full per-product schema (rich product cards in AI shopping answers), see [Custom JSON-LD Recipes → Product](./json-ld.md#product).
+
+### Marketing site (single page)
+
+```ts
+import { postBuild } from 'aeo.js/angular';
+
+await postBuild({
+ title: 'Acme Cloud',
+ description: 'Enterprise-grade infrastructure for modern teams',
+ url: 'https://acme.cloud',
+ schema: {
+ enabled: true,
+ organization: {
+ name: 'Acme Inc.',
+ url: 'https://acme.cloud',
+ logo: 'https://acme.cloud/logo.png',
+ sameAs: [
+ 'https://twitter.com/acme',
+ 'https://github.com/acme',
+ 'https://linkedin.com/company/acme',
+ ],
+ },
+ },
+});
+```
+
## Further Reading
+- [CLI Reference](./cli.md) — full CLI flag/command reference
+- [Custom JSON-LD Recipes](./json-ld.md) — FAQ, HowTo, Product, Article, Recipe, Event
- [aeo.js Reference Configuration](https://aeojs.org/reference/configuration/)
- [Generated Files](https://aeojs.org/features/generated-files/)
- [GEO Audit & Citability](https://aeojs.org/features/audit/)
+- [Back to Overview](./README.md)
diff --git a/docs/cli.md b/docs/cli.md
new file mode 100644
index 0000000..9911086
--- /dev/null
+++ b/docs/cli.md
@@ -0,0 +1,350 @@
+# CLI Reference
+
+The `aeo.js` (or `aeojs`) CLI works on any project — framework integrations call into it under the hood, and you can invoke it directly for vanilla / static-site workflows.
+
+## Installation
+
+```bash
+# Zero-install via npx
+npx aeo.js
+
+# Or installed
+npm install --save-dev aeo.js
+npx aeo.js
+```
+
+Both `aeo.js` and `aeojs` are registered as binaries — they're identical, use whichever your shell prefers.
+
+## Commands at a glance
+
+| Command | Writes files? | Use for |
+|---|---|---|
+| [`init`](#init) | ✅ — creates `aeo.config.ts` | First-time project setup |
+| [`generate`](#generate) | ✅ — writes AEO files to `outDir` | Production builds, CI |
+| [`check`](#check) | ❌ — read-only | Quick audit, PR gating |
+| [`report`](#report) | ❌ — read-only | Full citability + platform-hint report |
+
+## Global flags
+
+These work on every command:
+
+| Flag | Description |
+|---|---|
+| `--help`, `-h` | Print help and exit |
+| `--version`, `-v` | Print version and exit |
+
+Flags can use either form: `--out public` or `--out=public`.
+
+---
+
+## `init`
+
+Scaffold an `aeo.config.ts` in the current directory.
+
+```bash
+npx aeo.js init
+```
+
+**Behavior**
+- Writes `aeo.config.ts` to the current working directory using a templated default config.
+- Fails (exit 1) if `aeo.config.ts` already exists — won't overwrite. Delete it manually first if you want a fresh template.
+
+**Generated config (excerpt)**
+
+```ts
+import { defineConfig } from 'aeo.js';
+
+export default defineConfig({
+ title: 'My Site',
+ url: 'https://example.com',
+ description: 'A site optimized for AI discovery',
+ generators: { robotsTxt: true, llmsTxt: true, /* … */ },
+ robots: { allow: ['/'], disallow: ['/admin'], crawlDelay: 0 },
+ widget: { enabled: true, position: 'bottom-right', /* … */ },
+});
+```
+
+---
+
+## `generate`
+
+Generate all enabled AEO files based on your config and project state.
+
+```bash
+npx aeo.js generate [options]
+```
+
+**Options**
+
+| Flag | Type | Default | Description |
+|---|---|---|---|
+| `--out ` | string | auto-detected from framework | Where to write the AEO files |
+| `--url ` | string | `https://example.com` | Production URL — drives `sitemap.xml`, absolute URLs in `llms.txt`, JSON-LD |
+| `--title ` | string | `My Site` | Site title for `llms.txt` and JSON-LD |
+| `--no-widget` | boolean | widget enabled | Disable widget injection / generation |
+
+**What it does**
+1. Detects your framework (Next.js, Astro, Nuxt, Vite, Angular, Webpack, or static).
+2. Resolves config from CLI flags + defaults. See [Configuration files](#configuration-files) below — the standalone CLI does **not** currently read `aeo.config.{ts,js}`.
+3. Walks `outDir` for HTML and/or `contentDir` for markdown.
+4. Writes the enabled generators:
+ - `robots.txt` — AI crawler directives + sitemap reference
+ - `llms.txt` — short LLM-readable site summary
+ - `llms-full.txt` — full content dump for LLMs
+ - `sitemap.xml` — standard XML sitemap
+ - `ai-index.json` — chunked content for embedding pipelines
+ - `docs.json` — page manifest
+ - `schema.json` — JSON-LD structured data (if `schema.enabled`)
+ - Per-page `.md` files (if `generators.rawMarkdown`)
+5. Prints a summary of written files.
+
+**Exit codes**
+- `0` — generation succeeded
+- `1` — one or more generators threw (error message printed; partial files may exist)
+
+**Examples**
+
+```bash
+# Static site
+npx aeo.js generate --url https://mysite.com --title "My Site" --out public
+
+# Staging build with different URL
+npx aeo.js generate --url https://staging.mysite.com --title "Staging" --out public
+
+# Production build without the widget
+npx aeo.js generate --url https://mysite.com --title "My Site" --out public --no-widget
+
+# Inside a framework project (Next.js, Astro, Nuxt, etc.) — outDir is auto-detected
+npx aeo.js generate --url https://mysite.com --title "My Site"
+```
+
+---
+
+## `check`
+
+Run a fast, read-only AEO + GEO readiness audit. **Does not write any files.**
+
+```bash
+npx aeo.js check [options]
+```
+
+**Options**
+
+| Flag | Type | Description |
+|---|---|---|
+| `--out ` | string | Output dir to inspect (defaults to auto-detected) |
+| `--url ` | string | Override the configured URL |
+| `--title ` | string | Override the configured title |
+| `--json` | boolean | Emit machine-readable JSON instead of the formatted report |
+
+**Default (formatted) output**
+
+```text
+[aeo.js] AEO Configuration Check
+────────────────────────────────────────
+ Framework: next
+ Content dir: content
+ Output dir: public
+ Title: My Site
+ URL: https://mysite.com
+ Widget: enabled
+
+ Generators:
+ + robots.txt
+ + llms.txt
+ + llms-full.txt
+ + raw markdown
+ + docs.json
+ + sitemap.xml
+ + ai-index.json
+ + schema.json
+
+ Config file: found
+
+═══════════════════════════════════════
+ GEO Readiness Score: 84/100 (Good)
+═══════════════════════════════════════
+ ✓ AI Access: 20/20
+ ✓ Content Structure: 18/20
+ ✓ Schema Presence: 20/20
+ ⚠ Meta Quality: 12/20
+ ⚠ Citability: 14/20
+
+Issues:
+ ⚠ Meta Quality: 2 pages have descriptions shorter than 50 chars
+ ⚠ Citability: 3 pages have fewer than 2 FAQ-style headings
+```
+
+**JSON output (`--json`)**
+
+```json
+{
+ "framework": "next",
+ "config": {
+ "title": "My Site",
+ "url": "https://mysite.com",
+ "outDir": "public"
+ },
+ "audit": {
+ "score": 84,
+ "grade": "Good",
+ "categories": [
+ { "name": "AI Access", "score": 20, "max": 20, "checks": [...] },
+ { "name": "Content Structure", "score": 18, "max": 20, "checks": [...] },
+ { "name": "Schema Presence", "score": 20, "max": 20, "checks": [...] },
+ { "name": "Meta Quality", "score": 12, "max": 20, "checks": [...] },
+ { "name": "Citability", "score": 14, "max": 20, "checks": [...] }
+ ],
+ "issues": [
+ { "category": "Meta Quality", "severity": "warning", "message": "…", "fix": "…" }
+ ]
+ }
+}
+```
+
+**Scripting pattern: fail CI when the score drops**
+
+```bash
+SCORE=$(npx aeo.js check --json | jq '.audit.score')
+if [ "$SCORE" -lt 70 ]; then
+ echo "GEO score $SCORE is below 70 — failing build"
+ exit 1
+fi
+```
+
+---
+
+## `report`
+
+Run a deeper analysis than `check`: per-page citability scores, platform-specific hints (ChatGPT, Claude, Perplexity, Google AI Overviews, Bing Copilot), and a prioritized fix list.
+
+```bash
+npx aeo.js report [options]
+```
+
+**Options**
+
+| Flag | Type | Description |
+|---|---|---|
+| `--out ` | string | Output dir to inspect |
+| `--url ` | string | Override the configured URL |
+| `--title ` | string | Override the configured title |
+| `--json` | boolean | Emit JSON instead of markdown |
+
+**Default output**: a long-form markdown report covering:
+- Overall score breakdown (same five categories as `check`)
+- Per-page citability scores (Answer Blocks, Self-Containment, Statistical Density, Structure)
+- Platform-specific hints — ChatGPT, Claude, Perplexity, Google AI Overviews, Bing Copilot
+- Prioritized fix list ranked by impact
+
+**JSON output**: stable shape with `categories`, `pages`, `platformHints`, `issues`. Stream into your own dashboards or LLM-driven tooling.
+
+```bash
+# Pipe report straight into a markdown file
+npx aeo.js report > aeo-report.md
+
+# Or JSON for tooling
+npx aeo.js report --json > aeo-report.json
+```
+
+---
+
+## Configuration files
+
+> **Heads up:** the standalone CLI (`generate`, `check`, `report`) currently configures itself from **CLI flags only** — it does not load `aeo.config.ts` or `aeo.config.js`. Tracked as a follow-up. For now: pass `--url` / `--title` / `--out` on the command line, or use a framework integration which **does** read the config (see below).
+
+`npx aeo.js init` scaffolds an `aeo.config.ts` template — it's the canonical place to keep your settings, but today the file is consumed by framework integrations rather than the CLI itself:
+
+```ts
+// vite.config.ts (or next.config.mjs / astro.config.mjs / etc.)
+import aeoConfig from './aeo.config';
+import { aeoVitePlugin } from 'aeo.js/vite';
+
+export default {
+ plugins: [aeoVitePlugin(aeoConfig)],
+};
+```
+
+For raw CLI invocations on a static site, pass the values directly:
+
+```bash
+npx aeo.js generate \
+ --url https://mysite.com \
+ --title "My Site" \
+ --out public
+```
+
+A `package.json` script keeps this repeatable without a config file:
+
+```jsonc
+{
+ "scripts": {
+ "aeo": "aeo.js generate --url https://mysite.com --title \"My Site\" --out public"
+ }
+}
+```
+
+## Framework auto-detection
+
+`generate` and `check` detect your framework by inspecting your `package.json` `dependencies` and `devDependencies` (see [src/core/detect.ts](https://github.com/multivmlabs/aeo.js/blob/main/src/core/detect.ts) for the exact logic). The first match wins, in this order:
+
+| Order | Framework | Detected via (package) | Default `outDir` |
+|---|---|---|---|
+| 1 | Next.js | `next` | `public` |
+| 2 | Nuxt | `nuxt` or `@nuxt/kit` | `.output/public` |
+| 3 | Astro | `astro` or `@astrojs/astro` | `dist` |
+| 4 | Remix | `@remix-run/dev` | `build/client` |
+| 5 | SvelteKit | `@sveltejs/kit` | `build` |
+| 6 | Angular | `@angular/core` | `dist` |
+| 7 | Docusaurus | `@docusaurus/core` | `build` |
+| 8 | Vite | `vite` | `dist` |
+| — | Unknown / vanilla | none of the above | `dist` |
+
+A project that has both `next` and `vite` in its `package.json` resolves as Next.js because of the order. Config files (`next.config.mjs`, `angular.json`, etc.) are **not** consulted — only the dependency list.
+
+Detection only affects the default `outDir` and `contentDir`. You can always override with `--out` or set `outDir` in the config.
+
+## Common patterns
+
+### One-shot generation for a static site
+
+```bash
+npx aeo.js generate --url https://mysite.com --title "My Site" --out .
+```
+
+### Production build hook (any framework)
+
+```jsonc
+// package.json
+{
+ "scripts": {
+ "build": "your-build-command",
+ "postbuild": "aeo.js generate"
+ }
+}
+```
+
+### PR check (GitHub Actions)
+
+```yaml
+- run: npx aeo.js check --json | tee audit.json
+- run: |
+ SCORE=$(jq '.audit.score' audit.json)
+ [ "$SCORE" -ge 70 ] || { echo "GEO score $SCORE below 70"; exit 1; }
+```
+
+### Multi-environment build
+
+```bash
+# Staging
+npx aeo.js generate --url https://staging.mysite.com --title "Staging"
+
+# Production
+npx aeo.js generate --url https://mysite.com --title "Production"
+```
+
+## Further Reading
+
+- [Vanilla JS / Static HTML Guide](./vanilla.md) — full no-framework workflow
+- [aeo.js Reference Configuration](https://aeojs.org/reference/configuration/) — every config field documented
+- [Back to Overview](./README.md)
diff --git a/docs/json-ld.md b/docs/json-ld.md
new file mode 100644
index 0000000..65c90d1
--- /dev/null
+++ b/docs/json-ld.md
@@ -0,0 +1,413 @@
+# Custom JSON-LD Recipes
+
+aeo.js generates `WebSite`, `Organization`, and `WebPage` schemas automatically when `schema.enabled: true`. For richer page-type-specific schemas — FAQ, HowTo, Product, Article, Recipe, Event — you'll add them yourself in your page templates.
+
+This doc is a framework-agnostic catalog of copy-paste recipes. Each one is paired with the **safe escape helper** that prevents `` (and U+2028/U+2029) in dynamic values from breaking out of the ``. If a page title or body field ever contains `...`, the rendered HTML executes arbitrary JS. The five replacements above neutralize every script-breakout vector.
+
+> aeo.js's own auto-generated JSON-LD already uses this serializer internally. The recipes below are for **custom** JSON-LD you add on top.
+
+## FAQ Page
+
+For pages with a list of questions and answers. Each `Question` should be the heading, each `Answer` should be the full answer text (Google requires the whole thing, not a truncated preview).
+
+```ts
+const faqSchema = {
+ '@context': 'https://schema.org',
+ '@type': 'FAQPage',
+ mainEntity: [
+ {
+ '@type': 'Question',
+ name: 'What is Answer Engine Optimization?',
+ acceptedAnswer: {
+ '@type': 'Answer',
+ text: 'AEO is the practice of making your content discoverable and citable by AI-powered answer engines like ChatGPT, Claude, and Perplexity.',
+ },
+ },
+ {
+ '@type': 'Question',
+ name: 'How is AEO different from SEO?',
+ acceptedAnswer: {
+ '@type': 'Answer',
+ text: 'SEO optimizes for ranking on search engines. AEO optimizes for being cited in AI-generated answers — different signal mix, different file outputs.',
+ },
+ },
+ ],
+};
+```
+
+aeo.js **auto-detects** FAQ patterns in your page content: headings that end with `?` followed by an answer paragraph. If your FAQ matches that shape, you don't need to write the schema manually — set `schema.enabled: true` and the generator emits it.
+
+## HowTo
+
+For step-by-step instructions. Strong signal for ChatGPT and Perplexity when users ask "how do I…" questions.
+
+```ts
+const howToSchema = {
+ '@context': 'https://schema.org',
+ '@type': 'HowTo',
+ name: 'How to deploy a Next.js site to Vercel',
+ description: 'Step-by-step guide to deploying a Next.js application to Vercel.',
+ totalTime: 'PT5M',
+ step: [
+ {
+ '@type': 'HowToStep',
+ position: 1,
+ name: 'Install the Vercel CLI',
+ text: 'Run `npm install -g vercel` to install the Vercel CLI globally.',
+ },
+ {
+ '@type': 'HowToStep',
+ position: 2,
+ name: 'Authenticate',
+ text: 'Run `vercel login` and follow the prompts to authenticate.',
+ },
+ {
+ '@type': 'HowToStep',
+ position: 3,
+ name: 'Deploy',
+ text: 'Run `vercel --prod` from your project root.',
+ },
+ ],
+};
+```
+
+aeo.js **auto-detects** HowTo patterns: headings like `Step 1:` / `Step 2:` or a sequence of numbered `## How to …` headings. Two or more step headings trigger automatic schema generation.
+
+## Article / BlogPosting
+
+For long-form content. Tells AI engines the author, publish date, and update history — improves citability and shows up in Perplexity's "Sources" list.
+
+```ts
+const articleSchema = {
+ '@context': 'https://schema.org',
+ '@type': 'BlogPosting', // or 'NewsArticle', 'TechArticle', 'Article'
+ headline: 'Optimizing your site for AI search engines in 2026',
+ description: 'A practical guide to AEO for technical content sites.',
+ image: 'https://mysite.com/og/article-cover.png',
+ datePublished: '2026-05-14T10:00:00Z',
+ dateModified: '2026-05-14T10:00:00Z',
+ author: {
+ '@type': 'Person',
+ name: 'Jane Author',
+ url: 'https://mysite.com/authors/jane',
+ },
+ publisher: {
+ '@type': 'Organization',
+ name: 'My Site',
+ logo: {
+ '@type': 'ImageObject',
+ url: 'https://mysite.com/logo.png',
+ },
+ },
+ mainEntityOfPage: {
+ '@type': 'WebPage',
+ '@id': 'https://mysite.com/blog/optimizing-for-ai-search',
+ },
+};
+```
+
+> Always use ISO-8601 strings for `datePublished` / `dateModified`. A raw `Date` object passed through a template literal renders as `"Thu May 14 2026 ..."` and breaks validators. If your CMS returns a `Date`, wrap it: `new Date(d).toISOString()`.
+
+## Product
+
+For e-commerce product pages. Pulls into Google's product cards and AI shopping answers.
+
+```ts
+const productSchema = {
+ '@context': 'https://schema.org',
+ '@type': 'Product',
+ name: 'Acme Espresso Machine',
+ description: 'A semi-automatic espresso machine with built-in grinder.',
+ image: [
+ 'https://mysite.com/products/espresso/cover.jpg',
+ 'https://mysite.com/products/espresso/side.jpg',
+ ],
+ sku: 'ACM-ESP-001',
+ brand: { '@type': 'Brand', name: 'Acme' },
+ offers: {
+ '@type': 'Offer',
+ url: 'https://mysite.com/products/espresso',
+ priceCurrency: 'USD',
+ price: '899.00',
+ availability: 'https://schema.org/InStock',
+ itemCondition: 'https://schema.org/NewCondition',
+ },
+ aggregateRating: {
+ '@type': 'AggregateRating',
+ ratingValue: '4.7',
+ reviewCount: '142',
+ },
+};
+```
+
+## Recipe
+
+For cooking / instructional content. Strong signal for AI assistants answering "how do I make…" or "recipe for…" queries.
+
+```ts
+const recipeSchema = {
+ '@context': 'https://schema.org',
+ '@type': 'Recipe',
+ name: 'Classic Margherita Pizza',
+ image: 'https://mysite.com/recipes/margherita.jpg',
+ description: 'A simple Neapolitan-style margherita with fresh basil.',
+ author: { '@type': 'Person', name: 'Chef Alice' },
+ datePublished: '2026-05-01T12:00:00Z',
+ prepTime: 'PT30M',
+ cookTime: 'PT10M',
+ totalTime: 'PT40M',
+ recipeYield: '2 pizzas',
+ recipeCategory: 'Main course',
+ recipeCuisine: 'Italian',
+ nutrition: {
+ '@type': 'NutritionInformation',
+ calories: '850 kcal',
+ },
+ recipeIngredient: [
+ '500g type-00 flour',
+ '325ml water',
+ '10g sea salt',
+ '2g fresh yeast',
+ '200g San Marzano tomatoes',
+ '125g fresh mozzarella',
+ 'Fresh basil leaves',
+ ],
+ recipeInstructions: [
+ {
+ '@type': 'HowToStep',
+ name: 'Mix the dough',
+ text: 'Combine flour, water, salt, and yeast. Knead for 10 minutes.',
+ },
+ {
+ '@type': 'HowToStep',
+ name: 'Proof',
+ text: 'Let rest at room temperature for 24 hours.',
+ },
+ {
+ '@type': 'HowToStep',
+ name: 'Bake',
+ text: 'Stretch into 30cm circles. Top and bake at 500°F for 6–8 minutes.',
+ },
+ ],
+};
+```
+
+## Event
+
+For events with a date, location, and organizer.
+
+```ts
+const eventSchema = {
+ '@context': 'https://schema.org',
+ '@type': 'Event',
+ name: 'AEO Summit 2026',
+ startDate: '2026-09-15T09:00:00-07:00',
+ endDate: '2026-09-15T17:00:00-07:00',
+ eventStatus: 'https://schema.org/EventScheduled',
+ eventAttendanceMode: 'https://schema.org/MixedEventAttendanceMode',
+ location: [
+ {
+ '@type': 'Place',
+ name: 'Moscone Center',
+ address: {
+ '@type': 'PostalAddress',
+ streetAddress: '747 Howard St',
+ addressLocality: 'San Francisco',
+ addressRegion: 'CA',
+ postalCode: '94103',
+ addressCountry: 'US',
+ },
+ },
+ {
+ '@type': 'VirtualLocation',
+ url: 'https://aeo-summit.example/live',
+ },
+ ],
+ organizer: {
+ '@type': 'Organization',
+ name: 'AEO Summit',
+ url: 'https://aeo-summit.example',
+ },
+ offers: {
+ '@type': 'Offer',
+ url: 'https://aeo-summit.example/tickets',
+ price: '299',
+ priceCurrency: 'USD',
+ availability: 'https://schema.org/InStock',
+ validFrom: '2026-06-01T00:00:00-07:00',
+ },
+};
+```
+
+## VideoObject
+
+For pages with embedded video. Improves discoverability in Google's video carousels and helps AI assistants reference the video.
+
+```ts
+const videoSchema = {
+ '@context': 'https://schema.org',
+ '@type': 'VideoObject',
+ name: 'aeo.js: a 5-minute tour',
+ description: 'Walk through aeo.js setup in five minutes — install, init, generate.',
+ thumbnailUrl: ['https://mysite.com/thumbs/aeo-tour-1280x720.jpg'],
+ uploadDate: '2026-05-01T12:00:00Z',
+ duration: 'PT5M12S',
+ contentUrl: 'https://mysite.com/videos/aeo-tour.mp4',
+ embedUrl: 'https://mysite.com/embed/aeo-tour',
+ publisher: {
+ '@type': 'Organization',
+ name: 'My Site',
+ logo: { '@type': 'ImageObject', url: 'https://mysite.com/logo.png' },
+ },
+};
+```
+
+## BreadcrumbList
+
+Tells AI engines (and search engines) the hierarchy path of the current page. Useful on deep pages — `Home → Blog → 2026 → Optimizing for AI`.
+
+```ts
+const breadcrumbSchema = {
+ '@context': 'https://schema.org',
+ '@type': 'BreadcrumbList',
+ itemListElement: [
+ { '@type': 'ListItem', position: 1, name: 'Home', item: 'https://mysite.com/' },
+ { '@type': 'ListItem', position: 2, name: 'Blog', item: 'https://mysite.com/blog' },
+ { '@type': 'ListItem', position: 3, name: '2026', item: 'https://mysite.com/blog/2026' },
+ { '@type': 'ListItem', position: 4, name: 'Optimizing for AI' },
+ ],
+};
+```
+
+## Injecting safely — per framework
+
+### Next.js (App Router)
+
+```tsx
+// app/page.tsx
+import { serializeJsonForHtml } from '@/lib/serialize-json-ld';
+
+export default function Page() {
+ return (
+ <>
+
+ {/* page content */}
+ >
+ );
+}
+```
+
+### Astro
+
+```astro
+---
+import { serializeJsonForHtml } from '../lib/serialize-json-ld';
+---
+
+
+```
+
+### Nuxt / Vue (useHead)
+
+```vue
+
+```
+
+### Svelte / SvelteKit
+
+Use a real `
+
+
+
+
+```
+
+### React + Helmet
+
+```tsx
+import { Helmet } from 'react-helmet-async';
+import { serializeJsonForHtml } from './lib/serialize-json-ld';
+
+
+
+
+```
+
+### Vanilla HTML
+
+```html
+
+
+```
+
+## Validation
+
+After deploying, paste the page URL into one of these:
+
+- [Schema Markup Validator](https://validator.schema.org/) — quickest, official schema.org
+- [Google Rich Results Test](https://search.google.com/test/rich-results) — confirms eligibility for Google's enhanced results
+- [Bing Webmaster URL Inspection](https://www.bing.com/webmasters/url-inspection) — Bing/Copilot-specific feedback
+
+## Best Practices
+
+- **One `
+