feat: enhance accessibility with skip to main content button and improve navigation handling

🔧 fix: update navigation href resolution to include localized paths

🆕 feat: add new FeatureIcon component for feature boxes

🎨 style: improve styling for prose elements in richtext blocks

🛠️ refactor: streamline medialib image loading and caching logic

📦 chore: update mock data handling to support new medialib entries

🔄 chore: synchronize i18n initialization and locale management

📝 docs: update video tour descriptions to reflect recent changes
This commit is contained in:
2026-05-12 13:55:32 +00:00
parent 8fb26fdeba
commit e84b87ed16
41 changed files with 1523 additions and 338 deletions
+102 -11
View File
@@ -22,6 +22,9 @@
stripLanguageFromPath,
} from "./lib/i18n"
const CONTENT_MEDIA_LOOKUP = ["blocks.heroImage.image:medialib", "blocks.image:medialib"].join(",")
const NAVIGATION_CONTENT_LOOKUP = "elements.page:content"
let { url = "" }: { url?: string } = $props()
initScrollRestoration()
@@ -50,6 +53,48 @@
}
})
function focusMainContent(updateHash = false) {
if (typeof window === "undefined") {
return
}
const mainContent = document.getElementById("main-content")
if (!(mainContent instanceof HTMLElement)) {
return
}
if (updateHash) {
window.history.pushState(window.history.state, "", "#main-content")
}
window.requestAnimationFrame(() => {
mainContent.scrollIntoView({ block: "start" })
mainContent.focus()
})
}
function handleSkipToMainContent() {
focusMainContent(true)
}
$effect(() => {
if (typeof window === "undefined") {
return
}
const handleHashChange = () => {
if (window.location.hash === "#main-content") {
focusMainContent()
}
}
window.addEventListener("hashchange", handleHashChange)
return () => {
window.removeEventListener("hashchange", handleHashChange)
}
})
// metrics
let oldPath = $state("")
$effect(() => {
@@ -74,6 +119,16 @@
let contentEntry = $state<ContentEntry | null>(null)
let headerNav = $state<NavigationEntry | null>(null)
let footerNav = $state<NavigationEntry | null>(null)
function resolveNavigationHref(item: NavigationElement): string {
const resolvedPagePath = item._lookup?.page?.path || (item.page?.startsWith("/") ? item.page : "/")
const localized = localizedPath(resolvedPagePath || "/")
if (!item.hash) return localized
const normalizedHash = item.hash.startsWith("#") ? item.hash : `#${item.hash}`
return `${localized}${normalizedHash}`
}
let loading = $state(true)
let notFound = $state(false)
@@ -103,18 +158,42 @@
try {
// Load navigation
const [headerEntries, footerEntries] = await Promise.all([
getCachedEntries<"navigation">("navigation", { type: "header", language: lang }),
getCachedEntries<"navigation">("navigation", { type: "footer", language: lang }),
getCachedEntries<"navigation">(
"navigation",
{ type: "header", language: lang },
"sort",
undefined,
undefined,
undefined,
{ lookup: NAVIGATION_CONTENT_LOOKUP }
),
getCachedEntries<"navigation">(
"navigation",
{ type: "footer", language: lang },
"sort",
undefined,
undefined,
undefined,
{ lookup: NAVIGATION_CONTENT_LOOKUP }
),
])
headerNav = headerEntries[0] || null
footerNav = footerEntries[0] || null
// Load content for current path
const contentEntries = await getCachedEntries<"content">("content", {
lang,
path: routePath,
active: true,
})
const contentEntries = await getCachedEntries<"content">(
"content",
{
lang,
path: routePath,
active: true,
},
"sort",
undefined,
undefined,
undefined,
{ lookup: CONTENT_MEDIA_LOOKUP }
)
if (contentEntries.length > 0) {
contentEntry = contentEntries[0]
@@ -157,6 +236,9 @@
{#if contentEntry?.meta?.description}
<meta name="description" content={contentEntry.meta.description} />
{/if}
{#if contentEntry?.meta?.keywords?.length}
<meta name="keywords" content={contentEntry.meta.keywords.join(", ")} />
{/if}
</svelte:head>
<LoadingBar />
@@ -168,6 +250,15 @@
? 'bg-white/80 backdrop-blur-lg shadow-sm'
: 'bg-transparent'}"
>
<button
type="button"
aria-controls="main-content"
onclick={handleSkipToMainContent}
class="sr-only absolute left-4 top-4 z-50 rounded bg-brand-600 px-4 py-2 text-sm font-semibold text-white focus:not-sr-only"
>
{$_("skipToMainContent")}
</button>
<div class="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
<!-- Logo -->
<a
@@ -184,7 +275,7 @@
{#if headerNav?.elements}
{#each headerNav.elements as item}
<a
href={localizedPath(item.page || "/")}
href={resolveNavigationHref(item)}
class="text-sm font-medium transition-colors duration-300 hover:text-brand-500 {scrolled
? 'text-gray-700'
: 'text-white/90'}"
@@ -248,7 +339,7 @@
{#if headerNav?.elements}
{#each headerNav.elements as item}
<a
href={localizedPath(item.page || "/")}
href={resolveNavigationHref(item)}
class="block text-gray-700 text-lg font-medium hover:text-brand-600 py-2"
>
{item.name}
@@ -273,7 +364,7 @@
</header>
<!-- ── Main Content ──────────────────────────────────────────── -->
<main>
<main id="main-content" tabindex="-1">
{#if loading}
<div class="min-h-screen flex items-center justify-center">
<div class="w-8 h-8 border-4 border-brand-200 border-t-brand-600 rounded-full animate-spin"></div>
@@ -319,7 +410,7 @@
</a>
{:else}
<a
href={localizedPath(item.page || "/")}
href={resolveNavigationHref(item)}
class="text-sm hover:text-white transition-colors"
>
{item.name}