Hardening responsiveness and SEO
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
@@ -253,11 +254,17 @@
|
||||
];
|
||||
|
||||
let sidebarOpen = false;
|
||||
let headerSearchOpen = false;
|
||||
let isMobileHeader = false;
|
||||
let theme: ThemeMode = 'dark';
|
||||
let selectedPaletteIndex = 0;
|
||||
$: isHomepage = $page.url.pathname === '/';
|
||||
$: if (isHomepage && sidebarOpen) {
|
||||
$: if (isHomepage && (sidebarOpen || headerSearchOpen)) {
|
||||
sidebarOpen = false;
|
||||
headerSearchOpen = false;
|
||||
}
|
||||
$: if (!isMobileHeader && headerSearchOpen) {
|
||||
headerSearchOpen = false;
|
||||
}
|
||||
|
||||
const applyPalette = (index: number, persist = false) => {
|
||||
@@ -289,6 +296,18 @@
|
||||
applyPalette(index, true);
|
||||
};
|
||||
|
||||
const toggleHeaderSearch = () => {
|
||||
headerSearchOpen = !headerSearchOpen;
|
||||
if (headerSearchOpen) {
|
||||
sidebarOpen = false;
|
||||
}
|
||||
};
|
||||
|
||||
afterNavigate(() => {
|
||||
sidebarOpen = false;
|
||||
headerSearchOpen = false;
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
if (!browser) return;
|
||||
|
||||
@@ -313,10 +332,30 @@
|
||||
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) {
|
||||
sidebarOpen = false;
|
||||
}
|
||||
updateHeaderBreakpoint();
|
||||
|
||||
const cleanup = () => {
|
||||
if ('removeEventListener' in mediaQuery) {
|
||||
@@ -329,6 +368,12 @@
|
||||
} else {
|
||||
navBreakpoint.removeListener(handleNavBreakpoint);
|
||||
}
|
||||
if ('removeEventListener' in headerBreakpoint) {
|
||||
headerBreakpoint.removeEventListener('change', handleHeaderBreakpoint);
|
||||
} else {
|
||||
headerBreakpoint.removeListener(handleHeaderBreakpoint);
|
||||
}
|
||||
window.removeEventListener('keydown', handleEscape);
|
||||
};
|
||||
|
||||
if ('addEventListener' in mediaQuery) {
|
||||
@@ -342,6 +387,11 @@
|
||||
} else {
|
||||
navBreakpoint.addListener(handleNavBreakpoint);
|
||||
}
|
||||
if ('addEventListener' in headerBreakpoint) {
|
||||
headerBreakpoint.addEventListener('change', handleHeaderBreakpoint);
|
||||
} else {
|
||||
headerBreakpoint.addListener(handleHeaderBreakpoint);
|
||||
}
|
||||
|
||||
return cleanup;
|
||||
});
|
||||
@@ -349,7 +399,7 @@
|
||||
|
||||
<svelte:head>
|
||||
<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 -->
|
||||
<script>
|
||||
var _mtm = window._mtm = window._mtm || [];
|
||||
@@ -362,13 +412,20 @@
|
||||
<!-- End Matomo Tag Manager -->
|
||||
</svelte:head>
|
||||
|
||||
<a href="#main-content" class="skip-link">Skip to main content</a>
|
||||
|
||||
<header class="site-header">
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;">
|
||||
<div class="header-left">
|
||||
{#if !isHomepage}
|
||||
<button
|
||||
type="button"
|
||||
class="hamburger"
|
||||
on:click={() => (sidebarOpen = !sidebarOpen)}
|
||||
on:click={() => {
|
||||
sidebarOpen = !sidebarOpen;
|
||||
if (sidebarOpen) {
|
||||
headerSearchOpen = false;
|
||||
}
|
||||
}}
|
||||
aria-label="Toggle menu"
|
||||
aria-controls="site-navigation"
|
||||
aria-expanded={sidebarOpen ? 'true' : 'false'}
|
||||
@@ -381,15 +438,35 @@
|
||||
</a>
|
||||
</div>
|
||||
<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>
|
||||
</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">
|
||||
{#if !isHomepage}
|
||||
<Sidebar bind:open={sidebarOpen} />
|
||||
{/if}
|
||||
<main class="main-content">
|
||||
<main id="main-content" class="main-content" tabindex="-1">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { categories, calculators } from '$lib/data/calculators';
|
||||
import CategoryCard from '$lib/components/CategoryCard.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 }> = {
|
||||
fluids: { label: 'Fluids', icon: '💧' },
|
||||
@@ -19,18 +20,43 @@
|
||||
}));
|
||||
const totalCalculators = calculators.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>
|
||||
|
||||
<svelte:head>
|
||||
<title>HowDoYouConvert.com — Free Unit Conversion Calculators</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." />
|
||||
<title>{pageTitle}</title>
|
||||
<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>
|
||||
|
||||
<section class="hero">
|
||||
<h1>How Do You Convert?</h1>
|
||||
<p>Fast, bidirectional unit conversions with no ads.</p>
|
||||
<div class="search-center">
|
||||
<SearchBar />
|
||||
<SearchBar idPrefix="home-search" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -4,35 +4,95 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { onMount } from 'svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { buildSeoMeta, canonicalUrl, SITE_NAME, SITE_URL, toJsonLd } from '$lib/seo';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
$: calc = data.calculator;
|
||||
$: 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(() => {
|
||||
if (!browser) return;
|
||||
window.scrollTo({ top: 0 });
|
||||
return afterNavigate(() => {
|
||||
window.scrollTo({ top: 0 });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{calc.name} — HowDoYouConvert.com</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." />
|
||||
<title>{pageTitle}</title>
|
||||
<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>
|
||||
|
||||
<nav class="breadcrumbs">
|
||||
<a href="/">Home</a>
|
||||
<span class="sep">›</span>
|
||||
<a href="/category/{calc.category}">{data.categoryIcon} {data.categoryLabel}</a>
|
||||
<span class="sep">›</span>
|
||||
<span>{calc.name}</span>
|
||||
<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 aria-current="page">{calc.name}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<Calculator config={calc} />
|
||||
<h1 class="page-title calculator-page-title">{calc.name}</h1>
|
||||
|
||||
<Calculator config={calc} showTitle={false} />
|
||||
|
||||
<div class="seo-content">
|
||||
{#if calc.descriptionHTML}
|
||||
|
||||
@@ -1,18 +1,80 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import { buildSeoMeta, canonicalUrl, SITE_NAME, SITE_URL, toJsonLd } from '$lib/seo';
|
||||
|
||||
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>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.label} Converters — HowDoYouConvert.com</title>
|
||||
<meta name="description" content="Browse all {data.label.toLowerCase()} unit converters. Free online calculators for converting between {data.label.toLowerCase()} units." />
|
||||
<title>{pageTitle}</title>
|
||||
<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>
|
||||
|
||||
<nav class="breadcrumbs">
|
||||
<a href="/">Home</a>
|
||||
<span class="sep">›</span>
|
||||
<span>{data.icon} {data.label}</span>
|
||||
<nav class="breadcrumbs" aria-label="Breadcrumb">
|
||||
<ol>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li aria-current="page">{data.icon} {data.label}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<h1 class="page-title">{data.icon} {data.label} Converters</h1>
|
||||
|
||||
Reference in New Issue
Block a user