From 92035dec2e0756a6329a449d8e7980a8d57ca1da Mon Sep 17 00:00:00 2001 From: HusseinAdeiza Date: Sat, 9 May 2026 18:21:56 +0100 Subject: [PATCH 01/11] docs: add comprehensive framework integration guides - Add docs directory with framework-specific guides - Create overview README (331 lines) - Add Next.js integration guide (551 lines) - Add Astro integration guide (413 lines) - Add Nuxt integration guide (448 lines) - Add Vite integration guide (529 lines) Total: 2,272 lines of comprehensive documentation Each guide includes: - Prerequisites and installation steps - Basic and advanced configuration - Framework-specific best practices - Deployment instructions - Troubleshooting section - Real-world examples Addresses the lack of detailed framework integration documentation. --- docs/README.md | 331 +++++++++++++++++++++++++++++ docs/astro.md | 413 ++++++++++++++++++++++++++++++++++++ docs/nextjs.md | 551 +++++++++++++++++++++++++++++++++++++++++++++++++ docs/nuxt.md | 448 ++++++++++++++++++++++++++++++++++++++++ docs/vite.md | 529 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 2272 insertions(+) create mode 100755 docs/README.md create mode 100755 docs/astro.md create mode 100755 docs/nextjs.md create mode 100755 docs/nuxt.md create mode 100755 docs/vite.md diff --git a/docs/README.md b/docs/README.md new file mode 100755 index 0000000..f575856 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,331 @@ +# Framework Integration Guides + +Comprehensive guides for integrating aeo.js with popular web frameworks to optimize your site for AI search engines like ChatGPT, Claude, Perplexity, and more. + +## Overview + +aeo.js (Answer Engine Optimization) helps make your website discoverable and understandable by AI-powered search engines. These guides provide step-by-step instructions for integrating aeo.js with your preferred framework. + +## What is AEO? + +Answer Engine Optimization (AEO) is the process of optimizing your website to be better understood and indexed by AI search engines. Unlike traditional SEO which focuses on keyword rankings, AEO focuses on: + +- **Structured content** - Making your content machine-readable +- **Semantic markup** - Using JSON-LD and schema.org +- **AI discovery files** - Generating llms.txt, robots.txt, sitemaps +- **Context signals** - Providing clear page descriptions and metadata + +## Supported Frameworks + +aeo.js provides native integrations for the following frameworks: + +| Framework | Guide | Integration Type | Status | +|-----------|-------|------------------|--------| +| Next.js | [nextjs.md](./nextjs.md) | Plugin + Middleware | ✅ Stable | +| Astro | [astro.md](./astro.md) | Native Integration | ✅ Stable | +| Nuxt | [nuxt.md](./nuxt.md) | Module | ✅ Stable | +| Vite | [vite.md](./vite.md) | Plugin | ✅ Stable | +| Angular | Coming soon | Post-build | 🚧 Beta | +| Webpack | Coming soon | Plugin | 🚧 Beta | + +## Quick Start + +Choose your framework: + +### Next.js +```bash +npm install aeo.js +``` + +```js +// next.config.mjs +import { withAeo } from 'aeo.js/next'; + +export default withAeo({ + aeo: { + title: 'My Site', + description: 'Optimized for AI discovery', + url: 'https://mysite.com', + }, +}); +``` + +[→ Full Next.js Guide](./nextjs.md) + +### Astro +```bash +npm install aeo.js +``` + +```js +// astro.config.mjs +import { aeoAstroIntegration } from 'aeo.js/astro'; + +export default defineConfig({ + integrations: [ + aeoAstroIntegration({ + title: 'My Site', + url: 'https://mysite.com', + }), + ], +}); +``` + +[→ Full Astro Guide](./astro.md) + +### Nuxt +```bash +npm install aeo.js +``` + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + modules: ['aeo.js/nuxt'], + aeo: { + title: 'My Site', + url: 'https://mysite.com', + }, +}); +``` + +[→ Full Nuxt Guide](./nuxt.md) + +### Vite +```bash +npm install aeo.js +``` + +```js +// vite.config.ts +import { aeoVitePlugin } from 'aeo.js/vite'; + +export default defineConfig({ + plugins: [ + aeoVitePlugin({ + title: 'My Site', + url: 'https://mysite.com', + }), + ], +}); +``` + +[→ Full Vite Guide](./vite.md) + +## What Gets Generated? + +When you integrate aeo.js, it automatically generates: + +### 1. llms.txt +A structured file that AI search engines use to understand your site's content and structure. + +``` +# My Site + +> Optimized for AI discovery + +## Site Information +- URL: https://mysite.com +- Description: A comprehensive resource for... +- Last Updated: 2026-05-09 + +## Pages +- /about: About our company and mission +- /blog: Technical articles and tutorials +- /products: Our product offerings +``` + +### 2. Enhanced robots.txt +``` +User-agent: * +Allow: / + +User-agent: GPTBot +Allow: / + +User-agent: Claude-Web +Allow: / + +Sitemap: https://mysite.com/sitemap.xml +``` + +### 3. XML Sitemap +```xml + + + + https://mysite.com/ + 2026-05-09 + 1.0 + + + +``` + +### 4. JSON-LD Structured Data +Automatically injects schema.org structured data into your pages: + +```json +{ + "@context": "https://schema.org", + "@type": "WebSite", + "name": "My Site", + "url": "https://mysite.com", + "description": "Optimized for AI discovery" +} +``` + +## Configuration Options + +All frameworks support these common configuration options: + +```typescript +interface AeoConfig { + // Required + title: string; // Your site title + url: string; // Your site URL + + // Optional + description?: string; // Site description + keywords?: string[]; // SEO keywords + author?: string; // Site author + language?: string; // Default: 'en' + + // Generation options + generateLLMsTxt?: boolean; // Default: true + generateRobotsTxt?: boolean; // Default: true + generateSitemap?: boolean; // Default: true + generateJsonLd?: boolean; // Default: true + + // Advanced + customPages?: PageConfig[]; // Custom page metadata + excludePaths?: string[]; // Paths to exclude + includePaths?: string[]; // Specific paths to include + sitemapPriority?: Record; // Per-page priorities +} +``` + +## Common Use Cases + +### Blog / Content Site +Perfect for making your articles discoverable by AI assistants: + +```js +{ + title: 'Tech Blog', + description: 'In-depth technical tutorials and guides', + keywords: ['javascript', 'typescript', 'web development'], + generateJsonLd: true, // Enable article schema +} +``` + +### Documentation Site +Optimize technical documentation for AI-powered search: + +```js +{ + title: 'API Documentation', + description: 'Complete API reference and guides', + customPages: [ + { path: '/api', title: 'API Reference', priority: 1.0 }, + { path: '/guides', title: 'Getting Started Guides', priority: 0.9 }, + ], +} +``` + +### E-commerce Site +Help AI understand your product catalog: + +```js +{ + title: 'Online Store', + description: 'Quality products delivered fast', + generateJsonLd: true, // Enable Product schema + excludePaths: ['/checkout', '/account'], // Exclude private pages +} +``` + +### SaaS Product +Optimize your marketing site and product pages: + +```js +{ + title: 'My SaaS Product', + description: 'The best tool for...', + customPages: [ + { path: '/', title: 'Home', priority: 1.0 }, + { path: '/features', title: 'Features', priority: 0.9 }, + { path: '/pricing', title: 'Pricing', priority: 0.9 }, + { path: '/docs', title: 'Documentation', priority: 0.8 }, + ], +} +``` + +## Best Practices + +1. **Set accurate metadata** - Provide clear, descriptive titles and descriptions +2. **Use semantic HTML** - Structure your content with proper headings +3. **Include alt text** - Describe images for better AI understanding +4. **Add structured data** - Use JSON-LD for rich content markup +5. **Keep content fresh** - Update llms.txt when content changes +6. **Test with AI** - Ask ChatGPT or Claude about your site +7. **Monitor performance** - Track AI referrals in analytics + +## Troubleshooting + +### Files not generating? + +Check that: +- Build process completes successfully +- Output directory has write permissions +- Configuration is valid + +### AI not finding my site? + +Ensure: +- llms.txt is publicly accessible at `/llms.txt` +- robots.txt allows AI crawlers +- Sitemap is linked in robots.txt +- DNS and SSL are properly configured + +### Framework-specific issues? + +See the detailed framework guides: +- [Next.js Troubleshooting](./nextjs.md#troubleshooting) +- [Astro Troubleshooting](./astro.md#troubleshooting) +- [Nuxt Troubleshooting](./nuxt.md#troubleshooting) +- [Vite Troubleshooting](./vite.md#troubleshooting) + +## Migration Guides + +### From Manual AEO +If you've been manually creating llms.txt and robots.txt: + +1. Remove manual files from your public directory +2. Install and configure aeo.js +3. Run build to generate files automatically +4. Verify generated files match your requirements +5. Commit the configuration, delete manual files + +### From Other AEO Tools +Coming soon - guides for migrating from other AEO solutions. + +## Getting Help + +- **Issues**: [GitHub Issues](https://github.com/multivmlabs/aeo.js/issues) +- **Discussions**: [GitHub Discussions](https://github.com/multivmlabs/aeo.js/discussions) +- **Updates**: Follow [@multivmlabs](https://twitter.com/multivmlabs) + +## Contributing + +Found a better way to configure aeo.js for your framework? Have an example to share? + +We welcome contributions! See the main [CONTRIBUTING.md](../CONTRIBUTING.md) for guidelines. + +## License + +MIT - See [LICENSE](../LICENSE) for details. + +--- + +**Ready to optimize your site for AI discovery? Choose your framework guide above and get started!** diff --git a/docs/astro.md b/docs/astro.md new file mode 100755 index 0000000..0c1c35c --- /dev/null +++ b/docs/astro.md @@ -0,0 +1,413 @@ +# Astro Integration Guide + +Complete guide for integrating aeo.js with Astro to optimize your site for AI-powered search engines. + +## Prerequisites + +- **Astro**: 3.0 or higher +- **Node.js**: 18.0 or higher +- **Package Manager**: npm, yarn, or pnpm + +## Installation + +### Step 1: Install aeo.js + +```bash +npm install aeo.js +# or +yarn add aeo.js +# or +pnpm add aeo.js +``` + +### Step 2: Add Integration to Astro Config + +```javascript +// astro.config.mjs +import { defineConfig } from 'astro/config'; +import { aeoAstroIntegration } from 'aeo.js/astro'; + +export default defineConfig({ + site: 'https://mysite.com', + integrations: [ + aeoAstroIntegration({ + title: 'My Astro Site', + description: 'Built with Astro and optimized for AI discovery', + url: 'https://mysite.com', + keywords: ['astro', 'static-site', 'performance'], + }), + ], +}); +``` + +### Step 3: Build and Verify + +```bash +npm run build +``` + +Check that these files were generated in your `dist` directory: +- `dist/llms.txt` +- `dist/robots.txt` +- `dist/sitemap.xml` + +## Configuration + +### Basic Configuration + +```typescript +interface AstroAeoConfig { + title: string; // Required + url: string; // Required (matches site in astro.config) + description?: string; // Recommended + keywords?: string[]; + language?: string; // Default: 'en' +} +``` + +### Advanced Configuration + +```javascript +export default defineConfig({ + site: 'https://mysite.com', + integrations: [ + aeoAstroIntegration({ + // Basic info + title: 'My Astro Site', + url: 'https://mysite.com', + description: 'Lightning-fast static site built with Astro', + + // SEO + keywords: ['astro', 'jamstack', 'static-site-generator'], + language: 'en', + author: 'Your Name', + + // Generation options + generateLLMsTxt: true, + generateRobotsTxt: true, + generateSitemap: true, + generateJsonLd: true, + + // Custom pages + customPages: [ + { + path: '/', + title: 'Home', + description: 'Welcome to our lightning-fast site', + priority: 1.0, + }, + { + path: '/blog', + title: 'Blog', + description: 'Latest articles', + priority: 0.9, + }, + ], + + // Path filtering + excludePaths: [ + '/admin/*', + '/api/*', + ], + }), + ], +}); +``` + +## Content Collections Integration + +Astro's content collections work seamlessly with aeo.js: + +```typescript +// src/content/config.ts +import { defineCollection, z } from 'astro:content'; + +const blog = defineCollection({ + schema: z.object({ + title: z.string(), + description: z.string(), + pubDate: z.date(), + author: z.string(), + tags: z.array(z.string()), + }), +}); + +export const collections = { blog }; +``` + +## Adding Structured Data + +### Page-Level JSON-LD + +```astro +--- +// src/pages/blog/[slug].astro +import { getEntry } from 'astro:content'; + +const { slug } = Astro.params; +const post = await getEntry('blog', slug); + +const schema = { + '@context': 'https://schema.org', + '@type': 'BlogPosting', + headline: post.data.title, + description: post.data.description, + datePublished: post.data.pubDate.toISOString(), + author: { + '@type': 'Person', + name: post.data.author, + }, +}; +--- + + + + {post.data.title} + + + + +``` + +### Dynamic Meta from API + +```vue + +``` + +## Content Module Integration + +### Setup Nuxt Content + +```bash +npm install @nuxt/content +``` + +```typescript +// nuxt.config.ts +export default defineNuxtConfig({ + modules: ['@nuxt/content', 'aeo.js/nuxt'], + + content: { + highlight: { + theme: 'github-dark', + }, + }, + + aeo: { + title: 'My Blog', + url: 'https://myblog.com', + }, +}); +``` + +### Content-Driven Pages + +```vue + + + +``` + +## Best Practices + +### 1. App-Level Configuration + +```vue + + + + +``` + +### 2. Server Routes for Dynamic Sitemaps + +```typescript +// server/routes/sitemap.xml.ts +export default defineEventHandler(async (event) => { + const posts = await $fetch('/api/posts'); + + const sitemap = ` + + + https://mysite.com/ + 1.0 + + ${posts.map(post => ` + + https://mysite.com/blog/${post.slug} + ${post.updatedAt} + 0.8 + + `).join('')} + + `; + + setHeader(event, 'Content-Type', 'application/xml'); + return sitemap; +}); +``` + +### 3. Environment-Specific Config + +```typescript +// nuxt.config.ts +const isProd = process.env.NODE_ENV === 'production'; + +export default defineNuxtConfig({ + modules: ['aeo.js/nuxt'], + + aeo: { + title: 'My Site', + url: isProd ? 'https://mysite.com' : 'http://localhost:3000', + generateSitemap: isProd, + }, +}); +``` + +### 4. Composables for Structured Data + +```typescript +// composables/useStructuredData.ts +export const useStructuredData = (type: string, data: any) => { + useHead({ + script: [ + { + type: 'application/ld+json', + children: JSON.stringify({ + '@context': 'https://schema.org', + '@type': type, + ...data, + }), + }, + ], + }); +}; +``` + +Usage: +```vue + +``` + +## Deployment + +### Vercel + +```json +// vercel.json +{ + "buildCommand": "npm run build", + "outputDirectory": ".output/public" +} +``` + +### Netlify + +```toml +# netlify.toml +[build] + command = "npm run build" + publish = ".output/public" +``` + +### Node Server + +```bash +npm run build +node .output/server/index.mjs +``` + +## Troubleshooting + +### Module Not Found + +**Problem**: `Cannot find module 'aeo.js/nuxt'` + +**Solution**: +```bash +rm -rf node_modules .nuxt +npm install +``` + +### Files in Wrong Directory + +**Problem**: AEO files in wrong output location. + +**Solution**: Check Nuxt version - v3 uses `.output/public` + +### SSR vs Static Generation + +**Problem**: Different behavior between `nuxt build` and `nuxt generate`. + +**Solution**: Use `nuxt generate` for static sites: +```bash +npx nuxi generate +``` + +## Examples + +### Blog with Categories + +```vue + + + + +``` + +### E-commerce Product Pages + +```vue + +``` + +## Further Reading + +- [Nuxt Documentation](https://nuxt.com/docs) +- [Nuxt Content](https://content.nuxt.com) +- [Nuxt SEO](https://nuxtseo.com) +- [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 new file mode 100755 index 0000000..27232c6 --- /dev/null +++ b/docs/vite.md @@ -0,0 +1,529 @@ +# Vite Integration Guide + +Complete guide for integrating aeo.js with Vite to optimize your site for AI-powered search engines. + +## Prerequisites + +- **Vite**: 4.0 or higher +- **Node.js**: 18.0 or higher +- **Framework**: Works with React, Vue, Svelte, Solid, or vanilla JS + +## Installation + +### Step 1: Install aeo.js + +```bash +npm install aeo.js +# or +yarn add aeo.js +# or +pnpm add aeo.js +``` + +### Step 2: Add Plugin to Vite Config + +```typescript +// vite.config.ts +import { defineConfig } from 'vite'; +import { aeoVitePlugin } from 'aeo.js/vite'; + +export default defineConfig({ + plugins: [ + aeoVitePlugin({ + title: 'My Vite Site', + description: 'Built with Vite and optimized for AI discovery', + url: 'https://mysite.com', + keywords: ['vite', 'fast', 'modern'], + }), + ], +}); +``` + +### Step 3: Build and Verify + +```bash +npm run build +``` + +Check generated files in `dist`: +- `dist/llms.txt` +- `dist/robots.txt` +- `dist/sitemap.xml` + +## Framework-Specific Setup + +### React + Vite + +```typescript +// vite.config.ts +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { aeoVitePlugin } from 'aeo.js/vite'; + +export default defineConfig({ + plugins: [ + react(), + aeoVitePlugin({ + title: 'My React App', + url: 'https://myapp.com', + description: 'React application optimized for AI', + }), + ], +}); +``` + +### Vue + Vite + +```typescript +// vite.config.ts +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import { aeoVitePlugin } from 'aeo.js/vite'; + +export default defineConfig({ + plugins: [ + vue(), + aeoVitePlugin({ + title: 'My Vue App', + url: 'https://myapp.com', + description: 'Vue application with AEO', + }), + ], +}); +``` + +### Svelte + Vite + +```typescript +// vite.config.ts +import { defineConfig } from 'vite'; +import { svelte } from '@sveltejs/vite-plugin-svelte'; +import { aeoVitePlugin } from 'aeo.js/vite'; + +export default defineConfig({ + plugins: [ + svelte(), + aeoVitePlugin({ + title: 'My Svelte App', + url: 'https://myapp.com', + }), + ], +}); +``` + +## Configuration + +### Basic Configuration + +```typescript +aeoVitePlugin({ + title: string; // Required + url: string; // Required + description?: string; + keywords?: string[]; + language?: string; +}) +``` + +### Advanced Configuration + +```typescript +aeoVitePlugin({ + // Basic info + title: 'My Vite Site', + url: 'https://mysite.com', + description: 'Lightning-fast web application', + + // SEO + keywords: ['vite', 'performance', 'modern-web'], + language: 'en', + author: 'Your Name', + + // Generation options + generateLLMsTxt: true, + generateRobotsTxt: true, + generateSitemap: true, + generateJsonLd: true, + + // Custom pages + customPages: [ + { + path: '/', + title: 'Home', + description: 'Welcome to our app', + priority: 1.0, + }, + { + path: '/features', + title: 'Features', + description: 'App features', + priority: 0.9, + }, + ], + + // Path filtering + excludePaths: [ + '/admin/*', + '/_dist/*', + ], +}) +``` + +## Adding Metadata + +### React with Helmet + +```bash +npm install react-helmet-async +``` + +```typescript +// App.tsx +import { Helmet } from 'react-helmet-async'; + +export default function App() { + return ( + <> + + My App + + + +
{/* App content */}
+ + ); +} +``` + +### Vue with useHead + +```bash +npm install @unhead/vue +``` + +```vue + + + +``` + +### Svelte with svelte:head + +```svelte + + My Svelte App + + {@html ` + (or U+2028/U+2029, common in copy-pasted user content) would break out of the script block and execute as JavaScript. The same anti-pattern existed across every framework example that emitted custom JSON-LD: - docs/nextjs.md — dangerouslySetInnerHTML in both the App Router layout and Pages Router blog post examples - docs/astro.md — set:html in the page-level and global-site schema examples - docs/nuxt.md — useHead `children` in three places (basic example, dynamic-meta example, useStructuredData composable) - docs/vite.md — Helmet, @unhead/vue useHead, and Svelte {@html} Each guide now hoists a single serializeJsonForHtml helper at the top of its "Adding Structured Data" / "Adding Metadata" / "Page Meta & SEO" section, mirroring the same escape set aeo.js uses internally in src/core/schema.ts (<, >, &, 
, 
). Every downstream injection point imports and uses the helper. The accompanying note clarifies: aeo.js's own injected JSON-LD is already safe via this same helper internally; only user-authored custom additions need the explicit escape. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/astro.md | 21 ++++++++++++++++++-- docs/nextjs.md | 20 +++++++++++++++++-- docs/nuxt.md | 28 ++++++++++++++++++++++---- docs/vite.md | 54 +++++++++++++++++++++++++++++++++----------------- 4 files changed, 97 insertions(+), 26 deletions(-) diff --git a/docs/astro.md b/docs/astro.md index e34a394..ec22b6d 100755 --- a/docs/astro.md +++ b/docs/astro.md @@ -125,12 +125,27 @@ export const collections = { blog }; ## Adding Structured Data +> Astro's `set:html` directive injects the value verbatim — it does **not** escape characters that can terminate the surrounding `` (or U+2028/U+2029) would break out and execute as JavaScript. Run the payload through a serializer that escapes those characters first — the helper below is the same `serializeJsonForHtml` aeo.js uses internally ([src/core/schema.ts](https://github.com/multivmlabs/aeo.js/blob/main/src/core/schema.ts)). aeo.js's own injected JSON-LD is already safe; only your custom additions need this. + +```ts +// src/lib/serialize-json-ld.ts +export function serializeJsonForHtml(value: unknown): string { + return JSON.stringify(value) + .replace(//g, '\\u003E') + .replace(/&/g, '\\u0026') + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029'); +} +``` + ### Page-Level JSON-LD ```astro --- // src/pages/blog/[slug].astro import { getEntry } from 'astro:content'; +import { serializeJsonForHtml } from '../lib/serialize-json-ld'; const { slug } = Astro.params; const post = await getEntry('blog', slug); @@ -152,7 +167,7 @@ const schema = { {post.data.title} - ` (or U+2028/U+2029) would break out and execute as JavaScript. Run the payload through a serializer that escapes those characters first — this is the same `serializeJsonForHtml` aeo.js uses internally ([src/core/schema.ts](https://github.com/multivmlabs/aeo.js/blob/main/src/core/schema.ts)). aeo.js's own injected JSON-LD is already safe; only your custom additions need this. + +```typescript +// app/lib/serialize-json-ld.ts +export function serializeJsonForHtml(value: unknown): string { + return JSON.stringify(value) + .replace(//g, '\\u003E') + .replace(/&/g, '\\u0026') + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029'); +} +``` + ```typescript // app/layout.tsx import { Metadata } from 'next'; +import { serializeJsonForHtml } from './lib/serialize-json-ld'; export const metadata: Metadata = { title: 'My Next.js Site', @@ -171,7 +186,7 @@ export default function RootLayout({ ` (or U+2028/U+2029) would break out and execute as JavaScript. Run the payload through a serializer that escapes those characters first — the helper below is the same `serializeJsonForHtml` aeo.js uses internally ([src/core/schema.ts](https://github.com/multivmlabs/aeo.js/blob/main/src/core/schema.ts)). aeo.js's own injected JSON-LD is already safe; only your custom additions need this. + +```ts +// utils/serialize-json-ld.ts +export function serializeJsonForHtml(value: unknown): string { + return JSON.stringify(value) + .replace(//g, '\\u003E') + .replace(/&/g, '\\u0026') + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029'); +} +``` + ### Using useHead Composable ```vue ` (or U+2028 / U+2029, anywhere in the payload including user-controlled titles) breaks out of the script block and executes as arbitrary JavaScript. Run the payload through a serializer that escapes those characters first — this is the same `serializeJsonForHtml` aeo.js uses internally ([src/core/schema.ts](https://github.com/multivmlabs/aeo.js/blob/main/src/core/schema.ts)). aeo.js's own injected JSON-LD is already safe; only your custom additions need this. + +```typescript +// src/lib/serialize-json-ld.ts +export function serializeJsonForHtml(value: unknown): string { + return JSON.stringify(value) + .replace(//g, '\\u003E') + .replace(/&/g, '\\u0026') + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029'); +} +``` + ### React with Helmet ```bash @@ -168,21 +182,22 @@ npm install react-helmet-async ```typescript // App.tsx import { Helmet } from 'react-helmet-async'; +import { serializeJsonForHtml } from './lib/serialize-json-ld'; export default function App() { + const schema = serializeJsonForHtml({ + '@context': 'https://schema.org', + '@type': 'WebApplication', + name: 'My App', + url: 'https://myapp.com', + }); + return ( <> My App - +
{/* App content */}
@@ -199,6 +214,7 @@ npm install @unhead/vue ```vue + My Svelte App - {@html ` - `}
From 2f14dbf5e4ea23c6bc325b600c1301767b4fcb1a Mon Sep 17 00:00:00 2001 From: ruben-cytonic Date: Thu, 14 May 2026 12:37:09 +0100 Subject: [PATCH 05/11] docs: fix TypeScript correctness in dynamic-route examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two TS-strict-mode errors Greptile flagged in copy-paste-able snippets: 1. docs/astro.md — Astro.params.slug is `string | undefined`, so passing it directly to getEntry('blog', slug) fails type-checking. Added an explicit guard with Astro.redirect('/404') before the call. 2. docs/nextjs.md — generateMetadata had no type annotation on { params }, raising "implicitly has any" under "strict": true. Added an explicit Props type for Next.js 14 (sync params) and a note pointing users to the async params form for Next.js 15+. Both examples now compile cleanly under strict mode. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/astro.md | 1 + docs/nextjs.md | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/astro.md b/docs/astro.md index ec22b6d..0495ad4 100755 --- a/docs/astro.md +++ b/docs/astro.md @@ -148,6 +148,7 @@ import { getEntry } from 'astro:content'; import { serializeJsonForHtml } from '../lib/serialize-json-ld'; const { slug } = Astro.params; +if (!slug) return Astro.redirect('/404'); const post = await getEntry('blog', slug); const schema = { diff --git a/docs/nextjs.md b/docs/nextjs.md index 91c7b30..5ca557e 100755 --- a/docs/nextjs.md +++ b/docs/nextjs.md @@ -208,9 +208,13 @@ export default function RootLayout({ // app/blog/[slug]/page.tsx import { Metadata } from 'next'; -export async function generateMetadata({ params }): Promise { +// Next.js 14 and below: params is a plain object. +// For Next.js 15+, params is a Promise — see note below. +type Props = { params: { slug: string } }; + +export async function generateMetadata({ params }: Props): Promise { const post = await getPost(params.slug); - + return { title: post.title, description: post.excerpt, @@ -224,6 +228,8 @@ export async function generateMetadata({ params }): Promise { } ``` +> **Next.js 15+** introduced async params. Change the type to `{ params: Promise<{ slug: string }> }` and `await` it: `const { slug } = await params;`. + ## Pages Router Integration ### Custom _app.tsx From ce7f2a534c10be92d8e7618a73a1b7bdade636bf Mon Sep 17 00:00:00 2001 From: ruben-cytonic Date: Thu, 14 May 2026 12:50:47 +0100 Subject: [PATCH 06/11] docs: more TypeScript/runtime safety in dynamic-route examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five fixes from Greptile's third pass: 1. docs/astro.md — getEntry returns CollectionEntry | undefined in Astro 3+. Added an `if (!post) return Astro.redirect('/404');` guard right after the call to prevent a runtime TypeError when a slug isn't in the collection. 2. docs/nextjs.md — BlogPost({ post }) had no prop type annotation, raising TS7031 implicit-any under strict mode. Added an inline BlogPostProps type matching the schema fields the example reads. 3. docs/nextjs.md — Dynamic sitemap example interpolated post.updatedAt directly, which would produce "Thu May 14 2026 ..." (Date.toString) for a JS Date from Prisma/Drizzle/etc., breaking sitemap.xml validators. Now wraps it: new Date(post.updatedAt).toISOString(). 4. docs/nuxt.md — route.params.slug is `string | string[]` in Vue Router 4 (catch-all support). Added a typeof-narrowing line so the template literal is type-safe and a catch-all route doesn't silently produce a comma-joined URL. 5. docs/nuxt.md — Same lastmod fix as #3 for the server-route sitemap example. All examples now compile and behave correctly under strict mode + typical Date-returning ORMs. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/astro.md | 1 + docs/nextjs.md | 13 +++++++++++-- docs/nuxt.md | 6 ++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/astro.md b/docs/astro.md index 0495ad4..b466c13 100755 --- a/docs/astro.md +++ b/docs/astro.md @@ -150,6 +150,7 @@ import { serializeJsonForHtml } from '../lib/serialize-json-ld'; const { slug } = Astro.params; if (!slug) return Astro.redirect('/404'); const post = await getEntry('blog', slug); +if (!post) return Astro.redirect('/404'); const schema = { '@context': 'https://schema.org', diff --git a/docs/nextjs.md b/docs/nextjs.md index 5ca557e..1a6ea9e 100755 --- a/docs/nextjs.md +++ b/docs/nextjs.md @@ -259,7 +259,16 @@ export default function App({ Component, pageProps }: AppProps) { import Head from 'next/head'; import { serializeJsonForHtml } from '@/lib/serialize-json-ld'; -export default function BlogPost({ post }) { +type BlogPostProps = { + post: { + title: string; + excerpt: string; + publishedAt: string; + author: { name: string }; + }; +}; + +export default function BlogPost({ post }: BlogPostProps) { return ( <> @@ -309,7 +318,7 @@ export default async function handler( ${posts.map(post => ` https://mysite.com/blog/${post.slug} - ${post.updatedAt} + ${new Date(post.updatedAt).toISOString()} 0.8 `).join('')} diff --git a/docs/nuxt.md b/docs/nuxt.md index c6eb28f..730192f 100755 --- a/docs/nuxt.md +++ b/docs/nuxt.md @@ -153,7 +153,9 @@ useHead({ import { serializeJsonForHtml } from '~/utils/serialize-json-ld'; const route = useRoute(); -const { data: post } = await useFetch(`/api/posts/${route.params.slug}`); +// route.params values are `string | string[]` in Vue Router 4 — narrow before use +const slug = typeof route.params.slug === 'string' ? route.params.slug : route.params.slug[0]; +const { data: post } = await useFetch(`/api/posts/${slug}`); useHead({ title: () => `${post.value?.title} | My Blog`, @@ -270,7 +272,7 @@ export default defineEventHandler(async (event) => { ${posts.map(post => ` https://mysite.com/blog/${post.slug} - ${post.updatedAt} + ${new Date(post.updatedAt).toISOString()} 0.8 `).join('')} From 27d28e4bbbbb0bf6f38b9890f6eafbeed6426c96 Mon Sep 17 00:00:00 2001 From: ruben-cytonic Date: Thu, 14 May 2026 13:02:08 +0100 Subject: [PATCH 07/11] docs(nuxt): narrow route.params in Examples section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same Vue Router 4 fix already applied to the "Dynamic Meta from API" example, now applied to the two remaining sites Greptile flagged: - "Blog with Categories" — route.params.category narrowed before use in fetch URL, useHead title/description, and template binding. - "E-commerce Product Pages" — route.params.id narrowed before use in fetch URL. Both prevent the strict-mode type error and the catch-all-route silent-failure where [...slug].vue would yield a comma-joined string in the URL. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/nuxt.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/nuxt.md b/docs/nuxt.md index 730192f..915c2cb 100755 --- a/docs/nuxt.md +++ b/docs/nuxt.md @@ -397,21 +397,23 @@ npx nuxi generate