Skip to content

Astro — SEO & JSON-LD

Every field you need for head tags and structured data is already on entry.data — the loader surfaces the full article. You emit the tags; Ghost Writr doesn’t inject anything into the page.

Set the title, description, and canonical from entry.data. seoTitle falls back to title server-side, so it’s always a string; the rest can be null, so guard them.

src/pages/blog/[slug].astro (head)
---
const { seoTitle, seoDescription, canonicalUrl, image } = p.data;
const canonical = canonicalUrl ?? new URL(p.id, Astro.site).href;
---
<title>{seoTitle}</title>
<meta name="description" content={seoDescription ?? ""} />
<link rel="canonical" href={canonical} />
<meta property="og:title" content={seoTitle} />
{seoDescription && <meta property="og:description" content={seoDescription} />}
{image && <meta property="og:image" content={image.url} />}
{image?.width && <meta property="og:image:width" content={String(image.width)} />}

canonicalUrl is the article’s declared canonical (set when the content is syndicated from elsewhere). When it’s null, the page is its own canonical — fall back to the page URL, as above. This is your call; see SEO & JSON-LD.

Build the graph in frontmatter and emit it with set:html (it’s trusted, generated server-side from your own data — not user input):

src/pages/blog/[slug].astro (json-ld)
---
const a = p.data;
const url = a.canonicalUrl ?? new URL(p.id, Astro.site).href;
const author =
a.author.kind === "real"
? {
"@type": "Person",
name: a.author.name,
jobTitle: a.author.jobTitle ?? undefined,
...(a.author.sameAs.length ? { sameAs: a.author.sameAs } : {}),
}
: { "@type": "Organization", name: a.author.name };
const jsonLd = {
"@context": "https://schema.org",
"@type": a.formatType, // schema.org type hint, e.g. "Article"
headline: a.title,
description: a.seoDescription ?? undefined,
inLanguage: a.inLanguage,
datePublished: a.publishedAt.toISOString(),
dateModified: a.updatedAt.toISOString(),
mainEntityOfPage: url,
...(a.image ? { image: a.image.url } : {}),
author,
};
---
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
  • formatType → JSON-LD @type (schema.org hint, e.g. "Article").
  • inLanguage → BCP-47 inLanguage and an html lang.
  • publishedAt / updatedAtdatePublished / dateModified (they’re Dates — call .toISOString()).
  • imageog:image and JSON-LD image (with width/height/srcset/alt when present).
  • canonicalUrl<link rel="canonical"> and mainEntityOfPage.

See Collection schema for the full entry.data shape and The article shape for the underlying contract.