Table of contents
- TL;DR
- Contents
- What every platform has in common
- What our scans show
- Ghost
- 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
- Lovable
- Lovable doesn't give you a metadata file. It gives you an auditor.
- Set the title and description by asking, not by editing
- Let the review add the missing h1, then check what it actually wrote
- JSON-LD is a finding, not a code snippet you paste in
- Read the review panel and the rendered HTML. They can disagree.
- Next.js
- Three mechanisms, four signals
- What 120 Next.js sites actually get wrong
- Add the JSON-LD block Next.js will not add for you
- Set title and description where the framework can see them
- The streaming trap that hides your title from AI crawlers
- Write the h1 yourself, once per page
- Scan it, then read the four booleans
- Replit
- Replit doesn't have a settings panel. It has an audit that runs after you publish.
- The gap: Lighthouse scores three of these signals, and skips the fourth
- Fix the title and meta description first, this part Replit already catches
- JSON-LD lives in the same file, but you have to check it yourself
- The h1 problem is Autoscale's rendering order, not a missing element
- Confirm all four signals on the domain that actually ships
- Webflow
- Webflow writes three of the four signals in its SEO panel and leaves the h1 on the canvas
- Title tag and meta description, and the template pattern that fills every CMS item
- JSON-LD belongs in Schema markup, not in custom code
- The three ways an h1 goes wrong on the canvas
- Wix
- Wix splits the four signals across three surfaces
- Title tag and meta description in SEO basics
- JSON-LD: Advanced SEO per page, SEO Settings per page type
- The h1 is a text element, and a blank line breaks it
- WordPress
- On WordPress the h1 belongs to the theme, and that is where C3 fails
- Why the heading goes missing
- Set the Title block to H1 in the Site Editor
- Classic themes: the title tag is opt-in
- Meta description and JSON-LD come from one plugin, not three
- If the scan still reports h1: 0
- Verify all four signals on any platform
- Where AIScan fits, and where it doesn't
One page needs one <h1>, a <title>, a meta description and a JSON-LD block, and AIScan check C3 (structured HTML) passes only when all four are in the HTML the server returns. Who writes each of the four depends on the platform: Ghost writes three from one theme helper, Next.js writes two from the Metadata API, Lovable hands all four to an audit panel, and Replit leaves them in one index.html that its own SEO rating only half checks, Webflow writes three in a panel and leaves the h1 on the canvas, Wix spreads them over three surfaces, and WordPress leaves the h1 to the theme.
TL;DR
- The test is one scan.
npx aiscan-cli yoursite.com, then read the C3 row. It printsh1: N, title: bool, meta description: bool, JSON-LD: bool, and onlyh1: 1with three trues is a pass. - Check the raw HTML, not the browser.
curl -sL https://yoursite.com/ | grep -c '<h1'shows what a crawler that does not run JavaScript receives. - JSON-LD is the signal most platforms leave to you. Across 120 Next.js sites in our own scan history, 23 of the 39 that fail C3 fail on structured data alone (measured 9 September 2026).
- Do not paste a second block over an automatic one. Ghost already emits an
Articleblock; a hand-written one that disagrees gives crawlers two answers. - Seven platforms, one job. This guide covers Ghost, Lovable, Next.js, Replit, Webflow, Wix and WordPress, each in its own section. On WordPress, 50 of the 57 sites that fail C3 fail on the heading count alone (measured 8 September 2026), because the h1 belongs to the theme and no SEO plugin touches it.
Contents
| Platform | Jump to | Who writes the four signals |
|---|---|---|
| Ghost | #ghost | The theme: {{meta_title}} and {{ghost_head}} write title, description and Article JSON-LD; the h1 is the post title |
| Lovable | #lovable | Nobody by default: the SEO & AI search review finds the gaps, Try to fix asks the agent to close them |
| Next.js | #next-js | The Metadata API writes title and description; you write the JSON-LD script and the h1 |
| Replit | #replit | You or SEO Agent, in client/index.html; the SEO Rating scores three of the four |
| Webflow | #webflow | Page settings write the title, description and JSON-LD on a paid plan; the h1 is the tag on a Designer element |
| Wix | #wix | SEO basics holds title and description, Advanced SEO holds JSON-LD, and the h1 is a Heading 1 text element |
| WordPress | #wordpress | The SEO plugin writes description and JSON-LD; the h1 is the Title block level in your theme template |
What every platform has in common
C3 accepts one arrangement and no other: exactly one <h1>, a <title> with text in it, a meta description, and structured data in at least one JSON-LD block. The details differ by platform, but four rules hold everywhere, so each section below only says what is different:
- Read the delivered HTML. What counts is what the server returns before any script runs. A heading or schema block that a browser builds afterwards is invisible to a fetch that does not run JavaScript.
- One h1. Two usually means a layout and a page both rendered one, or an editor block added a second.
- JSON-LD must match the page. schema.org's Article type and Google's structured data introduction both describe markup as a statement about the visible content. A copied block with placeholder values is a mismatch, not a pass.
- Presence is not correctness. C3 records that a block exists. It does not grade the
@type, whether the graph parses, or whether the description is unique. Validate the contents with Google's Rich Results Test.
What our scans show
First-party data from the AIScan scan corpus, one latest scan per host on rubric 2026.08.2, taken 9 September 2026.
| Group | Sites | Pass C3 | Fail C3 |
|---|---|---|---|
| All scanned sites | 501 | 248 | 253 |
| Detected as Next.js | 120 | 81 | 39 |
| Detected as WordPress (8 September 2026 scan) | 129 | 72 | 57 |
Of the 39 failing Next.js sites, 23 had a correct h1, title and description and no JSON-LD at all. The framework did its part; nobody added the one block it leaves to the developer. The WordPress row is the mirror image: 50 of its 57 failures were the heading count, with title, description and schema present. It comes from the 8 September scan of 498 sites, so its sample date differs from the rows above. Ghost, Lovable, Replit, Webflow and Wix sample sizes are too small to publish a split, so those sections describe the mechanism instead.
Ghost
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.
Lovable
Lovable doesn't give you a metadata file. It gives you an auditor.
Most other platforms has a place where title, meta description and schema live: a PHP template, a metadata export, a settings panel. Lovable has none of those for a project you didn't build the head tags into yourself. According to Lovable's own publishing documentation, fetched 23 September 2026, "Lovable generates your site's metadata while it builds your app: the site title, meta description, and site icon." There is no file to open and no field to type into for that part.
What Lovable gives you instead is a checklist that reads your actual code and your deployed pages, called the SEO & AI search review. Open it from More → SEO & AI search in the project toolbar, then click Scan project. Four of its findings map straight onto C3, listed here as verified on docs.lovable.dev:
| SEO & AI search review finding | What it covers | Maps to C3 |
|---|---|---|
| Page basics | lang attribute, viewport configuration | Supporting signal |
| Metadata | Title and meta description per page | Title + description |
| Structured data | JSON-LD present, parseable, matching the page | JSON-LD |
| Content structure | Missing or generic h1, skipped heading levels | h1 |
Each failing row gets a Try to fix button that hands the specific fix to the agent, or you can click Try to fix all and send every open finding in one message. The review runs on unpublished projects too, so nothing here waits for a live domain.
In our own corpus of 501 scanned sites, re-measured 9 September 2026, 248 currently pass all four C3 signals at once, worth knowing before you assume a partial score is unusual.
Set the title and description by asking, not by editing
There is a genuine trap here for anyone used to a settings page: the title and description are not global. Lovable's documentation states plainly that "metadata is set per page… Lovable generates a unique title and description for every page of your app based on its content," and that changing it means asking in the project chat, for example: "Change my site title to 'Acme Task Tracker' and write a meta description about team to-do lists." That request is treated as a normal build message and spends credits like any other edit, according to the same page.
| To check the result | Open this | What you see |
|---|---|---|
| While editing | The page selector above the preview | That page's Social and Search cards |
| From the Publish dialog | The ⋮ menu, or the favicon next to the URL | Social & search appearance |
| Right after publishing | The Your website is live screen | An Edit link next to the site's title |
A metadata change only reaches the live site on your next publish, exactly like a code edit. If the project is already published, Lovable's reply ends with a Publish to update the live site button, so you don't have to hunt for the Publish dialog separately.
Let the review add the missing h1, then check what it actually wrote
The Content structure check fails on a missing h1, a heading that skips from h1 to h3, or an h1 so generic it says nothing about the page, in Lovable's own words: "H1 is missing or too generic" and "Heading levels skip from H1 to H3." Clicking Try to fix sends that finding to the agent, which edits the component that renders the page rather than a template file, because on a Lovable project there usually isn't a separate template to edit.
Read the diff before you accept it. An agent asked to add an h1 on a page that already has a large, unstyled heading sometimes wraps a second element instead of promoting the first one, which trades a missing-h1 finding for a two-heading finding on the next scan. Confirming the tag count after every fix, not just after the whole pass, catches that in one page load instead of a full review cycle.
JSON-LD is a finding, not a code snippet you paste in
The Structured data check is stricter than "does a script tag carrying JSON-LD exist." Lovable's own documentation for the check names three separate failure modes, verified on docs.lovable.dev: JSON-LD that "cannot be parsed," JSON-LD that "does not match the visible page," and a page that "clearly qualifies for a supported Google Search feature and already shows the facts that feature needs, but carries no markup for it." It's also explicit about what does not count as a problem: a missing generic WebSite or Organization block, and a missing FAQPage markup, are both called out as non-issues on their own.
Try to fix here either adds a new JSON-LD block matched to the page's actual content, a product page gets Product, an article gets Article, or repairs an existing block so its fields match what's on the page. If you'd rather brief the agent yourself, describe the entity and the fields you want rather than pasting a generic schema.org example. A copied Organization snippet with placeholder values is exactly the "does not match the visible page" failure the check is built to catch.
Read the review panel and the rendered HTML. They can disagree.
A Scanning your project… header means results are stale; wait for it to finish before trusting a green check. After a Try to fix pass completes, the review marks that finding Fixed provisionally and only confirms it on the next scan, so run Scan again rather than taking the badge at face value.
Then check the deployed page itself, because Lovable's review and a crawler don't always see the same HTML:
URL=https://yoursite.com/some-page
curl -s "$URL" | grep -c '<h1' # expect 1
curl -s "$URL" | grep -o 'application/ld+json' | wc -l # expect 1 or more
curl -sI "$URL" | grep -i x-robots-tag # a "noindex" here means you tested a preview host
If the curl output looks right but AIScan still reports a failure, the gap is almost always the template split covered in our full Lovable setup guide: an older React + Vite project only serves its pre-rendered HTML to crawlers Lovable has verified, and AIScan isn't on that list.
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 JSON-LD, and it does not write your <h1>. Those two are ordinary markup you render in a component, and on real sites that split is exactly where check C3 breaks. Across 120 Next.js sites in our own scan history, 34 of the 39 that miss C3 miss it on JSON-LD alone. This guide fixes all four signals in the App Router, in the order they actually fail.
Three mechanisms, four signals
C3 wants four things on the page and accepts only one arrangement of them: a heading count of exactly one, a <title> with text in it, a meta description, and structured data in at least one JSON-LD block. Next.js covers two of the four from a single export and leaves the rest to you.
The Metadata API is the covered half. According to the generateMetadata reference, metadata objects from every segment in a route are merged shallowly from the root layout down to the page, and duplicate keys are replaced by the last segment that defines them. So a title.template in app/layout.tsx decorates every child title, and a description set in the root layout survives on any page that never sets its own.
JSON-LD is the uncovered half, and the docs are direct about it. The Next.js JSON-LD guide says, verbatim: "Our current recommendation for JSON-LD is to render structured data as a <script> tag in your layout.js or page.js components." There is no jsonLd metadata field. The framework's own site follows that advice: a plain fetch of the Next.js homepage on 9 September 2026 returned two application/ld+json blocks and both sit in the body, not the head.
The <h1> has no framework mechanism at all. Nothing in the Metadata API touches body markup, so a single visible heading is yours to get right.
What 120 Next.js sites actually get wrong
We grouped the C3 evidence strings across our own corpus on 9 September 2026, deduped to one latest scan per host on rubric 2026.08.2. C3 passes on 248 of 501 sites. Of the 120 detected as Next.js, 81 pass and 39 do not.
| Failing pattern on Next.js | Sites |
|---|---|
| Everything correct except JSON-LD | 23 |
| No JSON-LD, plus a heading-count problem | 9 |
| Heading count wrong, JSON-LD present | 4 |
| Missing title or meta description | 3 |
Read the first row twice. Twenty-three of the 39 failures have one <h1>, a title, a description and no structured data: the framework doing its job, and nobody reaching the one step it left out. Our WordPress guide found the mirror image, where 50 of 57 failures were the heading count.
Add the JSON-LD block Next.js will not add for you
Render it inside the page component, next to your content. According to the same guide, JSON.stringify does not sanitise strings used in injection attacks, so replace < with its unicode escape:
// app/blog/[slug]/page.tsx
export default async function Page({ params }) {
const { slug } = await params
const post = await getPost(slug)
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
description: post.excerpt,
datePublished: post.date,
}
return (
<article>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, '\\u003c'),
}}
/>
<h1>{post.title}</h1>
{/* ... */}
</article>
)
}
Use a native <script> rather than next/script. In its own words, the guide notes that next/script is built for loading executable JavaScript, and structured data is not code.
Set title and description where the framework can see them
Both come from one export, and both have a rule that fails quietly.
// app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: { template: '%s | Acme', default: 'Acme' },
description: 'One sentence describing the site, 150 to 160 characters.',
}
The quiet rule, according to the reference: the metadata object and generateMetadata exports are only supported in Server Components. Put export const metadata in a file carrying 'use client' and nothing is emitted, no warning appears, and C3 reports title: false. The reason given is that metadata has to resolve on the server before the page renders so it can go into the initial HTML response. Keep page.tsx a Server Component and move interactive parts into their own client file.
The streaming trap that hides your title from AI crawlers
When generateMetadata waits on runtime data, Next.js streams the page first. The reference says the resolved tags are then "appended to the <body> tag", and adds that this was verified with bots that execute JavaScript and inspect the full DOM. Crawlers that cannot run JavaScript get a blocking render instead, and Next.js decides which is which from the user agent.
That list is a single regular expression, HTML_LIMITED_BOT_UA_RE, in the framework's source. We fetched it on 9 September 2026 and matched it against the 33 crawler tokens in our own AI crawler inventory.
| Matched by the blocking list | Not matched |
|---|---|
| Applebot, Applebot-Extended | GPTBot, ChatGPT-User, OAI-SearchBot |
facebookexternalhit | ClaudeBot, Claude-User, Claude-SearchBot |
| Five Google tokens, including Google-Extended | PerplexityBot, Perplexity-User, CCBot |
| 8 of 33 | Amazonbot, meta-externalagent, and 16 more: 25 of 33 |
None of the eleven crawlers last measured as non-rendering appears on it. Googlebot is absent too, and correctly so, because it renders JavaScript. The others do not have that excuse. If your metadata defers to request time, add the crawlers you care about:
// next.config.ts
const config = {
htmlLimitedBots: /GPTBot|ClaudeBot|PerplexityBot|CCBot|facebookexternalhit/i,
}
export default config
Or make the metadata static, which is faster and needs no list. The docs note that overriding this option can lengthen response times.
Write the h1 yourself, once per page
Thirteen of the 39 failing Next.js sites report a heading count other than one, ten of them zero. Both causes are layout-shaped: a root layout rendering a site name as <h1> while the page renders another, or a visible title that is styled text in a <div>. Put exactly one <h1> in the page component, keep the site name in the layout as a <p> or a <span>, and let the title tag carry the branding through title.template.
Scan it, then read the four booleans
Run the scan first. It reads all four signals in one pass and prints the evidence string C3 decided on:
npx aiscan-cli yoursite.com
Look at the C3 row. It returns h1: N, title: bool, meta description: bool, JSON-LD: bool, and only h1: 1 with all three true is a pass. You can paste the URL at AIScan instead if you would rather not install anything.
To check by hand, read the initial HTML the way a non-rendering crawler does:
URL=https://yoursite.com
curl -sL --compressed "$URL" -o /tmp/p.html
grep -c '<h1' /tmp/p.html
grep -o '<title>[^<]*</title>' /tmp/p.html
grep -c 'name="description"' /tmp/p.html
grep -c 'application/ld+json' /tmp/p.html
You want 1, one line of real text, 1, and at least 1. Validate the block itself with Google's Rich Results Test or the Schema Markup Validator, both named in the Next.js guide.
Replit
Replit doesn't have a settings panel. It has an audit that runs after you publish.
Most other platforms keeps title, description and schema somewhere you can open before going live: a template file, a metadata export, a settings tab. Replit's equivalent, the SEO Rating, only exists once a deployment is public. According to Replit's own documentation on the feature, verified on 24 September 2026, publishing an Autoscale, Reserved VM or Static app produces a Lighthouse audit of your live URLs within a minute or two, shown as a badge in the Publishing tool: Healthy, Needs Work or Weak.
If the rating has room to grow, SEO Agent turns the findings into one-click fixes. It lives in the Growth pane and groups issues into four categories:
| SEO Agent category | What it covers |
|---|---|
| AI Readiness | Signals aimed at crawlers that aren't traditional search engines |
| Crawlability & Discovery | robots.txt, sitemap.xml, link structure |
| Landing Page Rendering & Metadata | Title tags, meta descriptions, heading order, alt text |
| Performance Proxies | Load-time signals that feed into the same score |
Two things gate the tool itself, both stated on the same page: the app must already be published, and the builder must be on a paid plan. A free-plan project shows an upgrade prompt where the fix buttons would sit. Replit's documentation, fetched from docs.replit.com the same day, lists structured data among what SEO Agent will fix: "Structured data: JSON-LD markup that helps search engines show rich results." Title tags, meta descriptions and heading order are on that same list. Read only that list and C3 looks fully covered by one "optimize my SEO" request to Agent.
The gap: Lighthouse scores three of these signals, and skips the fourth
| Lighthouse SEO audit | Automatically scored? |
|---|---|
Document has a <title> element | Yes |
| Document has a meta description | Yes |
| Heading elements appear in a sequentially descending order | Yes |
| Structured data is valid | No, manual check only |
Google's own Lighthouse documentation for the last row, fetched from developer.chrome.com on 24 September 2026, places "Structured data is valid" under a section it calls Manual checks, the audits Lighthouse cannot score on its own and instead leaves for a human to confirm with Google's Rich Results Test. That means an app can carry a Healthy SEO Rating with zero JSON-LD anywhere on the page, because the one signal that would catch it was never part of the score to begin with. AIScan's C3 check reads the delivered HTML directly and reports a genuine pass or fail on structured data, which is the reading Replit's own badge cannot give you.
Fix the title and meta description first, this part Replit already catches
On an Agent-built app, the front end lives under client/, with Vite's root pointed at that folder and build.outDir set to dist/public, the same scaffold covered in the llms.txt guide for Replit. The <title> and <meta name="description"> tags sit in the <head> of client/index.html, and because Vite ships that file once at build time, both tags are present in the raw response even though the rest of the page is client-rendered.
Two ways to set them:
- Ask Agent. Open the Growth pane, find the SEO Rating card, click Run scan with Agent, then expand Landing Page Rendering & Metadata. Fix with Agent on the title or description row hands that one finding to Agent; Fix all with Agent sends every open issue at once. Review the change, click Apply changes to main version, then republish.
- Edit the file directly. Open client/index.html in the file tree and write the two tags by hand. A generic scaffold title such as "Vite + React + TS" is the most common finding here.
Most Agent-built apps serve one page to every route, so getting these two tags right in that single file covers the whole app. An app with genuinely separate routed pages needs per-route title and description handling, typically a library such as react-helmet-async in a client-rendered React app, which is a larger change than SEO Agent's one-click fix.
JSON-LD lives in the same file, but you have to check it yourself
Add structured data to the same <head>, either by asking Agent for it (it is on SEO Agent's own documented fix list) or by pasting a script tag directly:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "Your App Name",
"url": "https://yourapp.replit.app",
"applicationCategory": "BusinessApplication"
}
</script>
Because the SEO Rating never scores this, a Healthy badge is not evidence it worked. Confirm it two ways instead: fetch the raw page and look for the script tag yourself, then run the URL through Google's Rich Results Test, the same manual step Lighthouse's own documentation points to in place of an automatic score.
The h1 problem is Autoscale's rendering order, not a missing element
This is the signal most likely to fail on an Agent-built app, and Replit's own rating is the least likely tool to catch it. The production server, per the scaffold's server/vite.ts, serves the same client/index.html shell for every path and lets your React code fill in the actual content, including any <h1>, after the browser runs the JavaScript bundle. Lighthouse executes that JavaScript in a real browser before it audits the page, so the SEO Rating sees the finished heading and can report success. A fetch that never runs JavaScript, curl, most AI agents, AIScan's own crawler, sees the static shell and nothing else: zero headings in it.
Static Deployments don't have this problem. According to Replit's deployment documentation, a Static Deployment serves pre-rendered files directly rather than running a server, so whatever heading is baked into the build is what every reader gets, JavaScript or not. On Autoscale and Reserved VM apps built by Agent, the fix is to put a real <h1> matching the text your app actually renders as its main heading directly inside client/index.html, so it exists before any script runs rather than only after. Preview any Agent-applied fix before accepting it: a heading added next to an existing large unstyled one can produce two h1s instead of one.
Confirm all four signals on the domain that actually ships
Fetch the deployed app the way an agent would, without a browser:
curl -s https://yourapp.replit.app/ | grep -oE '<h1[^>]*>[^<]*</h1>'
curl -s https://yourapp.replit.app/ | grep -oE '<title>[^<]*</title>'
curl -s https://yourapp.replit.app/ | grep -c 'application/ld+json'
Expect exactly one <h1> carrying real page text, one non-generic <title>, and at least one application/ld+json match. Zero on any of those three is the finding to fix, regardless of what the Growth pane currently shows.
Webflow
Webflow writes three of the four signals in its SEO panel and leaves the h1 on the canvas
On a paid Site plan or a paid Workspace, a page's title tag, meta description and JSON-LD all have fields in Page settings. The heading level has no field: it is the tag on a Designer element, and that is where most Webflow pages lose C3. Webflow's accessibility article Use descriptive, sequential headings tells authors to "use one H1 per page that describes its purpose (or only use multiple H1s when a page truly has more than one purpose)". That parenthesis is sensible for a screen reader and a fail for a check that counts.
| C3 signal | Where it lives in Webflow | Who can set it |
|---|---|---|
<title> | Pages panel > Page settings > SEO settings > Title tag | Any role except Reviewer, on a paid plan |
| Meta description | Same panel, Meta description | Same |
| JSON-LD | Page settings > Schema markup | Same |
Exactly one <h1> | Nowhere: the tag on a Designer element | Anyone who can edit the canvas |
Webflow's own sites show the split. Fetched on 10 September 2026 with a browser user agent, webflow.com returned one title, one meta description, one JSON-LD block and two <h1>; university.webflow.com returned the same three and one <h1>. Two hosts is not a survey, but it is the shape of the problem: the panel-owned signals were right on both and the heading count was wrong on one.
Title tag and meta description, and the template pattern that fills every CMS item
Open the Pages panel, hover the page, click the Settings icon and scroll to SEO settings. Type a title tag under 60 characters and a meta description of 150 to 160 characters, then Save and Publish. Nothing in either field reaches a crawler before a publish.
There is no site-wide fallback. Webflow states that "You can't set SEO settings globally, and SEO settings apply only to the page on which you've set them." A WordPress plugin templates a description for posts you never touched; Webflow leaves the field empty, and an empty meta description fails C3. On a Collection page, set the pattern once: in Page settings > SEO settings, use Add field inside the Title tag and Meta description boxes and bind them to the item name and a summary field. Every item, including next year's, inherits it.
JSON-LD belongs in Schema markup, not in custom code
Go to Pages panel > hover the page > Settings icon > Schema markup, paste your JSON-LD or click Generate schema markup, then Save and publish. Check it with the Schema Markup Validator, which Webflow's article Add schema markup in Webflow to improve SEO and AEO names. Per Webflow's FAQ, markup in this field lands in the <head>, while markup pasted into the Before </body> tag box lands at the end of the document. Both parse, but the head is where validators, scanners and most agents look first. Existing custom-code schema keeps working, so nothing forces a migration, but keep each page to one route to avoid duplicate structured data.
On a Collection page the field takes dynamic bindings, with one limit: Plain text, Number, Date/Time, Option, Switch, Image, Video link, Link, File, Color, Email and Phone fields work; Reference, Multi-reference and Multi-image fields do not. If a Product schema wants a brand stored as a Reference, store a plain-text copy instead.
The three ways an h1 goes wrong on the canvas
Select the heading in the Designer, open the element settings and set the tag to H1.
| What the scan reports | What is on the canvas | The fix |
|---|---|---|
h1: 0 | The headline is a Text Block or Div with large type: it looks like a headline and parses as a paragraph | Set the element tag to H1, restyle if needed |
h1: 2 or more | A hero heading plus a second heading inside a Symbol or component reused in the navigation or footer | Retag the one inside the component; every page using it is fixed at once |
| One h1, same text everywhere | On a Collection page the H1 is static template text | Bind the H1 to the item's own name field |
Verify on the published domain, never the Designer preview, because unpublished settings do not exist yet:
curl -s https://yoursite.com/your-page \
| grep -o -E '<h1|<title>|name="description"|application/ld\+json' | sort | uniq -c
A pass prints one <h1, one <title>, one name="description" and at least one application/ld+json. Our platform detector has a matching blind spot: on the scan corpus it classifies zero sites as Webflow, although every Webflow page ships <meta content="Webflow" name="generator"/>. That is logged as a bug on our side and does not change your C3 result. For the rest of the Webflow rubric, see the Webflow setup guide and the llms.txt route.
Wix
Wix splits the four signals across three surfaces
Title tag and meta description sit in a page's SEO basics panel. JSON-LD is one tab over, in Advanced SEO, or at dashboard level in SEO Settings. The h1 is in no panel: it is a Heading 1 text element on the canvas, which is why it is the signal that goes missing. Nothing in the editor stops you deleting the one Heading 1 a template shipped with, or styling a plain text element to look like it.
| Signal | Where it lives on Wix | Check by hand |
|---|---|---|
| Title tag | Page's SEO basics panel | curl -s <url> | grep -o '<title>[^<]*' |
| Meta description | Same panel, next field down | curl -s <url> | grep -o 'name="description"[^>]*' |
| JSON-LD | Advanced SEO tab, or the SEO Settings dashboard | curl -s <url> | grep -c 'application/ld+json' |
| H1 | A Heading 1 text element | curl -s <url> | grep -o '<h1[^>]*>' |
Title tag and meta description in SEO basics
In the editor click Pages & Menu, More Actions beside the page, then SEO basics. Enter the headline under Title tag (title in search results) and the summary under Meta description (description in search results), then Publish. Per Wix's help article on title tags and meta descriptions (verified 25 September 2026), a title should stay under 60 characters and a description should run 50 to 160. Wix recommends Keyword | Location | Brand Name for local businesses and Keyword | Brand Name otherwise, and says AI bots rely on an accurate meta description when summarizing a page. C3 checks presence only, so the format is good practice and not part of the grade.
JSON-LD: Advanced SEO per page, SEO Settings per page type
From SEO basics click Advanced SEO, then Structured Data Markup, then +Add New Markup, paste the JSON-LD and click Apply. Wix's structured data article (verified 25 September 2026) says it accepts JSON-LD only, no Microdata or RDFa, caps each markup under 7,000 characters and allows five per page. Type names are case-sensitive: blogposting instead of BlogPosting will not validate.
| Route | Where | Scope | Best for |
|---|---|---|---|
| Advanced SEO tab | Per page, in the editor | That page only | An About page or a landing page |
| SEO Settings dashboard | Site dashboard > SEO & GEO > Tools and settings | Every page of one page type, with variables | Blog posts, Stores products, Bookings services |
Wix adds Local Business markup to the homepage once you enter a business name and location, and presets markup on Stores, Bookings, Blog and Events pages. That homepage block does not cover other pages, because C3 is scored per page. An AI-generated markup toggle in SEO Settings covers published Wix Blog posts only; every other page type needs the manual route.
The h1 is a text element, and a blank line breaks it
To add one, click Add Elements > Text > Add Heading 1 and drag it onto the page. To retag existing text without changing its look, select it, click Edit Text > SEO & accessibility and pick a tag under Choose HTML tag. Wix's heading-tags article (fetched 25 September 2026) warns that assigning Heading 1 to a text box containing a blank line break generates an empty <h1>. A check that counts tags sees it as present; a reader sees nothing where the headline should be. And because Heading 1 is a style choice, a second one dropped in by hand renders as a second <h1>.
Scan the live domain, not a wixsite.com preview:
npx aiscan-cli yoursite.com
curl -s https://yoursite.com | grep -Eo '<title>[^<]*|name="description"[^>]*|application/ld\+json|<h1[^>]*>'
Expect a title, one description tag, at least one JSON-LD script and exactly one <h1. The served HTML updates right after Publish; only Google's cached snippet lags. The file half of the job is in the Wix llms.txt route and the Wix robots.txt guide.
WordPress
On WordPress the h1 belongs to the theme, and that is where C3 fails
In our scan corpus on 8 September 2026, 72 of 129 WordPress sites passed C3. Of the 57 that did not, 50 missed on one signal alone: the heading count. Their title, meta description and schema were present. The page had no <h1> or several, and no SEO plugin fixes that, because the heading is a block attribute stored in the theme's template.
| C3 signal | WordPress sites missing it (of 129) |
|---|---|
Exactly one <h1> | 50 (39 with none, 11 with more than one) |
| Meta description | 24 |
| JSON-LD | 18 |
<title> | 2 |
The plugin-shaped half of C3 is mostly solved already. The theme-shaped half is not. Ten sites reported exactly h1: 2.
Why the heading goes missing
In a block theme the post title is rendered by a block, not typed into the page. According to the core blocks reference on developer.wordpress.org (fetched 8 September 2026), core/post-title carries a level attribute, saved in template markup and shipped by whoever built the theme. If they set it to 2, your posts have no h1 and nothing in the post editor says so. WordPress's documentation notes "Post Title block was renamed to Title block." The markup still says core/post-title; the label in the sidebar reads Title. The opposite failure has the same cause: core/site-title also has level, so a header that renders the site name as an h1 on top of a post title h1 gives two.
Set the Title block to H1 in the Site Editor
- Go to Appearance > Editor.
- Click Templates and open the template your single posts use.
- Select the Title block and set its level to H1.
- Click Save.
<!-- wp:post-title {"level":1,"isLink":true} /-->
For the second h1, open Appearance > Editor > Patterns, edit the header template part and give the Site Title block a level other than H1. A site name repeated on every page is not the subject of any page. Changing the level can change how the title looks, because themes style by level; set the level first, then adjust size in the block's typography controls.
Classic themes: the title tag is opt-in
Core prints the title tag from one function, quoted from developer.wordpress.org:
function _wp_render_title_tag() {
if ( ! current_theme_supports( 'title-tag' ) ) {
return;
}
echo '<title>' . wp_get_document_title() . '</title>' . "\n";
}
No add_theme_support( 'title-tag' ), no title tag. Add it inside an after_setup_theme callback in a child theme's functions.php. To change the text without a plugin, filter document_title_parts or document_title_separator. The Appearance > Editor menu exists only on block themes; on a classic theme the h1 is a PHP template call to the_title() inside a heading tag.
Meta description and JSON-LD come from one plugin, not three
Run exactly one SEO plugin. Two on one site is the reliable way to ship two meta descriptions and two competing graphs. ThinkRank is the one we recommend: it handles robots.txt, robots meta, schema, sitemaps and llms.txt from a single plugin and imports settings from Rank Math, Yoast, All in One SEO and SEOPress. Reading a live ThinkRank install's schema settings on 8 September 2026 showed auto_generate_schema and auto_deploy on, with Organization, Person and WebSite enabled. Yoast, Rank Math, All in One SEO and SEOPress also write the description and JSON-LD. None of the five sets your h1.
If the scan still reports h1: 0
Four causes cover almost all of it. You edited a template your posts do not render through. A caching plugin or CDN is serving yesterday's HTML, so purge and re-scan. The level is set inside a pattern that keeps re-applying it. Or the block sits in a query loop that renders archives, not single posts. A page builder bypasses the Site Editor entirely, so set the tag on the builder element that outputs the title. After a theme switch or update, re-scan: template markup ships with the theme, and a level you fixed can return to 2. Scan a real post as well as the homepage, because C3 is scored per page. Plain curl is enough on WordPress because it renders on the server, which is not true of every stack; React and Next.js sites need a different test. More WordPress steps are in the setup guide, and what Google does with snippets once the markup is right is in the AI Overviews guide. The check definitions are at /docs/platforms/wordpress.
Verify all four signals on any platform
Whatever the platform, finish with the same check. Run the scan and read the C3 row:
npx aiscan-cli yoursite.com
Then confirm by hand against the published domain, never the preview address:
URL=https://yoursite.com/some-page
curl -sL --compressed "$URL" -o /tmp/p.html
grep -c '<h1' /tmp/p.html
grep -o '<title>[^<]*</title>' /tmp/p.html
grep -c 'name="description"' /tmp/p.html
grep -c 'application/ld+json' /tmp/p.html
You want 1, one line of real text, 1, and at least 1. If AIScan still disagrees with the platform's own panel, the platform is usually reading source or a rendered DOM while AIScan reads the bytes a plain fetch receives; the section for your platform says which.
Where AIScan fits, and where it doesn't
AIScan reads the HTML your server returns and reports the four C3 signals as a pass or fail, free and with no account. It does not rewrite your files, judge whether your JSON-LD type is the best choice, or validate the graph. Pair it with a schema validator when the markup itself, not just its presence, is in question. If an app of this kind is the front end for headless WordPress, the schema, robots rules, sitemaps and llms.txt still come from the WordPress origin, which is what ThinkRank manages.
Check definitions for this dimension are at /docs/checks/content. The same signals for a CMS are in the full AI readiness setup guide and the llms.txt guide for every platform. Every platform guide lives at aiscan.site/guides.
Updated on 29 October 2026. Sections re-checked against the platforms' own documentation on the dates they state.
Frequently asked questions
Which platform leaves the most work to me for C3?
Next.js and Replit. Next.js writes the title and description from the Metadata API and leaves the JSON-LD script and the h1 to you. Replit gives you one index.html to edit and a rating that never scores JSON-LD. Ghost writes three of the four automatically, and Lovable routes all four through its SEO and AI search review.
Can one scan tell me which of the four signals is missing?
Yes. The C3 row prints h1, title, meta description and JSON-LD as separate values, for example h1: 1, title: true, meta description: true, JSON-LD: false, so you fix the one that is false rather than all four.
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.
I clicked "Try to fix" on a failing finding and the review still shows it as failing.
The badge only shows "Fixed" provisionally right after the agent finishes; the review confirms it on the next scan, not the one that just ran. Click Scan again after the fix lands, and check that the change actually reached the page you're testing, since a fix applied to one route doesn't touch a duplicate component used on another.
AIScan reports C3 failing, but Lovable's own SEO & AI search review shows everything green.
This is almost always the template split, not a wrong scan. On an older React + Vite project, Lovable pre-renders pages only for crawlers it verifies, and AIScan is an unverified client, so it can see the empty shell while Lovable's own review, which reads your code directly, sees a fully formed page. Migrating to TanStack Start removes the gap, because then every request gets the same HTML.
Do I need to migrate to TanStack Start before I can fix C3?
No. The Metadata, Structured data and Content structure checks are code-only checks, and Lovable's documentation states they run on any project, including unpublished ones. Fix them on either template. The template only decides who is allowed to see the result once it's live.
My scan shows h1: 2 after I asked Lovable to fix a missing heading.
The agent likely wrapped a second element in an h1 tag instead of promoting the large heading that was already on the page. Read the diff Try to fix produced, remove the duplicate tag, and confirm the count with a curl grep on the h1 tag before you move to the next finding.
My scan says title: false, but I set metadata in page.tsx. What went wrong?
The file is almost certainly a Client Component. Next.js only supports the metadata object and generateMetadata exports in Server Components, and it fails silently: no build warning, no runtime error, no tags in the head. Check the top of the file for 'use client'. If it is there, keep page.tsx as a Server Component, export the metadata from it, and move the interactive parts into a separate client file that page.tsx imports.
C3 reports JSON-LD: false, but I can see the ld+json script in browser devtools. Why?
Devtools shows you the DOM after JavaScript has run. The scan reads the initial HTML response. If the script tag is emitted by a Client Component, or by anything that only runs after hydration, it is not in that response and a non-rendering crawler never sees it. Run curl on the URL and grep for application/ld+json. If the count is zero there but non-zero in devtools, move the script into a Server Component.
The scan says h1: 0 but my page clearly has a big heading at the top.
A large heading is not automatically an h1. Two things cause this. Either the visible title is styled text inside a div or a p, which is common when a design system exposes a Heading component with a size prop but no semantic level, or the heading renders only on the client. Inspect the element and confirm the tag is literally h1, then confirm it survives a plain curl of the page.
Does the Next.js Metadata API have a JSON-LD field?
No. There is no jsonLd key on the metadata object and no file convention for structured data, the way there is for robots.ts, sitemap.ts and opengraph-image. The framework's own JSON-LD guide tells you to render a script tag of type application/ld+json inside your layout.js or page.js component. That is the supported route, and it is why structured data is the signal most often missing on Next.js sites.
My SEO Rating says Healthy, but AIScan still fails C3. Why?
Because the rating and the check measure different things. Replit's SEO Rating is a Lighthouse audit that scores your title tag, meta description and heading order, but Lighthouse's own documentation lists structured data as a manual check it never scores. A Healthy badge can coexist with zero JSON-LD on the page, which is exactly what AIScan's C3 check reports.
Does SEO Agent work on the free plan?
No. Replit's documentation states SEO Agent is available to builders on a paid plan; a free-plan project sees an upgrade prompt where the Run scan and Fix with Agent buttons would otherwise appear. The SEO Rating badge itself still shows on the free plan once you publish.
curl shows no h1 on my page, but it looks correct in the browser. What's going on?
Your app is almost certainly an Agent-built Autoscale deployment, which serves one static index.html shell for every path and lets React render the actual heading into the page after the browser runs the JavaScript bundle. A browser executes that script and shows the finished heading. A tool that fetches the page without running JavaScript, including curl and most AI agents, sees the empty shell instead.
Where do I edit the title and meta description by hand?
In client/index.html, inside the <head>. Vite's root is set to the client folder and ships that file once at build time, so the <title> and <meta name="description"> tags it contains are present in the raw HTML response regardless of how the rest of the page renders.
AIScan reports h1: 2 on my Webflow home page. Where is the second one?
Almost always inside a Symbol or component that is reused across the site, such as a navigation block, a call-to-action panel or a footer. Open the Navigator in the Designer, look for a second element tagged H1, and retag it as H2 or a Text Block. Because the change is made inside the component, every page that uses it is corrected at the same time.
The Schema markup field is missing or I cannot generate schema. What is wrong?
Webflow's own FAQ lists the reasons: the site is not on a paid Site plan and not part of a paid Workspace, your role is Reviewer, Webflow AI is switched off in Workspace Settings > General > Workspace, the page does not have enough content to generate from, or the field already contains code and needs clearing first. Schema pasted manually still requires the paid plan.
I already added schema through custom code. Do I have to move it?
No. Webflow's article says schema added using custom code continues to work as expected and there is no need to move existing structured data into the Schema markup field. What it does advise against is putting schema in both places on the same page, because that produces duplicate structured data. Pick one location per page.
AIScan reports h1: 0 on my Wix page, but I can see a headline. What's wrong?
Two likely causes. First, the text is styled to look like a big headline but was never assigned the Heading 1 tag; select it, click Edit Text, then SEO & accessibility, and check the Choose HTML tag value. Second, the Heading 1 text box contains a blank line break, which Wix renders as an empty heading element that a naive check counts as present but a real reader sees as nothing.
Can a Wix page have two H1 elements?
Yes, and nothing in the editor stops it. Heading 1 is a style choice you apply to any text element, so dragging a second Heading 1 onto a page that already has one produces two top-level heading elements in the rendered page, which splits the one heading signal a page should carry.
My JSON-LD markup won't save in the Advanced SEO tab. What's failing?
Wix enforces a hard 7,000-character cap per markup and accepts JSON-LD only, not Microdata or RDFa. A common silent failure is a wrong type string, like blogposting instead of BlogPosting; JSON-LD type names are case-sensitive.
My scan says h1: 0 but I can see the post title on the page.
You are seeing a heading, just not an h1. In a block theme the Title block stores its heading level as a level attribute, and a theme that ships it as 2 renders a visually identical title inside an <h2>. Open Appearance > Editor > Templates, select the Title block on your single-post template, and set the level to H1.
AIScan reports h1: 2 on every page of my site. Where is the second one?
Almost always the header. The Site Title block carries the same level attribute as the Title block, so a header template part rendering the site name as an h1 adds one to every page on the site. Open Appearance > Editor > Patterns, edit the header template part, and give the Site Title block a level other than H1.
I installed an SEO plugin and C3 is still partial. Why?
Because SEO plugins write the title tag, the meta description and the JSON-LD, and none of them writes your h1. Across 129 WordPress sites in our corpus on 8 September 2026, 50 of the 57 that miss C3 miss it on the heading count alone. Run the scan again and read which of the four fields is still false.

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
AI readiness setup guide by platform (2026)
Verified 17, 18, 20 and 22 October 2026. Updated on 22 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,…
llms.txt on Astro, Docusaurus, Framer, Ghost, Hugo, Next.js, Replit, Shopify, Squarespace, Webflow and Wix
Updated on 26 October 2026. Every platform section was checked on the dates shown inside it. Squarespace, Webflow and Wix joined this guide on 26 October 2026, so all eleven platforms that had their…
Fix soft 404s on Astro, Docusaurus, Lovable, Next.js, Webflow and Wix
A soft 404 is a page that says "not found" while the status line says 200. On the six platforms below, the default behaviour ranges from a correct 404 with a deadend body Webflow, Wix, Docusaurus…
