JSON-LD Schema in Astro: Article, Review, Person, and Breadcrumbs

Build reusable JSON-LD schema in Astro for articles, reviews, authors, publishers, and breadcrumbs while avoiding hardcoded identity data.

Quick answer

How do you add JSON-LD structured data to an Astro site?

Build a reusable Astro component that creates JSON-LD objects for Article, Review, BreadcrumbList, Person, and Organization from typed props. Keep author, publisher, and site values in one config file, pass page-specific data from frontmatter, and validate the rendered production output before publishing.

JSON-LD structured data validated in Google Rich Results Test for an Astro project
First-hand experience: Based on direct hands-on use. Built live on astro-content-lab.vercel.app. The hardcoding mistake described here happened in real time, not as a teaching device.

I asked a simple question before writing any code: should the guide pages use HowTo schema?

I remembered something — HowTo rich results don’t really show up on Google anymore. I wasn’t sure if that was still true or if I was thinking of an old update. Worth checking before building something that does nothing.

Turns out I was right, and the situation is worse than I remembered.


Why I Dropped HowTo Schema

Google deprecated HowTo rich results in September 2023. The schema.org type still exists, but Google Search no longer displays that result format. Adding valid HowTo markup therefore does not make a tutorial eligible for a Google HowTo rich result.

FAQ is different. Google did not announce the same full deprecation. In August 2023 it restricted FAQ rich results to well-known, authoritative government and health sites. A normal developer blog should not expect the expandable FAQ treatment.

This matters because the instinct is to add every schema type you can find. That creates more code to maintain without creating more value. Structured data should describe visible content accurately. It can make a page eligible for supported search features, but Google does not guarantee a rich result even when the markup validates.

Decision: Guide pages in this project use Article schema, not HowTo. FAQPage markup is generated only when the same questions and answers are visible on the page. I am not treating it as an AI-citation shortcut. Google explicitly says no special structured data is required for its generative AI features.

Sources: Google’s HowTo and FAQ change announcement, general structured-data guidelines, and Google’s generative AI search guidance.


One Component, Five Schema Types

Rather than building a separate component per schema type, I built one SchemaOrg.astro that switches behavior based on a type prop.

---
type Props = ArticleSchema | ReviewSchema | BreadcrumbSchema | OrganizationSchema | PersonSchema

const props = Astro.props
let schema: Record<string, any> = {}

if (props.type === 'Article') {
  schema = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: props.title,
    datePublished: props.publishedAt.toISOString(),
    author: { '@type': 'Person', name: authorName, url: AUTHOR_URL },
    publisher: { '@type': 'Organization', name: SITE_NAME },
    // ...
  }
}
// ... Review, BreadcrumbList, Organization, Person follow the same pattern
---

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

Each slug page imports it once and passes the relevant props:

<SchemaOrg
  type="Review"
  title={review.data.title}
  description={review.data.description}
  publishedAt={review.data.publishedAt}
  itemReviewed={review.data.productName}
  rating={review.data.rating}
/>

This is the same pattern as Callout.astro from earlier in the series — one component, a type prop, internal branching. It scales better than five separate files that all do nearly the same thing.


Person vs Organization — Use Both, Correctly

I had this backwards in my head before checking. The question was: does Google show an individual author’s name, or the site’s brand name, on search results?

The answer is both — they’re not competing, they serve different roles in the same schema object.

author should be Person — this is who wrote the content. Google can attach an individual’s name to a piece of content, which matters for E-E-A-T signals and how AI systems attribute information.

publisher should identify the organization publishing it. For Article markup, Google recommends including publisher information such as the organization name and logo where applicable.

author: { '@type': 'Person', name: authorName, url: AUTHOR_URL },
publisher: { '@type': 'Organization', name: SITE_NAME },

Both live inside the same Article or Review schema. No need for a separate page or component for Organization — it’s nested data, not a standalone entity in this project’s case.

Person does get its own standalone schema on the About page, since that page exists specifically to describe the author.


The Hardcoding Bug — and Then I Did It Again

While writing the SchemaOrg component, I had 'Steven Doan' hardcoded directly as a string inside the author field.

The fix: move author info into config.ts, the same file already holding SITE_NAME and SITE_URL from the meta tags work earlier in this series.

// src/config.ts
export const SITE_NAME = 'Astro Content Lab'
export const SITE_URL = 'https://astro-content-lab.vercel.app'
export const AUTHOR_NAME = 'Steven Doan'
export const AUTHOR_URL = 'https://doancongtuan.com'
export const AUTHOR_BIO = 'Freelance web developer. 10+ years building WordPress sites. Now learning Astro in public.'

One file. Anyone forking the project changes four lines, and every Person, Organization, Article, and Review schema across the entire site updates automatically.

This is the same idea as WordPress’s site settings page — one place where identity lives, every template reads from it instead of hardcoding it.

I fixed it. Felt good about it.

Then, two steps later, while wiring up BreadcrumbList across the four slug page types, I wrote this:

<SchemaOrg
  type="BreadcrumbList"
  items={[
    { name: 'Home', url: 'https://astro-content-lab.vercel.app/' },
    { name: 'Reviews', url: 'https://astro-content-lab.vercel.app/reviews/' },
    { name: review.data.title, url: Astro.url.href },
  ]}
/>

Same exact mistake. Hardcoded the production URL directly into JSX, four times, across four files — right after fixing the identical problem with the author’s name.

Fixed by importing SITE_URL into each slug page and interpolating it:

import { SITE_URL } from '../../config'

<SchemaOrg
  type="BreadcrumbList"
  items={[
    { name: 'Home', url: `${SITE_URL}/` },
    { name: 'Reviews', url: `${SITE_URL}/reviews/` },
    { name: review.data.title, url: Astro.url.href },
  ]}
/>

Why the Repeat Mistake Matters More Than the Fix

The interesting part isn’t the bug. Hardcoded strings are an easy, forgivable mistake. The interesting part is that it happened twice in the same session, on the same category of problem, right after explicitly identifying and fixing it the first time.

That’s not really a code problem. It’s a habit problem. Knowing the rule (“don’t hardcode site-specific values”) and actually applying it consistently across every new piece of code you write are two different skills. The first time, I was reactive — fixing something already wrong. The second time, I was writing new code and didn’t apply the same standard proactively.

For a project meant to be cloned and reused, this matters more than it would on a one-off client site. Every hardcoded value is a thing someone else has to find and fix manually, in a part of the codebase they probably won’t think to check.

The rule going forward: before writing any string that includes a site name, URL, or author name, ask whether it should come from config.ts instead. Not after writing it — before.


What’s Actually in Place Now

After Articles 17A and 17B, the relevant pages in astro-content-lab ship with:

  • Meta description, canonical URL, robots tag
  • Open Graph and Twitter Card meta tags, with per-page images
  • Article schema for blog posts, guides, and comparisons
  • Review schema with rating for review pages
  • BreadcrumbList schema for all four content types
  • Person schema on the About page
  • Person/Organization nested correctly inside every Article and Review
  • Zero hardcoded site or author values — all of it traces back to one config file

None of this required a plugin. There is no SEO settings panel or admin UI, which is either clean or inconvenient depending on who edits the site. For this developer-owned project, a typed component and one config file are enough.

The next step is making those pages discoverable through a sitemap and a clear robots.txt file.


Frequently Asked Questions

Is HowTo schema still worth implementing in 2026?
Google deprecated HowTo rich results in September 2023, so the markup no longer creates that search appearance. The schema.org type remains valid, but it adds maintenance without a Google rich-result benefit for this project. I use Article markup for editorial guides and tutorials instead.
Does FAQ schema still matter for a normal content site?
FAQPage remains a valid schema.org type, but Google has limited FAQ rich results to well-known government and health sites since 2023. Keep the markup only when the questions and answers are visible and useful to readers. Do not add it as a promised ranking or AI-citation tactic.
Should I use Organization or Person schema for my site?
Use both, in their correct roles. Person schema describes the author — Google can show an individual's name attached to content. Organization schema describes the publisher or brand. In Article and Review schema, author should be Person and publisher should be Organization.
How do I avoid hardcoding author and site info in schema components?
Create one config file that exports SITE_NAME, AUTHOR_NAME, AUTHOR_URL, and SITE_URL. Schema components import those constants while page-specific values still come from frontmatter. This keeps identity data consistent and makes a future author, publisher, or domain change possible without searching every template.
What schema types are useful for this Astro content site?
Article helps describe editorial pages, BreadcrumbList describes their place in the site, and eligible product review markup can support review snippets when it follows Google's policies. Person and Organization clarify author and publisher identity. Valid markup creates eligibility and context, not a guaranteed rich result.