From e81b8c1f0ed5e47aefb02b9e0060fb047f4efcca Mon Sep 17 00:00:00 2001 From: ruben-cytonic Date: Thu, 14 May 2026 14:05:29 +0100 Subject: [PATCH 1/8] docs: add vanilla.md, cli.md, json-ld.md + expand angular/webpack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filling the four gaps surfaced by reading all existing guides end-to-end: 1. docs/vanilla.md (new, ~420 lines) — first-class guide for the no-framework / static-HTML / hand-rolled JS workflow. Covers init, generate, check, common setups (Eleventy / Hugo / Jekyll / SFTP / single-page), CI integration, widget injection, troubleshooting. 2. docs/cli.md (new, ~320 lines) — comprehensive CLI reference. Every command (init, generate, check, report), every flag, exit codes, formatted + JSON output shapes, framework auto-detection table, scripting/CI patterns. 3. docs/json-ld.md (new, ~410 lines) — copy-paste JSON-LD recipes for FAQ, HowTo, Article/BlogPosting, Product, Recipe, Event, VideoObject, BreadcrumbList. Each pairs with the safe serializeJsonForHtml helper so users don't introduce script-tag XSS. Per-framework injection examples (Next/Astro/Nuxt/Svelte/React-Helmet/vanilla). 4. docs/angular.md — added Deployment (Vercel/Netlify/Cloudflare/Firebase) and Examples (Blog, Docs Site, E-commerce SPA, Marketing single-page) sections so it matches the structural depth of the other framework guides. ~155 lines added. 5. docs/webpack.md — same enhancement: Deployment (Vercel/Netlify/CF/ GH Pages) and Examples (Multi-page marketing, Docs Site, SPA with runtime routes, E-commerce with disabled widget) sections. ~175 lines added. 6. docs/README.md — added Vanilla row to the framework table, a new "Additional references" section linking cli.md + json-ld.md, and a Vanilla quick-start snippet next to the framework snippets. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/README.md | 25 +++ docs/angular.md | 155 ++++++++++++++++++ docs/cli.md | 322 +++++++++++++++++++++++++++++++++++++ docs/json-ld.md | 411 ++++++++++++++++++++++++++++++++++++++++++++++ docs/vanilla.md | 420 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/webpack.md | 174 ++++++++++++++++++++ 6 files changed, 1507 insertions(+) create mode 100644 docs/cli.md create mode 100644 docs/json-ld.md create mode 100644 docs/vanilla.md diff --git a/docs/README.md b/docs/README.md index 642402b..f6fe939 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,25 @@ 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 init # scaffold aeo.config.ts +npx aeo.js generate # generate all AEO files +``` + +Or one-shot, no config file: + +```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..44bb00b --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,322 @@ +# 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 | from config or `https://example.com` | Production URL — drives `sitemap.xml`, absolute URLs in `llms.txt`, JSON-LD | +| `--title ` | string | from config or `My Site` | Site title for `llms.txt` and JSON-LD | +| `--no-widget` | boolean | (widget enabled if config says so) | Disable widget injection / generation | + +**What it does** +1. Detects your framework (Next.js, Astro, Nuxt, Vite, Angular, Webpack, or static). +2. Reads `aeo.config.{ts,js}` if present; otherwise uses CLI flags + defaults. +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 +# Use everything from aeo.config.ts +npx aeo.js generate + +# Override URL/title without editing the config +npx aeo.js generate --url https://staging.mysite.com --title "Staging" + +# Static site, no config file +npx aeo.js generate --url https://mysite.com --title "My Site" --out public + +# Production build without the widget +npx aeo.js generate --no-widget +``` + +--- + +## `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 <dir>` | string | Output dir to inspect (defaults to auto-detected) | +| `--url <url>` | string | Override the configured URL | +| `--title <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 <dir>` | string | Output dir to inspect | +| `--url <url>` | string | Override the configured URL | +| `--title <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 + +The CLI auto-discovers (in order): + +1. `aeo.config.ts` +2. `aeo.config.js` + +Both export a default `AeoConfig` object (see [README.md](./README.md#configuration-options) for the full type). Flags passed on the command line override config values. + +**No config file?** Everything uses defaults plus CLI flags. Fine for one-off invocations; recommend committing a config for repeatable builds. + +## Framework auto-detection + +`generate` and `check` detect your framework by looking for telltale files in the cwd: + +| Framework | Detected via | +|---|---| +| Next.js | `next.config.{js,mjs,ts}` | +| Astro | `astro.config.{mjs,ts}` | +| Nuxt | `nuxt.config.{js,mjs,ts}` | +| Vite | `vite.config.{js,mjs,ts}` | +| Angular | `angular.json` | +| Webpack | `webpack.config.{js,mjs,ts}` | +| Static / vanilla | None of the above | + +Detection only affects the default `outDir` and `contentDir`. You can always override with `--out`. + +## 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..2e3f9dc --- /dev/null +++ b/docs/json-ld.md @@ -0,0 +1,411 @@ +# 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 `</script>` (and U+2028/U+2029) in dynamic values from breaking out of the `<script>` tag. + +## The safe serializer + +Always run JSON-LD payloads through this helper before injecting them into a `<script>` tag. It mirrors `serializeJsonForHtml` from [src/core/schema.ts](https://github.com/multivmlabs/aeo.js/blob/main/src/core/schema.ts). + +```ts +// lib/serialize-json-ld.ts +export function serializeJsonForHtml(value: unknown): string { + return JSON.stringify(value) + .replace(/</g, '\\u003C') + .replace(/>/g, '\\u003E') + .replace(/&/g, '\\u0026') + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029'); +} +``` + +Why this matters: `JSON.stringify(...)` does not escape `</script>`. If a page title or body field ever contains `</script>...<script>alert(1)</script>`, 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 ( + <> + <script + type="application/ld+json" + dangerouslySetInnerHTML={{ __html: serializeJsonForHtml(faqSchema) }} + /> + {/* page content */} + </> + ); +} +``` + +### Astro + +```astro +--- +import { serializeJsonForHtml } from '../lib/serialize-json-ld'; +--- + +<script type="application/ld+json" set:html={serializeJsonForHtml(faqSchema)} /> +``` + +### Nuxt / Vue (useHead) + +```vue +<script setup lang="ts"> +import { serializeJsonForHtml } from '~/utils/serialize-json-ld'; + +useHead({ + script: [ + { + type: 'application/ld+json', + children: serializeJsonForHtml(faqSchema), + }, + ], +}); +</script> +``` + +### Svelte / SvelteKit + +```svelte +<script lang="ts"> + import { serializeJsonForHtml } from './lib/serialize-json-ld'; + const schema = serializeJsonForHtml(faqSchema); +</script> + +<svelte:head> + {@html `<script type="application/ld+json">${schema}</script>`} +</svelte:head> +``` + +### React + Helmet + +```tsx +import { Helmet } from 'react-helmet-async'; +import { serializeJsonForHtml } from './lib/serialize-json-ld'; + +<Helmet> + <script type="application/ld+json">{serializeJsonForHtml(faqSchema)}</script> +</Helmet> +``` + +### Vanilla HTML + +```html +<script type="application/ld+json" id="faq-schema"></script> +<script type="module"> + import { faqSchema } from './data.js'; + function serializeJsonForHtml(v) { + return JSON.stringify(v) + .replace(/</g, '\\u003C') + .replace(/>/g, '\\u003E') + .replace(/&/g, '\\u0026') + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029'); + } + document.getElementById('faq-schema').textContent = serializeJsonForHtml(faqSchema); +</script> +``` + +## 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 `<script type="application/ld+json">` per schema type.** Don't merge multiple `@type`s into a single JSON array unless you're using `@graph`. +- **Always escape via `serializeJsonForHtml`.** No exceptions, even for "trivially static" data — schemas evolve. +- **ISO dates everywhere.** `new Date(d).toISOString()` is your friend. +- **Absolute URLs in `image`, `url`, `logo`.** Relative URLs work less reliably in AI crawlers than in browsers. +- **Test before you ship.** Schemas with subtle typos (missing `@type`, wrong `@context`) silently fail in production. + +## Further Reading + +- [Schema.org type reference](https://schema.org/docs/full.html) +- [Google's structured data gallery](https://developers.google.com/search/docs/appearance/structured-data/search-gallery) +- [aeo.js schema generator source](https://github.com/multivmlabs/aeo.js/blob/main/src/core/schema.ts) +- [Back to Overview](./README.md) diff --git a/docs/vanilla.md b/docs/vanilla.md new file mode 100644 index 0000000..bcaa0c6 --- /dev/null +++ b/docs/vanilla.md @@ -0,0 +1,420 @@ +# Vanilla JS / Static HTML Guide + +Use aeo.js with a plain HTML site, a hand-rolled JavaScript bundle, or any static-site generator (Eleventy, Hugo, Jekyll, Pelican, etc.) — no framework required. Everything works through the `aeo.js` CLI. + +## Prerequisites + +- Node.js 18+ +- A folder with HTML files (your built site) — anywhere on disk + +## Installation + +aeo.js can be invoked one-off via `npx` (zero install) or added as a dev-dependency for repeatable builds. + +```bash +# Zero-install (latest) +npx aeo.js generate --url https://mysite.com --title "My Site" + +# Or install as a dev-dependency +npm install --save-dev aeo.js +# yarn add -D aeo.js +# pnpm add -D aeo.js +``` + +## Quick Start + +### Step 1: Initialize a config file (optional but recommended) + +```bash +npx aeo.js init +``` + +This drops an `aeo.config.ts` into your project root with sensible defaults you can edit: + +```ts +import { defineConfig } from 'aeo.js'; + +export default defineConfig({ + title: 'My Site', + url: 'https://mysite.com', + description: 'A site optimized for AI discovery', + + generators: { + robotsTxt: true, + llmsTxt: true, + llmsFullTxt: true, + rawMarkdown: true, + sitemap: true, + aiIndex: true, + schema: true, + }, + + schema: { + enabled: true, + organization: { name: 'My Company', url: 'https://mysite.com' }, + defaultType: 'WebPage', + }, +}); +``` + +Don't want a TS config? Skip `init` and pass everything via flags or use `aeo.config.js` / `aeo.config.json` (CLI auto-detects). + +### Step 2: Generate the AEO files + +```bash +npx aeo.js generate +``` + +The CLI scans your output directory and emits all enabled files alongside your HTML. + +### Step 3: Verify + +```bash +ls public/ # or wherever your site is + +robots.txt +llms.txt +llms-full.txt +sitemap.xml +ai-index.json +docs.json +schema.json +``` + +Visit `https://yoursite.com/llms.txt` after deploying to confirm. + +## How aeo.js Discovers Your Pages + +The CLI looks for content in two places — set whichever fits your project: + +| Source | Config | Use when | +|---|---|---| +| **`contentDir`** | `contentDir: 'docs'` | You have handwritten markdown files (e.g. blog posts in `content/blog/*.md`). aeo.js parses front-matter, scans recursively, and pulls each into the index. | +| **Built HTML in `outDir`** | `outDir: 'public'` | You have a built static site. aeo.js walks the directory for `.html` files, extracts `<title>`, meta description, and rendered text from `<main>` (or the body if no `<main>` exists). | +| **`pages` array** | `pages: [{ pathname: '/about', title: '…' }]` | You want explicit control or have routes that aren't in `contentDir` / `outDir`. Manual entries are merged with auto-discovered ones. | + +You can mix all three — `pages` is additive, not exclusive. + +## Common Setups + +### Hand-rolled HTML site + +```text +my-site/ +├── index.html +├── about/index.html +├── pricing/index.html +└── aeo.config.ts +``` + +```ts +// aeo.config.ts +import { defineConfig } from 'aeo.js'; + +export default defineConfig({ + title: 'My Site', + url: 'https://mysite.com', + outDir: '.', // The HTML files live at the project root +}); +``` + +```bash +npx aeo.js generate +``` + +The output files land next to your `index.html`. Ready to deploy. + +### Markdown blog (no framework) + +```text +my-blog/ +├── content/ +│ ├── intro-post.md +│ └── second-post.md +├── public/ # Your existing HTML build +│ ├── index.html +│ └── intro-post/index.html +└── aeo.config.ts +``` + +```ts +// aeo.config.ts +import { defineConfig } from 'aeo.js'; + +export default defineConfig({ + title: 'My Blog', + url: 'https://myblog.dev', + description: 'Technical articles on the web platform', + contentDir: 'content', // Pull post bodies from here + outDir: 'public', // Drop generated files here +}); +``` + +aeo.js will read each `.md` file's front-matter (title, description, date), pull its body content, and emit a unified `llms.txt` / `ai-index.json` / `sitemap.xml`. + +### Eleventy / Hugo / Jekyll + +These SSGs already emit a finished `_site/` or `public/` of HTML. Treat that folder as your `outDir`: + +```ts +// aeo.config.ts +export default defineConfig({ + title: 'My Site', + url: 'https://mysite.com', + outDir: '_site', // Eleventy default + // outDir: 'public', // Hugo default + // outDir: '_site', // Jekyll default +}); +``` + +```jsonc +// package.json +{ + "scripts": { + "build": "eleventy", // or hugo / bundle exec jekyll build + "postbuild": "aeo.js generate" + } +} +``` + +`npm` runs `postbuild` automatically after `build`, so `npm run build` produces the full site **and** the AEO files in one step. + +### Single-file landing page + +Even a single `index.html` benefits from AEO files. The minimum: + +```bash +npx aeo.js generate \ + --url https://mysite.com \ + --title "My Site" \ + --out . +``` + +You get `robots.txt`, `llms.txt`, `sitemap.xml`, and `schema.json` next to your HTML. + +## Auditing without generating + +`aeo.js` ships an audit command that scores your AEO readiness without writing any files. Useful in CI: + +```bash +npx aeo.js check +``` + +Output: + +```text +[aeo.js] AEO Configuration Check +──────────────────────────────────────── + Framework: static + Output dir: public + Title: My Site + URL: https://mysite.com + Widget: enabled + + Generators: + + robots.txt + + llms.txt + + llms-full.txt + + 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 +``` + +JSON output for scripting: + +```bash +npx aeo.js check --json > audit.json +``` + +See [cli.md](./cli.md) for the full CLI reference. + +## CI Integration + +### GitHub Actions + +```yaml +# .github/workflows/aeo-check.yml +name: AEO readiness +on: + pull_request: + paths: + - 'content/**' + - 'public/**' + - 'aeo.config.ts' + +jobs: + audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - run: npx aeo.js check --json | tee audit.json + - run: | + SCORE=$(jq '.audit.score' audit.json) + if [ "$SCORE" -lt 70 ]; then + echo "GEO score below 70 — failing build" >&2 + exit 1 + fi +``` + +## Deployment + +The generated files are plain static assets — deploy them with whatever you'd already use. + +### Vercel + +```jsonc +// vercel.json +{ + "buildCommand": "npm run build && aeo.js generate", + "outputDirectory": "public" +} +``` + +### Netlify + +```toml +# netlify.toml +[build] + command = "npm run build && npx aeo.js generate" + publish = "public" +``` + +### Cloudflare Pages + +In the dashboard, set the build command to `npm run build && npx aeo.js generate` and the output directory to your build folder. No `wrangler.toml` changes needed. + +### GitHub Pages + +```yaml +# .github/workflows/deploy.yml +name: Deploy +on: + push: + branches: [main] + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - run: npm ci && npm run build + - run: npx aeo.js generate + - uses: actions/upload-pages-artifact@v3 + with: + path: public + - uses: actions/deploy-pages@v4 +``` + +### Plain SFTP / S3 / any host + +```bash +npm run build +npx aeo.js generate +# Upload the contents of public/ (or whatever your outDir is) to your host +rsync -avz --delete public/ user@host:/var/www/mysite/ +``` + +## Adding the Widget + +The widget is the small Human ↔ AI toggle that lets visitors see how AI engines view your site. To add it to a static HTML site: + +```html +<!DOCTYPE html> +<html> + <head> + <title>My Site + + + + + + + +``` + +Or, if you're bundling your own JS: + +```bash +npm install aeo.js +``` + +```js +import { AeoWidget } from 'aeo.js/widget'; + +new AeoWidget({ + config: { + title: 'My Site', + url: 'https://mysite.com', + }, +}); +``` + +Disable widget generation entirely: + +```bash +npx aeo.js generate --no-widget +``` + +## Best Practices + +- **Always set `url`.** Without it, `sitemap.xml`, `llms.txt`, and JSON-LD fall back to `https://example.com` and the audit will flag it. +- **Run `aeo.js generate` after your HTML build, not before.** Otherwise the discovery scan finds nothing to index. +- **Commit `aeo.config.ts`** so contributors run the same configuration. Don't commit the generated files — regenerate them in CI/CD. +- **Use `aeo.js check` in PRs.** Fail the build if the score drops. + +## Troubleshooting + +### `llms.txt` is empty / has no pages +The CLI didn't find any HTML or markdown. Check: +- `outDir` points at your built site (containing `*.html` files), not your source +- `contentDir` exists and has `.md` / `.mdx` files +- Or pass `pages: [...]` explicitly in config + +### `sitemap.xml` only has the root URL +Same as above — the CLI didn't find page-level HTML/markdown. If your site is a SPA without prerendered routes, list them via `pages: [...]` in the config. + +### `aeo.js check` reports low Citability score +Citability scores how easily an LLM can quote your page. Common boosts: add FAQ headings (`## What is X?`), include statistics with numbers, use short topic-focused paragraphs. See the full breakdown in [Generated Files](https://aeojs.org/features/audit/). + +### CLI says "Unknown framework" +That's expected — you're not using one. The CLI defaults to a generic static-site flow, which is exactly what you want here. + +## Further Reading + +- [CLI Reference](./cli.md) — every flag, every command +- [Custom JSON-LD Recipes](./json-ld.md) — FAQ, HowTo, Product, Article snippets +- [aeo.js Reference Configuration](https://aeojs.org/reference/configuration/) +- [Back to Overview](./README.md) + +--- + +**Need help?** [Open an issue](https://github.com/multivmlabs/aeo.js/issues) diff --git a/docs/webpack.md b/docs/webpack.md index d693d88..3bef14b 100644 --- a/docs/webpack.md +++ b/docs/webpack.md @@ -210,8 +210,182 @@ The plugin scans `compilation.assets` after `afterEmit`. If you use a custom emi ### Sitemap is missing pages Multi-page SPAs that render at runtime are invisible at build time. Add the routes via the `pages` option (shown above) so the sitemap picks them up. +## Deployment + +The generated AEO files land in webpack's `output.path` next to your bundles — deploy them as static assets. + +### Vercel + +```jsonc +// vercel.json +{ + "buildCommand": "npm run build", + "outputDirectory": "dist" +} +``` + +### Netlify + +```toml +# netlify.toml +[build] + command = "npm run build" + publish = "dist" +``` + +### Cloudflare Pages + +In the dashboard: +- **Build command:** `npm run build` +- **Build output directory:** `dist` (or whatever your `output.path` is) + +### GitHub Pages + +```yaml +# .github/workflows/deploy.yml +name: Deploy +on: + push: + branches: [main] + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - run: npm ci && npm run build + - uses: actions/upload-pages-artifact@v3 + with: + path: dist + - uses: actions/deploy-pages@v4 +``` + +Because `AeoWebpackPlugin` runs inside the webpack build, no extra CI step is needed — the AEO files are part of the artifact. + +## Examples + +### Multi-page marketing site + +```js +// webpack.config.js +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const { AeoWebpackPlugin } = require('aeo.js/webpack'); + +module.exports = { + entry: { main: './src/index.js' }, + plugins: [ + new HtmlWebpackPlugin({ template: './src/index.html', filename: 'index.html' }), + new HtmlWebpackPlugin({ template: './src/about.html', filename: 'about/index.html' }), + new HtmlWebpackPlugin({ template: './src/pricing.html', filename: 'pricing/index.html' }), + new HtmlWebpackPlugin({ template: './src/contact.html', filename: 'contact/index.html' }), + new AeoWebpackPlugin({ + title: 'My Marketing Site', + url: 'https://mysite.com', + description: 'A modern marketing site', + schema: { + enabled: true, + organization: { + name: 'My Company', + url: 'https://mysite.com', + logo: 'https://mysite.com/logo.png', + }, + }, + }), + ], +}; +``` + +### Documentation Site (multi-entry build) + +```js +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const { AeoWebpackPlugin } = require('aeo.js/webpack'); + +module.exports = { + entry: { docs: './src/docs.js' }, + plugins: [ + new HtmlWebpackPlugin({ template: './src/getting-started.html', filename: 'getting-started/index.html' }), + new HtmlWebpackPlugin({ template: './src/api-reference.html', filename: 'api-reference/index.html' }), + new HtmlWebpackPlugin({ template: './src/guides.html', filename: 'guides/index.html' }), + new AeoWebpackPlugin({ + title: 'My API Documentation', + url: 'https://docs.myapi.com', + description: 'Complete API reference and integration guides', + schema: { + enabled: true, + organization: { name: 'My API', url: 'https://docs.myapi.com' }, + }, + }), + ], +}; +``` + +### SPA with runtime routes + +For SPAs where most pages render at runtime, supplement with `pages` so the sitemap reflects all routes: + +```js +const { AeoWebpackPlugin } = require('aeo.js/webpack'); + +module.exports = { + // ... + plugins: [ + new AeoWebpackPlugin({ + title: 'My SPA', + url: 'https://myspa.com', + pages: [ + { pathname: '/', title: 'Home' }, + { pathname: '/dashboard', title: 'Dashboard' }, + { pathname: '/pricing', title: 'Pricing' }, + { pathname: '/about', title: 'About' }, + ], + robots: { + allow: ['/'], + disallow: ['/dashboard'], // Block crawlers from the auth-gated route + }, + }), + ], +}; +``` + +### E-commerce build with disabled widget + +```js +const { AeoWebpackPlugin } = require('aeo.js/webpack'); + +module.exports = { + plugins: [ + new AeoWebpackPlugin({ + title: 'My Store', + url: 'https://mystore.com', + description: 'Quality products, fast shipping', + robots: { + allow: ['/'], + disallow: ['/checkout', '/account', '/api'], + }, + schema: { + enabled: true, + organization: { name: 'My Store', url: 'https://mystore.com' }, + }, + widget: { enabled: false }, // Don't inject the Human↔AI widget on customer-facing pages + }), + ], +}; +``` + +For per-product `Product` schema (rich product cards in AI shopping answers), see [Custom JSON-LD Recipes → Product](./json-ld.md#product). + ## Further Reading +- [CLI Reference](./cli.md) — direct CLI usage as an alternative to the plugin +- [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) From 51ecb5e6ffd588f8aa304ed61e4291d517d2ae3b Mon Sep 17 00:00:00 2001 From: ruben-cytonic Date: Thu, 14 May 2026 14:16:57 +0100 Subject: [PATCH 2/8] docs(website): surface vanilla + JSON-LD recipes in Starlight sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new docs/vanilla.md, docs/cli.md, and docs/json-ld.md from the parent commit live at the repo root for GitHub/Context7 consumption. The aeojs.org Starlight site reads from website/src/content/docs/ — a separate tree that needed equivalent entries for the nav menu to reflect the new content. Adds: - website/src/content/docs/frameworks/vanilla.mdx (~110 lines) - website/src/content/docs/features/json-ld.mdx (~165 lines) Updates: - website/src/content/docs/features/cli.mdx — adds the `report` command (previously missing) and the --json flag/scripting pattern. - website/astro.config.mjs — adds two sidebar entries: "Vanilla JS / Static HTML" under Frameworks, "JSON-LD Recipes" under Features. Both new pages are concise Starlight-flavored summaries that link to the deeper docs/*.md references on GitHub for the full catalog. Build: `bun run build` clean, 21 pages emitted (up from 19). Co-Authored-By: Claude Opus 4.7 (1M context) --- website/astro.config.mjs | 2 + website/src/content/docs/features/cli.mdx | 26 ++- website/src/content/docs/features/json-ld.mdx | 192 ++++++++++++++++++ .../src/content/docs/frameworks/vanilla.mdx | 121 +++++++++++ 4 files changed, 339 insertions(+), 2 deletions(-) create mode 100644 website/src/content/docs/features/json-ld.mdx create mode 100644 website/src/content/docs/frameworks/vanilla.mdx diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 9731af0..c0fa73b 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -69,6 +69,7 @@ export default defineConfig({ { label: 'Nuxt', slug: 'frameworks/nuxt' }, { label: 'Angular', slug: 'frameworks/angular' }, { label: 'Webpack', slug: 'frameworks/webpack' }, + { label: 'Vanilla JS / Static HTML', slug: 'frameworks/vanilla' }, ], }, { @@ -78,6 +79,7 @@ export default defineConfig({ { label: 'Widget', slug: 'features/widget' }, { label: 'CLI', slug: 'features/cli' }, { label: 'Schema & Open Graph', slug: 'features/schema-og' }, + { label: 'JSON-LD Recipes', slug: 'features/json-ld' }, { label: 'Audit & Citability', slug: 'features/audit' }, ], }, diff --git a/website/src/content/docs/features/cli.mdx b/website/src/content/docs/features/cli.mdx index ca3e5ea..3dda3c9 100644 --- a/website/src/content/docs/features/cli.mdx +++ b/website/src/content/docs/features/cli.mdx @@ -32,10 +32,27 @@ This generates a starter config with all options documented. ### `check` -Validate your AEO setup and show what would be generated: +Validate your AEO setup and get a GEO readiness score (0–100). Does not write any files — safe to run in CI: ```bash -npx aeo.js check +npx aeo.js check # formatted output +npx aeo.js check --json # machine-readable JSON for scripting +``` + +Fail a CI build if the score drops below a threshold: + +```bash +SCORE=$(npx aeo.js check --json | jq '.audit.score') +[ "$SCORE" -ge 70 ] || { echo "GEO score $SCORE below 70"; exit 1; } +``` + +### `report` + +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 > aeo-report.md +npx aeo.js report --json > aeo-report.json ``` ## Options @@ -46,9 +63,12 @@ npx aeo.js check | `--url ` | Site URL | | `--title ` | Site title | | `--no-widget` | Disable widget generation | +| `--json` | JSON output (for `check` and `report`) | | `--help`, `-h` | Show help | | `--version`, `-v` | Show version | +Both `--flag value` and `--flag=value` forms are supported. + ## Configuration file The CLI looks for `aeo.config.ts` (or `aeo.config.js`) in your project root. Create one with `npx aeo.js init`: @@ -65,3 +85,5 @@ export default defineConfig({ ``` See the full [Configuration reference](/reference/configuration/) for all options. + +A complete CLI reference with every flag, exit codes, JSON output shapes, framework auto-detection table, and CI scripting patterns is in [docs/cli.md on GitHub](https://github.com/multivmlabs/aeo.js/blob/main/docs/cli.md). diff --git a/website/src/content/docs/features/json-ld.mdx b/website/src/content/docs/features/json-ld.mdx new file mode 100644 index 0000000..63ab66d --- /dev/null +++ b/website/src/content/docs/features/json-ld.mdx @@ -0,0 +1,192 @@ +--- +title: JSON-LD Recipes +description: Copy-paste structured-data recipes for FAQ, HowTo, Product, Article, Recipe, Event, VideoObject, and BreadcrumbList — each paired with an XSS-safe serializer. +--- + +import { Aside, Tabs, TabItem } from '@astrojs/starlight/components'; + +aeo.js auto-generates `WebSite`, `Organization`, and `WebPage` schemas when `schema.enabled: true`. For richer page-type-specific schemas, drop these into your page templates. + +## The safe serializer + +`JSON.stringify(...)` does **not** escape `</script>`. A schema value containing `</script>` (or U+2028 / U+2029) breaks out of the script block and executes as JavaScript. Always run JSON-LD payloads through this helper before injection: + +```ts +// lib/serialize-json-ld.ts +export function serializeJsonForHtml(value: unknown): string { + return JSON.stringify(value) + .replace(/</g, '\\u003C') + .replace(/>/g, '\\u003E') + .replace(/&/g, '\\u0026') + .replace(/
/g, '\\u2028') + .replace(/
/g, '\\u2029'); +} +``` + +<Aside> +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. +</Aside> + +## FAQ Page + +```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.', + }, + }, + ], +}; +``` + +aeo.js **auto-detects** FAQ patterns (heading ending with `?` + answer paragraph) and emits this schema for you when `schema.enabled: true`. + +## HowTo + +```ts +const howToSchema = { + '@context': 'https://schema.org', + '@type': 'HowTo', + name: 'How to deploy a Next.js site to Vercel', + totalTime: 'PT5M', + step: [ + { '@type': 'HowToStep', position: 1, name: 'Install CLI', text: 'Run `npm install -g vercel`.' }, + { '@type': 'HowToStep', position: 2, name: 'Login', text: 'Run `vercel login`.' }, + { '@type': 'HowToStep', position: 3, name: 'Deploy', text: 'Run `vercel --prod`.' }, + ], +}; +``` + +aeo.js **auto-detects** `Step 1:` / `Step 2:` heading patterns. + +## Article / BlogPosting + +```ts +const articleSchema = { + '@context': 'https://schema.org', + '@type': 'BlogPosting', + headline: 'Optimizing your site for AI search engines in 2026', + 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' }, + publisher: { + '@type': 'Organization', + name: 'My Site', + logo: { '@type': 'ImageObject', url: 'https://mysite.com/logo.png' }, + }, +}; +``` + +<Aside type="caution"> +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. Wrap with `new Date(d).toISOString()`. +</Aside> + +## Product + +```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'], + 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', + }, + aggregateRating: { + '@type': 'AggregateRating', + ratingValue: '4.7', + reviewCount: '142', + }, +}; +``` + +## BreadcrumbList + +```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: 'Article Title' }, + ], +}; +``` + +## Injecting safely — per framework + +<Tabs> + <TabItem label="Next.js"> + ```tsx + import { serializeJsonForHtml } from '@/lib/serialize-json-ld'; + + <script + type="application/ld+json" + dangerouslySetInnerHTML={{ __html: serializeJsonForHtml(faqSchema) }} + /> + ``` + </TabItem> + <TabItem label="Astro"> + ```astro + --- + import { serializeJsonForHtml } from '../lib/serialize-json-ld'; + --- + + <script type="application/ld+json" set:html={serializeJsonForHtml(faqSchema)} /> + ``` + </TabItem> + <TabItem label="Nuxt"> + ```vue + <script setup lang="ts"> + import { serializeJsonForHtml } from '~/utils/serialize-json-ld'; + + useHead({ + script: [{ + type: 'application/ld+json', + children: serializeJsonForHtml(faqSchema), + }], + }); + </script> + ``` + </TabItem> + <TabItem label="Svelte"> + ```svelte + <script lang="ts"> + import { serializeJsonForHtml } from './lib/serialize-json-ld'; + const schema = serializeJsonForHtml(faqSchema); + </script> + + <svelte:head> + {@html `<script type="application/ld+json">${schema}</script>`} + </svelte:head> + ``` + </TabItem> +</Tabs> + +## Validation + +After deploying, paste your URL into one of these: + +- [Schema Markup Validator](https://validator.schema.org/) — official, quickest +- [Google Rich Results Test](https://search.google.com/test/rich-results) — Google rich-result eligibility +- [Bing Webmaster URL Inspection](https://www.bing.com/webmasters/url-inspection) — Bing/Copilot-specific feedback + +<Aside> +Full catalog (including **Recipe**, **Event**, **VideoObject**, and more per-framework injection variants like React Helmet and vanilla HTML) is in [docs/json-ld.md on GitHub](https://github.com/multivmlabs/aeo.js/blob/main/docs/json-ld.md). +</Aside> diff --git a/website/src/content/docs/frameworks/vanilla.mdx b/website/src/content/docs/frameworks/vanilla.mdx new file mode 100644 index 0000000..baddc0c --- /dev/null +++ b/website/src/content/docs/frameworks/vanilla.mdx @@ -0,0 +1,121 @@ +--- +title: Vanilla JS / Static HTML +description: Use aeo.js with a plain HTML site, hand-rolled JS, or any static-site generator — no framework required. +--- + +import { Tabs, TabItem, Aside } from '@astrojs/starlight/components'; + +aeo.js works on any project that produces static HTML. Use the CLI directly — no plugin, no module, no framework. + +## Quick Start + +<Tabs> + <TabItem label="Zero-install (npx)"> + ```bash + npx aeo.js init + npx aeo.js generate --url https://mysite.com --title "My Site" + ``` + </TabItem> + <TabItem label="Installed"> + ```bash + npm install --save-dev aeo.js + npx aeo.js init + npx aeo.js generate + ``` + </TabItem> +</Tabs> + +`init` scaffolds `aeo.config.ts`. `generate` walks your output directory, extracts page content, and emits `robots.txt`, `llms.txt`, `sitemap.xml`, `ai-index.json`, and `schema.json` next to your HTML. + +## How aeo.js Discovers Your Pages + +| Source | Config | Use when | +|---|---|---| +| **`contentDir`** | `contentDir: 'docs'` | You have handwritten `.md` / `.mdx` files | +| **Built HTML in `outDir`** | `outDir: 'public'` | You have a built static site with `*.html` files | +| **`pages` array** | `pages: [{ pathname: '/about', title: '…' }]` | Explicit control for runtime-only routes | + +Mix all three — `pages` is additive. + +## Common Setups + +### Hand-rolled HTML site + +```ts +// aeo.config.ts +import { defineConfig } from 'aeo.js'; + +export default defineConfig({ + title: 'My Site', + url: 'https://mysite.com', + outDir: '.', // HTML at project root +}); +``` + +### Eleventy / Hugo / Jekyll + +```jsonc +// package.json +{ + "scripts": { + "build": "eleventy", // or hugo / jekyll build + "postbuild": "aeo.js generate" + } +} +``` + +```ts +// aeo.config.ts +export default defineConfig({ + title: 'My Site', + url: 'https://mysite.com', + outDir: '_site', // Eleventy / Jekyll default; use 'public' for Hugo +}); +``` + +### Markdown blog (no framework) + +```ts +export default defineConfig({ + title: 'My Blog', + url: 'https://myblog.dev', + contentDir: 'content', // pull post bodies from here + outDir: 'public', // write generated files here +}); +``` + +## Adding the Widget + +```html +<script type="module"> + import { AeoWidget } from 'https://esm.sh/aeo.js/widget'; + new AeoWidget({ + config: { + title: 'My Site', + url: 'https://mysite.com', + widget: { enabled: true, position: 'bottom-right', size: 'small' }, + }, + }); +</script> +``` + +Or install and bundle locally with `import { AeoWidget } from 'aeo.js/widget'`. + +## CI Integration + +```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; } +``` + +<Aside> +Full walkthrough with deployment recipes (Vercel, Netlify, Cloudflare, GitHub Pages) is in the [Vanilla Guide on GitHub](https://github.com/multivmlabs/aeo.js/blob/main/docs/vanilla.md). +</Aside> + +## Further Reading + +- [CLI Reference](/features/cli/) — every command, every flag +- [JSON-LD Recipes](/features/json-ld/) — FAQ, HowTo, Product, Article, Recipe, Event +- [Configuration Reference](/reference/configuration/) From 896b01befd49076230a437c43700873b498bcc55 Mon Sep 17 00:00:00 2001 From: ruben-cytonic <ruben@cytonic.com> Date: Thu, 14 May 2026 14:22:20 +0100 Subject: [PATCH 3/8] docs: 4 review fixes (detection table, init template, npx, svelte:head) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. docs/cli.md — Framework auto-detection table claimed detection reads config files (next.config.mjs, angular.json, etc.). The real logic in src/core/detect.ts inspects package.json dependencies — no config files are read. Rewrote the table to show the actual detection package per framework, the precedence order, the default outDir, and a note that config files are not consulted. Webpack is removed (it has no entry in detect.ts and falls through to unknown); Remix / SvelteKit / Docusaurus are added since they're actually detected. 2. docs/vanilla.md — The aeo.config.ts template shown didn't match the one cmdInit() actually writes. Real template lacks generators.schema and the top-level schema block, and includes manifest: true. Updated the example to mirror src/cli.ts exactly, with a callout block showing how to opt into JSON-LD generation when users want it. 3. docs/vanilla.md — Vercel buildCommand used bare `aeo.js generate` while Netlify used `npx aeo.js generate`. Vercel doesn't reliably put node_modules/.bin on PATH for buildCommand strings; use npx for parity. 4. docs/json-ld.md + website/src/content/docs/features/json-ld.mdx + docs/vite.md — Svelte JSON-LD example wrapped the entire <script> tag inside {@html ...}, hiding the element from the Svelte compiler. Use a real <script> element inside <svelte:head> and limit {@html} to the text content, so the compiler manages the element boundary. Build: `bun run build` clean, 21 pages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- docs/cli.md | 28 +++++++----- docs/json-ld.md | 4 +- docs/vanilla.md | 45 ++++++++++++++++--- docs/vite.md | 2 +- website/src/content/docs/features/json-ld.mdx | 2 +- 5 files changed, 59 insertions(+), 22 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 44bb00b..1e93697 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -262,19 +262,23 @@ Both export a default `AeoConfig` object (see [README.md](./README.md#configurat ## Framework auto-detection -`generate` and `check` detect your framework by looking for telltale files in the cwd: +`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: -| Framework | Detected via | -|---|---| -| Next.js | `next.config.{js,mjs,ts}` | -| Astro | `astro.config.{mjs,ts}` | -| Nuxt | `nuxt.config.{js,mjs,ts}` | -| Vite | `vite.config.{js,mjs,ts}` | -| Angular | `angular.json` | -| Webpack | `webpack.config.{js,mjs,ts}` | -| Static / vanilla | None of the above | - -Detection only affects the default `outDir` and `contentDir`. You can always override with `--out`. +| 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 diff --git a/docs/json-ld.md b/docs/json-ld.md index 2e3f9dc..65c90d1 100644 --- a/docs/json-ld.md +++ b/docs/json-ld.md @@ -347,6 +347,8 @@ useHead({ ### Svelte / SvelteKit +Use a real `<script>` element inside `<svelte:head>` and limit `{@html}` to the **text content** so the Svelte compiler stays aware of the element boundary: + ```svelte <script lang="ts"> import { serializeJsonForHtml } from './lib/serialize-json-ld'; @@ -354,7 +356,7 @@ useHead({ </script> <svelte:head> - {@html `<script type="application/ld+json">${schema}</script>`} + <script type="application/ld+json">{@html schema}</script> </svelte:head> ``` diff --git a/docs/vanilla.md b/docs/vanilla.md index bcaa0c6..3ed1baa 100644 --- a/docs/vanilla.md +++ b/docs/vanilla.md @@ -29,34 +29,65 @@ npm install --save-dev aeo.js npx aeo.js init ``` -This drops an `aeo.config.ts` into your project root with sensible defaults you can edit: +This drops an `aeo.config.ts` into your project root with sensible defaults you can edit. The generated file matches the template in [src/cli.ts](https://github.com/multivmlabs/aeo.js/blob/main/src/cli.ts): ```ts import { defineConfig } from 'aeo.js'; export default defineConfig({ + // Required title: 'My Site', - url: 'https://mysite.com', + url: 'https://example.com', + + // Optional description: 'A site optimized for AI discovery', + // Toggle individual generators generators: { robotsTxt: true, llmsTxt: true, llmsFullTxt: true, rawMarkdown: true, + manifest: true, sitemap: true, aiIndex: true, - schema: true, }, - schema: { + // Customize robots.txt + robots: { + allow: ['/'], + disallow: ['/admin'], + crawlDelay: 0, + }, + + // Widget configuration + widget: { enabled: true, - organization: { name: 'My Company', url: 'https://mysite.com' }, - defaultType: 'WebPage', + position: 'bottom-right', + humanLabel: 'Human', + aiLabel: 'AI', + showBadge: true, + theme: { + background: 'rgba(18, 18, 24, 0.9)', + text: '#C0C0C5', + accent: '#E8E8EA', + badge: '#4ADE80', + }, }, }); ``` +> **Need JSON-LD?** The template intentionally omits `generators.schema` and the top-level `schema` block — add them when you want structured-data generation: +> +> ```ts +> generators: { /* …, */ schema: true }, +> schema: { +> enabled: true, +> organization: { name: 'My Company', url: 'https://mysite.com' }, +> defaultType: 'WebPage', +> }, +> ``` + Don't want a TS config? Skip `init` and pass everything via flags or use `aeo.config.js` / `aeo.config.json` (CLI auto-detects). ### Step 2: Generate the AEO files @@ -279,7 +310,7 @@ The generated files are plain static assets — deploy them with whatever you'd ```jsonc // vercel.json { - "buildCommand": "npm run build && aeo.js generate", + "buildCommand": "npm run build && npx aeo.js generate", "outputDirectory": "public" } ``` diff --git a/docs/vite.md b/docs/vite.md index 248276d..dd09cc8 100755 --- a/docs/vite.md +++ b/docs/vite.md @@ -257,7 +257,7 @@ useHead({ <svelte:head> <title>My Svelte App - {@html ``} +
diff --git a/website/src/content/docs/features/json-ld.mdx b/website/src/content/docs/features/json-ld.mdx index 63ab66d..f8b348c 100644 --- a/website/src/content/docs/features/json-ld.mdx +++ b/website/src/content/docs/features/json-ld.mdx @@ -173,7 +173,7 @@ const breadcrumbSchema = { - {@html ``} + ``` From 673368490d20aad07061deab0f1e71fa13658997 Mon Sep 17 00:00:00 2001 From: ruben-cytonic Date: Thu, 14 May 2026 14:25:50 +0100 Subject: [PATCH 4/8] docs(website): fix literal U+2028/U+2029 chars in json-ld.mdx serializer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Python sanitization sweep I ran earlier only covered docs/*.md, so the matching serializer snippet in website/src/content/docs/features/ json-ld.mdx still had the actual U+2028 and U+2029 characters embedded between the regex delimiters. That made the two .replace() calls visually identical in editors and contradicted the PR's own claim that all serializer copies use escape syntax. Replaced the literal chars with /
/g and /
/g escape forms, matching the canonical version in docs/json-ld.md and src/core/schema.ts. Build remains clean (21 pages). Co-Authored-By: Claude Opus 4.7 (1M context) --- website/src/content/docs/features/json-ld.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/content/docs/features/json-ld.mdx b/website/src/content/docs/features/json-ld.mdx index f8b348c..8eb343f 100644 --- a/website/src/content/docs/features/json-ld.mdx +++ b/website/src/content/docs/features/json-ld.mdx @@ -18,8 +18,8 @@ export function serializeJsonForHtml(value: unknown): string { .replace(//g, '\\u003E') .replace(/&/g, '\\u0026') - .replace(/
/g, '\\u2028') - .replace(/
/g, '\\u2029'); + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029'); } ``` From 151f3ce30eb418167a9a4d6816d99af7bcac6e43 Mon Sep 17 00:00:00 2001 From: ruben-cytonic Date: Thu, 14 May 2026 14:56:10 +0100 Subject: [PATCH 5/8] docs: clarify CLI does not load aeo.config.{ts,js} MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Greptile correctly flagged that docs claimed `aeo.js generate`/`check`/ `report` read `aeo.config.{ts,js}` if present, but src/cli.ts never actually imports those files — cmdGenerate, cmdCheck, and cmdReport all call resolveConfig with CLI flags only. A user setting `url` in aeo.config.ts and running bare `aeo.js generate` would silently get the https://example.com default. This commit corrects the documentation across four files to describe reality. (Wiring up actual config-file loading in the CLI is a real feature that's a follow-up.) - docs/cli.md * "What it does" step 2: was "Reads aeo.config.{ts,js}"; now "Resolves config from CLI flags + defaults. The standalone CLI does not currently read aeo.config.{ts,js}." * "Configuration files" section: replaced auto-discover claim with a clear callout. Shows the canonical pattern (import config into a framework plugin, OR pass flags directly to the CLI). * Default columns for --url / --title no longer say "from config". - docs/vanilla.md * Step 1: kept `init` as the scaffolder but no longer claims the CLI reads it. * Step 2: now shows the flag-based invocation as the canonical CLI usage, with a package.json script pattern for repeatability. * "How aeo.js Discovers Your Pages": clarified that --out is the only CLI surface; contentDir and pages require programmatic API. - website/src/content/docs/features/cli.mdx * Configuration File section gains a