Hardening responsiveness and SEO
This commit is contained in:
@@ -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';
|
||||
|
||||
export let config: CalculatorDef;
|
||||
export let showTitle = true;
|
||||
|
||||
let val1 = '';
|
||||
let val2 = '';
|
||||
@@ -70,12 +71,16 @@
|
||||
</script>
|
||||
|
||||
<div class="calculator-card">
|
||||
<div class="calc-header">
|
||||
<h2>{config.name}</h2>
|
||||
{#if config.teaser}
|
||||
<p class="calc-subtitle">{config.teaser}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if showTitle || config.teaser}
|
||||
<div class="calc-header">
|
||||
{#if showTitle}
|
||||
<h2>{config.name}</h2>
|
||||
{/if}
|
||||
{#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="input-group">
|
||||
@@ -181,6 +186,9 @@
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-weight: 400;
|
||||
}
|
||||
.calc-subtitle.no-title {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.calc-body {
|
||||
display: grid;
|
||||
@@ -261,6 +269,10 @@
|
||||
background: var(--accent-dark);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.swap-btn:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px var(--accent-glow);
|
||||
}
|
||||
|
||||
.calc-footer {
|
||||
display: flex;
|
||||
@@ -284,16 +296,34 @@
|
||||
color: #fff;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.clear-btn:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px var(--accent-glow);
|
||||
}
|
||||
.formula-hint {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
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) {
|
||||
.calc-header {
|
||||
padding: 1.2rem 1.2rem 0.9rem;
|
||||
}
|
||||
.calc-body {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
.calc-body.three-col {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -307,5 +337,8 @@
|
||||
.swap-btn:hover {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
.calc-footer {
|
||||
padding: 0.9rem 1.25rem 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -97,4 +97,11 @@
|
||||
font-weight: 600;
|
||||
margin-left: 0.35rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.example-card {
|
||||
margin: 0 1.25rem 1.25rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -98,4 +98,17 @@
|
||||
.chart-output-unit {
|
||||
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>
|
||||
|
||||
@@ -68,4 +68,14 @@
|
||||
font-size: 0.85rem;
|
||||
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>
|
||||
|
||||
@@ -2,11 +2,27 @@
|
||||
import { searchCalculators } from '$lib/data/calculators';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let idPrefix = 'search';
|
||||
|
||||
let query = '';
|
||||
let focused = false;
|
||||
let selectedIndex = -1;
|
||||
let lastQuery = '';
|
||||
|
||||
$: 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) {
|
||||
if (e.key === 'ArrowDown') {
|
||||
@@ -33,12 +49,13 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="search-wrapper" class:active={focused && results.length > 0}>
|
||||
<div class="search-wrapper" class:active={isOpen}>
|
||||
<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">
|
||||
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.35-4.35" />
|
||||
</svg>
|
||||
<input
|
||||
id={inputId}
|
||||
type="text"
|
||||
bind:value={query}
|
||||
on:focus={() => (focused = true)}
|
||||
@@ -46,6 +63,12 @@
|
||||
on:keydown={handleKeydown}
|
||||
placeholder="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}
|
||||
<button
|
||||
@@ -61,11 +84,12 @@
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if focused && results.length > 0}
|
||||
<ul class="results" role="listbox" aria-label="Conversion suggestions">
|
||||
{#if isOpen}
|
||||
<ul class="results" id={listboxId} role="listbox" aria-label="Conversion suggestions">
|
||||
{#each results as result, i}
|
||||
<li>
|
||||
<button
|
||||
id={`${idPrefix}-option-${i}`}
|
||||
type="button"
|
||||
class="result-item"
|
||||
class:selected={i === selectedIndex}
|
||||
@@ -162,6 +186,10 @@
|
||||
.result-item.selected {
|
||||
background: var(--hover-bg);
|
||||
}
|
||||
.result-item:focus-visible {
|
||||
outline: none;
|
||||
background: var(--hover-bg);
|
||||
}
|
||||
.result-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -131,12 +131,31 @@
|
||||
}
|
||||
|
||||
export let open = false;
|
||||
$: isSidebarHidden = !isDesktop && !open;
|
||||
|
||||
function closeSidebar() {
|
||||
open = false;
|
||||
}
|
||||
|
||||
function handleWindowKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && open && !isDesktop) {
|
||||
closeSidebar();
|
||||
}
|
||||
}
|
||||
</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">
|
||||
<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>
|
||||
<nav aria-label="Calculator categories">
|
||||
{#each categoryUnitGroups as group}
|
||||
@@ -200,10 +219,8 @@
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{#if open}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="overlay" on:click={() => (open = false)}></div>
|
||||
{#if open && !isDesktop}
|
||||
<button type="button" class="overlay" aria-label="Close sidebar" on:click={closeSidebar}></button>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
@@ -243,6 +260,11 @@
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
.close-btn:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px var(--accent-glow);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
nav {
|
||||
padding: 0.5rem 0;
|
||||
@@ -265,6 +287,10 @@
|
||||
.cat-toggle:hover {
|
||||
background: var(--hover-bg);
|
||||
}
|
||||
.cat-toggle:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: inset 0 0 0 2px var(--accent-glow);
|
||||
}
|
||||
.cat-toggle.active {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
@@ -306,6 +332,11 @@
|
||||
color: var(--accent);
|
||||
background: var(--hover-bg);
|
||||
}
|
||||
.cat-list li a:focus-visible {
|
||||
outline: none;
|
||||
color: var(--accent);
|
||||
background: var(--hover-bg);
|
||||
}
|
||||
.cat-list li a.current {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
@@ -337,6 +368,10 @@
|
||||
.unit-toggle:hover {
|
||||
background: var(--hover-bg);
|
||||
}
|
||||
.unit-toggle:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: inset 0 0 0 2px var(--accent-glow);
|
||||
}
|
||||
.unit-toggle.expanded {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
@@ -362,9 +397,15 @@
|
||||
color: var(--accent);
|
||||
background: var(--hover-bg);
|
||||
}
|
||||
.unit-list li a:focus-visible {
|
||||
outline: none;
|
||||
color: var(--accent);
|
||||
background: var(--hover-bg);
|
||||
}
|
||||
|
||||
.overlay {
|
||||
display: none;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@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');
|
||||
Reference in New Issue
Block a user