Skip to content

Structured Data in Next.js: How JSON-LD Gets You Cited by Google and AI

Structured data is how you tell Google and AI what your page means, not just what it says. Here’s the Next.js way to ship it.

John Cravey with EleviFounder10 min read

Search engines and AI answer-engines read your page twice: once as words, and once as data. The words tell them what you wrote. The data — structured data, in the JSON-LD format — tells them what it means. That this is a business named Acme, in Plano, that repairs roofs, open until 6pm, rated 4.8. Ship it and you become eligible for rich results in Google and, increasingly, a citation in an AI answer. Skip it and you’re asking a machine to infer all of that from prose. The Next.js JSON-LD guide is refreshingly blunt about how to do it: render a `<script>` tag. This article is that guide, translated into a plan for whatever size you run.

What JSON-LD is, in one paragraph

JSON-LD is a small block of JSON that describes an entity using the shared vocabulary at schema.org. You embed it in your page inside a `<script type="application/ld+json">` tag. Google reads it to build rich results — the star ratings, FAQ drop-downs, breadcrumb trails, and business panels you see in search. AI systems increasingly read the same markup to decide what your page is about and whether to cite it. It’s the difference between a crawler guessing and a crawler being told.

The Next.js pattern: a script tag, rendered on the server

There’s no special API for this, and that’s deliberate — JSON-LD is data, not executable code, so a native `<script>` tag is the right tool, not the `next/script` component. You build a plain object, stringify it, and render it in your Server Component so it’s in the HTML the crawler receives on the first request.

// app/blog/[slug]/page.tsx
export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPost(slug);

  const jsonLd = {
    "@context": "https://schema.org",
    "@type": "Article",
    headline: post.title,
    description: post.description,
    datePublished: post.publishedAt,
    author: { "@type": "Organization", name: "Acme Co" },
  };

  return (
    <article>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{
          __html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
        }}
      />
      {/* …the article… */}
    </article>
  );
}

Type it so you can’t ship a broken schema

schema.org has hundreds of types and thousands of properties, and it’s easy to typo one into uselessness. The `schema-dts` package gives you TypeScript types for the whole vocabulary, so your editor autocompletes valid properties and the build fails if you invent one.

import type { Product, WithContext } from "schema-dts";

const jsonLd: WithContext<Product> = {
  "@context": "https://schema.org",
  "@type": "Product",
  name: "Standing Desk",
  image: "https://acme.co/desk.png",
  description: "Sit-stand desk, 5-year warranty.",
};

The schemas that actually earn something

You don’t need all of schema.org. Five types cover almost every business site, and each maps to a specific win:

  • Organization (or LocalBusiness) — in your root layout, once. Establishes the entity behind the whole site: name, logo, address, hours, contact. This is the anchor everything else links back to, and the single highest-value schema for a local business.
  • Article — on every blog post. Feeds the author, publish date, and headline that Google and AI use to attribute and cite your content.
  • Product — on product/service pages. Powers price, availability, and review rich results in shopping and search.
  • FAQPage — on any page with real Q&A. Earns the expandable FAQ rich result and gives AI engines clean, extractable question-answer pairs.
  • BreadcrumbList — on deep pages. Renders the breadcrumb trail in search and helps crawlers understand your site hierarchy.

Validate before you trust it

Never assume your markup is valid because it compiled. Paste a rendered URL into Google’s Rich Results Test to see exactly which rich results you’re eligible for, and the generic Schema Markup Validator to catch structural errors. Do this on a representative page of each type — one article, one product, one location — not every page. If the tool shows the entity and no errors, you’re done. And re-run it after any change to your content model or page templates, because a refactor that renames a field or restructures a component is exactly the kind of edit that silently breaks markup a validator would have caught in seconds.

A worked example: the local business panel

For a local business, one schema does most of the heavy lifting: `LocalBusiness`, in the root layout, describing the entity behind the whole site. Here’s what a real one looks like. The `@type` narrows to your category (`Roofer`, `Dentist`, `Plumber` — schema.org has hundreds), and the properties map to the business panel Google can show and the facts an AI answer can cite.

const jsonLd = {
  "@context": "https://schema.org",
  "@type": "Roofer",
  name: "Acme Roofing",
  image: "https://acme.co/storefront.jpg",
  telephone: "+1-972-555-0100",
  address: {
    "@type": "PostalAddress",
    streetAddress: "100 Main St",
    addressLocality: "Plano",
    addressRegion: "TX",
    postalCode: "75024",
  },
  openingHours: "Mo-Fr 08:00-18:00",
  aggregateRating: {
    "@type": "AggregateRating",
    ratingValue: "4.8",
    reviewCount: "312",
  },
};

Ship that once and you become eligible for the rich business treatment in search and a clean, machine-readable summary of who you are — name, location, hours, rating — for any AI system deciding whether to recommend you. Follow Google’s local business structured-data guidelines for the exact required and recommended properties; the closer you match them, the more of the rich result you unlock. One rule: the facts in your schema must match what’s visible on the page. Marking up a 4.8 rating you don’t display, or hours that contradict your footer, is exactly the kind of mismatch that gets structured data ignored or penalized.

Where structured data and AI answers meet

The reason structured data matters more every quarter is that the surfaces reading it are multiplying. It used to be Google alone, building rich results. Now every AI answer-engine that summarizes “who’s a good X near me” or “what does Y cost” is reading the same signals to decide what your business is and whether it’s a confident enough answer to cite. A page that spells out its entity — this is a business, here’s its name, category, location, and the questions it answers — is far easier for a model to represent correctly than a wall of prose it has to interpret. That’s why the two schemas we push hardest for local businesses are `LocalBusiness` (who you are) and `FAQPage` (the exact questions buyers ask, in clean question-answer pairs a model can lift directly). Google documents the FAQ structured-data format precisely; matching it is what turns your FAQ into an extractable answer instead of buried text. We go deeper on the answer-engine strategy in our AEO write-up, and it pairs directly with the outward-facing map you hand AI in AGENTS.md and llms.txt.

Keeping schema honest as the site changes

Structured data has one failure mode that’s worse than not having it: having it and letting it go stale. If your markup claims hours you’ve since changed, a rating you no longer display, a price that moved, or an author who left, you’ve created a mismatch between what the machine reads and what the human sees. Google’s structured-data policies are explicit that markup must reflect the visible content of the page, and violating that can get your rich results suppressed or, in bad cases, earn a manual action. So the rule is simple: generate schema from the same data that renders the page, never hand-write it as a parallel copy that can drift.

When your schema is generated — Article from your post data, LocalBusiness from your business record, Product from your catalog — it can’t contradict the page, because it’s reading the same source. That’s the real reason we push generated schema over hand-authored blocks: not just convenience, but correctness that survives every content edit. Set a light cadence to re-validate: run your top page of each type through the Rich Results Test once a quarter and after any change to your content model. If you display reviews, keep the aggregate rating in the markup tied to the real review data, not a number someone typed in once. Treat structured data as a live contract with the machines reading your site, and it keeps paying off. Let it rot, and it turns from an asset into a liability that’s hard to even notice, because nothing on the page looks broken. The teams that win with schema aren’t the ones who add the most types; they’re the ones whose markup never lies, because it’s wired to the same data the page renders and re-checked on a cadence rather than set once and forgotten.

What this means for your business

The markup is universal; the strategy scales with how many pages and entities you manage.

For agencies

Structured data is a repeatable, defensible line item. Build a typed schema module into your Next.js starter — `buildOrganizationJsonLd`, `buildArticleJsonLd`, `buildLocalBusinessJsonLd` — and every client site ships correct markup by construction. That consistency is your moat: it’s the difference between “we added some schema” and “every page on every client site resolves to a validated entity.” Offer a structured-data audit as a paid discovery for prospects — most sites you’ll audit have none, or have invalid markup a validator flags in seconds, and that gap sells the engagement. Wire the Article schema to the client’s Organization `@id` so their author authority compounds across their whole blog.

For micro businesses (1–5 people)

One schema matters more than all the others for you: LocalBusiness, in your root layout, with your real name, address, phone, and hours. That single block is what makes you eligible for the business panel and map treatment that local searchers actually click. Add FAQPage markup to your top service page — take the five questions customers actually ask and mark them up — and you become extractable by AI answers for exactly the queries your buyers type. It’s maybe an hour of work for the two schemas that move the needle for a local shop. Everything else is optional until you have more pages.

For small businesses (SMEs)

You have enough pages that structured data should be generated from your content, not hand-written. Emit Article schema from your blog data, Product or Service schema from your services data, and LocalBusiness once for the whole site. The leverage is that new content ships correct markup automatically — publish a post, get valid Article schema for free. Run the Rich Results Test on one page of each type after you wire it, then trust the generator. If you sell products or take reviews, the Product and review schemas can put stars in your search listing, which lifts click-through on the pages where it appears.

For mid-size companies

Your challenge is scale and correctness across thousands of pages and multiple content systems. Centralize schema generation in one typed module, validate it in CI (schema-dts turns invalid markup into a build failure, which is exactly what you want at this scale), and define entity relationships deliberately — every Article `author` and every page `publisher` pointing at one canonical Organization `@id`, so your entity graph is coherent rather than a thousand disconnected snippets. This is where structured data stops being an SEO tactic and becomes part of how AI systems model your brand. Treat it like the data contract it is, with ownership, tests, and a validator in the pipeline.

How we run this at Frontend Horizon

Every FH site emits an Organization entity site-wide, Article schema on every post wired to that Organization, and LocalBusiness plus FAQ markup where the business is local. It’s generated from a single typed module and validated, so it can’t silently rot. If you want to know what rich results and AI citations your current markup is leaving on the table, run a free discovery — we’ll validate your top pages and show you the gaps. Keep reading: the metadata playbook covers the words half of the equation, and sitemaps and robots.txt make sure crawlers reach the pages your schema describes.

Written by
John Cravey
Founder

Founder of Frontend Horizon. Writes most of the long-form work on the FH blog.

Older post
Titles, Meta Descriptions, and Social Cards: The Next.js Metadata Playbook for Every Business Size
Keep reading

More from the blog

AI·10 min

AI Content Engines: Automating SEO Blog Production With n8n

A keyword goes in one end, a published post comes out the other. Here's how to build that without publishing garbage.

AI·9 min

Multi-Agent Content Systems: Research to Published Post on Autopilot

One model writing a whole post is a generalist. A team of narrow agents, each doing one job, is a system. Here's the difference.

SEO·9 min

Automating Keyword and Competitor Research With AI

Your competitors' best pages and your buyers' real questions are public. The only question is who reads them first, and how often.