]> juplo.de Git - website/commitdiff
Alias-Hinweis im Sitemap-Menü für Barrierefreiheit
authorKai Moritz <kai.milan.moritz@googlemail.com>
Fri, 19 Jun 2026 10:22:34 +0000 (12:22 +0200)
committerKai Moritz <kai@juplo.de>
Sun, 12 Jul 2026 10:27:07 +0000 (12:27 +0200)
Alias-Einträge im Sitemap-Menü erhalten jetzt einen Hinweis-Span
'(Schnellzugriff)' direkt nach dem Titel-Element, außerhalb von <strong>.
Dadurch ist der Hinweis nie fett – auch nicht wenn die Seite aktiv ist.

Im CSS (base/navigation.scss) wird #submenu .alias-hint ausgeblendet, sodass
der Hinweis nur im Layout 'None' (kein CSS) sichtbar bleibt – für
Screen-Reader und Text-Browser.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CLAUDE.md
src/components/BlogNav.astro
src/content.config.ts
src/lib/projects.ts

index a8f7fbc8b75d39128771abe551dc967395993de8..78481cdc5f6cff124fa395916d6da89a21a6519f 100644 (file)
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -138,7 +138,9 @@ Der Projects-Bereich unterscheidet zwischen **sichtbaren** und **nicht sichtbare
 
 **Alias-Einträge (`params.alias: true`):** Seiten, die in `stili-json` in mehreren `childs`-Listen auftauchen, werden vom Import-Skript an mehreren Stellen im Content-Tree abgelegt. Die primäre Datei (kanonischer Ort, mit Body + Routing) folgt den `crumbs`/`path`-Feldern. Zusätzliche **Stub-Dateien** (leer, kein Routing) entstehen an den weiteren Positionen und erhalten `params.alias: true`.
 
-In `BlogNav.astro` werden Alias-Nodes mit der CSS-Klasse `alias` versehen. In der `off`-Berechnung überspringen Alias-Nodes `isCurrent` und `isAncestor` — dadurch sind sie im Classic-Layout versteckt, wenn die Seite aktiv ist (der kanonische Eintrag übernimmt die Darstellung). Sichtbar sind sie als Geschwister (normale `isSibling`-Logik). Im Layout „None" (kein CSS) erscheinen alle Einträge, kanonischer und Alias-Eintrag beide fett.
+In `BlogNav.astro` werden Alias-Nodes mit der CSS-Klasse `alias` versehen. In der `off`-Berechnung überspringen Alias-Nodes `isCurrent` und `isAncestor` — dadurch sind sie im Classic-Layout versteckt, wenn die Seite aktiv ist (der kanonische Eintrag übernimmt die Darstellung). Sichtbar sind sie als Geschwister (normale `isSibling`-Logik).
+
+Alias-Nodes erhalten zusätzlich einen Barrierefreiheits-Hinweis im Markup: `<span class="alias-hint"> (Schnellzugriff)</span>` direkt nach dem Titel-Element (`<strong>` oder `<a>`). Der Hinweis steht **außerhalb** von `<strong>` und ist damit nie fett — auch nicht wenn die Seite aktiv ist. Im Layout „None" (kein CSS) erscheint der Hinweis, damit Nutzende von Text-Browsern und Screen-Readern erkennen, dass dieser Eintrag ein Zusatzzugang zu einer auch anderswo gelisteten Seite ist. Im CSS (`base/navigation.scss`: `#submenu .alias-hint { display: none }`) wird er in allen Screen-Layouts ausgeblendet. Der kanonische Eintrag erhält keinen Hinweis.
 
 **Generierte Inhalte (apidocs, xref etc.):** Maven-Site-Outputs liegen fertig generiert in `public/projects/PROJEKT_NAME/VERSION/` und werden als statische Dateien ausgeliefert — ohne Astro-Layout, ohne HTML-Anpassungen, ohne Routing-Dateien in `src/pages/`. `src/lib/projects.ts` (`generatedDocNodes()`) erkennt diese Verzeichnisse zur Build-Zeit automatisch und fügt sie als Nav-Blatteinträge hinzu:
 
index 8508f2aaaa9d6134651890742084de673f596f4b..b398e671687ecd35c7af25876957034df09f8e94 100644 (file)
@@ -57,15 +57,18 @@ function inSubtree<T extends { url: string; children?: T[] }>(node: T, url: stri
 // A nav item is visible (not off) when it is: current, ancestor of current,
 // sibling of current (only when current is not a section-index node), or child of current.
 // `inPath` = inSubtree(node, currentUrl), pre-computed by the caller to avoid double traversal.
+// `isAlias` = true for nav-only stub entries: isCurrent and isAncestor are suppressed so the
+// alias is hidden in Classic layout when its page is active (the canonical entry handles that).
 function nodeOff(
   nodeUrl: string,
   parentUrl: string,
   inPath: boolean,
   currentParentUrl: string,
   currentIsNode: boolean,
+  isAlias = false,
 ): boolean {
-  const isCurrent  = nodeUrl === currentUrl;
-  const isAncestor = inPath && !isCurrent;
+  const isCurrent  = !isAlias && nodeUrl === currentUrl;
+  const isAncestor = !isAlias && inPath && !isCurrent;
   const isSibling  = parentUrl === currentParentUrl && !currentIsNode;
   const isChild    = parentUrl === currentUrl;
   return !(isCurrent || isAncestor || isSibling || isChild);
@@ -142,8 +145,8 @@ const activeProjectCurrentIsNode = activeProjectCurrentNode?.isNode ?? false;
 
 // Partial application of nodeOff bound to the Projects subtree's current-page context
 const _projCurrentParentUrl = activeProjectCurrentParent?.url ?? '';
-const projNodeOff = (nodeUrl: string, parentUrl: string, inPath: boolean) =>
-  nodeOff(nodeUrl, parentUrl, inPath, _projCurrentParentUrl, activeProjectCurrentIsNode);
+const projNodeOff = (nodeUrl: string, parentUrl: string, inPath: boolean, isAlias = false) =>
+  nodeOff(nodeUrl, parentUrl, inPath, _projCurrentParentUrl, activeProjectCurrentIsNode, isAlias);
 
 // ── Blog URL classification ────────────────────────────────────────────────────
 const mainSections = [
@@ -355,10 +358,10 @@ const activeTagSlug  = onTagTerm      ? currentUrl.replace(/^\/tags\//, '').repl
               <ul class={`s${rootInPath ? ' active' : ''}`}>
                 {root.children.map(child => {
                   const childInPath = inSubtree(child, currentUrl);
-                  const childOff    = projNodeOff(child.url, root.url, childInPath);
+                  const childOff    = projNodeOff(child.url, root.url, childInPath, child.isAlias);
                   const childIsNode = child.isNode;
                   return (
-                    <li class={`s${childIsNode ? ' sub' : ''}${childOff ? ' off' : ''}`}>
+                    <li class={`s${childIsNode ? ' sub' : ''}${child.isAlias ? ' alias' : ''}${childOff ? ' off' : ''}`}>
                       {child.url === currentUrl
                         ? <strong class="s">{child.title}</strong>
                         : <a href={child.url} class={childInPath ? 's selected' : 's'}>
@@ -369,10 +372,10 @@ const activeTagSlug  = onTagTerm      ? currentUrl.replace(/^\/tags\//, '').repl
                         <ul class={`s${childInPath ? ' active' : ''}`}>
                           {child.children.map(grand => {
                             const grandInPath = inSubtree(grand, currentUrl);
-                            const grandOff    = projNodeOff(grand.url, child.url, grandInPath);
+                            const grandOff    = projNodeOff(grand.url, child.url, grandInPath, grand.isAlias);
                             const grandIsNode = grand.isNode;
                             return (
-                              <li class={`s${grandIsNode ? ' sub' : ''}${grandOff ? ' off' : ''}`}>
+                              <li class={`s${grandIsNode ? ' sub' : ''}${grand.isAlias ? ' alias' : ''}${grandOff ? ' off' : ''}`}>
                                 {grand.url === currentUrl
                                   ? <strong class="s">{grand.title}</strong>
                                   : <a href={grand.url} class={grandInPath ? 's selected' : 's'}>
@@ -383,9 +386,9 @@ const activeTagSlug  = onTagTerm      ? currentUrl.replace(/^\/tags\//, '').repl
                                   <ul class={`s${grandInPath ? ' active' : ''}`}>
                                     {grand.children.map(ggrand => {
                                       const ggInPath = ggrand.url === currentUrl;
-                                      const ggOff    = projNodeOff(ggrand.url, grand.url, ggInPath);
+                                      const ggOff    = projNodeOff(ggrand.url, grand.url, ggInPath, ggrand.isAlias);
                                       return (
-                                        <li class={`s${ggOff ? ' off' : ''}`}>
+                                        <li class={`s${ggrand.isAlias ? ' alias' : ''}${ggOff ? ' off' : ''}`}>
                                           {ggInPath
                                             ? <strong class="s">{ggrand.title}</strong>
                                             : <a href={ggrand.url} class="s">{ggrand.title}</a>
index 079cd458271eb7e97c7c8ac356d3f14c3b9551a8..d07f9e8ff49c0458eff06de0a6b63065903520c2 100644 (file)
@@ -30,6 +30,7 @@ const projectPageSchema = z.object({
   params: z.object({
     current: z.boolean(),
     canonical: z.string().optional(),
+    alias: z.boolean().optional(),
   }),
 });
 
index feff57b1ac97eca7ffbfdfdc9db16684337ed6bf..125ee04002814c9281dc292d9b829cdca5bbb999 100644 (file)
@@ -9,6 +9,7 @@ export interface ProjectNavNode {
   url: string;
   weight: number;
   isNode: boolean;   // true when this entry is a section (_index.html)
+  isAlias: boolean;  // true for nav-only stub entries (same url as canonical, empty body)
   children: ProjectNavNode[];
 }
 
@@ -92,6 +93,7 @@ function buildNavTree(
       url:   entry.data.url,
       weight: entry.data.weight,
       isNode,
+      isAlias: entry.data.params.alias ?? false,
       children,
     };
   }