---
title: "How to publish schema.org Offer JSON-LD on your pricing page on Ghost"
slug: pricing-schema-markup-ghost
published: 2026-10-16T13:25:48.523219+00:00
updated: 2026-10-18T03:09:38.90034+00:00
author: "M Asif Rahman"
author_url: https://ar.bd
category: "AI Readiness"
tags: check:M4, platform:ghost, Ghost, pricing schema, JSON-LD, AI readiness
description: "Ghost writes Article and Organization schema but no pricing. Add one per-page Offer JSON-LD block so AIScan check M4 and shopping agents read your plans."
url: https://aiscan.site/blog/pricing-schema-markup-ghost
---

Ghost writes its own structured data for every page, and none of it is pricing. On Ghost 6.71 the home page carries `WebSite` and `Organization`, and a page carries `Article`, `Person` and `Organization`. No `Offer` anywhere. If you sell memberships or plans from a Ghost page, an agent asked "what does this cost?" has to read your prose. This guide adds one JSON-LD block to the pricing page, in about 10 minutes, so AIScan check M4 reads your plans as data.

## Quick summary

| If you want to… | Do this | Where in Ghost | AIScan check |
|---|---|---|---|
| Publish plan prices as data | Paste one `Product` block with an `Offer` per billing period | **Page settings > Code injection** on the pricing page | M4 |
| Avoid putting it on every page | Never use the site-wide field for this | Not **Settings > Advanced > Code Injection** | M4 |
| Get the numbers right | Convert Content API cents to decimals, uppercase the currency | `500` becomes `"5.00"`, `usd` becomes `"USD"` | M4 |
| Confirm it landed | `npx aiscan-cli yoursite.com/pricing` | Terminal or [aiscan.site](https://aiscan.site/) | M4 |

## Why Ghost does not do this for you

`{{ghost_head}}` is the theme helper that prints your meta description, the Schema.org JSON-LD, Open Graph tags and RSS links. According to [Ghost's developer docs](https://docs.ghost.org/themes/helpers/utility/ghost_head_foot), it also prints anything you put in Code Injection, site-wide or per page. That second job is the opening: Ghost generates the editorial schema, and you supply the commercial one.

Your tier prices live somewhere else. Ghost keeps them in the [Tiers endpoint of the Content API](https://docs.ghost.org/content-api/tiers), as integers in the smallest currency unit with a lowercase currency code. The documented example is `"monthly_price": 500`, `"yearly_price": 5000`, `"currency": "usd"`. Schema.org wants `"5.00"` and `"USD"`. Paste the raw values and the block parses fine and states a price a hundred times too high.

Commerce checks only run on sites that look like they sell. AIScan scanned the Ghost demo site and reported commerce as `none`, so M4 does not apply there. Connect Stripe and publish a pricing page, and M4 starts to count.

## Add the Offer block

1. Open the pricing page in the editor (**Pages > Pricing**). Keep its URL slug as `pricing`, because AIScan looks at `/pricing` first, then the homepage.
2. Read your real prices. Open `https://yoursite.com/ghost/api/content/tiers/?key=<content-api-key>&include=monthly_price,yearly_price` (it needs a Content API key from a custom integration), or just copy the numbers shown on the page.
3. Click the settings icon at the top right of the editor and open **Code injection**. Ghost's help calls this the Post Settings menu, and the same fields exist for posts and pages.
4. Paste the block below into the header field and replace the example values.
5. Click **Update**, then reload the live page.

```html
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Example Pro membership",
  "url": "https://example.com/pricing/",
  "offers": [
    {
      "@type": "Offer",
      "price": "9.00",
      "priceCurrency": "USD",
      "availability": "https://schema.org/InStock",
      "priceSpecification": {
        "@type": "UnitPriceSpecification",
        "price": "9.00",
        "priceCurrency": "USD",
        "billingDuration": "P1M"
      }
    },
    {
      "@type": "Offer",
      "price": "90.00",
      "priceCurrency": "USD",
      "availability": "https://schema.org/InStock",
      "priceSpecification": {
        "@type": "UnitPriceSpecification",
        "price": "90.00",
        "priceCurrency": "USD",
        "billingDuration": "P1Y"
      }
    }
  ]
}
</script>
```

Fields come from [schema.org/Offer](https://schema.org/Offer) and [schema.org/UnitPriceSpecification](https://schema.org/UnitPriceSpecification). Use the [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) three-letter currency code. A free tier on its own scores partial, so list at least one paid price. For more than one paid tier, add a second `Product` block or more `Offer` objects to the array, one per tier and billing period.

## Why per-page and not site-wide

Ghost says site-wide code injection appears on every single page. A site-wide `Offer` block would tell agents your blog index, your about page and every post cost $9.00 a month. Use the per-page field.

If you would rather generate prices from tier data in a theme template, Ghost's `{{#get "tiers"}}` helper can list them. I found no documented helper that divides by 100, and `{{price}}` prints a formatted display amount like `$5`, so I did not test that route. The hand-written block is the one this guide covers.

## Verify it landed

Lead with the scan:

```bash
npx aiscan-cli yoursite.com/pricing
```

Read check **M4**. Expected: pass, with the `Offer` found on `/pricing`. A failing site shows the evidence `no JSON-LD Offer found on /pricing or the homepage`.

If you would rather check by hand:

```bash
curl -s https://yoursite.com/pricing/ | grep -c 'application/ld+json'
```

Expected: `2`. Ghost's own block plus yours. A count of `1` means yours is missing from the rendered HTML. Then pull your block out and confirm it parses:

```bash
curl -s https://yoursite.com/pricing/ | python3 -c "import sys,re,json; [print(json.loads(b).get('@type')) for b in re.findall(r'<script type=\"application/ld\+json\">(.*?)</script>', sys.stdin.read(), re.S)]"
```

Expected: `Article` (or `WebPage`) and `Product`. A `JSONDecodeError` means a trailing comma or a curly quote crept in during the paste.

## What a scan cannot see

M4 reads structure, not truth. If you change a tier price in Ghost admin and forget the code injection field, the scan still passes and agents quote the old number. Add the field to whatever checklist you already run when prices change.

## Next step

Run the scan, read M4, then look at the rest of your Commerce checks in the [Commerce docs](https://aiscan.site/docs/checks/commerce). For the generic version of this fix and the WooCommerce case, read [Make your pricing machine-readable for shopping agents](https://aiscan.site/blog/machine-readable-pricing-schema). For the rest of your Ghost setup, see [the AI readiness setup for Ghost](https://aiscan.site/blog/ai-readiness-setup-guide#ghost) and [Ghost titles, meta and schema](https://aiscan.site/blog/title-meta-schema-ghost). All guides live at [/guides](https://aiscan.site/guides).

