1const FORECAST_PATH_PATTERN = /^\/forecast\/([^/]+?)(?:\.html)?\/?$/i; 2const FORECAST_CITIES_PATH = '/data/forecast-cities.json'; 3 4export function getForecastSlugFromPathname(pathname) { 5 if (typeof pathname !== 'string') return null; 6 7 const match = pathname.match(FORECAST_PATH_PATTERN); 8 if (!match) return null; 9 10 let slug = match[1]; 11 try { 12 slug = decodeURIComponent(slug); 13 } catch (err) { 14 // A malformed escape (e.g. a lone %) makes decodeURIComponent throw. 15 // This runs before the caller's try block, so an unhandled throw here 16 // would break page init for anyone arriving on a broken link. 17 console.warn(`Malformed forecast slug in path: ${pathname}`); 18 return null; 19 } 20 21 return slug.trim().toLowerCase(); 22} 23 24function setMetaAttribute(selector, attribute, value) { 25 const el = document.querySelector(selector); 26 if (el) { 27 el.setAttribute(attribute, value); 28 } 29} 30 31export async function resolveForecastCityTarget() { 32 const slug = getForecastSlugFromPathname(window.location.pathname); 33 if (!slug) return null; 34 35 try { 36 const res = await fetch(FORECAST_CITIES_PATH, { cache: 'no-store' }); 37 if (!res.ok) { 38 console.warn(`Could not load forecast cities from ${FORECAST_CITIES_PATH}`); 39 return null; 40 } 41 42 const cities = await res.json(); 43 const city = cities[slug]; 44 if (!city) return null; 45 46 return { 47 slug, 48 name: city.name, 49 lat: Number(city.lat), 50 lon: Number(city.lon) 51 }; 52 } catch (err) { 53 console.warn('Failed to resolve forecast city target from URL path.', err); 54 return null; 55 } 56} 57 58export function applyForecastCitySeo(cityTarget) { 59 if (!cityTarget) return; 60 61 const { slug, name } = cityTarget; 62 const title = `Aurora Forecast for ${name} - Real-Time Visibility`; 63 const description = `Current aurora borealis forecast for ${name}. Real-time Kp index and visibility probability map for ${name}.`; 64 const pageUrl = `https://auroramap.org/forecast/${slug}/`; 65 66 document.title = title; 67 setMetaAttribute('meta[name="description"]', 'content', description); 68 setMetaAttribute('meta[property="og:url"]', 'content', pageUrl); 69 setMetaAttribute('meta[property="og:title"]', 'content', title); 70 setMetaAttribute('meta[property="og:description"]', 'content', description); 71 setMetaAttribute('meta[property="twitter:url"]', 'content', pageUrl); 72 setMetaAttribute('meta[property="twitter:title"]', 'content', title); 73 setMetaAttribute('meta[property="twitter:description"]', 'content', description); 74 setMetaAttribute('link[rel="canonical"]', 'href', pageUrl); 75 76 const header = document.querySelector('header h1'); 77 if (header) { 78 header.textContent = `Aurora Forecast: ${name}`; 79 } 80 81 const { lat, lon } = cityTarget; 82 const existingSchema = document.getElementById('ld-place'); 83 if (existingSchema) existingSchema.remove(); 84 const script = document.createElement('script'); 85 script.type = 'application/ld+json'; 86 script.id = 'ld-place'; 87 script.textContent = JSON.stringify({ 88 '@context': 'https://schema.org', 89 '@type': 'Place', 90 name, 91 geo: { '@type': 'GeoCoordinates', latitude: lat, longitude: lon }, 92 url: pageUrl 93 }); 94 document.head.appendChild(script); 95}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.