Tenten AIGEO
Back to Blog
Technical AEOImplementation

Inject JSON-LD into Next.js App Router: Use Server Component to implement structured data

In Next.js App Router, the Server Component directly outputs `<script type="application/ld+json">`, allowing structured data to enter the initial HTML, and AI crawlers can read it without executing JavaScript. Contains a minimal page.tsx example, @graph multi-entity concatenation and curl verification steps before going online.

Tenten GEO TeamPublished 2026-07-125 min read
Schematic diagram: The structured data output by the server directly flows into the initial HTML read by the AI crawler.

The most reliable way to inject JSON-LD into App Router is to directly output a section of `<script type="application/ld+json">` in Server Component. In this way, the structured data will appear in the initial HTML returned by the server, and the AI ​​crawler can read it without executing JavaScript. The JSON-LD stuffed in afterwards using useEffect or Google Tag Manager is almost non-existent to the answer engine - most of them will not wait for your page to hydrate before they have finished crawling and left.

Why does it have to be output on the server side?

The difference is timing. Server Component writes JSON-LD into HTML string at the moment of request. The first file captured by crawlers such as GPTBot, ClaudeBot, PerplexityBot, and Google-Extended contains the complete schema. Client-side injection requires waiting for the browser to download and execute your bundle. Most AI crawlers do not run this step at all. There is a trap that is easy to misjudge here: Google's Rich Results Test will render JavaScript for you, so the data injected by the client looks normal in the test tool, and you think it has passed the test; but when it is actually read by the answer engine, what they get is the original HTML that has not been rendered, and there is no schema in it. When we do technical audits for our clients, this is the most common and easily overlooked item that loses points.

Minimum viable example: one page page.tsx

The implementation itself is very short. You don't need any additional packages or API routes, it's just done within the Server Component that renders the article. There are only three core things: getting data, grouping objects, and outputting scripts.

  1. In `app/blog/[slug]/page.tsx` (the default is Server Component), first `await` gets the data of this article.
  2. Assemble a pure JavaScript object `jsonLd`, put `"@context": "https://schema.org"` and `"@type": "Article"` at the beginning, and the remaining fields (headline, datePublished, author, image) are dynamically filled in with article data.
  3. Put at the front of the returned JSX: `<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd).replace(/</g, "\\u003c") }} />`.
  4. Don't import any client libraries, and don't add "use client" at the top of the file - once added, it becomes a client component and all previous efforts are lost.

Use @graph to string together multiple entities

A single page usually requires more than one schema: the brand itself is Organization, the article is Article, the navigation path is BreadcrumbList, and the Q&A section is FAQPage. Instead of outputting four independent script tags, a cleaner approach is to use an `@graph` array to load them into the same JSON-LD object, and then use `@id` to point to each other - for example, let the `publisher` of Article point to the `@id` of Organization, and let the last item of BreadcrumbList point to the `@id` of the current page. The answer engine can obtain the complete entity relationship diagram in one analysis, and it is less likely to catch contradictory or duplicate nodes.

  • Organization and WebSite: Place it in `app/layout.tsx` and share it once for the whole site. Do not declare it repeatedly on every page.
  • Article or BlogPosting: Place it in the `page.tsx` of the article, and the fields are dynamically filled in with the data on the current page.
  • BreadcrumbList: Grouped according to routing hierarchy, it is particularly helpful for AI to understand the structure of the site.
  • FAQPage: Only add it when the screen really has a question and answer section, and the text of `acceptedAnswer` must be consistent with the content of the screen. Don't make it fake for the sake of schema.
Infographic: Server Component outputs JSON-LD in the initial HTML, and client-side injection is skipped by the AI ​​crawler.
The JSON-LD output by the server enters the initial HTML, and the one injected by the client cannot wait for the crawler.

Make JSON-LD maintainable

Scatter the schema objects across the pages and no one will dare to change them after three months. Our approach is to extract a `lib/schema.ts` and use the `schema-dts` type (`import type { Article, WithContext } from "schema-dts"`) to constrain the return value of each generator function. If the field name is wrong or the type is wrong, TypeScript will report an error during the compilation phase. The page only calls `buildArticleSchema(post)`, gets the type-safe object and then outputs it. Once the change is made, it will take effect for the whole site. The URLs always use absolute paths and are concentrated in a `SITE_URL` constant to prevent `@id` from mismatching between the official site and the preview domain.

Common mistakes and verification before going online

  • The `url`, `image`, and `datePublished` in JSON-LD are inconsistent with the actual content of the screen - the answer engine detects a gap that reduces trust in the entire page.
  • Using relative paths as `@id` or `url` will point to the wrong location once you switch to the preview domain.
  • Forgot to escape `<`, or accidentally added `"use client"` at the top of the page, so that the script becomes client output.
  • Before going online, use Google Rich Results Test to check the syntax, and then use `curl` to directly capture the original HTML of the page to confirm that the script is really in the initial response and not only appears in the browser DevTools. This step is the most critical because it simulates exactly what the AI ​​crawler sees.
In the websites we have audited, most of the JSON-LD problems are not that the schema is written incorrectly, but that it only exists on the client side. There is no HTML returned by the server, and AI will treat it as if it does not exist.Tenten GEO Technical Audit

From implementation to citation

Moving JSON-LD to Server Component is the minimum threshold for content to be “machine-readable”. Next, you need to align entity naming, internal links, and cross-page consistency so that AI can stably cite you as a trusted source. If you are not sure whether the structured data on your site has entered the HTML that the crawler sees, you can make an appointment for a 30-minute GEO diagnosis. We will use the actual captured source code to help you take stock of the gaps, instead of just looking at the rendered screen.

Frequently asked questions

How to add JSON-LD to Next.js App Router?
Assemble a schema object in the Server Component (page.tsx or layout.tsx), use `<script type="application/ld+json">` with `dangerouslySetInnerHTML` to output, and escape the `<` in the string to `<`. This way the structured data will go into the initial HTML, which the AI ​​crawler can read without having to execute JavaScript.
Can I put JSON-LD using next/head or Metadata API?
App Router no longer has next/head, and the Metadata API does not support outputting any script tags. Next.js officially recommends rendering `<script type="application/ld+json">` directly in the JSX of the Server Component. This is currently the most stable way to put JSON-LD and the easiest way to be read by crawlers.
Why is it not recommended to use GTM or useEffect to inject structured data?
Both of these are executed on the client side, and JSON-LD will appear after the browser hydrates. Most AI crawlers only capture the initial HTML and do not run JavaScript, which means they cannot see your schema. Only by changing the output from the server to ensure that the structured data is actually read and referenced by the answer engine.

READY WHEN YOU ARE

How visible is your brand in AI answers?

In a 30-minute GEO diagnostic, we use real prompts to identify your visibility gaps across major AI engines and show you what to fix first.

Book a 30-minute diagnostic