JSON-LD in Next.js 15: Rich Snippets for SEO Landing Pages
A landing page can hit 800ms load time and score 100 in Lighthouse — yet still appear as a plain blue link in Google. A competitor with FAQ accordions directly in the SERP steals clicks because their FAQPage markup is configured correctly.
JSON-LD is Google’s recommended structured data format. In Next.js 15 App Router, Server Components render it into the initial HTML — no JavaScript required. Crawlers parse application/ld+json on the first response, whether you use Partial Prerendering or pure static generation.
This guide covers a production workflow: Schema.org types for service landing pages, TypeScript typing, Metadata API alignment, validation, and mistakes that break rich results.
Why Structured Data Still Matters in 2026
Google supports dozens of Schema.org types, but most B2B service landing pages need only a handful:
| Type | SERP benefit | Use case |
|---|---|---|
Organization | Knowledge panel, logo | Any commercial site |
FAQPage | Expandable Q&A in results | Landing pages with FAQ |
ProfessionalService | Service entity linking | Agency, consulting, dev shops |
BreadcrumbList | Breadcrumb trail | Multi-page sites |
Since 2024, Google has retired some rich result types (HowTo, Special Announcement), but FAQPage and Organization remain active. Markup doesn’t directly boost rankings — it helps Google understand content and format snippets. FAQ text in JSON-LD must match visible page content, or you risk a manual action for spam structured data.
Next.js 15’s advantage: JSON-LD ships server-side with HTML. Client-side injection via useEffect is risky — Googlebot may miss it, and you add unnecessary JavaScript.
Schema Selection: Organization, FAQPage, Service
Organization with @id
Every landing page starts with Organization. Use @id so other schemas reference the same entity:
const organizationSchema = {
'@context': 'https://schema.org',
'@type': 'Organization',
'@id': 'https://example.com/#organization',
name: 'Kel IT',
url: 'https://example.com',
logo: 'https://example.com/logo.png',
contactPoint: {
'@type': 'ContactPoint',
contactType: 'sales',
email: 'vic.kell@ya.ru',
},
};
FAQPage
The highest-impact rich result for landing pages. Map FAQ items to Question / Answer pairs:
const faqSchema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqItems.map((item) => ({
'@type': 'Question',
name: item.question,
acceptedAnswer: {
'@type': 'Answer',
text: item.answer,
},
})),
};
Use a single data source for both the FAQ UI component and schema generation — especially when content comes from a headless CMS.
Linking with @graph
Combine schemas in one script tag using @graph:
const schema = {
'@context': 'https://schema.org',
'@graph': [
organizationSchema,
{
'@type': 'WebSite',
'@id': 'https://example.com/#website',
publisher: { '@id': 'https://example.com/#organization' },
},
faqSchema,
],
};
Need similar development? Message on Telegram.
App Router Implementation
Create a reusable Server Component:
// components/json-ld.tsx
export function JsonLd({ data }: { data: Record<string, unknown> }) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
);
}
Server-controlled data makes dangerouslySetInnerHTML safe here — never pipe unsanitized user input into schema.
TypeScript with schema-dts
Install schema-dts for compile-time checks on @type fields:
import type { WithContext, Organization } from 'schema-dts';
const org: WithContext<Organization> = {
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'Kel IT',
url: 'https://example.com',
};
Page integration
// app/page.tsx
import { JsonLd } from '@/components/json-ld';
import { buildLandingSchema } from '@/lib/schema';
export default function LandingPage() {
return (
<>
<JsonLd data={buildLandingSchema()} />
<Hero />
<FAQ />
</>
);
}
For CMS-driven sites, fetch FAQ and settings in the schema builder during Server Component render. With ISR (revalidate: 3600), schema updates alongside HTML automatically.
Keep Metadata API in Sync
JSON-LD complements — doesn’t replace — the Metadata API. Titles, descriptions, and canonical URLs should match schema data:
export const metadata: Metadata = {
title: 'Web Development — Next.js 15, Astro 5, SEO',
description: 'Landing pages with rich snippets and Core Web Vitals 90+.',
metadataBase: new URL('https://example.com'),
alternates: { canonical: '/' },
openGraph: {
title: 'Web Development — Kel IT',
type: 'website',
images: [{ url: '/og.png', width: 1200, height: 630 }],
},
};
Extract shared values into a siteConfig object used by both metadata and buildOrganization() to prevent drift.
For multilingual sites, add alternates.languages in metadata and set inLanguage in JSON-LD per locale.
Validation Workflow
Before deploy:
- View Source — confirm
<script type="application/ld+json">exists in raw HTML (not just DevTools DOM) - Rich Results Test — zero errors on production URL
- Search Console → Enhancements — monitor FAQ and Organization status post-indexing
Add a unit test to catch FAQ desync during refactors:
it('FAQ schema matches content source', () => {
const schema = buildLandingSchema({ faqItems });
const faq = schema['@graph'].find((i) => i['@type'] === 'FAQPage');
expect(faq.mainEntity[0].name).toBe(faqItems[0].question);
});
Common Mistakes
- Invisible content in markup — FAQ in JSON-LD without matching on-page text triggers penalties
- Client-only injection — schema may not appear in crawled HTML; use Server Components
- Conflicting Organization blocks — use
@graphwith shared@id - Fake aggregateRating — only add ratings when verifiable reviews exist on the page
- HTML in Answer.text — strip tags before serialization; JSON-LD expects plain text
- Relative logo URLs — use absolute URLs; minimum 112×112 px recommended
Need help? Telegram → or vic.kell@ya.ru
FAQ
Does JSON-LD improve rankings?
Not directly. Indirectly yes — via CTR. Rich snippets take more SERP real estate. Markup also helps entity resolution (company, service, FAQ).
Where should JSON-LD live — layout or page?
Global schemas (Organization, WebSite) in layout.tsx. Page-specific schemas (FAQPage, Product) in individual routes. Never add FAQPage to pages without a visible FAQ section.
Does JSON-LD work with Partial Prerendering?
Yes. JSON-LD Server Components land in the static PPR shell. Crawlers receive schema in the first HTML response.
What about Astro 5?
Same approach: embed <script type="application/ld+json"> in .astro templates with Content Collections data. Astro’s static HTML output makes this straightforward.
Conclusion
JSON-LD in Next.js 15 is a low-effort way to improve how landing pages appear in search. Server Components ensure Schema.org markup arrives in the first byte of HTML. Link entities via @graph, sync with Metadata API, validate with Rich Results Test, and monitor Search Console Enhancements.
Rich snippets won’t fix weak content or poor Core Web Vitals — but in competitive niches, FAQ accordions in SERPs can be the difference between a click and a scroll-past. Set up schema once, keep it synced with your content pipeline, and let Search Console tell you when something breaks.