JSON-LD structured data for rich search results.
How It Works
Prestruct injects JSON-LD into every prerendered page via the buildJsonLd() function in ssr.config.js.
// ssr.config.js
export default {
// ...other config
buildJsonLd() {
return [
{
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'My Site',
url: 'https://example.com',
}
]
}
}
This generates:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "My Site",
"url": "https://example.com"
}
</script>
Common Schema Types
Organization
buildJsonLd() {
return [
{
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'Acme Corp',
url: 'https://example.com',
logo: 'https://example.com/logo.png',
description: 'We make great products',
sameAs: [
'https://twitter.com/acme',
'https://facebook.com/acme',
'https://linkedin.com/company/acme'
],
contactPoint: {
'@type': 'ContactPoint',
telephone: '+1-555-123-4567',
contactType: 'customer service',
availableLanguage: 'English'
}
}
]
}
WebSite + SearchAction
buildJsonLd() {
return [
{
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'Acme Site',
url: 'https://example.com',
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: 'https://example.com/search?q={search_term_string}'
},
'query-input': 'required name=search_term_string'
}
}
]
}
LocalBusiness
buildJsonLd() {
return [
{
'@context': 'https://schema.org',
'@type': 'LocalBusiness',
name: 'Acme Coffee Shop',
image: 'https://example.com/store.jpg',
address: {
'@type': 'PostalAddress',
streetAddress: '123 Main St',
addressLocality: 'San Francisco',
addressRegion: 'CA',
postalCode: '94102',
addressCountry: 'US'
},
geo: {
'@type': 'GeoCoordinates',
latitude: 37.7749,
longitude: -122.4194
},
openingHoursSpecification: [
{
'@type': 'OpeningHoursSpecification',
dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
opens: '07:00',
closes: '19:00'
}
],
telephone: '+1-555-123-4567',
priceRange: '$$'
}
]
}
Product
buildJsonLd() {
return [
{
'@context': 'https://schema.org',
'@type': 'Product',
name: 'Amazing Widget',
image: 'https://example.com/widget.jpg',
description: 'The best widget for your needs',
sku: 'WIDGET-001',
brand: {
'@type': 'Brand',
name: 'Acme'
},
offers: {
'@type': 'Offer',
url: 'https://example.com/products/widget',
priceCurrency: 'USD',
price: '29.99',
availability: 'https://schema.org/InStock',
seller: {
'@type': 'Organization',
name: 'Acme Corp'
}
}
}
]
}
FAQPage
buildJsonLd() {
return [
{
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: [
{
'@type': 'Question',
name: 'What is prestruct?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Prestruct is a build-time prerenderer for React/Vite apps.'
}
},
{
'@type': 'Question',
name: 'Does it work with Cloudflare Pages?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Yes, it is designed for Cloudflare Pages.'
}
}
]
}
]
}
Article/BlogPosting
Blog postings usually need per-route fields (headline, datePublished,
@id) – and prestruct calls buildJsonLd() with no arguments, so it
cannot vary by route. Two honest options:
- Site-wide blog metadata only: omit
BlogPostingfromssr.config.jsand extend the prerender script (see Advanced below) to inject per-post JSON-LD with real post fields. - Static placeholder: a
BlogPostinglist insidebuildJsonLd()would render the same object on every page – wrong for Google’s article markup, which must describe only the page it appears on. Do not ship per-route schema as site-wide JSON-LD.
Person
buildJsonLd() {
return [
{
'@context': 'https://schema.org',
'@type': 'Person',
name: 'John Doe',
jobTitle: 'Software Engineer',
image: 'https://example.com/john.jpg',
url: 'https://example.com/about/john',
sameAs: [
'https://twitter.com/johndoe',
'https://github.com/johndoe',
'https://linkedin.com/in/johndoe'
],
worksFor: {
'@type': 'Organization',
name: 'Acme Corp',
url: 'https://example.com'
}
}
]
}
Multiple Schema Types
You can return an array with multiple objects:
buildJsonLd() {
return [
// Organization (site-wide)
{
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'Acme Corp',
url: 'https://example.com',
},
// WebSite (for search)
{
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'Acme Corp',
url: 'https://example.com',
potentialAction: {
'@type': 'SearchAction',
target: 'https://example.com/search?q={search_term_string}',
'query-input': 'required name=search_term_string'
}
}
]
}
BreadcrumbList
Breadcrumb entries are per-route, so buildJsonLd()’s no-argument contract
means prestruct cannot generate them for you either. Same options as
BlogPosting above: keep the site-wide JSON-LD clean of breadcrumbs and inject
them per-route in a prerender extension, where the route path is actually
known. The mapping logic for reference:
// Inside a prerender extension -- routePath is available there,
// never inside buildJsonLd().
function breadcrumbLd(routePath, siteUrl) {
const parts = routePath.split('/').filter(Boolean)
let url = ''
const crumbs = parts.map((part, index) => {
url += '/' + part
return {
'@type': 'ListItem',
position: index + 1,
name: part.charAt(0).toUpperCase() + part.slice(1).replace(/-/g, ' '),
item: `${siteUrl}${url}/`
}
})
return crumbs.length
? [{ '@context': 'https://schema.org', '@type': 'BreadcrumbList', itemListElement: crumbs }]
: []
}
Validation
Google Rich Results Test
Use the Google Rich Results Test to validate your structured data.
Schema Markup Validator
Use the Schema Markup Validator for comprehensive validation.
Debugging
View Generated JSON-LD
grep -A 20 'application/ld+json' dist/index.html
Common Issues
Missing @context:
Every schema MUST have @context and @type.
Invalid @type: Use valid Schema.org types: https://schema.org/docs/full.html
Duplicate schemas: Only return one of each type per page.
URL mismatch: Canonical URLs in schema should match page URLs.
Advanced: Conditional Schema
Ground truth first: config.buildJsonLd() receives no arguments –
inject-brand calls it once per build and injects the result into every
prerendered page. It cannot vary per route by itself.
For per-route schemas, extend the prerender script (site-wide types still come
from buildJsonLd; the per-route map is your own addition):
// In prerender.js - inside the per-route render loop, after injectMeta
const routeSchemas = {
'/': ['Organization', 'WebSite'],
'/about': ['Organization'],
'/blog': ['BlogPosting'],
}
const types = routeSchemas[route.path] || []
if (types.length > 0) {
html = html.replace(
'</head>',
`<script type="application/ld+json">${JSON.stringify({
'@context': 'https://schema.org',
'@graph': types.map(t => ({ '@type': t }))
})}</script></head>`
)
}
Same caveat as the BlogPosting section above: per-route JSON-LD must describe only the page it appears on, or Google may reject it.
Next steps
- SEO Guide - meta tags, canonicals, sitemap behavior
- Config Reference -
buildJsonLdin context of the whole config