Schema strategy — your citation trust layer · 13 min read
JSON-LD templates you can ship today
Drop-in JSON-LD components for the five citation-critical schema types — Article, FAQPage, HowTo, Person, and Course — written for the Next.js App Router, brand-agnostic and copy-paste ready.
Lesson 1 ranked the schema types that earn citations. This lesson hands you the code. Everything below targets the Next.js App Router in TypeScript, pulls in no dependencies, and is deliberately brand-agnostic — change one constant and it's yours.
The one primitive everything builds on
Every schema block is the same HTML underneath: a <script type="application/ld+json"> tag holding stringified JSON. Write it once:
// components/json-ld.tsx
export function JsonLd({ data }: { data: Record<string, unknown> }) {
return (
<script
type="application/ld+json"
// JSON.stringify escapes the payload; safe for trusted, server-built data.
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
);
}
Render it anywhere in a Server Component — it does not have to live in <head>. Every template below returns one of these.
Then set your site identity in exactly one place, so the publisher entity is byte-identical across the whole site:
// lib/site.ts
export const SITE = {
url: "https://your-site.com",
name: "Your Site",
} as const;
Article (+ author + publisher)
The baseline for every content page. The three fields doing the heavy lifting: both dates, and an author linked to a real Person URL.
import { JsonLd } from "@/components/json-ld";
import { SITE } from "@/lib/site";
export function ArticleJsonLd(props: {
title: string;
description: string;
path: string; // "/articles/my-post"
authorName: string;
authorPath: string; // "/authors/jane"
datePublished: string; // ISO date
dateModified?: string;
}) {
return (
<JsonLd
data={{
"@context": "https://schema.org",
"@type": "Article",
headline: props.title,
description: props.description,
mainEntityOfPage: `${SITE.url}${props.path}`,
datePublished: props.datePublished,
dateModified: props.dateModified ?? props.datePublished,
author: {
"@type": "Person",
name: props.authorName,
url: `${SITE.url}${props.authorPath}`,
},
publisher: {
"@type": "Organization",
name: SITE.name,
url: SITE.url,
},
}}
/>
);
}
FAQPage
The highest-extractability type — engines lift these answers near-verbatim. Use it anywhere you have genuine Q&A visible on the page.
export function FaqJsonLd({ items }: { items: { question: string; answer: string }[] }) {
return (
<JsonLd
data={{
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: items.map((it) => ({
"@type": "Question",
name: it.question,
acceptedAnswer: { "@type": "Answer", text: it.answer },
})),
}}
/>
);
}
HowTo
For step-by-step tactical content. Steps get pulled directly into "how do I…" answers.
export function HowToJsonLd(props: { name: string; steps: string[] }) {
return (
<JsonLd
data={{
"@context": "https://schema.org",
"@type": "HowTo",
name: props.name,
step: props.steps.map((text, i) => ({
"@type": "HowToStep",
position: i + 1,
text,
})),
}}
/>
);
}
Person (the author entity)
Your E-E-A-T anchor. Render it on each author's bio page; the sameAs links are how engines reconcile your author with their real-world identity.
export function PersonJsonLd(props: {
name: string;
path: string; // "/authors/jane"
jobTitle?: string;
sameAs?: string[]; // LinkedIn, X, GitHub, etc.
}) {
return (
<JsonLd
data={{
"@context": "https://schema.org",
"@type": "Person",
name: props.name,
url: `${SITE.url}${props.path}`,
jobTitle: props.jobTitle,
sameAs: props.sameAs,
worksFor: { "@type": "Organization", name: SITE.name, url: SITE.url },
}}
/>
);
}
Course
For the curriculum and sales pages — signals the product type and is eligible for course-specific treatment.
export function CourseJsonLd(props: { name: string; description: string; path: string }) {
return (
<JsonLd
data={{
"@context": "https://schema.org",
"@type": "Course",
name: props.name,
description: props.description,
url: `${SITE.url}${props.path}`,
provider: { "@type": "Organization", name: SITE.name, url: SITE.url },
}}
/>
);
}
Stacking them on one page
Connected schema beats isolated schema (Lesson 1). A single lesson page can legitimately carry an Article and a FAQPage — render both:
export default function LessonPage() {
return (
<article>
<ArticleJsonLd
title="Choosing your priority surfaces"
description="A scoring framework for picking your search surfaces."
path="/lessons/01-fundamentals/03-priority-surfaces"
authorName="Your Name"
authorPath="/authors/your-name"
datePublished="2026-05-28"
/>
<FaqJsonLd
items={[
{
question: "Do I have to optimize for all five surfaces?",
answer: "No — pick two or three by audience fit, winnability, and conversion.",
},
]}
/>
{/* ...rest of the page... */}
</article>
);
}
Both reference the same Organization, so an engine reads one coherent publisher with multiple attributed entities — exactly the trust graph Lesson 1 described.
Your action checklist
- Add the
JsonLdprimitive and theSITEconstant - Wire
ArticleJsonLdinto your article/lesson template — every content page - Add
PersonJsonLdto each author bio page, with realsameAslinks - Add
FaqJsonLdto pages with genuine Q&A;HowToJsonLdto step-by-steps - Add
CourseJsonLdto the curriculum and buy pages - Confirm every date is real and every author URL resolves to a live bio
Sidebar — server-render it. Build JSON-LD in a Server Component from data you control, never from user input. Crawlers should see the schema in the initial HTML response; schema injected client-side after hydration is unreliable for the engines that matter.
The entities are on the page now. The last question is whether they're correct — a single malformed block can void the rest. Next: validating schema and the five mistakes that silently kill it.
→ Next: Validating your schema
This is one of the paid lessons. Unlock every module and every paywalled article for $199 one-time.