Table of contents
- Quick summary
- Ghost owns three of the four signals automatically, and the fourth is the one nobody edits
- The h1 is your post title, and there is no separate override
- Title and meta description live in Post settings, not the editor toolbar
- JSON-LD is generated for you, and hand-authoring it is a trap
- When you actually need Code Injection for schema
- Verify all four signals on the published page
- Where AIScan fits, and where it doesn't
- Fix it, then confirm it, then move on
Quick summary
| 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 {{meta_title}} helper | Post settings → Meta data → Meta title |
| Meta description | The {{meta_description}} helper, via {{ghost_head}} | Post settings → Meta data → Meta description |
JSON-LD (Article) | Ghost core, assembled from other fields | Nowhere directly. It reads your title, tags, excerpt and author bio |
Ghost gets three of these four right without you touching anything. The one it doesn't is the one every Ghost writer eventually fights: there is no field anywhere in the admin called "H1," because your post title is your H1, full stop.
Ghost owns three of the four signals automatically, and the fourth is the one nobody edits
Fetch a default Ghost theme's default.hbs and the mechanism is two lines. Casper, Ghost's own starter theme, puts this in the <head>, verified on the theme's own repository:
<title>{{meta_title}}</title>
...
{{ghost_head}}
{{meta_title}} is a Handlebars helper, not a raw field lookup. According to Ghost's own theme documentation, it "generates automatic meta data by default, but it can be overridden with custom content in the post settings menu." Leave the Meta Title field in your post settings blank and the helper falls back to the post's title. Fill it in and the <title> tag diverges from the H1 on purpose. That's a feature, not a bug, and it's why your headline and your search-result title don't have to read identically.
{{ghost_head}} does the heavy lifting. According to that same documentation, one helper call outputs the meta description, "Structured data Schema.org microformats in JSON/LD," Open Graph and Twitter Card tags, RSS discovery links, and anything you've dropped into Code Injection, global or per-page. Miss {{ghost_head}} in a hand-rolled default.hbs and every one of those signals vanishes from the rendered page at once. That's the single most common way an otherwise up-to-date Ghost site fails a structured-HTML check: not a missing setting, a missing helper call.
The h1 is your post title, and there is no separate override
Casper's post.hbs renders the heading with one line: <h1 class="article-title">{{title}}</h1>. That {{title}} is the raw post title, the same string that feeds your post URL slug and your browser tab. Ghost gives you a Meta Title field to change what search engines and the <title> tag show, and a Meta Description field to change the snippet, but no equivalent "H1 override" field. If you want different wording in your heading than in your search result, the only route is to write a Meta Title that differs from the actual title. The H1 will always match the title field, on every stock and custom theme built on Ghost's content API.
This is also where duplicate <h1> tags come from on Ghost specifically. Ghost's editor ships a Header card that a writer can drop into the body of a post, and a Header card set to its largest size renders its own <h1>. Put one at the top of a post on a theme that already renders {{title}} as an <h1>, and the page ships two. That's a structural duplicate with nothing to do with the theme being broken, and everything to do with an editorial habit.
Title and meta description live in Post settings, not the editor toolbar
Both fields sit in the same place: open a post in the editor, click the settings icon, and scroll to Meta data. In its own words, Ghost's documentation for that panel advises keeping "the meta title and description within the recommended character limit" and writing them "for humans." Leave either blank and Ghost falls back to the post title and an automated excerpt, rather than leaving the tag empty. That fallback is worth knowing before you conclude a check has failed: an empty-looking field in the editor does not mean an empty tag on the page.
| Field | If you fill it in | If you leave it blank |
|---|---|---|
| Meta title | Becomes the <title> tag and the JSON-LD headline | Falls back to the post title |
| Meta description | Becomes the meta description tag and the JSON-LD description | Falls back to the custom excerpt, then an automatic 50-word excerpt |
| Canonical URL | Overrides the automatic canonical tag | Ghost sets its own canonical automatically |
The Canonical URL field in that same panel is worth using deliberately: it overrides Ghost's automatic canonical tag, which matters when you're republishing a piece that first ran elsewhere and don't want two indexed copies competing.
JSON-LD is generated for you, and hand-authoring it is a trap
This is the signal with no dedicated field, and reading Ghost's own schema-building source explains why. Ghost core assembles a schema.org/Article object per post from fields you're already filling in for other reasons:
| Article field | Comes from |
|---|---|
headline | Meta title (or the post title, if blank) |
keywords | Your tags, joined |
description | Meta description, then custom excerpt, then an automatic excerpt |
image | The post's feature image |
datePublished / dateModified | The post's own timestamps |
author | The credited author's profile, bio and social links, as a nested Person object |
None of that is a form field labeled "schema." It's a side effect of the post you already wrote.
The trap is trying to fix a JSON-LD failure by hand-writing a <script type="application/ld+json"> block through Code Injection. Ghost renders both: its own automatic block from {{ghost_head}}, and yours. Nothing stops two structured-data blocks from coexisting on one page, but a hand-written block that disagrees with the automatic one, a different headline or a different date, hands a crawler two conflicting answers to the same question instead of one. If a scan reports missing JSON-LD on a page that should have Ghost's automatic block, the fix is almost never Code Injection. It's confirming {{ghost_head}} actually made it into the theme's default.hbs.
When you actually need Code Injection for schema
There is a legitimate use: schema types Ghost's own Article object doesn't cover, such as FAQPage or HowTo markup, or Product markup on a landing page built as a Ghost page rather than a post. According to Ghost's own help documentation, the feature lives in two places: Settings → Advanced → Code Injection for site-wide header and footer code, or, for one specific post, the Code Injection section inside that post's own settings panel, which accepts separate header and footer snippets scoped to that page alone. Add a second, different @type there and it sits next to the automatic Article block without conflict, because the two blocks describe different things. Add a second Article block and you're back in the trap above.
Verify all four signals on the published page
curl -sL https://yoursite.com/your-post/ -o /tmp/p.html
python3 - <<'PY'
import re
h = open('/tmp/p.html', encoding='utf8', errors='ignore').read()
print("title:", re.search(r'<title>(.*?)</title>', h).group(1))
print("h1 count:", len(re.findall(r'<h1[ >]', h)))
print("meta description:", 'name="description"' in h)
ld = re.findall(r'<script type="application/ld\+json">(.*?)</script>', h, re.S)
types = [re.search(r'"@type":"(\w+)"', b).group(1) for b in ld if re.search(r'"@type":"(\w+)"', b)]
print("JSON-LD blocks:", len(ld), "| types:", types)
PY
A healthy Ghost post prints exactly one <h1>, a <title> that may or may not match it depending on whether you set a custom Meta Title, one meta description tag, and exactly one JSON-LD block typed Article, two only if you deliberately added a second, different type through Code Injection. Two Article blocks, or a JSON-LD count of zero on a theme that includes {{ghost_head}}, both point at a specific, findable cause rather than a mystery.
Where AIScan fits, and where it doesn't
AIScan's check C3 confirms the four signals above are present in the rendered HTML: one <h1>, a <title> tag, a meta description, and a JSON-LD block. Run npx aiscan-cli against your own domain and it names C3 directly against your live page, free, with no account and no theme access needed. What it stops short of is validating the JSON-LD's contents: a present Article block satisfies the check whether or not the headline field is stale, the author object is well-formed, or the graph would actually pass Google's Rich Results Test. A present block is not the same claim as a correct one, and this check was never built to make that second claim. Pair it with a schema validator when the JSON-LD itself, not just its presence, is in question.
There's a Ghost-specific blind spot worth naming honestly: the check reads the rendered page, so it can't distinguish a theme that never had {{ghost_head}} from one that has it but is serving a stale deploy. Both look identical from outside. If a Ghost site that should pass keeps failing, check the theme file before assuming the platform stopped doing its job.
Fix it, then confirm it, then move on
Ghost's default behavior gets three of these four signals right the moment {{ghost_head}} is in your theme, which it is on Casper and on every unmodified theme built to Ghost's own documented conventions. The one thing worth auditing today is whether a custom theme still calls it, and whether any post is quietly carrying a Header-card duplicate <h1> left over from an old editorial habit. Scan the page, read the JSON-LD block count, and fix the one signal that's actually missing, not the three that were never broken. From here, aiscan.site/guides has the rest of the checklist for what agents can and can't see on your site.
Frequently asked questions
Why does my page title in Google look different from my post's heading?
They're allowed to differ on Ghost by design. The h1 always renders your post's literal title via the {{title}} helper, with no override field. The title tag renders the {{meta_title}} helper, which falls back to the post title but takes priority from the Meta Title field in Post settings → Meta data if you've filled one in. Setting a different Meta Title is the only way to make the two diverge.
Error: my scan reports JSON-LD missing, but Ghost adds it automatically. What's wrong?
The most common cause is a customized default.hbs that dropped the {{ghost_head}} helper call somewhere along the way. That single helper is what outputs the automatic Article schema, the meta description, and the Open Graph and Twitter Card tags together, so all three tend to disappear at once. Confirm the helper is still present in the theme file before assuming a settings problem.
Error: my post shows two h1 headings. Where does the second one come from?
Almost always a Header card placed inside the post body. Ghost's editor lets you add a Header card set to its largest size, which renders as its own h1-level heading independent of the theme's {{title}} heading. Remove the card or drop it to a smaller heading level, then re-check the rendered HTML.
Error: I set a custom Meta Description, but the page still shows my old excerpt.
Check whether a CDN or the browser cached the previous version of the page. Ghost regenerates the rendered HTML as soon as you save the post, so a mismatch after saving is almost always a caching layer serving a stale copy rather than a field that failed to save. Re-fetch the URL with a cache-busting query string to confirm.
Can I use Code Injection to write my own JSON-LD instead of relying on Ghost's automatic block?
You can, but it creates a second structured-data block rather than replacing the first. If your hand-written block uses the same @type as Ghost's automatic Article schema and disagrees with it on any field, you've shipped two conflicting answers to the same question. Code Injection is the right tool for a schema type Ghost doesn't generate on its own, such as FAQPage or HowTo, not for editing what Article already covers.
Does the automatic JSON-LD work the same way on Ghost pages as on posts?
The same underlying fields apply (meta title, meta description, feature image, author), but Ghost's schema builder treats pages and posts as separate contexts internally. In practice both get an Article-type schema object built from the same Meta data panel, so the fix for a missing or wrong field is identical on either content type.
Do I need Ghost(Pro) or a specific plan for any of this?
No. The meta_title, meta_description and ghost_head helpers, plus the automatic Article schema, are part of Ghost core and ship identically on a self-hosted install and on every Ghost(Pro) plan. Code Injection is also core, available at Settings → Advanced → Code Injection regardless of hosting.
Error: my meta description tag is empty even though I never touched the Meta Description field.
An empty field doesn't produce an empty tag. Ghost falls back first to a custom excerpt if one is set, then to an automatically generated excerpt of the post's first fifty words. A genuinely empty tag on a live page usually means {{ghost_head}} is missing from the theme, the same root cause as a missing JSON-LD block, rather than a content problem.
Related guides
How to ship one h1, title, meta description and JSON-LD on Webflow
Webflow's SEO panel now writes three of the four signals that AIScan's C3 check grades, and the fourth one is not in any panel. On a paid Site plan you can set a page's title tag, its meta…
How to ship one h1, title, meta description and JSON-LD on Next.js
Next.js hands you a Metadata API that writes your <title and your meta description into the head automatically. It does not write your JSONLD, and it does not write your <h1. Those two are ordinary…
Gate AI readiness in CI so a regression fails the build
A green pipeline is supposed to mean the site is fine. On an AI readiness check it often means something narrower: that the gate could not tell a file from a phantom. This step, which appears in a…
How to ship one h1, title, meta description and JSON-LD on WordPress
WordPress sites fail AIScan's C3 check more often than their owners expect, and almost never for the reason they expect. Across the 129 WordPress sites in our scan corpus, verified on 8 September…
