diff --git a/hdyc-svelte/src/hooks.server.ts b/hdyc-svelte/src/hooks.server.ts index 101ff4d..02656a2 100644 --- a/hdyc-svelte/src/hooks.server.ts +++ b/hdyc-svelte/src/hooks.server.ts @@ -1,4 +1,3 @@ -import path from 'node:path'; import type { Handle } from '@sveltejs/kit'; const MIME_TYPES: Record = { @@ -34,6 +33,8 @@ const LONG_CACHE_EXTENSIONS = new Set([ '.otf' ]); +const getExtension = (pathname: string): string => pathname.match(/\.[^./]+$/)?.[0].toLowerCase() ?? ''; + export const handle: Handle = async ({ event, resolve }) => { const response = await resolve(event); const pathname = event.url.pathname; @@ -43,7 +44,7 @@ export const handle: Handle = async ({ event, resolve }) => { const existing = response.headers.get('content-type'); const hasValidHeader = existing && existing.trim().length > 0; if (!hasValidHeader) { - const extension = path.extname(pathname).toLowerCase(); + const extension = getExtension(pathname); const mime = extension && MIME_TYPES[extension]; if (mime) { response.headers.set('content-type', mime); @@ -61,7 +62,7 @@ export const handle: Handle = async ({ event, resolve }) => { return response; } - const extension = path.extname(pathname).toLowerCase(); + const extension = getExtension(pathname); if (LONG_CACHE_EXTENSIONS.has(extension) && !contentType.includes('text/html')) { response.headers.set('cache-control', IMMUTABLE_ASSET_CACHE_CONTROL); } diff --git a/hdyc-svelte/src/lib/components/Calculator.svelte b/hdyc-svelte/src/lib/components/Calculator.svelte index b18fcf8..a804e2b 100644 --- a/hdyc-svelte/src/lib/components/Calculator.svelte +++ b/hdyc-svelte/src/lib/components/Calculator.svelte @@ -68,7 +68,7 @@ } const manualField = swapState.originalField; - const manualValue = swapState.originalValue; + const manualValue = swapState.originalValue === null ? '' : String(swapState.originalValue); if (manualField === 1) val1 = manualValue; else val2 = manualValue; swapState = null; @@ -441,6 +441,7 @@ margin: 0; } .input-group input[type='number'] { + appearance: textfield; -moz-appearance: textfield; } diff --git a/hdyc-svelte/src/lib/components/QuickConversionExample.svelte b/hdyc-svelte/src/lib/components/QuickConversionExample.svelte index 2831c77..1407027 100644 --- a/hdyc-svelte/src/lib/components/QuickConversionExample.svelte +++ b/hdyc-svelte/src/lib/components/QuickConversionExample.svelte @@ -26,11 +26,11 @@ ? formatConversionValue(offset) : ''; $: formulaExpression = supportsExample - ? `${exampleInput} × ${formattedFactorValue}${hasOffset ? ` + ${formattedOffsetValue}` : ''}` + ? `${exampleInput} x ${formattedFactorValue}${hasOffset ? ` ${offset > 0 ? '+' : '-'} ${formatConversionValue(Math.abs(offset))}` : ''}` : ''; $: reverseExampleValue = - supportsExample && config.factor !== 0 + supportsExample && typeof config.factor === 'number' && config.factor !== 0 ? (1 - offset) / config.factor : null; $: formattedReverseValue = formatConversionValue(reverseExampleValue); @@ -39,12 +39,18 @@ {#if supportsExample && result}

How to convert {config.labels.in1} to {config.labels.in2}

-

- 1 {config.labels.in1} = {formattedFactorValue}{hasOffset ? ` + ${formattedOffsetValue}` : ''} {config.labels.in2} -

-

- 1 {config.labels.in2} = {formattedReverseValue} {config.labels.in1} -

+ {#if hasOffset} +

+ Formula: {config.labels.in2} = ({config.labels.in1} x {formattedFactorValue}) {offset > 0 ? '+' : '-'} {formatConversionValue(Math.abs(offset))} +

+ {:else} +

+ 1 {config.labels.in1} = {formattedFactorValue} {config.labels.in2} +

+

+ 1 {config.labels.in2} = {formattedReverseValue} {config.labels.in1} +

+ {/if}

Example: convert {exampleInput} {config.labels.in1} to {config.labels.in2}

diff --git a/hdyc-svelte/src/lib/components/Sidebar.svelte b/hdyc-svelte/src/lib/components/Sidebar.svelte index 839f855..cc20fa9 100644 --- a/hdyc-svelte/src/lib/components/Sidebar.svelte +++ b/hdyc-svelte/src/lib/components/Sidebar.svelte @@ -148,7 +148,9 @@ if ('addEventListener' in navBreakpoint) { navBreakpoint.addEventListener('change', handleNavChange); } else { - navBreakpoint.addListener(handleNavChange); + (navBreakpoint as MediaQueryList & { + addListener: (listener: (event: MediaQueryListEvent) => void) => void; + }).addListener(handleNavChange); } return () => { @@ -156,7 +158,9 @@ if ('removeEventListener' in navBreakpoint) { navBreakpoint.removeEventListener('change', handleNavChange); } else { - navBreakpoint.removeListener(handleNavChange); + (navBreakpoint as MediaQueryList & { + removeListener: (listener: (event: MediaQueryListEvent) => void) => void; + }).removeListener(handleNavChange); } }; }); diff --git a/hdyc-svelte/src/lib/data/calculatorLoader.ts b/hdyc-svelte/src/lib/data/calculatorLoader.ts index be7c997..852b281 100644 --- a/hdyc-svelte/src/lib/data/calculatorLoader.ts +++ b/hdyc-svelte/src/lib/data/calculatorLoader.ts @@ -7,8 +7,8 @@ export interface CalculatorDef { name: string; category: string; type: string; - teaser: string; - labels: { in1: string; in2: string }; + teaser?: string; + labels: { in1: string; in2: string; in3?: string }; factor?: number; offset?: number; hidden?: boolean; diff --git a/hdyc-svelte/src/lib/data/calculators.ts b/hdyc-svelte/src/lib/data/calculators.ts index 0c7a5b4..05ee42b 100644 --- a/hdyc-svelte/src/lib/data/calculators.ts +++ b/hdyc-svelte/src/lib/data/calculators.ts @@ -1,5 +1,5 @@ // THIS FILE IS AUTO-GENERATED BY migrate.py -export type CalcType = 'standard' | 'inverse' | '3col' | '3col-mul' | 'base' | 'text-bin' | 'bin-text' | 'dms-dd' | 'dd-dms' | 'dec-frac' | 'db-int' | 'db-spl' | 'db-v' | 'db-w' | 'awg' | 'brinell-rockwell' | 'ev-lux' | 'aov' | 'swg' | 'rockwell-vickers' | 'sus-cst' | 'molarity'; +export type CalcType = 'standard' | 'inverse' | '3col' | '3col-mul' | 'base' | 'text-bin' | 'bin-text' | 'dms-dd' | 'dd-dms' | 'dec-frac' | 'db-int' | 'db-spl' | 'db-v' | 'db-w' | 'awg' | 'awg-swg' | 'brinell-rockwell' | 'cmil-dia' | 'cmil-swg' | 'ev-lux' | 'aov' | 'swg' | 'rockwell-vickers' | 'sus-cst' | 'molarity'; export interface CalculatorDef { slug: string; @@ -3172,28 +3172,95 @@ const slugIndex: Map = new Map( ); +const normalizedSlugCounts = calculators.reduce((counts, calc) => { + counts.set(calc.slug, (counts.get(calc.slug) ?? 0) + 1); + return counts; +}, new Map()); + +const normalizeText = (value: string): string => value.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim(); + +const labelsText = (calc: CalculatorDef): string => + normalizeText([calc.name, calc.slug, calc.labels.in1, calc.labels.in2, calc.labels.in3 ?? ''].join(' ')); + +const categoryRules: Array<[keyof typeof categories, RegExp]> = [ + ['number-systems', /\b(binary|hex|hexadecimal|octal|decimal|ascii|fraction)\b/], + ['temperature', /\b(celsius|fahrenheit|kelvin|rankine|delisle)\b/], + ['data', /\b(bit|bits|byte|bytes|nibble|nibbles|baud|kilobyte|megabyte|gigabyte|terabyte|kibibyte|mebibyte|gibibyte|tebibyte)\b/], + ['electrical', /\b(volt|volts|amp|amps|ampere|amperes|ohm|ohms|siemens|farad|farads|henry|henries|coulomb|coulombs|kva)\b/], + ['radiation', /\b(becquerel|curie|gray|rad|sievert|rem|roentgen|rutherford|disintegrations)\b/], + ['light', /\b(lumen|lumens|lux|candela|foot candles?)\b/], + ['pressure', /\b(pascal|pascals|bar|psi|atmosphere|atmospheres|mmhg|torr|inches of water|inches of mercury|millimeters of mercury|water column)\b/], + ['power', /\b(watt|watts|kilowatt|kilowatts|megawatt|horsepower|btu hour|btu per hour|calories per second)\b/], + ['energy', /\b(joule|joules|calorie|calories|kilocalorie|btu|erg|therm|electron volt|watt hour|kilowatt hour)\b/], + ['force', /\b(force|torque|newton|newtons|dyne|dynes|foot pound|inch pound|pound force|kilogram force)\b/], + ['volume', /\b(cubic|liter|liters|litre|litres|gallon|gallons|cup|cups|pint|pints|quart|quarts|fluid|milliliter|milliliters|teaspoon|tablespoon|drop|bushel|peck|acre feet|acre foot|drams?)\b/], + ['area', /\b(square|acre|acres|hectare|hectares|are|ares|barn|barns|section|sections|township|townships)\b/], + ['speed', /\b(miles per hour|kilometers per hour|meters per second|feet per second|yards per second|knot|knots|mach|rpm|rads|hertz|furlongs per fortnight)\b/], + ['time', /\b(second|seconds|minute|minutes|hour|hours|day|days|week|weeks|month|months|year|years|nanosecond|microsecond|millisecond)\b/], + ['angle', /\b(degree|degrees|radian|radians|gradian|gradians|arcminute|arcminutes|arcsecond|arcseconds|mils)\b/], + ['weight', /\b(gram|grams|kilogram|kilograms|pound|pounds|ounce|ounces|carat|carats|stone|slug|ton|tons|pennyweight|grain|grains|momme|dalton|daltons|amu|atomic mass)\b/], + ['length', /\b(meter|meters|metre|metres|inch|inches|feet|foot|yard|yards|mile|miles|cable|cables|fathom|fathoms|rod|rods|chain|chains|nautical|league|leagues|angstrom|nanometer|centimeter|millimeter|micron)\b/], +]; + +export function getEffectiveCategory(calc: CalculatorDef): string { + const text = labelsText(calc); + const match = categoryRules.find(([, pattern]) => pattern.test(text)); + return match?.[0] ?? calc.category; +} + +export function isReliableCalculator(calc: CalculatorDef): boolean { + if (calc.type !== 'standard') return true; + if (typeof calc.factor === 'number' || typeof calc.offset === 'number') return true; + return normalizeText(calc.labels.in1) === normalizeText(calc.labels.in2); +} + +export function isIndexableCalculator(calc: CalculatorDef): boolean { + return !calc.hidden && isReliableCalculator(calc) && normalizedSlugCounts.get(calc.slug) === 1; +} + +export function getCanonicalSlug(calc: CalculatorDef): string { + if (!calc.hidden) return calc.slug; + + const parts = calc.slug.match(/^(.*)-to-(.*)$/); + if (!parts) return calc.slug; + + const reverseSlug = `${parts[2]}-to-${parts[1]}`; + const reverse = slugIndex.get(reverseSlug); + return reverse && isIndexableCalculator(reverse) ? reverse.slug : calc.slug; +} + +export function getIndexableCalculators(): CalculatorDef[] { + const seen = new Set(); + return calculators.filter((calc) => { + if (!isIndexableCalculator(calc)) return false; + if (seen.has(calc.slug)) return false; + seen.add(calc.slug); + return true; + }); +} + export function getCalculatorBySlug(slug: string): CalculatorDef | undefined { return slugIndex.get(slug); } export function getCalculatorsByCategory(category: string): CalculatorDef[] { - return calculators.filter(c => c.category === category && !c.hidden); + return getIndexableCalculators().filter(c => getEffectiveCategory(c) === category); } export function getCategoriesWithCounts(): { key: string; label: string; icon: string; count: number }[] { return Object.entries(categories).map(([key, meta]) => ({ key, ...meta, - count: calculators.filter(c => c.category === key && !c.hidden).length, + count: getIndexableCalculators().filter(c => getEffectiveCategory(c) === key).length, })); } export function searchCalculators(query: string): CalculatorDef[] { const q = query.toLowerCase(); - return calculators.filter(c => + return getIndexableCalculators().filter(c => (c.name.toLowerCase().includes(q) || c.slug.includes(q) || c.labels.in1.toLowerCase().includes(q) || - c.labels.in2.toLowerCase().includes(q)) && !c.hidden + c.labels.in2.toLowerCase().includes(q)) ); } diff --git a/hdyc-svelte/src/lib/data/unitDefinitions.ts b/hdyc-svelte/src/lib/data/unitDefinitions.ts index 77a4699..ee76e6a 100644 --- a/hdyc-svelte/src/lib/data/unitDefinitions.ts +++ b/hdyc-svelte/src/lib/data/unitDefinitions.ts @@ -99,8 +99,31 @@ const normalizeLabel = (label?: string): string | undefined => { const categoryPriority = [...Object.keys(domainDefinitions)]; +const specificDefinitions: Array<[RegExp, string]> = [ + [/\bwatts?\b|\bkilowatts?\b|\bmegawatts?\b|\bhorsepower\b/i, 'measures the rate at which energy is transferred or converted. It is used for engines, appliances, electrical loads, and heat-transfer rates.'], + [/\bcalories?\b|\bjoules?\b|\bbtu\b|\bkilocalories?\b|\btherms?\b|\bwatt hours?\b|\bkilowatt hours?\b/i, 'measures energy, heat, or work. These units are used for food energy, physics calculations, heating systems, and stored electricity.'], + [/\bvolts?\b/i, 'measures electric potential difference. Voltage describes the electrical pressure that pushes current through a circuit.'], + [/\bamps?\b|\bamperes?\b/i, 'measures electric current. Current describes how much electric charge flows through a circuit each second.'], + [/\bohms?\b/i, 'measures electrical resistance. Resistance describes how strongly a component opposes current flow.'], + [/\bfarads?\b/i, 'measures capacitance. Capacitance describes how much electric charge a component can store for a given voltage.'], + [/\bhenr(y|ies)\b/i, 'measures inductance. Inductance describes how strongly a circuit resists changes in current.'], + [/\bpascal|bar|psi|atmosphere|mmhg|torr|inches of water|inches of mercury/i, 'measures pressure, or force applied over an area. Pressure units are common in fluids, gases, weather, vacuum systems, and mechanical specifications.'], + [/\bcubic\b|\bliters?\b|\bgallons?\b|\bcups?\b|\bpints?\b|\bquarts?\b|\bfluid\b|\bbushels?\b|\bpecks?\b|\bacre[- ]feet\b|\bacre[- ]foot\b/i, 'measures volume or capacity. Volume units describe liquids, gases, containers, flow totals, and three-dimensional space.'], + [/\bsquare\b|\bacres?\b|\bhectares?\b|\bares?\b|\bbarns?\b|\bsections?\b|\btownships?\b/i, 'measures area or surface coverage. Area units are used for land, floors, materials, and two-dimensional spaces.'], + [/\bmeters per second\b|\bmiles per hour\b|\bkilometers per hour\b|\bfeet per second\b|\bknots?\b|\bmach\b|\brpm\b/i, 'measures speed, velocity, or rotation rate. These units describe motion over distance or repeated turns over time.'], + [/\bseconds?\b|\bminutes?\b|\bhours?\b|\bdays?\b|\bweeks?\b|\bmonths?\b|\byears?\b/i, 'measures time or duration. Time units describe intervals, schedules, rates, and elapsed periods.'], + [/\bdegrees?\b|\bradians?\b|\bgradians?\b|\barcminutes?\b|\barcseconds?\b/i, 'measures angle or rotation. Angle units describe turns, bearings, geometry, and circular motion.'], + [/\bgrams?\b|\bkilograms?\b|\bpounds?\b|\bounces?\b|\bcarats?\b|\btons?\b|\bdaltons?\b|\bamu\b/i, 'measures mass or weight. These units are used for materials, ingredients, shipping weights, laboratory quantities, and scientific mass scales.'], + [/\bbytes?\b|\bbits?\b|\bnibbles?\b|\bkilobytes?\b|\bmegabytes?\b|\bgigabytes?\b|\bterabytes?\b|\bkibibytes?\b|\bmebibytes?\b|\bgibibytes?\b/i, 'measures digital information or storage size. Data units are used for files, memory, transfer sizes, and storage devices.'], + [/\bbinary\b|\boctal\b|\bdecimal\b|\bhex\b|\bhexadecimal\b|\bascii\b/i, 'is a representation format for numbers or text. These systems define how values are encoded, displayed, or interpreted.'], + [/\bbecquerel|curie|gray|rad|sievert|rem|roentgen|rutherford/i, 'measures radioactivity, absorbed dose, or exposure. Radiation units describe decay rates, ionizing energy, and biological dose effects.'], +]; + const buildDefinition = (label: string, categoryKey: string): string => { if (!label) return ''; + const specific = specificDefinitions.find(([pattern]) => pattern.test(label)); + if (specific) return `${label} ${specific[1]}`; + const domain = domainDefinitions[categoryKey] || domainDefinitions.other; const description = [domain.summary, domain.context].filter(Boolean).join(' '); return `${label} ${description}`; diff --git a/hdyc-svelte/src/lib/seo.ts b/hdyc-svelte/src/lib/seo.ts index 1c26fe1..262e6bc 100644 --- a/hdyc-svelte/src/lib/seo.ts +++ b/hdyc-svelte/src/lib/seo.ts @@ -1,3 +1,6 @@ +import type { CalculatorDef } from '$lib/data/calculators'; +import { categories, getCanonicalSlug, getEffectiveCategory, isIndexableCalculator } from '$lib/data/calculators'; + export const SITE_URL = 'https://howdoyouconvert.com'; export const SITE_NAME = 'HowDoYouConvert.com'; export const DEFAULT_ROBOTS = 'index,follow'; @@ -10,6 +13,8 @@ type SeoInput = { description: string; pathname: string; type?: OpenGraphType; + canonical?: string; + robots?: string; }; const normalizePathname = (pathname: string): string => { @@ -23,11 +28,11 @@ const normalizePathname = (pathname: string): string => { export const canonicalUrl = (pathname: string): string => `${SITE_URL}${normalizePathname(pathname)}`; -export const buildSeoMeta = ({ title, description, pathname, type = 'website' }: SeoInput) => { - const canonical = canonicalUrl(pathname); +export const buildSeoMeta = ({ title, description, pathname, type = 'website', canonical: canonicalOverride, robots = DEFAULT_ROBOTS }: SeoInput) => { + const canonical = canonicalOverride ?? canonicalUrl(pathname); return { canonical, - robots: DEFAULT_ROBOTS, + robots, og: { type, title, @@ -44,3 +49,52 @@ export const buildSeoMeta = ({ title, description, pathname, type = 'website' }: }; export const toJsonLd = (value: unknown): string => JSON.stringify(value).replace(/ + value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + +export const calculatorDescription = (calc: CalculatorDef): string => { + const category = categories[getEffectiveCategory(calc)]?.label ?? 'unit'; + if (['3col', '3col-mul'].includes(calc.type)) { + return `Compute ${calc.labels.in3 ?? 'the derived value'} using ${calc.labels.in1} and ${calc.labels.in2}. Enter any two fields to solve the third.`; + } + return `Convert ${calc.labels.in1} to ${calc.labels.in2} with a fast ${category.toLowerCase()} calculator. Includes a bidirectional converter, formula notes, examples, and reference values.`; +}; + +export const calculatorSeo = (calc: CalculatorDef) => { + const canonicalSlug = getCanonicalSlug(calc); + const noindex = canonicalSlug !== calc.slug || !isIndexableCalculator(calc); + const title = `${calc.name} — ${SITE_NAME}`; + const description = calculatorDescription(calc); + const canonical = canonicalUrl(`/${canonicalSlug}`); + + return { + ...buildSeoMeta({ + title, + description, + pathname: `/${calc.slug}`, + canonical, + robots: noindex ? 'noindex,follow' : DEFAULT_ROBOTS, + }), + title, + description, + canonicalUrl: canonical, + noindex, + }; +}; + +export const calculatorJsonLd = (calc: CalculatorDef, url: string) => ({ + '@context': 'https://schema.org', + '@type': 'WebApplication', + name: `${calc.name} Converter`, + url, + applicationCategory: 'UtilitiesApplication', + operatingSystem: 'Any', + isAccessibleForFree: true, + description: calculatorDescription(calc), +}); diff --git a/hdyc-svelte/src/routes/+layout.svelte b/hdyc-svelte/src/routes/+layout.svelte index f2be89f..1e97ee2 100644 --- a/hdyc-svelte/src/routes/+layout.svelte +++ b/hdyc-svelte/src/routes/+layout.svelte @@ -166,17 +166,23 @@ if ('removeEventListener' in mediaQuery) { mediaQuery.removeEventListener('change', handlePreferenceChange); } else { - mediaQuery.removeListener(handlePreferenceChange); + (mediaQuery as MediaQueryList & { + removeListener: (listener: (event: MediaQueryListEvent) => void) => void; + }).removeListener(handlePreferenceChange); } if ('removeEventListener' in navBreakpoint) { navBreakpoint.removeEventListener('change', handleNavBreakpoint); } else { - navBreakpoint.removeListener(handleNavBreakpoint); + (navBreakpoint as MediaQueryList & { + removeListener: (listener: (event: MediaQueryListEvent) => void) => void; + }).removeListener(handleNavBreakpoint); } if ('removeEventListener' in headerBreakpoint) { headerBreakpoint.removeEventListener('change', handleHeaderBreakpoint); } else { - headerBreakpoint.removeListener(handleHeaderBreakpoint); + (headerBreakpoint as MediaQueryList & { + removeListener: (listener: (event: MediaQueryListEvent) => void) => void; + }).removeListener(handleHeaderBreakpoint); } if (idleCallbackId !== null && typeof appWindow.cancelIdleCallback === 'function') { appWindow.cancelIdleCallback(idleCallbackId); @@ -193,18 +199,24 @@ if ('addEventListener' in mediaQuery) { mediaQuery.addEventListener('change', handlePreferenceChange); } else { - mediaQuery.addListener(handlePreferenceChange); + (mediaQuery as MediaQueryList & { + addListener: (listener: (event: MediaQueryListEvent) => void) => void; + }).addListener(handlePreferenceChange); } if ('addEventListener' in navBreakpoint) { navBreakpoint.addEventListener('change', handleNavBreakpoint); } else { - navBreakpoint.addListener(handleNavBreakpoint); + (navBreakpoint as MediaQueryList & { + addListener: (listener: (event: MediaQueryListEvent) => void) => void; + }).addListener(handleNavBreakpoint); } if ('addEventListener' in headerBreakpoint) { headerBreakpoint.addEventListener('change', handleHeaderBreakpoint); } else { - headerBreakpoint.addListener(handleHeaderBreakpoint); + (headerBreakpoint as MediaQueryList & { + addListener: (listener: (event: MediaQueryListEvent) => void) => void; + }).addListener(handleHeaderBreakpoint); } return cleanup; diff --git a/hdyc-svelte/src/routes/[slug]/+page.server.ts b/hdyc-svelte/src/routes/[slug]/+page.server.ts index 51b04f7..db165de 100644 --- a/hdyc-svelte/src/routes/[slug]/+page.server.ts +++ b/hdyc-svelte/src/routes/[slug]/+page.server.ts @@ -1,8 +1,9 @@ import { error } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; -import { getCalculatorBySlug, getCalculatorsByCategory, categories } from '$lib/data/calculators'; +import { getCalculatorBySlug, getCalculatorsByCategory, categories, getEffectiveCategory } from '$lib/data/calculators'; +import { calculatorJsonLd, calculatorSeo } from '$lib/seo'; -export const load: PageServerLoad = ({ params }) => { +export const load: PageServerLoad = ({ params, setHeaders }) => { const calc = getCalculatorBySlug(params.slug); if (!calc) { @@ -11,16 +12,26 @@ export const load: PageServerLoad = ({ params }) => { }); } - // Get related calculators from the same category (excluding this one) - const related = getCalculatorsByCategory(calc.category) + const effectiveCategory = getEffectiveCategory(calc); + + const related = getCalculatorsByCategory(effectiveCategory) .filter(c => c.slug !== calc.slug) .slice(0, 8); - const categoryMeta = categories[calc.category]; + const categoryMeta = categories[effectiveCategory]; + const seo = calculatorSeo(calc); + if (seo.noindex) { + setHeaders({ + 'X-Robots-Tag': 'noindex, follow' + }); + } return { calculator: calc, related, + seo, + jsonLd: JSON.stringify(calculatorJsonLd(calc, seo.canonicalUrl)), + category: effectiveCategory, categoryLabel: categoryMeta?.label ?? calc.category, categoryIcon: categoryMeta?.icon ?? '🔢' }; diff --git a/hdyc-svelte/src/routes/[slug]/+page.svelte b/hdyc-svelte/src/routes/[slug]/+page.svelte index 15d837c..febf2ea 100644 --- a/hdyc-svelte/src/routes/[slug]/+page.svelte +++ b/hdyc-svelte/src/routes/[slug]/+page.svelte @@ -1,20 +1,20 @@