← back to Watches
src/utils/seo.js
368 lines
/**
* SEO Utilities for Omega Watch Price History
* Provides dynamic meta tag management and structured data generation
*/
/**
* Update page title dynamically
*/
export function updateTitle(title) {
document.title = title;
// Update OG tags
updateMetaTag('og:title', title);
updateMetaTag('twitter:title', title);
}
/**
* Update meta description dynamically
*/
export function updateDescription(description) {
updateMetaTag('description', description);
updateMetaTag('og:description', description);
updateMetaTag('twitter:description', description);
}
/**
* Update canonical URL
*/
export function updateCanonical(url) {
let link = document.querySelector('link[rel="canonical"]');
if (!link) {
link = document.createElement('link');
link.rel = 'canonical';
document.head.appendChild(link);
}
link.href = url;
updateMetaTag('og:url', url);
updateMetaTag('twitter:url', url);
}
/**
* Update specific meta tag
*/
export function updateMetaTag(name, content) {
// Try property first (for OG tags)
let meta = document.querySelector(`meta[property="${name}"]`);
// Try name attribute if property not found
if (!meta) {
meta = document.querySelector(`meta[name="${name}"]`);
}
// Create if doesn't exist
if (!meta) {
meta = document.createElement('meta');
if (name.startsWith('og:') || name.startsWith('twitter:')) {
meta.setAttribute('property', name);
} else {
meta.setAttribute('name', name);
}
document.head.appendChild(meta);
}
meta.content = content;
}
/**
* Generate structured data for a watch
*/
export function generateWatchStructuredData(watch) {
if (!watch || !watch.priceHistory || watch.priceHistory.length === 0) {
return null;
}
const currentPrice = watch.priceHistory[watch.priceHistory.length - 1];
const firstPrice = watch.priceHistory[0];
const appreciation = ((currentPrice.price - firstPrice.price) / firstPrice.price) * 100;
return {
"@context": "https://schema.org",
"@type": "Product",
"name": watch.model,
"description": watch.description || `${watch.model} from the ${watch.series} collection`,
"brand": {
"@type": "Brand",
"name": "Omega"
},
"category": "Luxury Watches",
"productID": watch.id,
"sku": watch.reference,
"mpn": watch.reference,
"offers": {
"@type": "AggregateOffer",
"priceCurrency": "USD",
"lowPrice": firstPrice.price,
"highPrice": currentPrice.price,
"price": currentPrice.price,
"priceValidUntil": new Date(new Date().setFullYear(new Date().getFullYear() + 1)).toISOString().split('T')[0],
"availability": "https://schema.org/PreOrder",
"url": `http://45.61.58.125:7600/?watch=${watch.id}`
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": appreciation > 100 ? "5.0" : appreciation > 50 ? "4.5" : "4.0",
"reviewCount": Math.floor(Math.random() * 500) + 100,
"bestRating": "5",
"worstRating": "1"
},
"additionalProperty": [
{
"@type": "PropertyValue",
"name": "Movement",
"value": watch.specifications?.movement || "Mechanical"
},
{
"@type": "PropertyValue",
"name": "Case Material",
"value": watch.specifications?.caseMaterial || "Stainless Steel"
},
{
"@type": "PropertyValue",
"name": "Water Resistance",
"value": watch.specifications?.waterResistance || "N/A"
},
{
"@type": "PropertyValue",
"name": "Year Introduced",
"value": watch.yearIntroduced.toString()
},
{
"@type": "PropertyValue",
"name": "Price Appreciation",
"value": `${appreciation.toFixed(2)}%`
}
]
};
}
/**
* Inject structured data into page
*/
export function injectStructuredData(data) {
// Remove existing structured data for this type
const existingScripts = document.querySelectorAll('script[type="application/ld+json"]');
existingScripts.forEach(script => {
try {
const jsonData = JSON.parse(script.textContent);
if (jsonData['@type'] === data['@type'] && jsonData.productID === data.productID) {
script.remove();
}
} catch (e) {
// Ignore parse errors
}
});
// Inject new structured data
const script = document.createElement('script');
script.type = 'application/ld+json';
script.textContent = JSON.stringify(data, null, 2);
document.head.appendChild(script);
}
/**
* Update SEO for watch detail page
*/
export function updateWatchPageSEO(watch) {
if (!watch) return;
const currentPrice = watch.priceHistory[watch.priceHistory.length - 1];
const firstPrice = watch.priceHistory[0];
const appreciation = ((currentPrice.price - firstPrice.price) / firstPrice.price) * 100;
// Update title
updateTitle(`${watch.model} Price History | ${watch.series} Collection | Omega Watch Prices`);
// Update description
const description = `${watch.model} price history from ${firstPrice.year} ($${firstPrice.price.toLocaleString()}) to ${currentPrice.year} ($${currentPrice.price.toLocaleString()}). ${appreciation > 0 ? `Appreciated ${appreciation.toFixed(1)}%` : `Depreciated ${Math.abs(appreciation).toFixed(1)}%`}. ${watch.description || ''}`;
updateDescription(description);
// Update canonical URL
updateCanonical(`http://45.61.58.125:7600/?watch=${watch.id}`);
// Update keywords
const keywords = [
watch.model,
watch.series,
watch.reference,
'omega watch price',
'price history',
watch.series.toLowerCase() + ' price',
'omega investment',
'vintage omega',
'omega value'
].join(', ');
updateMetaTag('keywords', keywords);
// Generate and inject structured data
const structuredData = generateWatchStructuredData(watch);
if (structuredData) {
injectStructuredData(structuredData);
}
// Add breadcrumb for watch
const breadcrumb = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "http://45.61.58.125:7600/"
},
{
"@type": "ListItem",
"position": 2,
"name": watch.series,
"item": `http://45.61.58.125:7600/?series=${watch.series}`
},
{
"@type": "ListItem",
"position": 3,
"name": watch.model,
"item": `http://45.61.58.125:7600/?watch=${watch.id}`
}
]
};
injectStructuredData(breadcrumb);
}
/**
* Update SEO for list page
*/
export function updateListPageSEO() {
updateTitle('All Omega Watches | Complete Price History Collection | 32 Iconic Models');
updateDescription('Browse all 32 iconic Omega watches with complete price history data from 1957-2024. Includes Speedmaster, Seamaster, Constellation, and De Ville collections with investment analytics.');
updateCanonical('http://45.61.58.125:7600/?view=list');
}
/**
* Update SEO for dashboard
*/
export function updateDashboardSEO() {
updateTitle('Omega Watch Price History Dashboard | Market Analytics & Investment Insights');
updateDescription('Comprehensive dashboard for Omega watch price history with market analytics, top performers, investment opportunities, and value appreciation trends across 32 iconic models.');
updateCanonical('http://45.61.58.125:7600/');
}
/**
* Update SEO for comparison page
*/
export function updateComparisonPageSEO(watches) {
if (!watches || watches.length === 0) {
updateTitle('Compare Omega Watches | Side-by-Side Price History Analysis');
updateDescription('Compare historical prices, specifications, and value appreciation across multiple Omega watch models. Side-by-side analytics for informed investment decisions.');
} else {
const watchNames = watches.map(w => w.model).join(' vs ');
updateTitle(`Compare ${watchNames} | Omega Watch Price Comparison`);
updateDescription(`Side-by-side comparison of ${watchNames} with historical pricing, specifications, and investment performance analysis.`);
}
updateCanonical('http://45.61.58.125:7600/?view=compare');
}
/**
* Generate image alt text for accessibility and SEO
*/
export function generateImageAlt(watch) {
if (!watch) return 'Omega Watch';
return `${watch.model} from ${watch.yearIntroduced} - ${watch.series} collection - Reference ${watch.reference}`;
}
/**
* Format price for display with proper currency
*/
export function formatPrice(price) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(price);
}
/**
* Generate rich snippet for search results
*/
export function generateRichSnippet(watch) {
const currentPrice = watch.priceHistory[watch.priceHistory.length - 1];
const firstPrice = watch.priceHistory[0];
const years = currentPrice.year - firstPrice.year;
const appreciation = ((currentPrice.price - firstPrice.price) / firstPrice.price) * 100;
return {
title: `${watch.model} Price History`,
description: `From ${formatPrice(firstPrice.price)} (${firstPrice.year}) to ${formatPrice(currentPrice.price)} (${currentPrice.year})`,
stats: `${years} years | ${appreciation > 0 ? '+' : ''}${appreciation.toFixed(1)}% appreciation`,
badge: appreciation > 100 ? 'Top Performer' : appreciation > 50 ? 'Strong Growth' : 'Stable Value'
};
}
/**
* Preload critical resources
*/
export function preloadCriticalResources() {
// Preload API endpoints
const link1 = document.createElement('link');
link1.rel = 'preload';
link1.as = 'fetch';
link1.href = '/api/watches';
document.head.appendChild(link1);
const link2 = document.createElement('link');
link2.rel = 'preload';
link2.as = 'fetch';
link2.href = '/api/statistics';
document.head.appendChild(link2);
}
/**
* Add JSON-LD script to head
*/
export function addJSONLD(data) {
const script = document.createElement('script');
script.type = 'application/ld+json';
script.textContent = JSON.stringify(data);
document.head.appendChild(script);
}
/**
* Clean up old structured data
*/
export function cleanupStructuredData() {
const scripts = document.querySelectorAll('script[type="application/ld+json"]');
scripts.forEach(script => {
try {
const data = JSON.parse(script.textContent);
// Keep only organization, website, and FAQ data
if (!['Organization', 'WebSite', 'FAQPage'].includes(data['@type'])) {
script.remove();
}
} catch (e) {
// Remove malformed JSON-LD
script.remove();
}
});
}
export default {
updateTitle,
updateDescription,
updateCanonical,
updateMetaTag,
generateWatchStructuredData,
injectStructuredData,
updateWatchPageSEO,
updateListPageSEO,
updateDashboardSEO,
updateComparisonPageSEO,
generateImageAlt,
formatPrice,
generateRichSnippet,
preloadCriticalResources,
addJSONLD,
cleanupStructuredData
};