Table of contents
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 | 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, 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, 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
- Open the pricing page in the editor (Pages > Pricing). Keep its URL slug as
pricing, because AIScan looks at/pricingfirst, then the homepage. - 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. - 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.
- Paste the block below into the header field and replace the example values.
- Click Update, then reload the live page.
<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 and schema.org/UnitPriceSpecification. Use the 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:
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:
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:
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. For the generic version of this fix and the WooCommerce case, read Make your pricing machine-readable for shopping agents. For the rest of your Ghost setup, see the AI readiness setup for Ghost and Ghost titles, meta and schema. All guides live at /guides.
Frequently asked questions
Why does AIScan show M4 as not applicable on my Ghost site?
Commerce checks only run when the scanner sees selling signals, such as structured offers, a payment provider like Stripe, or routes like /pricing. A Ghost site with no paid tiers and no pricing page has none, so M4 returns na and does not affect your score.
I pasted the block and the scan still fails M4. What is wrong?
Fetch the live page with curl and count application/ld+json occurrences. Ghost's own block makes one, yours should make two. If you see one, the field was not saved, the page was not updated, or the theme is missing {{ghost_head}}, which is what prints Code Injection.
My Code Injection field is saved but nothing appears in the page source. Why?
Ghost documents that {{ghost_head}} outputs code injection. A custom or hand-edited theme that dropped that helper from default.hbs prints none of it, including your pricing block. Restore the helper just before the closing head tag.
Why do my prices look a hundred times too high to agents?
Ghost's Content API returns tier prices in the smallest currency unit, so 900 means 9.00. Copying the raw integer into price states 900.00. Divide by 100 and write it as a string such as "9.00".
Does the currency code need to be uppercase?
Use the ISO 4217 three-letter form in capitals, such as USD. Ghost's Content API returns a lowercase value like usd, so change it when you copy it across.
Can I put the block in Settings > Advanced > Code Injection instead?
You can, but Ghost outputs site-wide injection on every page, so every post and your about page would claim a price. Use the Code injection field on the pricing page only.
Do I need one block per tier?
Not necessarily. One Product can hold an Offer array with an entry per tier and billing period. Keep monthly and yearly prices as separate Offer objects with their own billingDuration.
Do I have to update the block when I change a tier price?
Yes. The block is static text and does not read your tiers. M4 checks structure, not accuracy, so a stale price still passes while agents quote it. Add the edit to your price-change checklist.

About the author
M Asif Rahman
Entrepreneur, curious about AI
I have been building companies for a long time, and lately most of my curiosity goes to AI. AIScan is a small personal project I built on a flight from Dubai to Narita. I use it to understand how ready the web is for AI agents, and I share what the scans teach me.
Related guides
How to ship one h1, title, meta description and JSON-LD on Ghost
| Signal | Who owns it on Ghost | Where you set it | |||| | <h1 | The theme, from your post's literal title | Nothing to set. It's the title field itself | | <title tag | The {{metatitle}} helper |…
AI readiness setup guide by platform (2026)
Verified 17 and 18 October 2026. Updated on 18 October 2026. An AIready site needs six things: a robots.txt that names AI crawlers D1, B2, a sitemap D2, a declared feed E5, an llms.txt C2, and…
How to publish a valid llms.txt on Ghost
Verified 30 August 2026. Ghost has no setting for llms.txt and no folder you can drop a file into that lands on the site root. The fix takes about fifteen minutes: one route in routes.yaml, one…
