Collection schema
The loader ships a default Zod schema covering the full article, so entry.data gives you author, tags, language, canonical, and timestamps without re-deriving anything. You only define a schema yourself if you want to narrow or extend it.
The default entry.data
Section titled “The default entry.data”Every entry validates against this shape:
| field | type | notes |
|---|---|---|
slug | string | URL-safe slug — also the entry id and your route param |
title | string | on-page title (your <h1>) |
seoTitle | string | SEO <title> (falls back to title server-side) |
seoDescription | string | null | meta description |
canonicalUrl | string | null | absolute canonical — you decide self vs syndication source |
formatType | string | schema.org type hint (e.g. "Article") |
inLanguage | string | BCP-47 language tag (e.g. "en") |
tags | string[] | topical tags (possibly empty) |
image | object | null | cover image, or null (see below) |
author | Author | byline — always present (see below) |
publishedAt | Date | publish date (coerced from the feed’s ISO string) |
createdAt | Date | row-created date |
updatedAt | Date | row-updated date — use for lastmod / dateModified |
ghostwritrId | string | the Ghost Writr article id, for round-tripping / debugging |
The Markdown is not in entry.data. The raw body is on entry.body, and the compiled body is on entry.rendered — render(entry) is what turns it into <Content />. See Rendering articles.
null when the article has no generated cover. Otherwise:
{ url: string; alt: string; width: number | null; height: number | null; srcset: string | null;}author
Section titled “author”Always present:
{ name: string; slug: string; bio: string | null; avatarUrl: string | null; jobTitle: string | null; sameAs: string[]; kind: "real" | "persona";}author.kind drives JSON-LD: "real" is a person (schema.org Person), "persona" is a brand byline (Organization). See SEO & JSON-LD.
A custom schema overrides the default
Section titled “A custom schema overrides the default”Per Astro’s loader contract, a schema you pass to defineCollection replaces the loader’s default — it is not merged. So if you supply one, you own the whole shape: any field you drop is gone from entry.data, and Astro validates each entry against yours.
A narrowed schema keeps entry.data lean:
import { defineCollection, z } from "astro:content";import { ghostwritr } from "@ghostwritr/astro";
export const collections = { articles: defineCollection({ loader: ghostwritr({ siteId: import.meta.env.GHOSTWRITR_SITE_ID }), schema: z.object({ slug: z.string(), title: z.string(), seoDescription: z.string().nullable(), publishedAt: z.coerce.date(), }), }),};Duplicate slugs fail closed
Section titled “Duplicate slugs fail closed”The slug is the entry id and your route param, so two articles sharing a slug would nondeterministically overwrite each other. The loader refuses: it throws a GhostwritrError with code INVALID_RESPONSE (and the offending slug in details), failing the build rather than shipping ambiguous content. See Error handling.