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 <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 + +> **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 `</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 + +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'; + const schema = serializeJsonForHtml(faqSchema); +</script> + +<svelte:head> + <script type="application/ld+json">{@html 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..9ba55c7 --- /dev/null +++ b/docs/vanilla.md @@ -0,0 +1,477 @@ +# 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) + +```bash +npx aeo.js init +``` + +This drops an `aeo.config.ts` into your project root with sensible defaults. 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://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, + }, + + // Customize robots.txt + robots: { + allow: ['/'], + disallow: ['/admin'], + crawlDelay: 0, + }, + + // Widget configuration + widget: { + enabled: true, + 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', +> }, +> ``` + +> **Important:** the standalone CLI does **not** currently load `aeo.config.{ts,js}` — it reads CLI flags + defaults only. The config file scaffolded here is intended to be imported into a framework config (e.g. `import aeoConfig from './aeo.config'` inside `vite.config.ts`) and passed to the framework plugin. For raw CLI usage on a static site, **always pass `--url` and `--title` on the command line**. + +### Step 2: Generate the AEO files + +Pass your URL and title as flags. The CLI scans your output directory and emits all enabled files alongside your HTML. + +```bash +npx aeo.js generate \ + --url https://mysite.com \ + --title "My Site" \ + --out public +``` + +For a one-line invocation that's easy to commit, drop it into a script: + +```jsonc +// package.json +{ + "scripts": { + "build:aeo": "aeo.js generate --url https://mysite.com --title \"My Site\" --out public" + } +} +``` + +### 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 itself only sees what you pass via flags. The richer `contentDir` / `pages` options are honored when you call the generators programmatically or via a framework integration — they're documented here so you know what's available: + +| Source | Option | Use when | +|---|---|---| +| **Built HTML in `outDir`** | `--out public` (CLI) or `outDir: 'public'` (programmatic) | 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). | +| **`contentDir`** | `contentDir: 'docs'` (programmatic) | 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. | +| **`pages` array** | `pages: [{ pathname: '/about', title: '…' }]` (programmatic) | Explicit control for runtime-only routes. Manual entries are merged with auto-discovered ones. | + +The CLI exposes `--out` for `outDir`; the richer options (`contentDir`, `pages`) require either a framework integration or calling the package's API directly: + +```js +// scripts/aeo.mjs +import { generateAEOFiles, resolveConfig } from 'aeo.js'; + +await generateAEOFiles(resolveConfig({ + title: 'My Site', + url: 'https://mysite.com', + contentDir: 'content', + outDir: 'public', + pages: [{ pathname: '/', title: 'Home', description: 'Welcome' }], +})); +``` + +## Common Setups + +### Hand-rolled HTML site + +```text +my-site/ +├── index.html +├── about/index.html +└── pricing/index.html +``` + +```jsonc +// package.json +{ + "scripts": { + "build:aeo": "aeo.js generate --url https://mysite.com --title \"My Site\" --out ." + } +} +``` + +```bash +npm run build:aeo +``` + +The output files land next to your `index.html`. Ready to deploy. + +### Eleventy / Hugo / Jekyll + +These SSGs already emit a finished `_site/` or `public/` of HTML — point `--out` at that folder. + +```jsonc +// package.json +{ + "scripts": { + "build": "eleventy", + "postbuild": "aeo.js generate --url https://mysite.com --title \"My Site\" --out _site" + } +} +``` + +For Hugo, use `--out public`; for Jekyll, `--out _site`. `npm` runs `postbuild` automatically after `build`, so `npm run build` produces the full site **and** the AEO files in one step. + +### Markdown blog (no framework) + +The CLI's `--out` flag covers the built-HTML case, but to pull `.md` front-matter and bodies from a `contentDir`, drop a small Node script next to your build: + +```text +my-blog/ +├── content/ +│ ├── intro-post.md +│ └── second-post.md +├── public/ # Your existing HTML build +│ ├── index.html +│ └── intro-post/index.html +└── scripts/aeo.mjs +``` + +```js +// scripts/aeo.mjs +import { generateAEOFiles, resolveConfig } from 'aeo.js'; + +await generateAEOFiles(resolveConfig({ + 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 +})); +``` + +```jsonc +// package.json +{ + "scripts": { + "build:aeo": "node scripts/aeo.mjs" + } +} +``` + +`generateAEOFiles` reads each `.md` file's front-matter, pulls its body content, and emits a unified `llms.txt` / `ai-index.json` / `sitemap.xml`. + +### 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. + +> **Always pass `--out`** matching your build output directory. Without it the CLI defaults to `dist/` for non-framework projects, so AEO files would land outside the directory you actually deploy. Each snippet below uses `--out public`; change it to match your build (e.g. `_site` for Eleventy/Jekyll, `dist` for Vite, `public` for Hugo). + +### Vercel + +```jsonc +// vercel.json +{ + "buildCommand": "npm run build && npx aeo.js generate --url https://mysite.com --title \"My Site\" --out public", + "outputDirectory": "public" +} +``` + +### Netlify + +```toml +# netlify.toml +[build] + command = "npm run build && npx aeo.js generate --url https://mysite.com --title \"My Site\" --out public" + publish = "public" +``` + +### Cloudflare Pages + +In the dashboard, set the build command to `npm run build && npx aeo.js generate --url https://mysite.com --title "My Site" --out public` and the output directory to `public`. 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 --url https://mysite.com --title "My Site" --out public + - 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 --url https://mysite.com --title "My Site" --out public +# Upload the contents of public/ 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 your `build:aeo` script in `package.json`** so contributors run the same flags. 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/vite.md b/docs/vite.md index 248276d..dd09cc8 100755 --- a/docs/vite.md +++ b/docs/vite.md @@ -257,7 +257,7 @@ useHead({ My Svelte App - {@html ``} +
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) 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..326531d 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,12 +63,21 @@ 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`: +import { Aside } from '@astrojs/starlight/components'; + +<Aside type="caution"> +The standalone CLI does **not** currently load `aeo.config.{ts,js}` — it configures itself from CLI flags + defaults only. `npx aeo.js init` still scaffolds the file as the canonical place for your settings, but today it's consumed by framework integrations rather than the CLI itself. +</Aside> + +Create one with `npx aeo.js init`: ```ts import { defineConfig } from 'aeo.js'; @@ -64,4 +90,22 @@ export default defineConfig({ }); ``` +Import it into your framework config so the integration picks it up: + +```ts +// vite.config.ts +import aeoConfig from './aeo.config'; +import { aeoVitePlugin } from 'aeo.js/vite'; + +export default { plugins: [aeoVitePlugin(aeoConfig)] }; +``` + +For raw CLI usage on a static site, pass values directly: + +```bash +npx aeo.js generate --url https://mysite.com --title "My Site" --out public +``` + 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..8eb343f --- /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(/\u2028/g, '\\u2028') + .replace(/\u2029/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> + <script type="application/ld+json">{@html 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..0e8a9cf --- /dev/null +++ b/website/src/content/docs/frameworks/vanilla.mdx @@ -0,0 +1,141 @@ +--- +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 generate --url https://mysite.com --title "My Site" --out public + ``` + </TabItem> + <TabItem label="Installed"> + ```bash + npm install --save-dev aeo.js + npx aeo.js generate --url https://mysite.com --title "My Site" --out public + ``` + </TabItem> +</Tabs> + +`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. + +<Aside type="caution"> +The standalone CLI configures itself from **flags only** — it does not load `aeo.config.{ts,js}`. Pass `--url` / `--title` / `--out` on the command line, or call the package's API programmatically (see "Calling the API directly" below) to access richer options like `contentDir` and `pages`. +</Aside> + +## How aeo.js Discovers Your Pages + +| Source | How to set | Use when | +|---|---|---| +| **Built HTML in `outDir`** | `--out public` (CLI) | You have a built static site with `*.html` files | +| **`contentDir`** | programmatic only — see below | You have handwritten `.md` / `.mdx` files | +| **`pages` array** | programmatic only — see below | Explicit control for runtime-only routes | + +The CLI exposes `--out` for `outDir`. For the richer options (`contentDir`, `pages`), call the package's API directly: + +```js +// scripts/aeo.mjs +import { generateAEOFiles, resolveConfig } from 'aeo.js'; + +await generateAEOFiles(resolveConfig({ + title: 'My Site', + url: 'https://mysite.com', + contentDir: 'content', + outDir: 'public', + pages: [{ pathname: '/', title: 'Home' }], +})); +``` + +## Common Setups + +### Hand-rolled HTML site + +```jsonc +// package.json +{ + "scripts": { + "build:aeo": "aeo.js generate --url https://mysite.com --title \"My Site\" --out ." + } +} +``` + +### Eleventy / Hugo / Jekyll + +```jsonc +// package.json +{ + "scripts": { + "build": "eleventy", + "postbuild": "aeo.js generate --url https://mysite.com --title \"My Site\" --out _site" + } +} +``` + +For Hugo use `--out public`; for Jekyll `--out _site`. The `postbuild` script runs automatically after `npm run build`. + +### Markdown blog (no framework) + +The CLI's `--out` flag covers built HTML; to also pull `.md` front-matter and bodies from a `contentDir`, use the programmatic API: + +```js +// scripts/aeo.mjs +import { generateAEOFiles, resolveConfig } from 'aeo.js'; + +await generateAEOFiles(resolveConfig({ + title: 'My Blog', + url: 'https://myblog.dev', + contentDir: 'content', // pull post bodies from here + outDir: 'public', // write generated files here +})); +``` + +```jsonc +// package.json +{ + "scripts": { + "build:aeo": "node scripts/aeo.mjs" + } +} +``` + +## 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/)