Adios
BlogNext.js SEO

Next.js SEO

How to Add JSON-LD in Next.js: Article, Product, Software, and Breadcrumb Schema

Add safe, accurate JSON-LD to Next.js App Router pages with Article, Product, SoftwareApplication, and BreadcrumbList examples.

Adios teamUpdated July 17, 20268 min read

Structured data should be a typed projection of the page users can see. It is not a second marketing channel where missing facts can be filled with optimistic guesses.

Choose the entity before the schema

Ask what the page primarily represents. A news or blog post may be an Article. A purchasable item may be a Product with an Offer. A software landing page may describe a SoftwareApplication. BreadcrumbList describes navigation hierarchy and can accompany another primary entity.

Do not select a type because its search appearance looks attractive. Search features have eligibility rules beyond valid JSON, and displaying markup does not guarantee a rich result. The first goal is an accurate machine-readable description that stays aligned with the visible page.

Render JSON-LD on the server

Build the object in a Server Component using the same record that renders the page. Serialize it into a script with type application/ld+json. Replace less-than characters in the serialized value so user-controlled strings cannot close the script element and inject markup.

Keep the helper small. It should serialize data safely, not decide business facts. Construct Article, Product, or SoftwareApplication objects in typed functions that can validate required fields and omit unknown optional values.

export function JsonLd({ data }) {
  const json = JSON.stringify(data).replaceAll("<", "\u003c");
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: json }}
    />
  );
}

Describe an article

Use the canonical headline, description, publication and modification dates, author, publisher, primary image, and main page URL. Update dateModified only for a substantive change to the article. A site-wide rebuild is not an editorial revision.

The author should match the visible byline. If the page credits an organization, mark up the organization; if it credits a person, use that person. Link to stable author or publisher pages when available and keep the logo URL absolute.

const article = {
  "@context": "https://schema.org",
  "@type": "Article",
  headline: post.title,
  description: post.excerpt,
  datePublished: post.publishedAt,
  dateModified: post.updatedAt,
  mainEntityOfPage: SITE_URL + "/blog/" + post.slug,
  author: { "@type": "Organization", name: "Acme" },
};

Describe a purchasable product

Product data can include the name, description, images, SKU, brand, and offers. Offer values such as price, currency, URL, condition, and availability must match what a shopper can see and buy. Generate them from the same commerce record used by the page.

Variants require a clear model. Do not emit several contradictory prices as one offer without explaining the range, and do not claim InStock when the checkout path rejects the item. Reviews and aggregate ratings must come from real visible review data and follow the relevant search policies.

const product = {
  "@context": "https://schema.org",
  "@type": "Product",
  name: item.name,
  image: item.images,
  sku: item.sku,
  offers: {
    "@type": "Offer",
    price: item.price.amount,
    priceCurrency: item.price.currency,
    availability: item.inStock
      ? "https://schema.org/InStock"
      : "https://schema.org/OutOfStock",
  },
};

Describe a SaaS application honestly

SoftwareApplication can identify the application name, category, operating system, URL, and description. Add an Offer only when the price and terms correspond to a visible option. If pricing varies or requires contact, represent that accurately rather than forcing a misleading zero-price offer.

Structured data cannot replace a clear product page. The visible page still needs to explain the job, audience, features, constraints, pricing path, and next action. Keep claims such as ratings or awards out unless the page and evidence support them.

Build breadcrumbs from canonical hierarchy

BreadcrumbList describes the path a user can follow through the site's information architecture. Each item has a position, name, and canonical URL. The visible breadcrumb and JSON-LD should agree. Do not include every route segment when the user-facing hierarchy uses a simpler label.

Generate breadcrumbs from route-owned data rather than parsing arbitrary URLs. A product may belong to several categories, but the page should choose the canonical hierarchy that its visible navigation and internal links reinforce.

const breadcrumbs = {
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  itemListElement: items.map((item, index) => ({
    "@type": "ListItem",
    position: index + 1,
    name: item.name,
    item: new URL(item.path, SITE_URL).toString(),
  })),
};

Validate data and failure behavior

Validate the JSON syntax, schema shape, and search-feature requirements, then compare every material value with the visible page. Test quotes, angle brackets, Unicode, missing images, no price, out-of-stock products, unpublished articles, and changed canonical URLs.

A missing optional field should usually omit the property. A missing required business fact may mean the page is not ready for that schema. Log validation failures during development and content ingestion rather than serving invented defaults.

Verify structured data on the promoted route

Adios lets the same Next.js release serve the visible page and JSON-LD from one persistent runtime. Deploy the candidate, fetch the generated route, and validate the script from the actual HTML. Build logs expose serialization or type failures; runtime logs help trace records that fail only with production data.

After the health check succeeds, keep the custom domain and managed TLS attached to the promoted version. Re-run validation against the canonical URL because host, redirects, environment values, and image URLs can differ from a local test.

env:
  NEXT_PUBLIC_SITE_URL: https://www.example.com

runtime:
  name: node@24
  port: 3000
  health_path: /api/health
  All articles