Enhance calculators and SEO features, improve category handling, and update metadata for better indexing

This commit is contained in:
Ben
2026-05-14 17:24:25 -07:00
parent 63bafc2feb
commit cf2b3d2e5d
14 changed files with 242 additions and 55 deletions
+4 -3
View File
@@ -1,4 +1,3 @@
import path from 'node:path';
import type { Handle } from '@sveltejs/kit';
const MIME_TYPES: Record<string, string> = {
@@ -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);
}
@@ -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;
}
@@ -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}
<section class="example-card">
<h3>How to convert {config.labels.in1} to {config.labels.in2}</h3>
<p class="example-note">
1 {config.labels.in1} = {formattedFactorValue}{hasOffset ? ` + ${formattedOffsetValue}` : ''} {config.labels.in2}
</p>
<p class="example-note">
1 {config.labels.in2} = {formattedReverseValue} {config.labels.in1}
</p>
{#if hasOffset}
<p class="example-note">
Formula: {config.labels.in2} = ({config.labels.in1} x {formattedFactorValue}) {offset > 0 ? '+' : '-'} {formatConversionValue(Math.abs(offset))}
</p>
{:else}
<p class="example-note">
1 {config.labels.in1} = {formattedFactorValue} {config.labels.in2}
</p>
<p class="example-note">
1 {config.labels.in2} = {formattedReverseValue} {config.labels.in1}
</p>
{/if}
<p class="example-line">
Example: convert {exampleInput} {config.labels.in1} to {config.labels.in2}
</p>
@@ -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);
}
};
});
+2 -2
View File
@@ -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;
+72 -5
View File
@@ -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<string, CalculatorDef> = new Map(
);
const normalizedSlugCounts = calculators.reduce((counts, calc) => {
counts.set(calc.slug, (counts.get(calc.slug) ?? 0) + 1);
return counts;
}, new Map<string, number>());
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<string>();
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))
);
}
@@ -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}`;
+57 -3
View File
@@ -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(/</g, '\\u003c');
export const escapeXml = (value: string): string =>
value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
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),
});
+18 -6
View File
@@ -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;
+16 -5
View File
@@ -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 ?? '🔢'
};
+13 -12
View File
@@ -1,20 +1,20 @@
<script lang="ts">
import Calculator from '$lib/components/Calculator.svelte';
import type { PageData } from './$types';
import { buildSeoMeta, canonicalUrl, SITE_NAME, SITE_URL, toJsonLd } from '$lib/seo';
import { buildSeoMeta, canonicalUrl, calculatorDescription, SITE_NAME, SITE_URL, toJsonLd } from '$lib/seo';
export let data: PageData;
$: calc = data.calculator;
$: related = data.related;
$: pageTitle = `${calc.name}${SITE_NAME}`;
$: pageDescription = ['3col', '3col-mul'].includes(calc.type)
? `Compute ${calc.labels.in3 ?? 'the derived value'} using ${calc.labels.in1} and ${calc.labels.in2}. Enter any two fields to solve the third.`
: `Convert ${calc.labels.in1} to ${calc.labels.in2} instantly with our free online calculator. Accurate two-way conversion with the exact formula shown.`;
$: pageTitle = data.seo?.title ?? `${calc.name}${SITE_NAME}`;
$: pageDescription = data.seo?.description ?? calculatorDescription(calc);
$: seo = buildSeoMeta({
title: pageTitle,
description: pageDescription,
pathname: `/${calc.slug}`,
canonical: data.seo?.canonicalUrl,
robots: data.seo?.robots,
});
$: breadcrumbJsonLd = toJsonLd({
'@context': 'https://schema.org',
@@ -30,7 +30,7 @@
'@type': 'ListItem',
position: 2,
name: data.categoryLabel,
item: canonicalUrl(`/category/${calc.category}`),
item: canonicalUrl(`/category/${data.category}`),
},
{
'@type': 'ListItem',
@@ -75,7 +75,7 @@
<nav class="breadcrumbs" aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li><a href="/category/{calc.category}">{data.categoryIcon} {data.categoryLabel}</a></li>
<li><a href="/category/{data.category}">{data.categoryIcon} {data.categoryLabel}</a></li>
<li aria-current="page">{calc.name}</li>
</ol>
</nav>
@@ -91,18 +91,19 @@
{@html calc.descriptionHTML}
{:else}
<h3>How to convert {calc.labels.in1} to {calc.labels.in2}</h3>
{#if calc.type === 'standard' && calc.factor}
{#if calc.type === 'standard' && (calc.factor || calc.offset)}
<p>
The conversion between {calc.labels.in1.toLowerCase()} and {calc.labels.in2.toLowerCase()}
uses a fixed multiplication factor. One {calc.labels.in1.toLowerCase().replace(/s$/, '')} equals
{calc.factor}{calc.offset ? ` plus an offset of ${calc.offset}` : ''} {calc.labels.in2.toLowerCase()}.
uses {calc.offset ? 'a linear formula with an offset' : 'a fixed multiplication factor'}.
{#if calc.offset}
This offset is common in temperature conversions, where scales differ not just in magnitude but also in their zero point.
{:else}
One {calc.labels.in1.toLowerCase()} equals {calc.factor} {calc.labels.in2.toLowerCase()}.
{/if}
</p>
<p>
To convert, multiply the value in {calc.labels.in1.toLowerCase()} by {calc.factor}{calc.offset ? `, then add ${calc.offset}` : ''}.
To convert in the opposite direction, {calc.offset ? `subtract ${calc.offset}, then ` : ''}divide by {calc.factor}.
To convert, multiply the value in {calc.labels.in1.toLowerCase()} by {calc.factor ?? 1}{calc.offset ? `, then ${calc.offset > 0 ? 'add' : 'subtract'} ${Math.abs(calc.offset)}` : ''}.
To convert in the opposite direction, {calc.offset ? `${calc.offset > 0 ? 'subtract' : 'add'} ${Math.abs(calc.offset)}, then ` : ''}divide by {calc.factor ?? 1}.
</p>
{:else if calc.type === '3col' || calc.type === '3col-mul'}
<p>
@@ -2,7 +2,7 @@ import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { getCalculatorsByCategory, categories } from '$lib/data/calculators';
export const load: PageServerLoad = ({ params }) => {
export const load: PageServerLoad = ({ params, setHeaders }) => {
const cat = params.category;
const meta = categories[cat];
@@ -11,6 +11,11 @@ export const load: PageServerLoad = ({ params }) => {
}
const calcs = getCalculatorsByCategory(cat);
if (calcs.length === 0) {
setHeaders({
'X-Robots-Tag': 'noindex, follow'
});
}
return {
category: cat,
@@ -15,7 +15,7 @@
card.style.setProperty('--calc-tooltip-translate', 'calc(-100% - 0.55rem)');
};
const resetCalcTooltipPosition = (event: MouseEvent) => {
const resetCalcTooltipPosition = (event: MouseEvent | FocusEvent) => {
const card = event.currentTarget as HTMLElement | null;
if (!card) return;
card.style.removeProperty('--calc-tooltip-left');
@@ -33,6 +33,7 @@
title: pageTitle,
description: pageDescription,
pathname: categoryPath,
robots: data.calculators.length > 0 ? 'index,follow' : 'noindex,follow',
});
$: breadcrumbJsonLd = toJsonLd({
'@context': 'https://schema.org',
@@ -1,18 +1,19 @@
import type { RequestHandler } from './$types';
import { calculators, categories } from '$lib/data/calculators';
import { getCategoriesWithCounts, getIndexableCalculators } from '$lib/data/calculators';
import { canonicalUrl, escapeXml } from '$lib/seo';
export const GET: RequestHandler = async () => {
const calculatorUrls = calculators.map(
const calculatorUrls = getIndexableCalculators().map(
(calc) => ` <url>
<loc>https://howdoyouconvert.com/${calc.slug}</loc>
<loc>${escapeXml(canonicalUrl(`/${calc.slug}`))}</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>`
);
const categoryUrls = Object.keys(categories).map(
const categoryUrls = getCategoriesWithCounts().filter(category => category.count > 0).map(
(category) => ` <url>
<loc>https://howdoyouconvert.com/category/${category}</loc>
<loc>${escapeXml(canonicalUrl(`/category/${category.key}`))}</loc>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>`
@@ -21,7 +22,7 @@ export const GET: RequestHandler = async () => {
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://howdoyouconvert.com/</loc>
<loc>${escapeXml(canonicalUrl('/'))}</loc>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>