Hardening responsiveness and SEO
This commit is contained in:
@@ -246,6 +246,28 @@ a {
|
|||||||
a:hover {
|
a:hover {
|
||||||
color: var(--accent-dark);
|
color: var(--accent-dark);
|
||||||
}
|
}
|
||||||
|
a:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link {
|
||||||
|
position: fixed;
|
||||||
|
top: 0.6rem;
|
||||||
|
left: 0.6rem;
|
||||||
|
z-index: 500;
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
color: var(--text);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.5rem 0.8rem;
|
||||||
|
transform: translateY(-160%);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
.skip-link:focus-visible {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Layout Shell ───────────────────────────────────────── */
|
/* ─── Layout Shell ───────────────────────────────────────── */
|
||||||
|
|
||||||
@@ -264,6 +286,12 @@ a:hover {
|
|||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.site-logo {
|
.site-logo {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -281,15 +309,51 @@ a:hover {
|
|||||||
.header-right {
|
.header-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-header-search {
|
||||||
|
width: min(420px, 42vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-icon-btn {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--input-bg);
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
.header-icon-btn:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.header-icon-btn:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px var(--accent-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-header-search {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--header-bg);
|
||||||
|
}
|
||||||
|
.mobile-header-search[hidden] {
|
||||||
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-toggle {
|
.theme-toggle {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 36px;
|
width: 44px;
|
||||||
height: 36px;
|
height: 44px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: var(--input-bg);
|
background: var(--input-bg);
|
||||||
@@ -395,12 +459,25 @@ a:hover {
|
|||||||
|
|
||||||
.hamburger {
|
.hamburger {
|
||||||
display: none;
|
display: none;
|
||||||
background: none;
|
align-items: center;
|
||||||
border: none;
|
justify-content: center;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
background: var(--input-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 50%;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-size: 1.4rem;
|
font-size: 1.2rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0.25rem;
|
padding: 0;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
.hamburger:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.hamburger:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px var(--accent-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-body {
|
.site-body {
|
||||||
@@ -419,6 +496,9 @@ a:hover {
|
|||||||
padding: clamp(1.5rem, 2vw, 3rem);
|
padding: clamp(1.5rem, 2vw, 3rem);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.main-content:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.site-footer {
|
.site-footer {
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
@@ -431,22 +511,35 @@ a:hover {
|
|||||||
/* ─── Page Utilities ─────────────────────────────────────── */
|
/* ─── Page Utilities ─────────────────────────────────────── */
|
||||||
|
|
||||||
.breadcrumbs {
|
.breadcrumbs {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.4rem;
|
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
.breadcrumbs ol {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.breadcrumbs li {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.breadcrumbs li + li::before {
|
||||||
|
content: '›';
|
||||||
|
opacity: 0.4;
|
||||||
|
margin-right: 0.4rem;
|
||||||
|
}
|
||||||
.breadcrumbs a {
|
.breadcrumbs a {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
.breadcrumbs a:hover {
|
.breadcrumbs a:hover {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
.breadcrumbs .sep {
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
@@ -459,6 +552,10 @@ a:hover {
|
|||||||
background-clip: text;
|
background-clip: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calculator-page-title {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.page-subtitle {
|
.page-subtitle {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@@ -531,6 +628,11 @@ a:hover {
|
|||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
.calc-list-item:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 3px var(--accent-glow);
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Related Converters ─────────────────────────────────── */
|
/* ─── Related Converters ─────────────────────────────────── */
|
||||||
|
|
||||||
@@ -554,6 +656,11 @@ a:hover {
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
.related-chip:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 3px var(--accent-glow);
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── SEO Content ────────────────────────────────────────── */
|
/* ─── SEO Content ────────────────────────────────────────── */
|
||||||
|
|
||||||
@@ -633,7 +740,7 @@ a:hover {
|
|||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.hamburger {
|
.hamburger {
|
||||||
display: block;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
.site-body {
|
.site-body {
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
@@ -642,9 +749,31 @@ a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
.site-header {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
.site-logo {
|
||||||
|
font-size: 1rem;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
.desktop-header-search {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.header-icon-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
.mobile-header-search {
|
||||||
|
padding: 0.65rem 1rem;
|
||||||
|
}
|
||||||
.main-content {
|
.main-content {
|
||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
}
|
}
|
||||||
|
.breadcrumbs {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.page-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
.hero h1 {
|
.hero h1 {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
}
|
}
|
||||||
@@ -665,6 +794,9 @@ a:hover {
|
|||||||
.stats-row {
|
.stats-row {
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
.seo-content {
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
.site-body {
|
.site-body {
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding-inline: 1rem;
|
padding-inline: 1rem;
|
||||||
|
|||||||
@@ -13,18 +13,38 @@ const MIME_TYPES: Record<string, string> = {
|
|||||||
'.otf': 'font/otf'
|
'.otf': 'font/otf'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const HTML_CACHE_CONTROL = 'public, max-age=0, must-revalidate';
|
||||||
|
const ASSET_404_CACHE_CONTROL = 'no-store';
|
||||||
|
|
||||||
export const handle: Handle = async ({ event, resolve }) => {
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
const response = await resolve(event);
|
const response = await resolve(event);
|
||||||
if (event.url.pathname.startsWith('/_app/')) {
|
const pathname = event.url.pathname;
|
||||||
|
const contentType = response.headers.get('content-type') ?? '';
|
||||||
|
|
||||||
|
if (pathname.startsWith('/_app/')) {
|
||||||
const existing = response.headers.get('content-type');
|
const existing = response.headers.get('content-type');
|
||||||
const hasValidHeader = existing && existing.trim().length > 0;
|
const hasValidHeader = existing && existing.trim().length > 0;
|
||||||
if (!hasValidHeader) {
|
if (!hasValidHeader) {
|
||||||
const extension = path.extname(event.url.pathname).toLowerCase();
|
const extension = path.extname(pathname).toLowerCase();
|
||||||
const mime = extension && MIME_TYPES[extension];
|
const mime = extension && MIME_TYPES[extension];
|
||||||
if (mime) {
|
if (mime) {
|
||||||
response.headers.set('content-type', mime);
|
response.headers.set('content-type', mime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Missing hashed assets should never be cached; otherwise stale HTML can
|
||||||
|
// keep pointing to already-rotated files long after a deployment.
|
||||||
|
if (response.status >= 400) {
|
||||||
|
response.headers.set('cache-control', ASSET_404_CACHE_CONTROL);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML documents should revalidate so they can reference the latest client
|
||||||
|
// bundle hashes after each deployment.
|
||||||
|
if (contentType.includes('text/html')) {
|
||||||
|
response.headers.set('cache-control', HTML_CACHE_CONTROL);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
@@ -1 +1,21 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Calculator favicon">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0" stop-color="#115e59" />
|
||||||
|
<stop offset="1" stop-color="#0284c7" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="64" height="64" rx="14" fill="url(#bg)" />
|
||||||
|
<rect x="17" y="8" width="30" height="48" rx="6" fill="#f8fafc" stroke="#0f172a" stroke-width="2" />
|
||||||
|
<rect x="22" y="14" width="20" height="9" rx="2" fill="#0f172a" />
|
||||||
|
<rect x="24" y="17" width="16" height="3" rx="1.5" fill="#67e8f9" />
|
||||||
|
<g fill="#0ea5e9">
|
||||||
|
<rect x="22" y="28" width="6" height="6" rx="1.5" />
|
||||||
|
<rect x="29" y="28" width="6" height="6" rx="1.5" />
|
||||||
|
<rect x="36" y="28" width="6" height="6" rx="1.5" />
|
||||||
|
<rect x="22" y="35" width="6" height="6" rx="1.5" />
|
||||||
|
<rect x="29" y="35" width="6" height="6" rx="1.5" />
|
||||||
|
<rect x="36" y="35" width="6" height="13" rx="1.5" fill="#f97316" />
|
||||||
|
<rect x="22" y="42" width="13" height="6" rx="1.5" fill="#14b8a6" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -8,6 +8,7 @@
|
|||||||
import QuickConversionTable from '$lib/components/QuickConversionTable.svelte';
|
import QuickConversionTable from '$lib/components/QuickConversionTable.svelte';
|
||||||
|
|
||||||
export let config: CalculatorDef;
|
export let config: CalculatorDef;
|
||||||
|
export let showTitle = true;
|
||||||
|
|
||||||
let val1 = '';
|
let val1 = '';
|
||||||
let val2 = '';
|
let val2 = '';
|
||||||
@@ -70,12 +71,16 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="calculator-card">
|
<div class="calculator-card">
|
||||||
<div class="calc-header">
|
{#if showTitle || config.teaser}
|
||||||
<h2>{config.name}</h2>
|
<div class="calc-header">
|
||||||
{#if config.teaser}
|
{#if showTitle}
|
||||||
<p class="calc-subtitle">{config.teaser}</p>
|
<h2>{config.name}</h2>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
{#if config.teaser}
|
||||||
|
<p class="calc-subtitle" class:no-title={!showTitle}>{config.teaser}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="calc-body" class:three-col={has3}>
|
<div class="calc-body" class:three-col={has3}>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
@@ -181,6 +186,9 @@
|
|||||||
color: rgba(255, 255, 255, 0.85);
|
color: rgba(255, 255, 255, 0.85);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
.calc-subtitle.no-title {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.calc-body {
|
.calc-body {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -261,6 +269,10 @@
|
|||||||
background: var(--accent-dark);
|
background: var(--accent-dark);
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
.swap-btn:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px var(--accent-glow);
|
||||||
|
}
|
||||||
|
|
||||||
.calc-footer {
|
.calc-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -284,16 +296,34 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
.clear-btn:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px var(--accent-glow);
|
||||||
|
}
|
||||||
.formula-hint {
|
.formula-hint {
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.calc-footer {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
.formula-hint {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
|
.calc-header {
|
||||||
|
padding: 1.2rem 1.2rem 0.9rem;
|
||||||
|
}
|
||||||
.calc-body {
|
.calc-body {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
padding: 1.25rem;
|
||||||
}
|
}
|
||||||
.calc-body.three-col {
|
.calc-body.three-col {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -307,5 +337,8 @@
|
|||||||
.swap-btn:hover {
|
.swap-btn:hover {
|
||||||
transform: rotate(270deg);
|
transform: rotate(270deg);
|
||||||
}
|
}
|
||||||
|
.calc-footer {
|
||||||
|
padding: 0.9rem 1.25rem 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -97,4 +97,11 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-left: 0.35rem;
|
margin-left: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.example-card {
|
||||||
|
margin: 0 1.25rem 1.25rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -98,4 +98,17 @@
|
|||||||
.chart-output-unit {
|
.chart-output-unit {
|
||||||
font-variant: petite-caps;
|
font-variant: petite-caps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.quick-chart {
|
||||||
|
margin: 0.75rem 1.25rem 1.25rem;
|
||||||
|
padding: 0.9rem 1rem;
|
||||||
|
}
|
||||||
|
.chart-row {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.chart-statement {
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -68,4 +68,14 @@
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.definition-card {
|
||||||
|
margin: 0 1.25rem 1.25rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.definition-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,11 +2,27 @@
|
|||||||
import { searchCalculators } from '$lib/data/calculators';
|
import { searchCalculators } from '$lib/data/calculators';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
export let idPrefix = 'search';
|
||||||
|
|
||||||
let query = '';
|
let query = '';
|
||||||
let focused = false;
|
let focused = false;
|
||||||
let selectedIndex = -1;
|
let selectedIndex = -1;
|
||||||
|
let lastQuery = '';
|
||||||
|
|
||||||
$: results = query.length >= 2 ? searchCalculators(query).slice(0, 8) : [];
|
$: results = query.length >= 2 ? searchCalculators(query).slice(0, 8) : [];
|
||||||
|
$: listboxId = `${idPrefix}-listbox`;
|
||||||
|
$: inputId = `${idPrefix}-input`;
|
||||||
|
$: isOpen = focused && results.length > 0;
|
||||||
|
$: activeDescendant = selectedIndex >= 0 && isOpen ? `${idPrefix}-option-${selectedIndex}` : undefined;
|
||||||
|
|
||||||
|
$: if (query !== lastQuery) {
|
||||||
|
selectedIndex = -1;
|
||||||
|
lastQuery = query;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (selectedIndex >= results.length) {
|
||||||
|
selectedIndex = results.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
function handleKeydown(e: KeyboardEvent) {
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
if (e.key === 'ArrowDown') {
|
if (e.key === 'ArrowDown') {
|
||||||
@@ -33,12 +49,13 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="search-wrapper" class:active={focused && results.length > 0}>
|
<div class="search-wrapper" class:active={isOpen}>
|
||||||
<div class="search-input-wrap">
|
<div class="search-input-wrap">
|
||||||
<svg class="search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.35-4.35" />
|
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.35-4.35" />
|
||||||
</svg>
|
</svg>
|
||||||
<input
|
<input
|
||||||
|
id={inputId}
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={query}
|
bind:value={query}
|
||||||
on:focus={() => (focused = true)}
|
on:focus={() => (focused = true)}
|
||||||
@@ -46,6 +63,12 @@
|
|||||||
on:keydown={handleKeydown}
|
on:keydown={handleKeydown}
|
||||||
placeholder="Search conversions..."
|
placeholder="Search conversions..."
|
||||||
aria-label="Search conversions"
|
aria-label="Search conversions"
|
||||||
|
role="combobox"
|
||||||
|
aria-autocomplete="list"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-expanded={isOpen ? 'true' : 'false'}
|
||||||
|
aria-controls={listboxId}
|
||||||
|
aria-activedescendant={activeDescendant}
|
||||||
/>
|
/>
|
||||||
{#if query}
|
{#if query}
|
||||||
<button
|
<button
|
||||||
@@ -61,11 +84,12 @@
|
|||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if focused && results.length > 0}
|
{#if isOpen}
|
||||||
<ul class="results" role="listbox" aria-label="Conversion suggestions">
|
<ul class="results" id={listboxId} role="listbox" aria-label="Conversion suggestions">
|
||||||
{#each results as result, i}
|
{#each results as result, i}
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
|
id={`${idPrefix}-option-${i}`}
|
||||||
type="button"
|
type="button"
|
||||||
class="result-item"
|
class="result-item"
|
||||||
class:selected={i === selectedIndex}
|
class:selected={i === selectedIndex}
|
||||||
@@ -162,6 +186,10 @@
|
|||||||
.result-item.selected {
|
.result-item.selected {
|
||||||
background: var(--hover-bg);
|
background: var(--hover-bg);
|
||||||
}
|
}
|
||||||
|
.result-item:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
background: var(--hover-bg);
|
||||||
|
}
|
||||||
.result-name {
|
.result-name {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,12 +131,31 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
export let open = false;
|
export let open = false;
|
||||||
|
$: isSidebarHidden = !isDesktop && !open;
|
||||||
|
|
||||||
|
function closeSidebar() {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWindowKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Escape' && open && !isDesktop) {
|
||||||
|
closeSidebar();
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside class="sidebar" class:open id="site-navigation" aria-hidden={open ? 'false' : 'true'}>
|
<svelte:window on:keydown={handleWindowKeydown} />
|
||||||
|
|
||||||
|
<aside
|
||||||
|
class="sidebar"
|
||||||
|
class:open={open}
|
||||||
|
id="site-navigation"
|
||||||
|
aria-hidden={isSidebarHidden ? 'true' : undefined}
|
||||||
|
inert={isSidebarHidden}
|
||||||
|
>
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<h3>All Converters</h3>
|
<h3>All Converters</h3>
|
||||||
<button class="close-btn" on:click={() => (open = false)} aria-label="Close sidebar">✕</button>
|
<button type="button" class="close-btn" on:click={closeSidebar} aria-label="Close sidebar">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<nav aria-label="Calculator categories">
|
<nav aria-label="Calculator categories">
|
||||||
{#each categoryUnitGroups as group}
|
{#each categoryUnitGroups as group}
|
||||||
@@ -200,10 +219,8 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{#if open}
|
{#if open && !isDesktop}
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<button type="button" class="overlay" aria-label="Close sidebar" on:click={closeSidebar}></button>
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
||||||
<div class="overlay" on:click={() => (open = false)}></div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -243,6 +260,11 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
}
|
}
|
||||||
|
.close-btn:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px var(--accent-glow);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
@@ -265,6 +287,10 @@
|
|||||||
.cat-toggle:hover {
|
.cat-toggle:hover {
|
||||||
background: var(--hover-bg);
|
background: var(--hover-bg);
|
||||||
}
|
}
|
||||||
|
.cat-toggle:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: inset 0 0 0 2px var(--accent-glow);
|
||||||
|
}
|
||||||
.cat-toggle.active {
|
.cat-toggle.active {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -306,6 +332,11 @@
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
background: var(--hover-bg);
|
background: var(--hover-bg);
|
||||||
}
|
}
|
||||||
|
.cat-list li a:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
color: var(--accent);
|
||||||
|
background: var(--hover-bg);
|
||||||
|
}
|
||||||
.cat-list li a.current {
|
.cat-list li a.current {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -337,6 +368,10 @@
|
|||||||
.unit-toggle:hover {
|
.unit-toggle:hover {
|
||||||
background: var(--hover-bg);
|
background: var(--hover-bg);
|
||||||
}
|
}
|
||||||
|
.unit-toggle:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: inset 0 0 0 2px var(--accent-glow);
|
||||||
|
}
|
||||||
.unit-toggle.expanded {
|
.unit-toggle.expanded {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -362,9 +397,15 @@
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
background: var(--hover-bg);
|
background: var(--hover-bg);
|
||||||
}
|
}
|
||||||
|
.unit-list li a:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
color: var(--accent);
|
||||||
|
background: var(--hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
.overlay {
|
.overlay {
|
||||||
display: none;
|
display: none;
|
||||||
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
|
|||||||
46
hdyc-svelte/src/lib/seo.ts
Normal file
46
hdyc-svelte/src/lib/seo.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
export const SITE_URL = 'https://howdoyouconvert.com';
|
||||||
|
export const SITE_NAME = 'HowDoYouConvert.com';
|
||||||
|
export const DEFAULT_ROBOTS = 'index,follow';
|
||||||
|
export const DEFAULT_TWITTER_CARD = 'summary';
|
||||||
|
|
||||||
|
type OpenGraphType = 'website' | 'article';
|
||||||
|
|
||||||
|
type SeoInput = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
pathname: string;
|
||||||
|
type?: OpenGraphType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizePathname = (pathname: string): string => {
|
||||||
|
const pathWithSlash = pathname.startsWith('/') ? pathname : `/${pathname}`;
|
||||||
|
const normalized = pathWithSlash.replace(/\/{2,}/g, '/');
|
||||||
|
if (normalized.length > 1 && normalized.endsWith('/')) {
|
||||||
|
return normalized.slice(0, -1);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const canonicalUrl = (pathname: string): string => `${SITE_URL}${normalizePathname(pathname)}`;
|
||||||
|
|
||||||
|
export const buildSeoMeta = ({ title, description, pathname, type = 'website' }: SeoInput) => {
|
||||||
|
const canonical = canonicalUrl(pathname);
|
||||||
|
return {
|
||||||
|
canonical,
|
||||||
|
robots: DEFAULT_ROBOTS,
|
||||||
|
og: {
|
||||||
|
type,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
url: canonical,
|
||||||
|
siteName: SITE_NAME,
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: DEFAULT_TWITTER_CARD,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toJsonLd = (value: unknown): string => JSON.stringify(value).replace(/</g, '\\u003c');
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
import { afterNavigate } from '$app/navigation';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
@@ -253,11 +254,17 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
let sidebarOpen = false;
|
let sidebarOpen = false;
|
||||||
|
let headerSearchOpen = false;
|
||||||
|
let isMobileHeader = false;
|
||||||
let theme: ThemeMode = 'dark';
|
let theme: ThemeMode = 'dark';
|
||||||
let selectedPaletteIndex = 0;
|
let selectedPaletteIndex = 0;
|
||||||
$: isHomepage = $page.url.pathname === '/';
|
$: isHomepage = $page.url.pathname === '/';
|
||||||
$: if (isHomepage && sidebarOpen) {
|
$: if (isHomepage && (sidebarOpen || headerSearchOpen)) {
|
||||||
sidebarOpen = false;
|
sidebarOpen = false;
|
||||||
|
headerSearchOpen = false;
|
||||||
|
}
|
||||||
|
$: if (!isMobileHeader && headerSearchOpen) {
|
||||||
|
headerSearchOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyPalette = (index: number, persist = false) => {
|
const applyPalette = (index: number, persist = false) => {
|
||||||
@@ -289,6 +296,18 @@
|
|||||||
applyPalette(index, true);
|
applyPalette(index, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleHeaderSearch = () => {
|
||||||
|
headerSearchOpen = !headerSearchOpen;
|
||||||
|
if (headerSearchOpen) {
|
||||||
|
sidebarOpen = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
afterNavigate(() => {
|
||||||
|
sidebarOpen = false;
|
||||||
|
headerSearchOpen = false;
|
||||||
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
|
|
||||||
@@ -313,10 +332,30 @@
|
|||||||
sidebarOpen = false;
|
sidebarOpen = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const headerBreakpoint = window.matchMedia('(max-width: 768px)');
|
||||||
|
const updateHeaderBreakpoint = (event?: MediaQueryListEvent) => {
|
||||||
|
const isCompact = event?.matches ?? headerBreakpoint.matches;
|
||||||
|
isMobileHeader = isCompact;
|
||||||
|
if (!isCompact) {
|
||||||
|
headerSearchOpen = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleHeaderBreakpoint = (event: MediaQueryListEvent) => {
|
||||||
|
updateHeaderBreakpoint(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEscape = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
sidebarOpen = false;
|
||||||
|
headerSearchOpen = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleEscape);
|
||||||
|
|
||||||
if (navBreakpoint.matches) {
|
if (navBreakpoint.matches) {
|
||||||
sidebarOpen = false;
|
sidebarOpen = false;
|
||||||
}
|
}
|
||||||
|
updateHeaderBreakpoint();
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
if ('removeEventListener' in mediaQuery) {
|
if ('removeEventListener' in mediaQuery) {
|
||||||
@@ -329,6 +368,12 @@
|
|||||||
} else {
|
} else {
|
||||||
navBreakpoint.removeListener(handleNavBreakpoint);
|
navBreakpoint.removeListener(handleNavBreakpoint);
|
||||||
}
|
}
|
||||||
|
if ('removeEventListener' in headerBreakpoint) {
|
||||||
|
headerBreakpoint.removeEventListener('change', handleHeaderBreakpoint);
|
||||||
|
} else {
|
||||||
|
headerBreakpoint.removeListener(handleHeaderBreakpoint);
|
||||||
|
}
|
||||||
|
window.removeEventListener('keydown', handleEscape);
|
||||||
};
|
};
|
||||||
|
|
||||||
if ('addEventListener' in mediaQuery) {
|
if ('addEventListener' in mediaQuery) {
|
||||||
@@ -342,6 +387,11 @@
|
|||||||
} else {
|
} else {
|
||||||
navBreakpoint.addListener(handleNavBreakpoint);
|
navBreakpoint.addListener(handleNavBreakpoint);
|
||||||
}
|
}
|
||||||
|
if ('addEventListener' in headerBreakpoint) {
|
||||||
|
headerBreakpoint.addEventListener('change', handleHeaderBreakpoint);
|
||||||
|
} else {
|
||||||
|
headerBreakpoint.addListener(handleHeaderBreakpoint);
|
||||||
|
}
|
||||||
|
|
||||||
return cleanup;
|
return cleanup;
|
||||||
});
|
});
|
||||||
@@ -349,7 +399,7 @@
|
|||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.png" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=2" />
|
||||||
<!-- Matomo Tag Manager -->
|
<!-- Matomo Tag Manager -->
|
||||||
<script>
|
<script>
|
||||||
var _mtm = window._mtm = window._mtm || [];
|
var _mtm = window._mtm = window._mtm || [];
|
||||||
@@ -362,13 +412,20 @@
|
|||||||
<!-- End Matomo Tag Manager -->
|
<!-- End Matomo Tag Manager -->
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
<a href="#main-content" class="skip-link">Skip to main content</a>
|
||||||
|
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<div style="display:flex;align-items:center;gap:0.75rem;">
|
<div class="header-left">
|
||||||
{#if !isHomepage}
|
{#if !isHomepage}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="hamburger"
|
class="hamburger"
|
||||||
on:click={() => (sidebarOpen = !sidebarOpen)}
|
on:click={() => {
|
||||||
|
sidebarOpen = !sidebarOpen;
|
||||||
|
if (sidebarOpen) {
|
||||||
|
headerSearchOpen = false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
aria-label="Toggle menu"
|
aria-label="Toggle menu"
|
||||||
aria-controls="site-navigation"
|
aria-controls="site-navigation"
|
||||||
aria-expanded={sidebarOpen ? 'true' : 'false'}
|
aria-expanded={sidebarOpen ? 'true' : 'false'}
|
||||||
@@ -381,15 +438,35 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<SearchBar />
|
{#if !isHomepage}
|
||||||
|
<div class="desktop-header-search">
|
||||||
|
<SearchBar idPrefix="header-search" />
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="header-icon-btn search-toggle"
|
||||||
|
on:click={toggleHeaderSearch}
|
||||||
|
aria-controls="mobile-header-search"
|
||||||
|
aria-expanded={headerSearchOpen ? 'true' : 'false'}
|
||||||
|
aria-label={headerSearchOpen ? 'Close search' : 'Open search'}
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">🔍</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{#if !isHomepage}
|
||||||
|
<div id="mobile-header-search" class="mobile-header-search" class:open={headerSearchOpen} hidden={!headerSearchOpen}>
|
||||||
|
<SearchBar idPrefix="mobile-header-search" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="site-body">
|
<div class="site-body">
|
||||||
{#if !isHomepage}
|
{#if !isHomepage}
|
||||||
<Sidebar bind:open={sidebarOpen} />
|
<Sidebar bind:open={sidebarOpen} />
|
||||||
{/if}
|
{/if}
|
||||||
<main class="main-content">
|
<main id="main-content" class="main-content" tabindex="-1">
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { categories, calculators } from '$lib/data/calculators';
|
import { categories, calculators } from '$lib/data/calculators';
|
||||||
import CategoryCard from '$lib/components/CategoryCard.svelte';
|
import CategoryCard from '$lib/components/CategoryCard.svelte';
|
||||||
import SearchBar from '$lib/components/SearchBar.svelte';
|
import SearchBar from '$lib/components/SearchBar.svelte';
|
||||||
|
import { buildSeoMeta, SITE_NAME, SITE_URL, toJsonLd } from '$lib/seo';
|
||||||
|
|
||||||
const requiredCategoryFallbacks: Record<string, { label: string; icon: string }> = {
|
const requiredCategoryFallbacks: Record<string, { label: string; icon: string }> = {
|
||||||
fluids: { label: 'Fluids', icon: '💧' },
|
fluids: { label: 'Fluids', icon: '💧' },
|
||||||
@@ -19,18 +20,43 @@
|
|||||||
}));
|
}));
|
||||||
const totalCalculators = calculators.length;
|
const totalCalculators = calculators.length;
|
||||||
const totalCategories = Object.keys(homepageCategories).length;
|
const totalCategories = Object.keys(homepageCategories).length;
|
||||||
|
const pageTitle = `${SITE_NAME} — Free Unit Conversion Calculators`;
|
||||||
|
const pageDescription = 'Convert between hundreds of units instantly. Free online calculators for length, weight, temperature, volume, area, speed, energy, power, data and more.';
|
||||||
|
const seo = buildSeoMeta({
|
||||||
|
title: pageTitle,
|
||||||
|
description: pageDescription,
|
||||||
|
pathname: '/',
|
||||||
|
});
|
||||||
|
const websiteJsonLd = toJsonLd({
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'WebSite',
|
||||||
|
name: SITE_NAME,
|
||||||
|
description: pageDescription,
|
||||||
|
url: SITE_URL,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>HowDoYouConvert.com — Free Unit Conversion Calculators</title>
|
<title>{pageTitle}</title>
|
||||||
<meta name="description" content="Convert between hundreds of units instantly. Free online calculators for length, weight, temperature, volume, area, speed, energy, power, data and more." />
|
<meta name="description" content={pageDescription} />
|
||||||
|
<meta name="robots" content={seo.robots} />
|
||||||
|
<link rel="canonical" href={seo.canonical} />
|
||||||
|
<meta property="og:type" content={seo.og.type} />
|
||||||
|
<meta property="og:title" content={seo.og.title} />
|
||||||
|
<meta property="og:description" content={seo.og.description} />
|
||||||
|
<meta property="og:url" content={seo.og.url} />
|
||||||
|
<meta property="og:site_name" content={seo.og.siteName} />
|
||||||
|
<meta name="twitter:card" content={seo.twitter.card} />
|
||||||
|
<meta name="twitter:title" content={seo.twitter.title} />
|
||||||
|
<meta name="twitter:description" content={seo.twitter.description} />
|
||||||
|
<script type="application/ld+json">{websiteJsonLd}</script>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<h1>How Do You Convert?</h1>
|
<h1>How Do You Convert?</h1>
|
||||||
<p>Fast, bidirectional unit conversions with no ads.</p>
|
<p>Fast, bidirectional unit conversions with no ads.</p>
|
||||||
<div class="search-center">
|
<div class="search-center">
|
||||||
<SearchBar />
|
<SearchBar idPrefix="home-search" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -4,35 +4,95 @@
|
|||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
import { buildSeoMeta, canonicalUrl, SITE_NAME, SITE_URL, toJsonLd } from '$lib/seo';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
$: calc = data.calculator;
|
$: calc = data.calculator;
|
||||||
$: related = data.related;
|
$: related = data.related;
|
||||||
|
$: pageTitle = `${calc.name} — ${SITE_NAME}`;
|
||||||
|
$: pageDescription = `Convert ${calc.labels.in1} to ${calc.labels.in2} instantly with our free online calculator. Accurate bidirectional conversion with the exact formula shown.`;
|
||||||
|
$: seo = buildSeoMeta({
|
||||||
|
title: pageTitle,
|
||||||
|
description: pageDescription,
|
||||||
|
pathname: `/${calc.slug}`,
|
||||||
|
});
|
||||||
|
$: breadcrumbJsonLd = toJsonLd({
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'BreadcrumbList',
|
||||||
|
itemListElement: [
|
||||||
|
{
|
||||||
|
'@type': 'ListItem',
|
||||||
|
position: 1,
|
||||||
|
name: 'Home',
|
||||||
|
item: SITE_URL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'ListItem',
|
||||||
|
position: 2,
|
||||||
|
name: data.categoryLabel,
|
||||||
|
item: canonicalUrl(`/category/${calc.category}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'ListItem',
|
||||||
|
position: 3,
|
||||||
|
name: calc.name,
|
||||||
|
item: seo.canonical,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
$: webPageJsonLd = toJsonLd({
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'WebPage',
|
||||||
|
name: calc.name,
|
||||||
|
description: pageDescription,
|
||||||
|
url: seo.canonical,
|
||||||
|
isPartOf: {
|
||||||
|
'@type': 'WebSite',
|
||||||
|
name: SITE_NAME,
|
||||||
|
url: SITE_URL,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
afterNavigate(() => {
|
||||||
|
if (!browser) return;
|
||||||
|
window.scrollTo({ top: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
window.scrollTo({ top: 0 });
|
window.scrollTo({ top: 0 });
|
||||||
return afterNavigate(() => {
|
|
||||||
window.scrollTo({ top: 0 });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{calc.name} — HowDoYouConvert.com</title>
|
<title>{pageTitle}</title>
|
||||||
<meta name="description" content="Convert {calc.labels.in1} to {calc.labels.in2} instantly with our free online calculator. Accurate bidirectional conversion with the exact formula shown." />
|
<meta name="description" content={pageDescription} />
|
||||||
|
<meta name="robots" content={seo.robots} />
|
||||||
|
<link rel="canonical" href={seo.canonical} />
|
||||||
|
<meta property="og:type" content={seo.og.type} />
|
||||||
|
<meta property="og:title" content={seo.og.title} />
|
||||||
|
<meta property="og:description" content={seo.og.description} />
|
||||||
|
<meta property="og:url" content={seo.og.url} />
|
||||||
|
<meta property="og:site_name" content={seo.og.siteName} />
|
||||||
|
<meta name="twitter:card" content={seo.twitter.card} />
|
||||||
|
<meta name="twitter:title" content={seo.twitter.title} />
|
||||||
|
<meta name="twitter:description" content={seo.twitter.description} />
|
||||||
|
<script type="application/ld+json">{breadcrumbJsonLd}</script>
|
||||||
|
<script type="application/ld+json">{webPageJsonLd}</script>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<nav class="breadcrumbs">
|
<nav class="breadcrumbs" aria-label="Breadcrumb">
|
||||||
<a href="/">Home</a>
|
<ol>
|
||||||
<span class="sep">›</span>
|
<li><a href="/">Home</a></li>
|
||||||
<a href="/category/{calc.category}">{data.categoryIcon} {data.categoryLabel}</a>
|
<li><a href="/category/{calc.category}">{data.categoryIcon} {data.categoryLabel}</a></li>
|
||||||
<span class="sep">›</span>
|
<li aria-current="page">{calc.name}</li>
|
||||||
<span>{calc.name}</span>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<Calculator config={calc} />
|
<h1 class="page-title calculator-page-title">{calc.name}</h1>
|
||||||
|
|
||||||
|
<Calculator config={calc} showTitle={false} />
|
||||||
|
|
||||||
<div class="seo-content">
|
<div class="seo-content">
|
||||||
{#if calc.descriptionHTML}
|
{#if calc.descriptionHTML}
|
||||||
|
|||||||
@@ -1,18 +1,80 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
import { buildSeoMeta, canonicalUrl, SITE_NAME, SITE_URL, toJsonLd } from '$lib/seo';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
|
$: pageTitle = `${data.label} Converters — ${SITE_NAME}`;
|
||||||
|
$: pageDescription = `Browse all ${data.label.toLowerCase()} unit converters. Free online calculators for converting between ${data.label.toLowerCase()} units.`;
|
||||||
|
$: categoryPath = `/category/${data.category}`;
|
||||||
|
$: seo = buildSeoMeta({
|
||||||
|
title: pageTitle,
|
||||||
|
description: pageDescription,
|
||||||
|
pathname: categoryPath,
|
||||||
|
});
|
||||||
|
$: breadcrumbJsonLd = toJsonLd({
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'BreadcrumbList',
|
||||||
|
itemListElement: [
|
||||||
|
{
|
||||||
|
'@type': 'ListItem',
|
||||||
|
position: 1,
|
||||||
|
name: 'Home',
|
||||||
|
item: SITE_URL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'ListItem',
|
||||||
|
position: 2,
|
||||||
|
name: `${data.label} Converters`,
|
||||||
|
item: seo.canonical,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
$: collectionJsonLd = toJsonLd({
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'CollectionPage',
|
||||||
|
name: `${data.label} Converters`,
|
||||||
|
description: pageDescription,
|
||||||
|
url: seo.canonical,
|
||||||
|
isPartOf: {
|
||||||
|
'@type': 'WebSite',
|
||||||
|
name: SITE_NAME,
|
||||||
|
url: SITE_URL,
|
||||||
|
},
|
||||||
|
mainEntity: {
|
||||||
|
'@type': 'ItemList',
|
||||||
|
itemListElement: data.calculators.map((calc, index) => ({
|
||||||
|
'@type': 'ListItem',
|
||||||
|
position: index + 1,
|
||||||
|
name: calc.name,
|
||||||
|
url: canonicalUrl(`/${calc.slug}`),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{data.label} Converters — HowDoYouConvert.com</title>
|
<title>{pageTitle}</title>
|
||||||
<meta name="description" content="Browse all {data.label.toLowerCase()} unit converters. Free online calculators for converting between {data.label.toLowerCase()} units." />
|
<meta name="description" content={pageDescription} />
|
||||||
|
<meta name="robots" content={seo.robots} />
|
||||||
|
<link rel="canonical" href={seo.canonical} />
|
||||||
|
<meta property="og:type" content={seo.og.type} />
|
||||||
|
<meta property="og:title" content={seo.og.title} />
|
||||||
|
<meta property="og:description" content={seo.og.description} />
|
||||||
|
<meta property="og:url" content={seo.og.url} />
|
||||||
|
<meta property="og:site_name" content={seo.og.siteName} />
|
||||||
|
<meta name="twitter:card" content={seo.twitter.card} />
|
||||||
|
<meta name="twitter:title" content={seo.twitter.title} />
|
||||||
|
<meta name="twitter:description" content={seo.twitter.description} />
|
||||||
|
<script type="application/ld+json">{breadcrumbJsonLd}</script>
|
||||||
|
<script type="application/ld+json">{collectionJsonLd}</script>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<nav class="breadcrumbs">
|
<nav class="breadcrumbs" aria-label="Breadcrumb">
|
||||||
<a href="/">Home</a>
|
<ol>
|
||||||
<span class="sep">›</span>
|
<li><a href="/">Home</a></li>
|
||||||
<span>{data.icon} {data.label}</span>
|
<li aria-current="page">{data.icon} {data.label}</li>
|
||||||
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<h1 class="page-title">{data.icon} {data.label} Converters</h1>
|
<h1 class="page-title">{data.icon} {data.label} Converters</h1>
|
||||||
|
|||||||
21
hdyc-svelte/static/favicon.svg
Normal file
21
hdyc-svelte/static/favicon.svg
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Calculator favicon">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0" stop-color="#115e59" />
|
||||||
|
<stop offset="1" stop-color="#0284c7" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="64" height="64" rx="14" fill="url(#bg)" />
|
||||||
|
<rect x="17" y="8" width="30" height="48" rx="6" fill="#f8fafc" stroke="#0f172a" stroke-width="2" />
|
||||||
|
<rect x="22" y="14" width="20" height="9" rx="2" fill="#0f172a" />
|
||||||
|
<rect x="24" y="17" width="16" height="3" rx="1.5" fill="#67e8f9" />
|
||||||
|
<g fill="#0ea5e9">
|
||||||
|
<rect x="22" y="28" width="6" height="6" rx="1.5" />
|
||||||
|
<rect x="29" y="28" width="6" height="6" rx="1.5" />
|
||||||
|
<rect x="36" y="28" width="6" height="6" rx="1.5" />
|
||||||
|
<rect x="22" y="35" width="6" height="6" rx="1.5" />
|
||||||
|
<rect x="29" y="35" width="6" height="6" rx="1.5" />
|
||||||
|
<rect x="36" y="35" width="6" height="13" rx="1.5" fill="#f97316" />
|
||||||
|
<rect x="22" y="42" width="13" height="6" rx="1.5" fill="#14b8a6" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -1,3 +1,4 @@
|
|||||||
# allow crawling everything by default
|
# allow crawling everything by default
|
||||||
User-agent: *
|
User-agent: *
|
||||||
Disallow:
|
Disallow:
|
||||||
|
Sitemap: https://howdoyouconvert.com/sitemap.xml
|
||||||
|
|||||||
Reference in New Issue
Block a user