← Back to curriculum

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 JsonLd primitive and the SITE constant
  • Wire ArticleJsonLd into your article/lesson template — every content page
  • Add PersonJsonLd to each author bio page, with real sameAs links
  • Add FaqJsonLd to pages with genuine Q&A; HowToJsonLd to step-by-steps
  • Add CourseJsonLd to the curriculum and buy pages
  • Confirm every date is real and every author URL resolves to a live bio

A request-time comparison. Server-rendered JSON-LD: the crawler's first byte already contains the entity graph — SEEN. Client-injected JSON-LD: the server sends an empty shell and JavaScript adds the schema only after hydration, so a crawler that doesn't run JS sees nothing — MISSED.

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.