Fixing Incorrect URLs in AI-Generated Schema
A block of generated JSON-LD can carry a dozen web addresses: the page itself, its images, the logo, the author page, the buy link and a list of social profiles. Each one is a small claim that search engines follow and check.
Generated markup gets them wrong in predictable ways, because the model copies what it saw in the HTML, including relative paths, preview hosts and tracking tags, or fills a missing address with a sensible-looking guess. This guide lists every place a URL hides in a block, the patterns that go wrong, a quick way to check them all, and how to stop the same mistake repeating.
Where URLs hide in a generated block
It is easy to check url and forget the rest. These are the properties that hold addresses
in the types generators most often produce, and what each one should point at.
| Property | Should point at | Must it load? |
|---|---|---|
@id | A stable identifier built from the canonical address, often with a fragment
such as #product | No; it names a thing rather than linking to it, but it should start with your real domain |
url | The canonical address of the page or entity | Yes, with a 200 status and no redirect |
mainEntityOfPage | The page that is mainly about this entity | Yes |
image, thumbnailUrl | The full-size image files, not a thumbnail or placeholder | Yes, and crawlable by Googlebot |
logo | Your actual logo file | Yes |
sameAs | Your own profiles on other sites | Yes, and they must really be yours |
author.url | The author's page on your site or a profile they own | Yes |
offers.url | The page where this item can be bought or booked | Yes |
item in a breadcrumb | Each level of the trail | Yes |
contentUrl, embedUrl | The media file or the embeddable player | Yes |
The schema.org addresses used as values, such as https://schema.org/InStock, are fixed
vocabulary terms. They are correct as written and do not need checking.
Why generated addresses go wrong
A language model does not browse your site. It is given the page's HTML or text and writes JSON-LD from it. Almost every URL fault traces back to something in that input, or to a gap the model filled.
- It was run on the wrong copy of the page. Generating from a staging host, a local preview, a page builder's preview link or a password-protected draft gives you markup full of addresses from that host.
- The address you gave it carried extras. A URL pasted from an email campaign or an
ad keeps
?utm_source=,?fbclid=or?gclid=, and the model faithfully copies it intourland@id. - The HTML used relative paths. Most sites write
src="/images/boiler.jpg". The model copies the path as it found it, so the markup says"image": "/images/boiler.jpg". - Lazy loading hid the real image. Many themes put a placeholder in
srcand the real file indata-srcorsrcset. The model may pick the placeholder, a tiny inlinedata:image or a 300-pixel thumbnail. - It filled a gap with a pattern. Asked for an Organization with a logo and social
profiles, a model can produce
https://example.com/logo.pngorhttps://www.facebook.com/brightwellheatingbecause those shapes are common, not because they exist. - The content is old. Posts written before a move to https, or before a domain change, still link with the old address, and the model repeats it.
Wrong and right, pattern by pattern
| Generated | Corrected | Why |
|---|---|---|
"/services/boiler-repair" | "https://www.example.co.uk/services/boiler-repair" | Some parsers resolve a relative path against the page and some do not, and the block breaks if it is ever copied elsewhere. |
"//cdn.example.co.uk/img/van.jpg" | "https://cdn.example.co.uk/img/van.jpg" | Protocol-relative addresses are a leftover from mixed http and https sites; state the scheme. |
"http://example.co.uk/about" | "https://www.example.co.uk/about" | Match
the scheme and host of your canonical, including or excluding www exactly as the site does. |
"https://staging.example.co.uk/..." | The live host | Staging hosts are usually blocked or password-protected, so every address fails. |
"...boiler-repair?utm_source=newsletter" | "...boiler-repair" | Tracking parameters create a second address for the same page. |
"...boiler-repair/" when the canonical has no slash | Match the canonical | A trailing-slash mismatch is a redirect or a duplicate. |
"...wp-content/uploads/van-300x200.jpg" | The full-size file | Thumbnails are often too small for image features. |
"data:image/gif;base64,..." | A real https image address | Inline placeholder images are not files anyone can fetch. |
"https://example.co.uk/Boiler Repair.jpg" | "https://example.co.uk/Boiler%20Repair.jpg" | Spaces must be encoded; better still, rename the file with hyphens. |
Addresses written with escaped slashes, such as https:\/\/www.example.co.uk\/about, are
not an error. PHP's json_encode() writes them that way by default and every JSON parser
reads them as ordinary slashes.
A corrected block
This is a generated Organization for a fictional heating firm after every address was corrected. The
generator had written a relative logo path, a staging url and two guessed social profiles,
one of which belonged to a different company.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"@id": "https://www.example.co.uk/#organization",
"name": "Brightwell Heating",
"url": "https://www.example.co.uk/",
"logo": {
"@type": "ImageObject",
"url": "https://www.example.co.uk/assets/brightwell-logo-512.png",
"width": 512,
"height": 512
},
"image": "https://www.example.co.uk/assets/brightwell-van-1200x800.jpg",
"sameAs": [
"https://www.linkedin.com/company/example-brightwell-heating",
"https://www.instagram.com/example_brightwell"
],
"contactPoint": {
"@type": "ContactPoint",
"contactType": "customer service",
"telephone": "+44 1632 960123",
"url": "https://www.example.co.uk/contact"
}
}
</script>
Note what was left out: the Facebook address the model suggested did not exist, so it went rather
than being "fixed" to a guess of our own. A short sameAs list of real profiles is worth
more than a long list with a dead entry.
Checking every address at once
Clicking through a dozen addresses by hand is slow and it is easy to miss the one in a nested object. This short Python script reads a generated block saved as a file, finds every string that looks like an address, and reports anything that is not absolute https, fails to load or redirects. It uses only the standard library.
import json, sys, urllib.request
def find_urls(node, path='$'):
if isinstance(node, dict):
for key, value in node.items():
yield from find_urls(value, path + '.' + key)
elif isinstance(node, list):
for i, value in enumerate(node):
yield from find_urls(value, '%s[%d]' % (path, i))
elif isinstance(node, str) and node.startswith(('http', '/', 'data:')):
yield path, node
block = json.load(open(sys.argv[1], encoding='utf-8'))
for path, url in find_urls(block):
if url.startswith('https://schema.org/') or path.endswith('@context'):
continue
if not url.startswith('https://'):
print('NOT ABSOLUTE HTTPS ', path, url)
continue
if path.endswith('@id'):
continue
req = urllib.request.Request(url, headers={'User-Agent': 'schema-url-check'})
try:
with urllib.request.urlopen(req, timeout=15) as r:
moved = '' if r.geturl() == url else ' redirects to ' + r.geturl()
print(r.status, path, url + moved)
except Exception as err:
print('FAILED', path, url, err)
Save the JSON without the script tags as block.json and run python3 check_urls.py block.json.
Anything marked FAILED, NOT ABSOLUTE HTTPS or "redirects to" needs attention. A redirect usually means
the canonical has a different host, scheme or trailing slash from the address in the markup.
The fix, step by step
Step 1
List every URL in the generated block
Go through the block property by property, including nested objects such as offers,
author, logo and breadcrumb entries, and write down every string that is a
web address. Include @id values and each entry in sameAs. Running the checking
script above does this for you and prints the path to each address, which makes it easy to find the right
line again when you come to edit the block.
Step 2
Make each address absolute and https
Rewrite every relative path such as /images/van.jpg as a full address beginning
with https:// and your live domain. Replace protocol-relative addresses that start with
// and any plain http:// addresses in the same way. If the block contains a
staging, preview or localhost host, replace the host throughout with a single search and replace, then
check the paths still exist on the live site.
Step 3
Swap in the canonical address
Open
the page source and copy the address from the rel="canonical" link tag. Use exactly that
for url and mainEntityOfPage, and as the base for any @id values,
matching its www, trailing slash and letter case. Strip tracking parameters such as utm_source,
gclid and fbclid. Keep query strings only where the canonical itself has them,
which is rare outside filtered listing pages.
Step 4
Check that every address resolves
Request
each address and confirm a 200 status with no redirect. Swap placeholders, inline data:
images and small thumbnails for the full-size image file, which you can find in the data-src
or srcset attribute in developer tools. Replace any address that redirects with its final
destination, and remove any that returns 404 unless you can find the page or file at its new location.
Step 5
Replace guessed logo and profile links
Treat every sameAs, logo and author.url as unconfirmed
until you have opened it and seen that it belongs to you. Profiles generated from the business name often
point at a stranger or at nothing, and a logo path such as /logo.png may be a guess. Keep
the ones you have verified, add real ones you know about, and delete the rest rather than guessing again.
Step 6
Fix the source so it stays fixed
Correcting
one block does not stop the next one repeating the fault. Regenerate from the live canonical address,
not a preview. Fix relative image paths in templates where you can, and make sure lazy-loaded images
keep a real file in srcset. Where markup is built by a template, take the site address from
one setting rather than typing it, then test the live URL in the Rich Results Test to confirm the corrected
addresses render.
Platform notes
WordPress
The site address comes from Settings, General, and old posts may still hold http:// links
from before the site moved to https. Image uploads have size variants with names like -300x200.jpg;
the generator can pick one of these from the post body, so check that image points at the
original file. Staging copies made by hosting tools use a different host, so never generate from the
staging site and copy the markup across.
Shopify
Liquid's image_url filter returns a protocol-relative address that starts with //cdn.shopify.com.
If you template generated markup in Liquid, add | prepend: 'https:'. The storefront can
also be reached on its .myshopify.com address; make sure markup uses your primary domain,
which the canonical_url object gives you.
Custom and static sites, frameworks
Build the site origin from a single environment variable or config value, and join paths to it when the markup is rendered, so a staging build writes staging addresses and a production build writes live ones. Check the production build's output, not the dev server, because many frameworks rewrite asset paths with hashes at build time.
Keeping addresses right
- One origin, one place. Take the scheme and host from a single setting everywhere markup is produced.
- Generate from the canonical. Copy the address from the canonical tag, not the browser bar, before you run the generator.
- Keep @id values stable. Once an
@idis published, leave it alone, even if the page title changes. Other blocks may refer to it. - Recheck after moves. A domain change, an https switch or a new URL structure makes every stored address in pasted markup out of date. Search your stored blocks for the old host.
- Verify profiles once, then reuse them. Keep a short list of confirmed
sameAsaddresses and paste from that list rather than accepting generated ones.
1. Are relative URLs allowed in JSON-LD?
The JSON-LD format can resolve a relative address against the page it sits on, so a relative path is not strictly invalid. In practice, not every tool that reads structured data resolves it the same way, and a block copied to another page or served from another host changes meaning without warning. Writing every address in full, with https and your canonical host, removes that doubt and costs nothing. It is the safest choice for generated markup.
2. Does the @id have to be a working web page?
No. An @id is an identifier that lets different blocks refer to the same thing, such as your organisation or a product. It does not have to load, and it often ends with a fragment like #organization that no server sees. It should still start with your real, canonical domain, so it is clearly yours, and it must never change once published, because other markup on your site may point at it and would lose the connection.
3. Why does my schema image work in the browser but fail in Google?
Your browser and Googlebot are treated differently. The image may sit on a host whose robots.txt blocks crawlers, behind hotlink protection that refuses requests without your site as referrer, or on a CDN address with a signed token that expires. The generated block may also point at a lazy-load placeholder that your browser swaps for the real file. Use URL Inspection in Search Console to see which resources Google could load.
4. The generator added social profiles I never gave it. Should I keep them?
Only after you have opened each one and confirmed it belongs to your business. A model asked for a complete Organization will often produce profile addresses built from your business name because that pattern is common in the examples it learned from. Some point at nothing, others at a different company with a similar name. Keep verified profiles, delete the rest, and never replace a bad guess with another guess of your own.
5. Should schema URLs have a trailing slash?
They should match your canonical address exactly. If the canonical tag on the page ends with a slash, use the slash in url, mainEntityOfPage and @id. If it does not, leave it off. A mismatch means the markup names an address that redirects, or a duplicate of the page, which weakens the link between the markup and the page Google indexes. The same rule applies to www, https and letter case.
6. Is it a problem that my URLs have backslashes before the slashes?
No. Addresses such as https:\/\/example.co.uk\/about are standard JSON escaping, which PHP writes by default and some other encoders produce too. Every JSON parser, including Google's, reads them as ordinary slashes, so the markup is fine. If you want the block to be easier for people to read, PHP's JSON_UNESCAPED_SLASHES option leaves the slashes plain, but it makes no difference to search engines. Spend the time on the addresses themselves instead: whether they are absolute, canonical and actually load.
7. Do I need to update schema after moving my site to a new domain?
Yes, if any of it is stored as pasted text. Markup produced by a plugin or template from a site setting updates itself when the setting changes, but generated blocks stored in custom fields, page content or theme files keep the old host. After a move, search your database and theme files for the old domain, update every stored block, and test a sample of pages in the Rich Results Test.
The AI Schema Generator reads your page content with an AI model, writes JSON-LD tailored to the content type and validates it against schema.org and Google rich result criteria before delivery, ready for you to review and publish.
Open AI Schema Generator Fixes โ