Compare commits
3 Commits
2273dfa0f5
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b028fdbda | |||
| 4a4b759a37 | |||
| d4bb8deb5e |
+2
-2
@@ -4,9 +4,9 @@
|
||||
"FROM node:20",
|
||||
"WORKDIR /app",
|
||||
"COPY hdyc-svelte/package*.json ./",
|
||||
"RUN npm install",
|
||||
"RUN npm ci",
|
||||
"COPY hdyc-svelte/ .",
|
||||
"RUN npm run build",
|
||||
"CMD [\"npm\", \"run\", \"start\"]"
|
||||
"CMD [\"node\", \"build\"]"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ const MIME_TYPES: Record<string, string> = {
|
||||
'.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 ASSET_404_CACHE_CONTROL = 'no-store';
|
||||
const LONG_CACHE_EXTENSIONS = new Set([
|
||||
@@ -71,6 +72,8 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
// bundle hashes after each deployment.
|
||||
if (contentType.includes('text/html')) {
|
||||
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;
|
||||
|
||||
@@ -39,14 +39,10 @@
|
||||
{#if supportsExample && result}
|
||||
<section class="example-card">
|
||||
<h3>How to convert {config.labels.in1} to {config.labels.in2}</h3>
|
||||
{#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}
|
||||
1 {config.labels.in1} = {formattedFactorValue}{hasOffset ? ` + ${formattedOffsetValue}` : ''} {config.labels.in2}
|
||||
</p>
|
||||
{#if !hasOffset}
|
||||
<p class="example-note">
|
||||
1 {config.labels.in2} = {formattedReverseValue} {config.labels.in1}
|
||||
</p>
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { getDefinition } from '$lib/data/unitDefinitions';
|
||||
import type { CalculatorDef } from '$lib/data/calculatorLoader';
|
||||
import { getDefinitionSync } from '$lib/data/unitDefinitions';
|
||||
import type { CalculatorDef } from '$lib/data/calculators';
|
||||
|
||||
export let config: CalculatorDef;
|
||||
|
||||
let label1 = 'Unit 1';
|
||||
let label2 = 'Unit 2';
|
||||
let def1: string | undefined;
|
||||
let def2: string | undefined;
|
||||
let def1 = '';
|
||||
let def2 = '';
|
||||
|
||||
$: label1 = config.labels.in1 || 'Unit 1';
|
||||
$: label2 = config.labels.in2 || 'Unit 2';
|
||||
$: {
|
||||
getDefinition(label1, config.category).then(d => { def1 = d; });
|
||||
getDefinition(label2, config.category).then(d => { def2 = d; });
|
||||
}
|
||||
$: def1 = getDefinitionSync(label1, config.category) ?? '';
|
||||
$: def2 = getDefinitionSync(label2, config.category) ?? '';
|
||||
</script>
|
||||
|
||||
<section class="definition-card">
|
||||
@@ -22,11 +20,11 @@
|
||||
<div class="definition-grid">
|
||||
<article>
|
||||
<strong>{label1}</strong>
|
||||
<p>{def1 ?? `Definition pending for ${label1}.`}</p>
|
||||
<p>{def1}</p>
|
||||
</article>
|
||||
<article>
|
||||
<strong>{label2}</strong>
|
||||
<p>{def2 ?? `Definition pending for ${label2}.`}</p>
|
||||
<p>{def2}</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -100,6 +100,9 @@ const normalizeLabel = (label?: string): string | undefined => {
|
||||
const categoryPriority = [...Object.keys(domainDefinitions)];
|
||||
|
||||
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.'],
|
||||
[/\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.'],
|
||||
@@ -177,3 +180,9 @@ export async function getDefinition(label: string, category?: string): Promise<s
|
||||
const entries = defs[normalized];
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ export const calculatorSeo = (calc: CalculatorDef) => {
|
||||
title,
|
||||
description,
|
||||
canonicalUrl: canonical,
|
||||
canonicalSlug,
|
||||
noindex,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
// Prerender the homepage as static HTML at build time.
|
||||
// adapter-node will serve this as a static file — no SSR round-trip.
|
||||
export const prerender = true;
|
||||
export const prerender = false;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
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 }) => {
|
||||
const calc = getCalculatorBySlug(params.slug);
|
||||
@@ -13,13 +13,17 @@ export const load: PageServerLoad = ({ params, setHeaders }) => {
|
||||
}
|
||||
|
||||
const effectiveCategory = getEffectiveCategory(calc);
|
||||
const seo = calculatorSeo(calc);
|
||||
|
||||
if (seo.canonicalSlug !== calc.slug) {
|
||||
throw redirect(301, `/${seo.canonicalSlug}`);
|
||||
}
|
||||
|
||||
const related = getCalculatorsByCategory(effectiveCategory)
|
||||
.filter(c => c.slug !== calc.slug)
|
||||
.slice(0, 8);
|
||||
|
||||
const categoryMeta = categories[effectiveCategory];
|
||||
const seo = calculatorSeo(calc);
|
||||
if (seo.noindex) {
|
||||
setHeaders({
|
||||
'X-Robots-Tag': 'noindex, follow'
|
||||
@@ -30,7 +34,7 @@ export const load: PageServerLoad = ({ params, setHeaders }) => {
|
||||
calculator: calc,
|
||||
related,
|
||||
seo,
|
||||
jsonLd: JSON.stringify(calculatorJsonLd(calc, seo.canonicalUrl)),
|
||||
jsonLd: toJsonLd(calculatorJsonLd(calc, seo.canonicalUrl)),
|
||||
category: effectiveCategory,
|
||||
categoryLabel: categoryMeta?.label ?? calc.category,
|
||||
categoryIcon: categoryMeta?.icon ?? '🔢'
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
<meta name="twitter:card" content={seo.twitter.card} />
|
||||
<meta name="twitter:title" content={seo.twitter.title} />
|
||||
<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">${webPageJsonLd}</script>`}
|
||||
</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, {
|
||||
headers: {
|
||||
'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'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# allow crawling everything by default
|
||||
User-agent: *
|
||||
Disallow:
|
||||
Sitemap: https://howdoyouconvert.com/sitemap.xml
|
||||
Reference in New Issue
Block a user