Compare commits

...

3 Commits

12 changed files with 57 additions and 33 deletions
+2 -2
View File
@@ -4,9 +4,9 @@
"FROM node:20", "FROM node:20",
"WORKDIR /app", "WORKDIR /app",
"COPY hdyc-svelte/package*.json ./", "COPY hdyc-svelte/package*.json ./",
"RUN npm install", "RUN npm ci",
"COPY hdyc-svelte/ .", "COPY hdyc-svelte/ .",
"RUN npm run build", "RUN npm run build",
"CMD [\"npm\", \"run\", \"start\"]" "CMD [\"node\", \"build\"]"
] ]
} }
+4 -1
View File
@@ -12,7 +12,8 @@ const MIME_TYPES: Record<string, string> = {
'.otf': 'font/otf' '.otf': 'font/otf'
}; };
const HTML_CACHE_CONTROL = 'public, max-age=0, s-maxage=3600, stale-while-revalidate=86400'; const HTML_CACHE_CONTROL = 'no-cache, max-age=0, must-revalidate, no-transform';
const EDGE_HTML_CACHE_CONTROL = 'max-age=300, stale-while-revalidate=86400';
const IMMUTABLE_ASSET_CACHE_CONTROL = 'public, max-age=31536000, immutable'; const IMMUTABLE_ASSET_CACHE_CONTROL = 'public, max-age=31536000, immutable';
const ASSET_404_CACHE_CONTROL = 'no-store'; const ASSET_404_CACHE_CONTROL = 'no-store';
const LONG_CACHE_EXTENSIONS = new Set([ const LONG_CACHE_EXTENSIONS = new Set([
@@ -71,6 +72,8 @@ export const handle: Handle = async ({ event, resolve }) => {
// bundle hashes after each deployment. // bundle hashes after each deployment.
if (contentType.includes('text/html')) { if (contentType.includes('text/html')) {
response.headers.set('cache-control', HTML_CACHE_CONTROL); response.headers.set('cache-control', HTML_CACHE_CONTROL);
response.headers.set('cdn-cache-control', EDGE_HTML_CACHE_CONTROL);
response.headers.set('cloudflare-cdn-cache-control', EDGE_HTML_CACHE_CONTROL);
} }
return response; return response;
@@ -39,14 +39,10 @@
{#if supportsExample && result} {#if supportsExample && result}
<section class="example-card"> <section class="example-card">
<h3>How to convert {config.labels.in1} to {config.labels.in2}</h3> <h3>How to convert {config.labels.in1} to {config.labels.in2}</h3>
{#if hasOffset}
<p class="example-note"> <p class="example-note">
Formula: {config.labels.in2} = ({config.labels.in1} x {formattedFactorValue}) {offset > 0 ? '+' : '-'} {formatConversionValue(Math.abs(offset))} 1 {config.labels.in1} = {formattedFactorValue}{hasOffset ? ` + ${formattedOffsetValue}` : ''} {config.labels.in2}
</p>
{:else}
<p class="example-note">
1 {config.labels.in1} = {formattedFactorValue} {config.labels.in2}
</p> </p>
{#if !hasOffset}
<p class="example-note"> <p class="example-note">
1 {config.labels.in2} = {formattedReverseValue} {config.labels.in1} 1 {config.labels.in2} = {formattedReverseValue} {config.labels.in1}
</p> </p>
@@ -1,20 +1,18 @@
<script lang="ts"> <script lang="ts">
import { getDefinition } from '$lib/data/unitDefinitions'; import { getDefinitionSync } from '$lib/data/unitDefinitions';
import type { CalculatorDef } from '$lib/data/calculatorLoader'; import type { CalculatorDef } from '$lib/data/calculators';
export let config: CalculatorDef; export let config: CalculatorDef;
let label1 = 'Unit 1'; let label1 = 'Unit 1';
let label2 = 'Unit 2'; let label2 = 'Unit 2';
let def1: string | undefined; let def1 = '';
let def2: string | undefined; let def2 = '';
$: label1 = config.labels.in1 || 'Unit 1'; $: label1 = config.labels.in1 || 'Unit 1';
$: label2 = config.labels.in2 || 'Unit 2'; $: label2 = config.labels.in2 || 'Unit 2';
$: { $: def1 = getDefinitionSync(label1, config.category) ?? '';
getDefinition(label1, config.category).then(d => { def1 = d; }); $: def2 = getDefinitionSync(label2, config.category) ?? '';
getDefinition(label2, config.category).then(d => { def2 = d; });
}
</script> </script>
<section class="definition-card"> <section class="definition-card">
@@ -22,11 +20,11 @@
<div class="definition-grid"> <div class="definition-grid">
<article> <article>
<strong>{label1}</strong> <strong>{label1}</strong>
<p>{def1 ?? `Definition pending for ${label1}.`}</p> <p>{def1}</p>
</article> </article>
<article> <article>
<strong>{label2}</strong> <strong>{label2}</strong>
<p>{def2 ?? `Definition pending for ${label2}.`}</p> <p>{def2}</p>
</article> </article>
</div> </div>
</section> </section>
@@ -100,6 +100,9 @@ const normalizeLabel = (label?: string): string | undefined => {
const categoryPriority = [...Object.keys(domainDefinitions)]; const categoryPriority = [...Object.keys(domainDefinitions)];
const specificDefinitions: Array<[RegExp, string]> = [ const specificDefinitions: Array<[RegExp, string]> = [
[/\btons? of tnt\b/i, 'measures explosive energy release. One ton of TNT is conventionally defined as 4.184 gigajoules of energy.'],
[/\batomic time units?\b/i, 'measures time on an atomic scale. One atomic unit of time is about 2.418884326505e-17 seconds.'],
[/\bastronomical units?\b/i, 'measures large distances in astronomy. One astronomical unit is the average Earth-Sun distance, exactly 149,597,870,700 meters.'],
[/\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.'], [/\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.'], [/\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.'], [/\bvolts?\b/i, 'measures electric potential difference. Voltage describes the electrical pressure that pushes current through a circuit.'],
@@ -177,3 +180,9 @@ export async function getDefinition(label: string, category?: string): Promise<s
const entries = defs[normalized]; const entries = defs[normalized];
return findByPriority(entries, category); return findByPriority(entries, category);
} }
export function getDefinitionSync(label: string, category?: string): string | undefined {
const normalized = normalizeLabel(label);
if (!normalized) return undefined;
return buildDefinition(normalized, category || 'other');
}
+1
View File
@@ -84,6 +84,7 @@ export const calculatorSeo = (calc: CalculatorDef) => {
title, title,
description, description,
canonicalUrl: canonical, canonicalUrl: canonical,
canonicalSlug,
noindex, noindex,
}; };
}; };
+1 -3
View File
@@ -1,3 +1 @@
// Prerender the homepage as static HTML at build time. export const prerender = false;
// adapter-node will serve this as a static file — no SSR round-trip.
export const prerender = true;
@@ -1,7 +1,7 @@
import { error } from '@sveltejs/kit'; import { error, redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { getCalculatorBySlug, getCalculatorsByCategory, categories, getEffectiveCategory } from '$lib/data/calculators'; import { getCalculatorBySlug, getCalculatorsByCategory, categories, getEffectiveCategory } from '$lib/data/calculators';
import { calculatorJsonLd, calculatorSeo } from '$lib/seo'; import { calculatorJsonLd, calculatorSeo, toJsonLd } from '$lib/seo';
export const load: PageServerLoad = ({ params, setHeaders }) => { export const load: PageServerLoad = ({ params, setHeaders }) => {
const calc = getCalculatorBySlug(params.slug); const calc = getCalculatorBySlug(params.slug);
@@ -13,13 +13,17 @@ export const load: PageServerLoad = ({ params, setHeaders }) => {
} }
const effectiveCategory = getEffectiveCategory(calc); const effectiveCategory = getEffectiveCategory(calc);
const seo = calculatorSeo(calc);
if (seo.canonicalSlug !== calc.slug) {
throw redirect(301, `/${seo.canonicalSlug}`);
}
const related = getCalculatorsByCategory(effectiveCategory) const related = getCalculatorsByCategory(effectiveCategory)
.filter(c => c.slug !== calc.slug) .filter(c => c.slug !== calc.slug)
.slice(0, 8); .slice(0, 8);
const categoryMeta = categories[effectiveCategory]; const categoryMeta = categories[effectiveCategory];
const seo = calculatorSeo(calc);
if (seo.noindex) { if (seo.noindex) {
setHeaders({ setHeaders({
'X-Robots-Tag': 'noindex, follow' 'X-Robots-Tag': 'noindex, follow'
@@ -30,7 +34,7 @@ export const load: PageServerLoad = ({ params, setHeaders }) => {
calculator: calc, calculator: calc,
related, related,
seo, seo,
jsonLd: JSON.stringify(calculatorJsonLd(calc, seo.canonicalUrl)), jsonLd: toJsonLd(calculatorJsonLd(calc, seo.canonicalUrl)),
category: effectiveCategory, category: effectiveCategory,
categoryLabel: categoryMeta?.label ?? calc.category, categoryLabel: categoryMeta?.label ?? calc.category,
categoryIcon: categoryMeta?.icon ?? '🔢' categoryIcon: categoryMeta?.icon ?? '🔢'
@@ -68,6 +68,7 @@
<meta name="twitter:card" content={seo.twitter.card} /> <meta name="twitter:card" content={seo.twitter.card} />
<meta name="twitter:title" content={seo.twitter.title} /> <meta name="twitter:title" content={seo.twitter.title} />
<meta name="twitter:description" content={seo.twitter.description} /> <meta name="twitter:description" content={seo.twitter.description} />
{@html `<script type="application/ld+json">${data.jsonLd}</script>`}
{@html `<script type="application/ld+json">${breadcrumbJsonLd}</script>`} {@html `<script type="application/ld+json">${breadcrumbJsonLd}</script>`}
{@html `<script type="application/ld+json">${webPageJsonLd}</script>`} {@html `<script type="application/ld+json">${webPageJsonLd}</script>`}
</svelte:head> </svelte:head>
@@ -0,0 +1,16 @@
import type { RequestHandler } from './$types';
const robots = `User-agent: *
Disallow:
Sitemap: https://howdoyouconvert.com/sitemap.xml
`;
export const GET: RequestHandler = () =>
new Response(robots, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'no-cache, max-age=0, must-revalidate, no-transform',
'CDN-Cache-Control': 'max-age=300, stale-while-revalidate=86400',
'Cloudflare-CDN-Cache-Control': 'max-age=300, stale-while-revalidate=86400'
}
});
@@ -33,7 +33,9 @@ ${calculatorUrls.join('\n')}
return new Response(sitemap, { return new Response(sitemap, {
headers: { headers: {
'Content-Type': 'application/xml', 'Content-Type': 'application/xml',
'Cache-Control': 'max-age=0, s-maxage=3600' 'Cache-Control': 'no-cache, max-age=0, must-revalidate, no-transform',
'CDN-Cache-Control': 'max-age=300, stale-while-revalidate=86400',
'Cloudflare-CDN-Cache-Control': 'max-age=300, stale-while-revalidate=86400'
} }
}); });
}; };
-4
View File
@@ -1,4 +0,0 @@
# allow crawling everything by default
User-agent: *
Disallow:
Sitemap: https://howdoyouconvert.com/sitemap.xml