Schema Injection for Headless Apps: Next.js, Astro, Nuxt, and Remix
Headless and JAMstack apps generate zero schema by default. Here's how to add complete, AI-optimized structured data to Next.js, Astro, Nuxt, and Remix ā with working code examples.
Schema Injection for Headless Apps: Next.js, Astro, Nuxt, and Remix
> TL;DR
> - Headless apps have no CMS schema plugins ā structured data must be added in code or via injection
> - AI agents can't discover or purchase from headless apps without schema ā the gap is total
> - Working code examples for every major framework
> - Or skip the code: one script tag handles it all ā
Updated: April 21, 2026
---
The Headless Schema Gap
Traditional CMS platforms (WordPress, Shopify) at least generate partial schema through plugins. Headless apps ā built on Next.js, Astro, Nuxt, Remix, or custom React ā generate zero schema by default.
Your headless app might be blazing fast, beautifully designed, and technically excellent. To an AI agent crawling the web for products, services, or software to recommend or purchase, it's a blank page.
Schema injection bridges this gap without requiring architectural changes.
---
Option A: Schema Injection Script (Zero Code)
Add one script tag to your app's . SchemaInject detects page type and generates the correct schema automatically.
Next.js (App Router)
In app/layout.tsx:
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
src="https://schema-inject-cdn.justinspollack.workers.dev/inject.js?key=YOUR_KEY"
async
/>
{children}
)
}
Next.js (Pages Router)
In pages/_document.tsx:
import { Html, Head, Main, NextScript } from 'next/document'export default function Document() {
return (
src="https://schema-inject-cdn.justinspollack.workers.dev/inject.js?key=YOUR_KEY"
async
/>
)
}
Astro
In src/layouts/Layout.astro:
---
// Layout.astro
---
src="https://schema-inject-cdn.justinspollack.workers.dev/inject.js?key=YOUR_KEY"
async
is:inline
>
Nuxt 3
In nuxt.config.ts:
export default defineNuxtConfig({
app: {
head: {
script: [
{
src: 'https://schema-inject-cdn.justinspollack.workers.dev/inject.js?key=YOUR_KEY',
async: true
}
]
}
}
})
Remix
In app/root.tsx:
import { Links, Meta, Outlet, Scripts } from "@remix-run/react";export default function App() {
return (
src="https://schema-inject-cdn.justinspollack.workers.dev/inject.js?key=YOUR_KEY"
async
/>
);
}
Get your key from the free audit ā
---
Option B: Native JSON-LD Components (Code Path)
For full control, implement schema as reusable components that accept your data as props.
Next.js ā Reusable Schema Component
// components/SchemaMarkup.tsx
interface SchemaProps {
schema: Record
}export function SchemaMarkup({ schema }: SchemaProps) {
return (
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(schema)
}}
/>
)
}
Usage on a SaaS product page:
// app/page.tsx
import { SchemaMarkup } from '@/components/SchemaMarkup'export default function HomePage() {
const schema = {
"@context": "https://schema.org/",
"@type": "SoftwareApplication",
"name": "YourApp",
"description": "...",
"applicationCategory": "BusinessApplication",
"offers": [{
"@type": "Offer",
"name": "Free",
"price": "0",
"priceCurrency": "USD"
}]
}
return (
<>
{/ page content /}
>
)
}
Astro ā Static Schema
---
// ProductPage.astro
const schema = {
"@context": "https://schema.org/",
"@type": "Product",
"name": Astro.props.productName,
"offers": {
"@type": "Offer",
"price": Astro.props.price,
"priceCurrency": "USD",
"availability": "https://schema.org/InStock"
}
}
---
---
Per-Page Schema for Dynamic Routes
For dynamic product/content pages, generate schema from your data source:
Next.js Dynamic Product Pages
// app/products/[slug]/page.tsx
import { SchemaMarkup } from '@/components/SchemaMarkup'
import { getProduct } from '@/lib/products'export default async function ProductPage({ params }: { params: { slug: string } }) {
const product = await getProduct(params.slug)
const schema = {
"@context": "https://schema.org/",
"@type": "Product",
"name": product.name,
"description": product.description,
"image": product.imageUrl,
"sku": product.sku,
"offers": {
"@type": "Offer",
"price": product.price.toString(),
"priceCurrency": "USD",
"availability": product.inStock
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock",
"hasMerchantReturnPolicy": {
"@type": "MerchantReturnPolicy",
"returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
"merchantReturnDays": 30,
"returnFees": "https://schema.org/FreeReturn"
}
},
"aggregateRating": product.reviewCount > 0 ? {
"@type": "AggregateRating",
"ratingValue": product.avgRating.toString(),
"reviewCount": product.reviewCount.toString()
} : undefined
}
return (
<>
{/ product page UI /}
>
)
}
---
Why Headless + Schema Injection Is the Right Architecture
The native code approach gives you control but requires ongoing maintenance. Every schema field you hardcode is a field you have to keep updated. Price changes, new products, updated policies ā all require code changes.
Schema injection decouples schema from your codebase:
For teams shipping fast, injection is the better default. Add native schema for specific pages that need customization.
ā Get your injection snippet ā
ā Schema injection API for build-time embedding ā
---
Related articles: