跳到主要内容

实操

How to add JSON-LD to an Astro site

Build the schema object in frontmatter, inject it with set:html, and avoid the two traps that break most Astro installs: dead expressions and a stray <.

更新于 2026年4月12日 · 约 3 分钟

Astro makes JSON-LD easy right up to the point where your markup silently renders as text or your data closes the script tag early. Both failures come from the same place: how Astro treats the contents of a <script> element. Get that right and the rest is bookkeeping.

Build the object in frontmatter

Keep the schema as a plain object in the component frontmatter, so it stays readable and typed:

---
const schema = {
  '@context': 'https://schema.org',
  '@type': 'Article',
  headline: 'How to add JSON-LD to Astro',
  datePublished: '2026-04-12',
  author: { '@type': 'Person', name: 'Jane Doe' },
};
---

Inject it with set:html

The contents of a <script> tag are raw text. Curly braces are not evaluated there, so this common mistake outputs the literal characters {JSON.stringify(schema)} into the page:

<!-- 错误:script 内部不做表达式求值,这里会原样输出 -->
<script type="application/ld+json">
  {JSON.stringify(schema)}
</script>

The correct pattern passes the serialised string through the set:html directive, which inserts it without HTML-escaping:

<script type="application/ld+json" set:html={JSON.stringify(schema)} />

You do not need is:inline here: a script driven by set:html is emitted as-is rather than bundled as a client module, which is exactly what a data block wants.

The less-than trap

Because set:html does not escape anything, a < inside your data is inserted raw. If any value contains the sequence that closes a script tag — for example user-generated text, or an HTML snippet stored in a field — the browser ends the block early and the structured data is broken.

Escape only the angle bracket, before injection:

// \u003c 是合法 JSON 转义,仍然表示 <,只是不会提前闭合 script
const json = JSON.stringify(schema).replace(/</g, '\\u003c');

This changes nothing about the parsed data. It only stops a stray < from terminating the tag.

Layout or page?

Put each type where its data actually lives.

TypeBest placed inWhy
OrganizationRoot layoutOne per site, needed almost everywhere
WebSite / SearchActionHome pageTied to the site root
BreadcrumbListThe page (or layout from the route)Mirrors the path of that URL
Article / BlogPostingThe article pageDates, author and headline are page-specific
ProductProduct detail pagePrice and availability are page-specific
FAQPageThe page that shows the Q&AQuestions must be visible there

A layout is the right home for anything that is identical on every page. Anything that varies belongs on the page, even if you pass it into the layout as a prop.

One script block or an @graph?

Multiple script blocks are valid, and each type can live in its own. That is the simplest thing to maintain. When entities need to reference each other, combine them into a single @graph and link with @id:

{
  "@context": "https://schema.org",
  "@graph": [
    { "@type": "Organization", "@id": "https://example.com/#org", "name": "Example Inc." },
    {
      "@type": "Article",
      "headline": "How to add JSON-LD to Astro",
      "publisher": { "@id": "https://example.com/#org" }
    }
  ]
}

The @id is what makes the graph work: the article points at the organization instead of repeating it.

Do not emit the same entity twice

The most common Astro-specific bug is a root layout that emits Organization and a page that also emits one. Two objects with the same type and different data make the parser guess. Emit a unique entity once and reference it by @id everywhere else.

Common mistakes

Writing braces inside the script tag. They are not evaluated. Use set:html.

Assuming set:html escapes. It does not. Escape < yourself.

Hard-coding a staging domain. A url or @id pointing at localhost fails validation the moment it reaches production.

Adding the markup with a client-side script. If the crawler does not run JavaScript, or the script throws, the structured data never exists.

Duplicating the entity across layout and page. One Organization, one WebSite, one canonical set of values.

Where this tool fits

The JSON-LD generator produces the object in the right shape and flags the required fields as you type, so the only Astro-specific work left is wrapping it in set:html and escaping the angle brackets.

Frequently asked questions

▸ Why does my JSON-LD render as literal text in Astro?

Because the contents of a script tag are treated as raw text, not as an expression. Writing curly braces inside the script element does not evaluate them. You have to inject the string with set:html or define:vars instead.

▸ Is set:html safe for JSON-LD?

It is the standard pattern. set:html inserts the string without HTML-escaping it, which is what keeps the JSON valid. The trade-off is that you must escape any less-than character yourself so the data cannot close the script tag early.

▸ Should the JSON-LD live in the layout or the page?

Put site-wide types such as Organization and WebSite in the root layout, and page-specific types such as Article, Product or BreadcrumbList on the page itself. What you should not do is emit the same entity in both places.

▸ Should I use one script or an @graph?

Both are valid. Multiple script blocks keep each type independent and are easy to reason about. An @graph lets entities reference each other by @id, which avoids repeating the publisher or organization on every block.

▸ How do I stop the same entity appearing twice?

Emit each unique entity once, and reference it from other blocks with the same @id. Two Organization objects with different data on one page force the parser to guess which is authoritative.