Modern HTML5 in Production · SEO, Sharing and Security
Metadata, Canonical URLs and Structured Data
Why the same product appeared in search results under five URLs and lost its price snippet, and how canonical links, metadata and valid JSON-LD fix it.
Scenario
Example app: product pages are reachable at many URLs: with tracking parameters (?utm_source=...), sort parameters, upper-case paths and both http and https. Structured data was added by hand, with the price written as "$12.00".
What you see in production
- Search results show several URLs for the same product, splitting ranking signals between them.
- The rich result with price and availability disappeared; the structured-data report lists "invalid price" errors.
- Shared links on social apps show the site logo and a generic title instead of the product.
The naive code
<head>
<title>Shop</title>
<script type="application/ld+json">
{ "@context": "https://schema.org", "@type": "Product", "name": "Ceramic Mug",
"offers": { "@type": "Offer", "price": "$12.00" } }
</script>
</head>
Reproduce it
The simulation normalises incoming URLs to one canonical form, then checks the product's structured data for problems.
const urls = [
"https://shop.example.com/products/ceramic-mug",
"https://shop.example.com/products/ceramic-mug?utm_source=newsletter",
"http://shop.example.com/products/ceramic-mug",
"https://shop.example.com/Products/Ceramic-Mug?sort=price",
"https://shop.example.com/products/ceramic-mug/",
];
function canonical(u) {
const url = new URL(u);
url.protocol = "https:";
url.pathname = url.pathname.toLowerCase().replace(/\/+$/, "");
url.search = ""; // tracking and sort parameters don't change the content
return url.toString();
}
console.log("distinct URLs:", urls.length, "-> canonical:", [...new Set(urls.map(canonical))]);
function checkProduct(data) {
const problems = [];
if (!data.name) problems.push("name missing");
const offer = data.offers || {};
if (!/^\d+(\.\d+)?$/.test(String(offer.price ?? ""))) problems.push(`price "${offer.price}" must be a number without currency symbols`);
if (!offer.priceCurrency) problems.push("priceCurrency missing");
if (!offer.availability) problems.push("availability missing (recommended)");
return problems;
}
const naive = { "@type": "Product", name: "Ceramic Mug", offers: { "@type": "Offer", price: "$12.00" } };
const fixed = { "@type": "Product", name: "Ceramic Mug",
offers: { "@type": "Offer", price: "12.00", priceCurrency: "USD", availability: "https://schema.org/InStock" } };
console.log("naive JSON-LD:", checkProduct(naive));
console.log("fixed JSON-LD:", checkProduct(fixed).length ? checkProduct(fixed) : "valid");
Output
distinct URLs: 5 -> canonical: [ 'https://shop.example.com/products/ceramic-mug' ] naive JSON-LD: [ 'price "$12.00" must be a number without currency symbols', 'priceCurrency missing', 'availability missing (recommended)' ] fixed JSON-LD: valid
Root cause
- Without a canonical URL, search engines see each variant as a separate page with duplicate content and must guess which one to show.
- Structured data must follow the vocabulary exactly:
priceis a number and the currency goes inpriceCurrency. - Missing title, description and Open Graph tags leave search engines and social apps to guess the preview.
The fix
- Add
<link rel="canonical" href="https://shop.example.com/products/ceramic-mug">on every variant, and redirect obvious duplicates (http → https, trailing slashes, case) with 301s. See consolidating duplicate URLs. - Write a unique
<title>and<meta name="description">per page, plus Open Graph tags (og:title,og:description,og:image,og:url) for link previews. - Generate JSON-LD from your data (never by hand), following schema.org/Product, and test it with the Rich Results Test.
- Keep structured data consistent with the visible page.
Trade-offs
- Canonical links are hints, not commands; consistent internal links and redirects make them effective.
- Rich results are never guaranteed even with valid data.
Detect it in production
- Search console reports for duplicate pages without canonicals and structured-data errors.
- An automated check in CI that renders key templates and validates their metadata and JSON-LD.
Key points
- One canonical URL per piece of content; redirect obvious duplicates.
- Unique titles, descriptions and Open Graph tags for every page.
- Generate valid JSON-LD from data and test it.
Practice questions
What does a canonical link do?
Answer
<link rel="canonical" href="..."> tells search engines which URL is the preferred version of duplicate or near-duplicate pages, consolidating ranking signals there.
What are Open Graph tags for?
Answer
Meta tags such as og:title, og:description, og:image and og:url that control how a page appears when shared in social apps and messengers.
What are common structured-data mistakes?
Answer
Prices with currency symbols or commas, missing required properties (priceCurrency), data that doesn't match the visible page, hand-written JSON that drifts from real data, and invalid JSON syntax.
Open in the app to mark lessons complete, take notes and track your progress.