]> juplo.de Git - website/commitdiff
rename BlogNav.astro → SitemapNav.astro content--via-astro
authorKai Moritz <kai.milan.moritz@googlemail.com>
Sun, 5 Jul 2026 17:56:45 +0000 (17:56 +0000)
committerKai Moritz <kai.milan.moritz@googlemail.com>
Sun, 5 Jul 2026 17:56:45 +0000 (17:56 +0000)
Die Komponente verwaltet die Navigation für alle drei Hauptbereiche
(Blog, Projects, About) — nicht nur den Blog. Der neue Name spiegelt
den tatsächlichen Zweck als seitenweite Sitemap-Navigation wider.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
23 files changed:
CLAUDE.md
src/components/BlogNav.astro [deleted file]
src/components/ProjectPage.astro
src/components/SitemapNav.astro [new file with mode: 0644]
src/pages/[slug].astro
src/pages/about.html.astro
src/pages/agb.html.astro
src/pages/blog/archive/[year].astro
src/pages/blog/archive/index.astro
src/pages/blog/categories/index.astro
src/pages/blog/index.astro
src/pages/blog/tags/index.astro
src/pages/categories/[term].astro
src/pages/contact.html.astro
src/pages/datenschutz.html.astro
src/pages/google-analytics.html.astro
src/pages/haftung-inhalte.html.astro
src/pages/haftung-links.html.astro
src/pages/impressum.html.astro
src/pages/index.astro
src/pages/projects/index.astro
src/pages/tags/[term].astro
src/pages/urheberrechte.html.astro

index 88e6268634d2aa5a023f7a994aaf3d794c31c75b..b4ce2aa171705d99191228d9bf128474fd02099c 100644 (file)
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -23,7 +23,7 @@ src/
   content/
     blog/         ← Blog-Artikel als Markdown (Content Collection, glob-Loader)
     projects/     ← Projektdokumentation als HTML + YAML-Frontmatter (Custom Loader)
-  components/     ← Wiederverwendbare Astro-Komponenten (BlogNav.astro, ProjectPage.astro, …)
+  components/     ← Wiederverwendbare Astro-Komponenten (SitemapNav.astro, ProjectPage.astro, …)
   lib/            ← Shared TypeScript-Helpers (posts.ts, projects.ts)
   styles/         ← SCSS-Quellen
 public/
@@ -50,7 +50,7 @@ body
       └── footer#footer       Copyright + Footer-Links
 ```
 
-Seiten befüllen die Slots mit `<Fragment slot="…">`. Den `menu`-Slot übernimmt immer `BlogNav.astro`.
+Seiten befüllen die Slots mit `<Fragment slot="…">`. Den `menu`-Slot übernimmt immer `SitemapNav.astro`.
 
 ---
 
@@ -85,9 +85,9 @@ Das Menü unter `nav#nav` ist bewusst am **Ende des HTML** platziert und dient p
 
 Ein `ul.s` ohne `active` blendet sich **nicht selbst** aus — nur `li.s.off` ist hidden.
 
-### Implementierung: `BlogNav.astro`
+### Implementierung: `SitemapNav.astro`
 
-`BlogNav.astro` empfängt `currentUrl` als Parameter und berechnet daraus vollständig in TypeScript alle Navigationszustände (`onBlog`, `onArchive`, `onCategoriesIndex`, `onProjectsSection` etc.). Alle CSS-Klassen werden direkt aus diesen Variablen abgeleitet. Nie werden Items per `{condition && <li>}` weggelassen — nur die `off`-Klasse steuert die Sichtbarkeit via CSS.
+`SitemapNav.astro` empfängt `currentUrl` als Parameter und berechnet daraus vollständig in TypeScript alle Navigationszustände (`onBlog`, `onArchive`, `onCategoriesIndex`, `onProjectsSection` etc.). Alle CSS-Klassen werden direkt aus diesen Variablen abgeleitet. Nie werden Items per `{condition && <li>}` weggelassen — nur die `off`-Klasse steuert die Sichtbarkeit via CSS.
 
 ### Section-`off`-Logik
 
@@ -122,7 +122,7 @@ Dabei ist `isSibling` nur aktiv, wenn die aktuelle Seite kein Section-Index-Node
 
 ### About-Bereich: Seitenstruktur
 
-Der About-Seitenbaum ist statisch in `BlogNav.astro` als TypeScript-Konstante (`NavPage[]`) definiert. Sortiert nach weight:
+Der About-Seitenbaum ist statisch in `SitemapNav.astro` als TypeScript-Konstante (`NavPage[]`) definiert. Sortiert nach weight:
 
 ```
 /about.html
@@ -152,7 +152,7 @@ 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).
+In `SitemapNav.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 im Classic-Layout ausgeblendet (da `navigation.scss` unter `:root[data-layout="classic"]` eingebunden ist — siehe Abschnitt „CSS-Layouts" unten). Der kanonische Eintrag erhält keinen Hinweis.
 
@@ -171,7 +171,7 @@ Alias-Nodes erhalten zusätzlich einen Barrierefreiheits-Hinweis im Markup: `<sp
 
 Die Projektdokumentation in `src/content/projects/` wird **nicht manuell gepflegt**, sondern mit dem **StILi Maven Skin** generiert und per `import-in-astro.sh` importiert.
 
-Die importierten Dateien sind HTML mit YAML-Frontmatter. Ein Custom Loader in `src/content.config.ts` (mit `js-yaml`) liest diese ein; Routing und Nav-Logik werden in `src/lib/projects.ts` und `BlogNav.astro` verwaltet.
+Die importierten Dateien sind HTML mit YAML-Frontmatter. Ein Custom Loader in `src/content.config.ts` (mit `js-yaml`) liest diese ein; Routing und Nav-Logik werden in `src/lib/projects.ts` und `SitemapNav.astro` verwaltet.
 
 **Verzeichnisstruktur (Beispiel hibernate-maven-plugin):**
 ```
@@ -296,7 +296,7 @@ Blog-Artikel und Projekt-Seiten können ein `url`-Feld im Frontmatter setzen. As
 ### Shared Helpers
 
 - `src/lib/posts.ts` — `getAllPosts`, `postUrl`, `getSummary`, `wordCount` für Blog-Seiten und RSS-Feeds
-- `src/lib/projects.ts` — `getActiveProject`, `getVisibleProjectNavTrees`, `buildNavTree`, `generatedDocNodes` für die Projects-Nav-Logik in `BlogNav.astro`
+- `src/lib/projects.ts` — `getActiveProject`, `getVisibleProjectNavTrees`, `buildNavTree`, `generatedDocNodes` für die Projects-Nav-Logik in `SitemapNav.astro`
 
 ### Content Collections
 
diff --git a/src/components/BlogNav.astro b/src/components/BlogNav.astro
deleted file mode 100644 (file)
index 77d6805..0000000
+++ /dev/null
@@ -1,486 +0,0 @@
----
-import type { CollectionEntry } from 'astro:content';
-import { getAllPosts, postUrl } from '../lib/posts';
-import { getActiveProject, getVisibleProjectNavTrees, type ProjectNavNode } from '../lib/projects';
-import { aboutSection, findAboutPath } from '../lib/about';
-
-interface Props {
-  currentUrl: string;
-}
-
-const { currentUrl } = Astro.props;
-
-const allPosts = await getAllPosts();
-
-// Group posts by year, years sorted descending
-const postsByYear: Record<string, CollectionEntry<'blog'>[]> = {};
-for (const post of allPosts) {
-  const year = post.data.date.getFullYear().toString();
-  if (!postsByYear[year]) postsByYear[year] = [];
-  postsByYear[year].push(post);
-}
-const years = Object.keys(postsByYear).sort((a, b) => Number(b) - Number(a));
-
-// Collect unique categories and tags from all posts
-const categorySet = new Map<string, number>();
-const tagSet = new Map<string, number>();
-for (const post of allPosts) {
-  for (const cat of post.data.categories ?? []) {
-    categorySet.set(cat, (categorySet.get(cat) ?? 0) + 1);
-  }
-  for (const tag of post.data.tags ?? []) {
-    tagSet.set(tag, (tagSet.get(tag) ?? 0) + 1);
-  }
-}
-
-const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
-const categories = [...categorySet.entries()].map(([name, count]) => ({
-  name,
-  displayName: capitalize(name),
-  url: `/categories/${name}/`,
-  count,
-})).sort((a, b) => a.displayName.localeCompare(b.displayName));
-const tags = [...tagSet.entries()].map(([name, count]) => ({
-  name,
-  displayName: capitalize(name),
-  url: `/tags/${name}/`,
-  count,
-})).sort((a, b) => a.displayName.localeCompare(b.displayName));
-
-// ── Shared nav helpers ────────────────────────────────────────────────────────
-
-// Generic tree traversal — works for both NavPage and ProjectNavNode
-function inSubtree<T extends { url: string; children?: T[] }>(node: T, url: string): boolean {
-  if (node.url === url) return true;
-  return (node.children ?? []).some(c => inSubtree(c, url));
-}
-
-// 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  = !isAlias && nodeUrl === currentUrl;
-  const isAncestor = !isAlias && inPath && !isCurrent;
-  const isSibling  = parentUrl === currentParentUrl && !currentIsNode;
-  const isChild    = parentUrl === currentUrl;
-  return !(isCurrent || isAncestor || isSibling || isChild);
-}
-
-// ── About section: static tree — see src/lib/about.ts ───────────────────────
-const _aboutPath         = findAboutPath(aboutSection, currentUrl);
-const onAboutSection     = _aboutPath !== null;
-const currentAboutParent = _aboutPath && _aboutPath.length >= 2 ? _aboutPath.at(-2)!.url : '';
-const currentAboutIsNode = _aboutPath ? (_aboutPath.at(-1)!.isNode ?? false) : false;
-
-// Partial application of nodeOff bound to the About subtree's current-page context
-const aboutNodeOff = (nodeUrl: string, parentUrl: string, inPath: boolean) =>
-  nodeOff(nodeUrl, parentUrl, inPath, currentAboutParent, currentAboutIsNode);
-
-// ── Projects section ──────────────────────────────────────────────────────────
-const activeProject     = await getActiveProject(currentUrl);
-const onProjectsSection = currentUrl === '/projects/' || activeProject !== null;
-// When on a SNAPSHOT page, render that version's tree; otherwise all visible trees.
-const projectTrees = onProjectsSection && activeProject && !activeProject.isCurrent
-  ? [activeProject]
-  : await getVisibleProjectNavTrees();
-
-// Single DFS pass: finds both the current node and its parent in a project tree
-function findNodeAndParent(
-  node: ProjectNavNode,
-  url: string,
-  parent: ProjectNavNode | null = null,
-): { node: ProjectNavNode; parent: ProjectNavNode | null } | null {
-  if (node.url === url) return { node, parent };
-  for (const c of node.children) {
-    const r = findNodeAndParent(c, url, node);
-    if (r) return r;
-  }
-  return null;
-}
-
-const _found                     = activeProject?.navRoot ? findNodeAndParent(activeProject.navRoot, currentUrl) : null;
-const activeProjectCurrentNode   = _found?.node ?? null;
-const activeProjectCurrentParent = _found?.parent ?? null;
-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, isAlias = false) =>
-  nodeOff(nodeUrl, parentUrl, inPath, _projCurrentParentUrl, activeProjectCurrentIsNode, isAlias);
-
-// ── Blog URL classification ────────────────────────────────────────────────────
-const mainSections = [
-  { name: 'blog',     url: '/blog/',      title: 'Blog' },
-  { name: 'projects', url: '/projects/',  title: 'Projects' },
-  { name: 'about',    url: '/about.html', title: 'About' },
-];
-
-const onBlog             = currentUrl === '/blog/';
-const isOnBlogPost       = allPosts.some(p => (p.data.url ?? `/${p.id}/`) === currentUrl);
-const onArchive          = currentUrl.startsWith('/blog/archive/') || isOnBlogPost;
-const onCategoriesIndex  = currentUrl === '/blog/categories/';
-const onCategoryTerm     = currentUrl.startsWith('/categories/');
-const onTagsIndex        = currentUrl === '/blog/tags/';
-const onTagTerm          = currentUrl.startsWith('/tags/');
-const blogSelected       = onBlog || onArchive || onCategoriesIndex || onCategoryTerm || onTagsIndex || onTagTerm;
-
-const categoriesSelected = onCategoriesIndex || onCategoryTerm;
-const tagsSelected       = onTagsIndex || onTagTerm;
-
-const archiveOff    = !blogSelected || onCategoryTerm || onTagTerm;
-const categoriesOff = !blogSelected || onArchive      || onTagTerm;
-const tagsOff       = !blogSelected || onArchive      || onCategoryTerm;
-
-// Active section for #menu highlight
-const activeSection = mainSections.find(s => {
-  if (s.name === 'blog')     return blogSelected;
-  if (s.name === 'about')    return onAboutSection;
-  if (s.name === 'projects') return onProjectsSection;
-  return currentUrl.startsWith(s.url);
-})?.name ?? '';
-
-// Top-level section off-logic
-const blogSectionOff     = onAboutSection || onProjectsSection;
-const projectsSectionOff = blogSelected   || onAboutSection;
-const aboutOff           = blogSelected   || onProjectsSection;
-
-// Active year / taxonomy term helpers
-function isActiveYear(year: string) {
-  return currentUrl === `/blog/archive/${year}/` ||
-    postsByYear[year]?.some(p => postUrl(p) === currentUrl) === true;
-}
-const activeCatSlug  = onCategoryTerm ? currentUrl.replace(/^\/categories\//, '').replace(/\/$/, '') : '';
-const activeTagSlug  = onTagTerm      ? currentUrl.replace(/^\/tags\//, '').replace(/\/$/, '') : '';
----
-
-<nav id="nav">
-  <hr class="n"/>
-  <a class="hide" href="#top" title="Show Content">Jump back to the top of the page</a>
-  <h1 class="nav">Navigation</h1>
-  <div>
-  <h2 id="sitedescription" class="nav menu">Description of the website structure</h2>
-    <ul id="menu" class="cf">
-      {mainSections.map(s => (
-        <li class={`m ${s.name}`}>
-          <a href={s.url} class={activeSection === s.name ? 'm selected' : 'm'}>{s.title}</a><span class="sep">:</span>
-          <span class="description">Wechselnde passende Beschreibung des Bereichs</span>
-        </li>
-      ))}
-    </ul>
-  </div>
-  <div id="sitemap">
-    <h2 class="nav">Sitemap</h2>
-    <div class="home">
-      {currentUrl === '/'
-        ? <strong class="s">
-            <span class="home-surrounding">You are at the </span>Home<span class="home-surrounding">-page of the site</span>
-          </strong>
-        : <a class="s selected" href="/">
-            <span class="home-surrounding">Go back to the </span>Home<span class="home-surrounding">-page of the site</span>
-          </a>
-      }
-    </div>
-    <ul id="submenu" class="s active">
-
-      {/* ── Blog subtree ─────────────────────────────────────────────────────── */}
-      <li class={`s sub${blogSectionOff ? ' off' : ''}`}>
-        {onBlog
-          ? <strong class="s">Blog</strong>
-          : <a href="/blog/" class={blogSelected ? 's selected' : 's'}>
-              {blogSelected ? <strong>Blog</strong> : 'Blog'}
-            </a>
-        }
-        <ul class={blogSelected ? 's active' : 's'}>
-          {/* Archive subtree — always rendered; off-class controls visibility via CSS */}
-          <li class={`s sub${archiveOff ? ' off' : ''}`}>
-            {currentUrl === '/blog/archive/'
-              ? <strong class="s">Archive</strong>
-              : <a href="/blog/archive/" class={onArchive ? 's selected' : 's'}>
-                  {onArchive ? <strong>Archive</strong> : 'Archive'}
-                </a>
-            }
-            <ul class={onArchive ? 's active' : 's'}>
-              {years.map(year => {
-                const active = isActiveYear(year);
-                // Year visible only when: on Archive root (all years are children) or this year is active.
-                // On Blog root, Categories, Tags, outside Blog: all years are off.
-                const yearOff = currentUrl !== '/blog/archive/' && !active;
-                return (
-                  <li class={`s${yearOff ? ' off' : ''}`}>
-                    {currentUrl === `/blog/archive/${year}/`
-                      ? <strong class="s">{year}</strong>
-                      : <a href={`/blog/archive/${year}/`} class={active ? 's selected' : 's'}>
-                          {active ? <strong>{year}</strong> : year}
-                        </a>
-                    }
-                    <ul class="s">
-                      {postsByYear[year].map(post => {
-                        const pUrl = postUrl(post);
-                        const current = pUrl === currentUrl;
-                        return (
-                          <li class="s nav-leaf">
-                            {current
-                              ? <strong class="s">{post.data.title}</strong>
-                              : <a href={pUrl} class="s">{post.data.title}</a>
-                            }
-                          </li>
-                        );
-                      })}
-                    </ul>
-                  </li>
-                );
-              })}
-            </ul>
-          </li>
-          {/* Categories subtree */}
-          {categories.length > 0 && (
-            <li class={`s${categoriesOff ? ' off' : ''}${categoriesSelected ? ' sub' : ''}`}>
-              {onCategoriesIndex
-                ? <strong class="s">Categories</strong>
-                : <a href="/blog/categories/" class={categoriesSelected ? 's selected' : 's'}>
-                    {categoriesSelected ? <strong>Categories</strong> : 'Categories'}
-                  </a>
-              }
-              {categoriesSelected ? (
-                <ul class="s active">
-                  {categories.map(cat => {
-                    const active = cat.name === activeCatSlug;
-                    return (
-                      <li class="s">
-                        {active
-                          ? <strong class="s">{cat.displayName}</strong>
-                          : <a href={cat.url} class="s">{cat.displayName}</a>
-                        }
-                      </li>
-                    );
-                  })}
-                </ul>
-              ) : (
-                <ul class="s">
-                  {categories.map(cat => (
-                    <li class="s nav-leaf">
-                      <a href={cat.url} class="s">{cat.displayName}</a>
-                    </li>
-                  ))}
-                </ul>
-              )}
-            </li>
-          )}
-          {/* Tags subtree */}
-          {tags.length > 0 && (
-            <li class={`s${tagsOff ? ' off' : ''}${tagsSelected ? ' sub' : ''}`}>
-              {onTagsIndex
-                ? <strong class="s">Tags</strong>
-                : <a href="/blog/tags/" class={tagsSelected ? 's selected' : 's'}>
-                    {tagsSelected ? <strong>Tags</strong> : 'Tags'}
-                  </a>
-              }
-              {tagsSelected ? (
-                <ul class="s active">
-                  {tags.map(tag => {
-                    const active = tag.name === activeTagSlug;
-                    return (
-                      <li class="s">
-                        {active
-                          ? <strong class="s">{tag.displayName}</strong>
-                          : <a href={tag.url} class="s">{tag.displayName}</a>
-                        }
-                      </li>
-                    );
-                  })}
-                </ul>
-              ) : (
-                <ul class="s">
-                  {tags.map(tag => (
-                    <li class="s nav-leaf">
-                      <a href={tag.url} class="s">{tag.displayName}</a>
-                    </li>
-                  ))}
-                </ul>
-              )}
-            </li>
-          )}
-        </ul>
-      </li>
-
-      {/* ── Projects subtree ─────────────────────────────────────────────────── */}
-      <li class={`s sub${projectsSectionOff ? ' off' : ''}`}>
-        {currentUrl === '/projects/'
-          ? <strong class="s">Projects</strong>
-          : <a href="/projects/" class={onProjectsSection ? 's selected' : 's'}>
-              {onProjectsSection ? <strong>Projects</strong> : 'Projects'}
-            </a>
-        }
-        <ul class={`s${onProjectsSection ? ' active' : ''}`}>
-          {projectTrees.map(info => {
-            const root       = info.navRoot;
-            const rootInPath = inSubtree(root, currentUrl);
-            const rootOff    = projNodeOff(root.url, '/projects/', rootInPath);
-            return (
-              <li class={`s sub${rootOff ? ' off' : ''}`}>
-                {root.url === currentUrl
-                  ? <strong class="s">{root.title}</strong>
-                  : <a href={root.url} class={rootInPath ? 's selected' : 's'}>
-                      {rootInPath ? <strong>{root.title}</strong> : root.title}
-                    </a>
-                }
-                <ul class={`s${rootInPath ? ' active' : ''}`}>
-                  {root.children.map(child => {
-                    const childInPath = inSubtree(child, currentUrl);
-                    const childOff    = projNodeOff(child.url, root.url, childInPath, child.isAlias);
-                    const childIsNode = child.isNode;
-                    return (
-                      <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'}>
-                              {childInPath ? <strong>{child.title}</strong> : child.title}
-                            </a>
-                        }
-                        {child.isAlias && <span class="alias-hint"> (Schnellzugriff)</span>}
-                        {childIsNode && child.children.length > 0 && (
-                          <ul class={`s${childInPath ? ' active' : ''}`}>
-                            {child.children.map(grand => {
-                              const grandInPath = inSubtree(grand, currentUrl);
-                              const grandOff    = projNodeOff(grand.url, child.url, grandInPath, grand.isAlias);
-                              const grandIsNode = grand.isNode;
-                              return (
-                                <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'}>
-                                        {grandInPath ? <strong>{grand.title}</strong> : grand.title}
-                                      </a>
-                                  }
-                                  {grand.isAlias && <span class="alias-hint"> (Schnellzugriff)</span>}
-                                  {grandIsNode && grand.children.length > 0 && (
-                                    <ul class={`s${grandInPath ? ' active' : ''}`}>
-                                      {grand.children.map(ggrand => {
-                                        const ggInPath = ggrand.url === currentUrl;
-                                        const ggOff    = projNodeOff(ggrand.url, grand.url, ggInPath, ggrand.isAlias);
-                                        return (
-                                          <li class={`s${ggrand.isAlias ? ' alias' : ''}${ggOff ? ' off' : ''}`}>
-                                            {ggInPath
-                                              ? <strong class="s">{ggrand.title}</strong>
-                                              : <a href={ggrand.url} class="s">{ggrand.title}</a>
-                                            }
-                                            {ggrand.isAlias && <span class="alias-hint"> (Schnellzugriff)</span>}
-                                          </li>
-                                        );
-                                      })}
-                                    </ul>
-                                  )}
-                                </li>
-                              );
-                            })}
-                          </ul>
-                        )}
-                      </li>
-                    );
-                  })}
-                </ul>
-              </li>
-            );
-          })}
-        </ul>
-      </li>
-
-      {/* ── About subtree ────────────────────────────────────────────────────── */}
-      <li class={`s sub${aboutOff ? ' off' : ''}`}>
-        {currentUrl === '/about.html'
-          ? <strong class="s">About</strong>
-          : <a href="/about.html" class={onAboutSection ? 's selected' : 's'}>
-              {onAboutSection ? <strong>About</strong> : 'About'}
-            </a>
-        }
-        <ul class={`s${onAboutSection ? ' active' : ''}`}>
-          {aboutSection.children!.map(child => {
-            const childInPath  = inSubtree(child, currentUrl);
-            const childOff     = aboutNodeOff(child.url, '/about.html', childInPath);
-            const childHasSubs = (child.children ?? []).length > 0;
-            return (
-              <li class={`s${childHasSubs ? ' sub' : ''}${childOff ? ' off' : ''}`}>
-                {child.url === currentUrl
-                  ? <strong class="s">{child.title}</strong>
-                  : <a href={child.url} class={childInPath ? 's selected' : 's'}>
-                      {childInPath ? <strong>{child.title}</strong> : child.title}
-                    </a>
-                }
-                {childHasSubs && (
-                  <ul class={`s${childInPath ? ' active' : ''}`}>
-                    {child.children!.map(grand => {
-                      const grandActive = grand.url === currentUrl;
-                      const grandOff    = aboutNodeOff(grand.url, child.url, grandActive);
-                      return (
-                        <li class={`s${grandOff ? ' off' : ''}`}>
-                          {grandActive
-                            ? <strong class="s">{grand.title}</strong>
-                            : <a href={grand.url} class="s">{grand.title}</a>
-                          }
-                        </li>
-                      );
-                    })}
-                  </ul>
-                )}
-              </li>
-            );
-          })}
-        </ul>
-      </li>
-
-    </ul>
-  </div>
-  <hr class="n"/>
-</nav>
-
-<script>
-  const descriptions: Record<string, string[]> = {
-    blog: [
-      'Java-Tipps und Tricks aus dem Entwickleralltag',
-      'Erfahrungsberichte aus Open-Source-Projekten',
-      'Technische Einblicke für Java-Entwickler',
-    ],
-    projects: [
-      'Open-Source-Werkzeuge für Maven und Java',
-      'Nützliche Plugins für den Entwickleralltag',
-      'Bewährte Lösungen für typische Java-Probleme',
-    ],
-    about: [
-      'Wer steckt hinter juplo.de?',
-      'Kontakt, Impressum und rechtliche Hinweise',
-      'Über den Autor und seine Arbeit',
-    ],
-  };
-
-  const indices: Record<string, number> = {};
-  const menuItems = document.querySelectorAll<HTMLElement>('#menu > li.m');
-
-  menuItems.forEach(li => {
-    const section = Array.from(li.classList).find(c => c !== 'm');
-    if (!section || !descriptions[section]) return;
-    const span = li.querySelector<HTMLElement>('span.description');
-    if (!span) return;
-    indices[section] = 0;
-    span.textContent = descriptions[section][0];
-  });
-
-  setInterval(() => {
-    menuItems.forEach(li => {
-      const section = Array.from(li.classList).find(c => c !== 'm');
-      if (!section || !descriptions[section]) return;
-      const span = li.querySelector<HTMLElement>('span.description');
-      if (!span) return;
-      indices[section] = (indices[section] + 1) % descriptions[section].length;
-      span.textContent = descriptions[section][indices[section]];
-    });
-  }, 5000);
-</script>
index 74533f7e06e1897ee7ca46c80ee099daf3809dab..e723a128f766fd457b3d9ea2f84405e7aa38db99 100644 (file)
@@ -4,7 +4,7 @@ import { codeToHtml, bundledLanguages } from 'shiki';
 import hljs from 'highlight.js';
 import BaseLayout from '../layouts/BaseLayout.astro';
 import Breadcrumb from '../components/Breadcrumb.astro';
-import BlogNav from '../components/BlogNav.astro';
+import SitemapNav from '../components/SitemapNav.astro';
 import { getActiveProject, findCanonicalPath } from '../lib/projects';
 
 interface Props {
@@ -119,6 +119,6 @@ const body = await highlightBody(entry.body);
   </Fragment>
 
   <Fragment slot="menu">
-    <BlogNav currentUrl={url} />
+    <SitemapNav currentUrl={url} />
   </Fragment>
 </BaseLayout>
diff --git a/src/components/SitemapNav.astro b/src/components/SitemapNav.astro
new file mode 100644 (file)
index 0000000..77d6805
--- /dev/null
@@ -0,0 +1,486 @@
+---
+import type { CollectionEntry } from 'astro:content';
+import { getAllPosts, postUrl } from '../lib/posts';
+import { getActiveProject, getVisibleProjectNavTrees, type ProjectNavNode } from '../lib/projects';
+import { aboutSection, findAboutPath } from '../lib/about';
+
+interface Props {
+  currentUrl: string;
+}
+
+const { currentUrl } = Astro.props;
+
+const allPosts = await getAllPosts();
+
+// Group posts by year, years sorted descending
+const postsByYear: Record<string, CollectionEntry<'blog'>[]> = {};
+for (const post of allPosts) {
+  const year = post.data.date.getFullYear().toString();
+  if (!postsByYear[year]) postsByYear[year] = [];
+  postsByYear[year].push(post);
+}
+const years = Object.keys(postsByYear).sort((a, b) => Number(b) - Number(a));
+
+// Collect unique categories and tags from all posts
+const categorySet = new Map<string, number>();
+const tagSet = new Map<string, number>();
+for (const post of allPosts) {
+  for (const cat of post.data.categories ?? []) {
+    categorySet.set(cat, (categorySet.get(cat) ?? 0) + 1);
+  }
+  for (const tag of post.data.tags ?? []) {
+    tagSet.set(tag, (tagSet.get(tag) ?? 0) + 1);
+  }
+}
+
+const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
+const categories = [...categorySet.entries()].map(([name, count]) => ({
+  name,
+  displayName: capitalize(name),
+  url: `/categories/${name}/`,
+  count,
+})).sort((a, b) => a.displayName.localeCompare(b.displayName));
+const tags = [...tagSet.entries()].map(([name, count]) => ({
+  name,
+  displayName: capitalize(name),
+  url: `/tags/${name}/`,
+  count,
+})).sort((a, b) => a.displayName.localeCompare(b.displayName));
+
+// ── Shared nav helpers ────────────────────────────────────────────────────────
+
+// Generic tree traversal — works for both NavPage and ProjectNavNode
+function inSubtree<T extends { url: string; children?: T[] }>(node: T, url: string): boolean {
+  if (node.url === url) return true;
+  return (node.children ?? []).some(c => inSubtree(c, url));
+}
+
+// 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  = !isAlias && nodeUrl === currentUrl;
+  const isAncestor = !isAlias && inPath && !isCurrent;
+  const isSibling  = parentUrl === currentParentUrl && !currentIsNode;
+  const isChild    = parentUrl === currentUrl;
+  return !(isCurrent || isAncestor || isSibling || isChild);
+}
+
+// ── About section: static tree — see src/lib/about.ts ───────────────────────
+const _aboutPath         = findAboutPath(aboutSection, currentUrl);
+const onAboutSection     = _aboutPath !== null;
+const currentAboutParent = _aboutPath && _aboutPath.length >= 2 ? _aboutPath.at(-2)!.url : '';
+const currentAboutIsNode = _aboutPath ? (_aboutPath.at(-1)!.isNode ?? false) : false;
+
+// Partial application of nodeOff bound to the About subtree's current-page context
+const aboutNodeOff = (nodeUrl: string, parentUrl: string, inPath: boolean) =>
+  nodeOff(nodeUrl, parentUrl, inPath, currentAboutParent, currentAboutIsNode);
+
+// ── Projects section ──────────────────────────────────────────────────────────
+const activeProject     = await getActiveProject(currentUrl);
+const onProjectsSection = currentUrl === '/projects/' || activeProject !== null;
+// When on a SNAPSHOT page, render that version's tree; otherwise all visible trees.
+const projectTrees = onProjectsSection && activeProject && !activeProject.isCurrent
+  ? [activeProject]
+  : await getVisibleProjectNavTrees();
+
+// Single DFS pass: finds both the current node and its parent in a project tree
+function findNodeAndParent(
+  node: ProjectNavNode,
+  url: string,
+  parent: ProjectNavNode | null = null,
+): { node: ProjectNavNode; parent: ProjectNavNode | null } | null {
+  if (node.url === url) return { node, parent };
+  for (const c of node.children) {
+    const r = findNodeAndParent(c, url, node);
+    if (r) return r;
+  }
+  return null;
+}
+
+const _found                     = activeProject?.navRoot ? findNodeAndParent(activeProject.navRoot, currentUrl) : null;
+const activeProjectCurrentNode   = _found?.node ?? null;
+const activeProjectCurrentParent = _found?.parent ?? null;
+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, isAlias = false) =>
+  nodeOff(nodeUrl, parentUrl, inPath, _projCurrentParentUrl, activeProjectCurrentIsNode, isAlias);
+
+// ── Blog URL classification ────────────────────────────────────────────────────
+const mainSections = [
+  { name: 'blog',     url: '/blog/',      title: 'Blog' },
+  { name: 'projects', url: '/projects/',  title: 'Projects' },
+  { name: 'about',    url: '/about.html', title: 'About' },
+];
+
+const onBlog             = currentUrl === '/blog/';
+const isOnBlogPost       = allPosts.some(p => (p.data.url ?? `/${p.id}/`) === currentUrl);
+const onArchive          = currentUrl.startsWith('/blog/archive/') || isOnBlogPost;
+const onCategoriesIndex  = currentUrl === '/blog/categories/';
+const onCategoryTerm     = currentUrl.startsWith('/categories/');
+const onTagsIndex        = currentUrl === '/blog/tags/';
+const onTagTerm          = currentUrl.startsWith('/tags/');
+const blogSelected       = onBlog || onArchive || onCategoriesIndex || onCategoryTerm || onTagsIndex || onTagTerm;
+
+const categoriesSelected = onCategoriesIndex || onCategoryTerm;
+const tagsSelected       = onTagsIndex || onTagTerm;
+
+const archiveOff    = !blogSelected || onCategoryTerm || onTagTerm;
+const categoriesOff = !blogSelected || onArchive      || onTagTerm;
+const tagsOff       = !blogSelected || onArchive      || onCategoryTerm;
+
+// Active section for #menu highlight
+const activeSection = mainSections.find(s => {
+  if (s.name === 'blog')     return blogSelected;
+  if (s.name === 'about')    return onAboutSection;
+  if (s.name === 'projects') return onProjectsSection;
+  return currentUrl.startsWith(s.url);
+})?.name ?? '';
+
+// Top-level section off-logic
+const blogSectionOff     = onAboutSection || onProjectsSection;
+const projectsSectionOff = blogSelected   || onAboutSection;
+const aboutOff           = blogSelected   || onProjectsSection;
+
+// Active year / taxonomy term helpers
+function isActiveYear(year: string) {
+  return currentUrl === `/blog/archive/${year}/` ||
+    postsByYear[year]?.some(p => postUrl(p) === currentUrl) === true;
+}
+const activeCatSlug  = onCategoryTerm ? currentUrl.replace(/^\/categories\//, '').replace(/\/$/, '') : '';
+const activeTagSlug  = onTagTerm      ? currentUrl.replace(/^\/tags\//, '').replace(/\/$/, '') : '';
+---
+
+<nav id="nav">
+  <hr class="n"/>
+  <a class="hide" href="#top" title="Show Content">Jump back to the top of the page</a>
+  <h1 class="nav">Navigation</h1>
+  <div>
+  <h2 id="sitedescription" class="nav menu">Description of the website structure</h2>
+    <ul id="menu" class="cf">
+      {mainSections.map(s => (
+        <li class={`m ${s.name}`}>
+          <a href={s.url} class={activeSection === s.name ? 'm selected' : 'm'}>{s.title}</a><span class="sep">:</span>
+          <span class="description">Wechselnde passende Beschreibung des Bereichs</span>
+        </li>
+      ))}
+    </ul>
+  </div>
+  <div id="sitemap">
+    <h2 class="nav">Sitemap</h2>
+    <div class="home">
+      {currentUrl === '/'
+        ? <strong class="s">
+            <span class="home-surrounding">You are at the </span>Home<span class="home-surrounding">-page of the site</span>
+          </strong>
+        : <a class="s selected" href="/">
+            <span class="home-surrounding">Go back to the </span>Home<span class="home-surrounding">-page of the site</span>
+          </a>
+      }
+    </div>
+    <ul id="submenu" class="s active">
+
+      {/* ── Blog subtree ─────────────────────────────────────────────────────── */}
+      <li class={`s sub${blogSectionOff ? ' off' : ''}`}>
+        {onBlog
+          ? <strong class="s">Blog</strong>
+          : <a href="/blog/" class={blogSelected ? 's selected' : 's'}>
+              {blogSelected ? <strong>Blog</strong> : 'Blog'}
+            </a>
+        }
+        <ul class={blogSelected ? 's active' : 's'}>
+          {/* Archive subtree — always rendered; off-class controls visibility via CSS */}
+          <li class={`s sub${archiveOff ? ' off' : ''}`}>
+            {currentUrl === '/blog/archive/'
+              ? <strong class="s">Archive</strong>
+              : <a href="/blog/archive/" class={onArchive ? 's selected' : 's'}>
+                  {onArchive ? <strong>Archive</strong> : 'Archive'}
+                </a>
+            }
+            <ul class={onArchive ? 's active' : 's'}>
+              {years.map(year => {
+                const active = isActiveYear(year);
+                // Year visible only when: on Archive root (all years are children) or this year is active.
+                // On Blog root, Categories, Tags, outside Blog: all years are off.
+                const yearOff = currentUrl !== '/blog/archive/' && !active;
+                return (
+                  <li class={`s${yearOff ? ' off' : ''}`}>
+                    {currentUrl === `/blog/archive/${year}/`
+                      ? <strong class="s">{year}</strong>
+                      : <a href={`/blog/archive/${year}/`} class={active ? 's selected' : 's'}>
+                          {active ? <strong>{year}</strong> : year}
+                        </a>
+                    }
+                    <ul class="s">
+                      {postsByYear[year].map(post => {
+                        const pUrl = postUrl(post);
+                        const current = pUrl === currentUrl;
+                        return (
+                          <li class="s nav-leaf">
+                            {current
+                              ? <strong class="s">{post.data.title}</strong>
+                              : <a href={pUrl} class="s">{post.data.title}</a>
+                            }
+                          </li>
+                        );
+                      })}
+                    </ul>
+                  </li>
+                );
+              })}
+            </ul>
+          </li>
+          {/* Categories subtree */}
+          {categories.length > 0 && (
+            <li class={`s${categoriesOff ? ' off' : ''}${categoriesSelected ? ' sub' : ''}`}>
+              {onCategoriesIndex
+                ? <strong class="s">Categories</strong>
+                : <a href="/blog/categories/" class={categoriesSelected ? 's selected' : 's'}>
+                    {categoriesSelected ? <strong>Categories</strong> : 'Categories'}
+                  </a>
+              }
+              {categoriesSelected ? (
+                <ul class="s active">
+                  {categories.map(cat => {
+                    const active = cat.name === activeCatSlug;
+                    return (
+                      <li class="s">
+                        {active
+                          ? <strong class="s">{cat.displayName}</strong>
+                          : <a href={cat.url} class="s">{cat.displayName}</a>
+                        }
+                      </li>
+                    );
+                  })}
+                </ul>
+              ) : (
+                <ul class="s">
+                  {categories.map(cat => (
+                    <li class="s nav-leaf">
+                      <a href={cat.url} class="s">{cat.displayName}</a>
+                    </li>
+                  ))}
+                </ul>
+              )}
+            </li>
+          )}
+          {/* Tags subtree */}
+          {tags.length > 0 && (
+            <li class={`s${tagsOff ? ' off' : ''}${tagsSelected ? ' sub' : ''}`}>
+              {onTagsIndex
+                ? <strong class="s">Tags</strong>
+                : <a href="/blog/tags/" class={tagsSelected ? 's selected' : 's'}>
+                    {tagsSelected ? <strong>Tags</strong> : 'Tags'}
+                  </a>
+              }
+              {tagsSelected ? (
+                <ul class="s active">
+                  {tags.map(tag => {
+                    const active = tag.name === activeTagSlug;
+                    return (
+                      <li class="s">
+                        {active
+                          ? <strong class="s">{tag.displayName}</strong>
+                          : <a href={tag.url} class="s">{tag.displayName}</a>
+                        }
+                      </li>
+                    );
+                  })}
+                </ul>
+              ) : (
+                <ul class="s">
+                  {tags.map(tag => (
+                    <li class="s nav-leaf">
+                      <a href={tag.url} class="s">{tag.displayName}</a>
+                    </li>
+                  ))}
+                </ul>
+              )}
+            </li>
+          )}
+        </ul>
+      </li>
+
+      {/* ── Projects subtree ─────────────────────────────────────────────────── */}
+      <li class={`s sub${projectsSectionOff ? ' off' : ''}`}>
+        {currentUrl === '/projects/'
+          ? <strong class="s">Projects</strong>
+          : <a href="/projects/" class={onProjectsSection ? 's selected' : 's'}>
+              {onProjectsSection ? <strong>Projects</strong> : 'Projects'}
+            </a>
+        }
+        <ul class={`s${onProjectsSection ? ' active' : ''}`}>
+          {projectTrees.map(info => {
+            const root       = info.navRoot;
+            const rootInPath = inSubtree(root, currentUrl);
+            const rootOff    = projNodeOff(root.url, '/projects/', rootInPath);
+            return (
+              <li class={`s sub${rootOff ? ' off' : ''}`}>
+                {root.url === currentUrl
+                  ? <strong class="s">{root.title}</strong>
+                  : <a href={root.url} class={rootInPath ? 's selected' : 's'}>
+                      {rootInPath ? <strong>{root.title}</strong> : root.title}
+                    </a>
+                }
+                <ul class={`s${rootInPath ? ' active' : ''}`}>
+                  {root.children.map(child => {
+                    const childInPath = inSubtree(child, currentUrl);
+                    const childOff    = projNodeOff(child.url, root.url, childInPath, child.isAlias);
+                    const childIsNode = child.isNode;
+                    return (
+                      <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'}>
+                              {childInPath ? <strong>{child.title}</strong> : child.title}
+                            </a>
+                        }
+                        {child.isAlias && <span class="alias-hint"> (Schnellzugriff)</span>}
+                        {childIsNode && child.children.length > 0 && (
+                          <ul class={`s${childInPath ? ' active' : ''}`}>
+                            {child.children.map(grand => {
+                              const grandInPath = inSubtree(grand, currentUrl);
+                              const grandOff    = projNodeOff(grand.url, child.url, grandInPath, grand.isAlias);
+                              const grandIsNode = grand.isNode;
+                              return (
+                                <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'}>
+                                        {grandInPath ? <strong>{grand.title}</strong> : grand.title}
+                                      </a>
+                                  }
+                                  {grand.isAlias && <span class="alias-hint"> (Schnellzugriff)</span>}
+                                  {grandIsNode && grand.children.length > 0 && (
+                                    <ul class={`s${grandInPath ? ' active' : ''}`}>
+                                      {grand.children.map(ggrand => {
+                                        const ggInPath = ggrand.url === currentUrl;
+                                        const ggOff    = projNodeOff(ggrand.url, grand.url, ggInPath, ggrand.isAlias);
+                                        return (
+                                          <li class={`s${ggrand.isAlias ? ' alias' : ''}${ggOff ? ' off' : ''}`}>
+                                            {ggInPath
+                                              ? <strong class="s">{ggrand.title}</strong>
+                                              : <a href={ggrand.url} class="s">{ggrand.title}</a>
+                                            }
+                                            {ggrand.isAlias && <span class="alias-hint"> (Schnellzugriff)</span>}
+                                          </li>
+                                        );
+                                      })}
+                                    </ul>
+                                  )}
+                                </li>
+                              );
+                            })}
+                          </ul>
+                        )}
+                      </li>
+                    );
+                  })}
+                </ul>
+              </li>
+            );
+          })}
+        </ul>
+      </li>
+
+      {/* ── About subtree ────────────────────────────────────────────────────── */}
+      <li class={`s sub${aboutOff ? ' off' : ''}`}>
+        {currentUrl === '/about.html'
+          ? <strong class="s">About</strong>
+          : <a href="/about.html" class={onAboutSection ? 's selected' : 's'}>
+              {onAboutSection ? <strong>About</strong> : 'About'}
+            </a>
+        }
+        <ul class={`s${onAboutSection ? ' active' : ''}`}>
+          {aboutSection.children!.map(child => {
+            const childInPath  = inSubtree(child, currentUrl);
+            const childOff     = aboutNodeOff(child.url, '/about.html', childInPath);
+            const childHasSubs = (child.children ?? []).length > 0;
+            return (
+              <li class={`s${childHasSubs ? ' sub' : ''}${childOff ? ' off' : ''}`}>
+                {child.url === currentUrl
+                  ? <strong class="s">{child.title}</strong>
+                  : <a href={child.url} class={childInPath ? 's selected' : 's'}>
+                      {childInPath ? <strong>{child.title}</strong> : child.title}
+                    </a>
+                }
+                {childHasSubs && (
+                  <ul class={`s${childInPath ? ' active' : ''}`}>
+                    {child.children!.map(grand => {
+                      const grandActive = grand.url === currentUrl;
+                      const grandOff    = aboutNodeOff(grand.url, child.url, grandActive);
+                      return (
+                        <li class={`s${grandOff ? ' off' : ''}`}>
+                          {grandActive
+                            ? <strong class="s">{grand.title}</strong>
+                            : <a href={grand.url} class="s">{grand.title}</a>
+                          }
+                        </li>
+                      );
+                    })}
+                  </ul>
+                )}
+              </li>
+            );
+          })}
+        </ul>
+      </li>
+
+    </ul>
+  </div>
+  <hr class="n"/>
+</nav>
+
+<script>
+  const descriptions: Record<string, string[]> = {
+    blog: [
+      'Java-Tipps und Tricks aus dem Entwickleralltag',
+      'Erfahrungsberichte aus Open-Source-Projekten',
+      'Technische Einblicke für Java-Entwickler',
+    ],
+    projects: [
+      'Open-Source-Werkzeuge für Maven und Java',
+      'Nützliche Plugins für den Entwickleralltag',
+      'Bewährte Lösungen für typische Java-Probleme',
+    ],
+    about: [
+      'Wer steckt hinter juplo.de?',
+      'Kontakt, Impressum und rechtliche Hinweise',
+      'Über den Autor und seine Arbeit',
+    ],
+  };
+
+  const indices: Record<string, number> = {};
+  const menuItems = document.querySelectorAll<HTMLElement>('#menu > li.m');
+
+  menuItems.forEach(li => {
+    const section = Array.from(li.classList).find(c => c !== 'm');
+    if (!section || !descriptions[section]) return;
+    const span = li.querySelector<HTMLElement>('span.description');
+    if (!span) return;
+    indices[section] = 0;
+    span.textContent = descriptions[section][0];
+  });
+
+  setInterval(() => {
+    menuItems.forEach(li => {
+      const section = Array.from(li.classList).find(c => c !== 'm');
+      if (!section || !descriptions[section]) return;
+      const span = li.querySelector<HTMLElement>('span.description');
+      if (!span) return;
+      indices[section] = (indices[section] + 1) % descriptions[section].length;
+      span.textContent = descriptions[section][indices[section]];
+    });
+  }, 5000);
+</script>
index 147466cdfdff420c2d2c2a38894866088a82388f..aad427fabbda8af4efeedc873bb9f96184d764fa 100644 (file)
@@ -2,7 +2,7 @@
 import { render } from 'astro:content';
 import BaseLayout from '../layouts/BaseLayout.astro';
 import Breadcrumb from '../components/Breadcrumb.astro';
-import BlogNav from '../components/BlogNav.astro';
+import SitemapNav from '../components/SitemapNav.astro';
 import TaxonomyAside from '../components/TaxonomyAside.astro';
 import { getAllPosts, postUrl as getPostUrl, getSummary } from '../lib/posts';
 
@@ -74,7 +74,7 @@ const dateDisplay = date.toLocaleDateString('en-US', { month: 'long', day: 'nume
   </Fragment>
 
   <Fragment slot="menu">
-    <BlogNav currentUrl={postUrl} />
+    <SitemapNav currentUrl={postUrl} />
   </Fragment>
 
   <Fragment slot="marginalcontent">
index 23920f6a5bc1aec3c660c5121b62b63c0ac2a740..635ffa5c2e90842b10706dc584f003a5738e7ec8 100644 (file)
@@ -1,7 +1,7 @@
 ---
 import BaseLayout from '../layouts/BaseLayout.astro';
 import Breadcrumb from '../components/Breadcrumb.astro';
-import BlogNav from '../components/BlogNav.astro';
+import SitemapNav from '../components/SitemapNav.astro';
 import { aboutSection, findAboutPath } from '../lib/about';
 
 const _path = findAboutPath(aboutSection, '/about.html')!;
@@ -58,7 +58,7 @@ const crumbs = [
   </Fragment>
 
   <Fragment slot="menu">
-    <BlogNav currentUrl="/about.html" />
+    <SitemapNav currentUrl="/about.html" />
   </Fragment>
 
   <Fragment slot="marginalcontent">
index 1253ef1873974c619021e5244f054d42e09247af..463946197b389de230c524d3eeb931060ed04eff 100644 (file)
@@ -1,7 +1,7 @@
 ---
 import BaseLayout from '../layouts/BaseLayout.astro';
 import Breadcrumb from '../components/Breadcrumb.astro';
-import BlogNav from '../components/BlogNav.astro';
+import SitemapNav from '../components/SitemapNav.astro';
 import { aboutSection, findAboutPath } from '../lib/about';
 
 const _path = findAboutPath(aboutSection, '/agb.html')!;
@@ -150,6 +150,6 @@ const crumbs = [
   </Fragment>
 
   <Fragment slot="menu">
-    <BlogNav currentUrl="/agb.html" />
+    <SitemapNav currentUrl="/agb.html" />
   </Fragment>
 </BaseLayout>
index 270f9823edd8be9f75210e4536ffe3c239ee05b9..be9d0bb3ec81f1ee9980cb2fe5d85d3a6304cabf 100644 (file)
@@ -1,7 +1,7 @@
 ---
 import BaseLayout from '../../../layouts/BaseLayout.astro';
 import Breadcrumb from '../../../components/Breadcrumb.astro';
-import BlogNav from '../../../components/BlogNav.astro';
+import SitemapNav from '../../../components/SitemapNav.astro';
 import { getAllPosts, postUrl, wordCount } from '../../../lib/posts';
 
 export async function getStaticPaths() {
@@ -62,6 +62,6 @@ const crumbs = [
   </Fragment>
 
   <Fragment slot="menu">
-    <BlogNav currentUrl={currentUrl} />
+    <SitemapNav currentUrl={currentUrl} />
   </Fragment>
 </BaseLayout>
index ac9dffd53af56b7f9a139d87cb4675513f6907c9..72e6917cd6cf8d385ea55103b71f75fd80990da6 100644 (file)
@@ -1,7 +1,7 @@
 ---
 import BaseLayout from '../../../layouts/BaseLayout.astro';
 import Breadcrumb from '../../../components/Breadcrumb.astro';
-import BlogNav from '../../../components/BlogNav.astro';
+import SitemapNav from '../../../components/SitemapNav.astro';
 import { getAllPosts, postUrl, wordCount } from '../../../lib/posts';
 
 const currentUrl = '/blog/archive/';
@@ -59,6 +59,6 @@ const crumbs = [
   </Fragment>
 
   <Fragment slot="menu">
-    <BlogNav currentUrl={currentUrl} />
+    <SitemapNav currentUrl={currentUrl} />
   </Fragment>
 </BaseLayout>
index 64d7e5f3c34fddb6b265a15dd5bb6c86c0fa2981..6c2e4b68e43e438a670cb7a81461b3ba256858e6 100644 (file)
@@ -2,7 +2,7 @@
 import { getCollection } from 'astro:content';
 import BaseLayout from '../../../layouts/BaseLayout.astro';
 import Breadcrumb from '../../../components/Breadcrumb.astro';
-import BlogNav from '../../../components/BlogNav.astro';
+import SitemapNav from '../../../components/SitemapNav.astro';
 
 const currentUrl = '/blog/categories/';
 
@@ -47,6 +47,6 @@ const crumbs = [
   </Fragment>
 
   <Fragment slot="menu">
-    <BlogNav currentUrl={currentUrl} />
+    <SitemapNav currentUrl={currentUrl} />
   </Fragment>
 </BaseLayout>
index cb2d7ae6e7b488bd5c088fe2eb73591c0e23e9e2..0e3ed5f8c9268765ad8b4b410907a4ac8917dd59 100644 (file)
@@ -1,7 +1,7 @@
 ---
 import BaseLayout from '../../layouts/BaseLayout.astro';
 import Breadcrumb from '../../components/Breadcrumb.astro';
-import BlogNav from '../../components/BlogNav.astro';
+import SitemapNav from '../../components/SitemapNav.astro';
 import { getAllPosts, postUrl, getSummary, wordCount } from '../../lib/posts';
 
 const currentUrl = '/blog/';
@@ -109,6 +109,6 @@ const crumbs = [
   </Fragment>
 
   <Fragment slot="menu">
-    <BlogNav currentUrl={currentUrl} />
+    <SitemapNav currentUrl={currentUrl} />
   </Fragment>
 </BaseLayout>
index 6a27f525d801b2633dbec8e44ac74d3b26551a20..5c4d5570f286eb23f3a93e32a187cd84ea00f7d2 100644 (file)
@@ -2,7 +2,7 @@
 import { getCollection } from 'astro:content';
 import BaseLayout from '../../../layouts/BaseLayout.astro';
 import Breadcrumb from '../../../components/Breadcrumb.astro';
-import BlogNav from '../../../components/BlogNav.astro';
+import SitemapNav from '../../../components/SitemapNav.astro';
 
 const currentUrl = '/blog/tags/';
 
@@ -47,6 +47,6 @@ const crumbs = [
   </Fragment>
 
   <Fragment slot="menu">
-    <BlogNav currentUrl={currentUrl} />
+    <SitemapNav currentUrl={currentUrl} />
   </Fragment>
 </BaseLayout>
index 9505e9aef5de284d19ba5c8ad687395ea607d19a..14acd16c9e34e21a390022536a30592f0fb93b0d 100644 (file)
@@ -1,7 +1,7 @@
 ---
 import BaseLayout from '../../layouts/BaseLayout.astro';
 import Breadcrumb from '../../components/Breadcrumb.astro';
-import BlogNav from '../../components/BlogNav.astro';
+import SitemapNav from '../../components/SitemapNav.astro';
 import { getAllPosts, postUrl, wordCount } from '../../lib/posts';
 
 export async function getStaticPaths() {
@@ -78,6 +78,6 @@ const crumbs = [
   </Fragment>
 
   <Fragment slot="menu">
-    <BlogNav currentUrl={currentUrl} />
+    <SitemapNav currentUrl={currentUrl} />
   </Fragment>
 </BaseLayout>
index 7cc610dfdce57a17dbc11568444d0750eedd32fe..658a61a177385b8d2521240949650e1609968874 100644 (file)
@@ -1,7 +1,7 @@
 ---
 import BaseLayout from '../layouts/BaseLayout.astro';
 import Breadcrumb from '../components/Breadcrumb.astro';
-import BlogNav from '../components/BlogNav.astro';
+import SitemapNav from '../components/SitemapNav.astro';
 import { aboutSection, findAboutPath } from '../lib/about';
 
 const _path = findAboutPath(aboutSection, '/contact.html')!;
@@ -44,6 +44,6 @@ const crumbs = [
   </Fragment>
 
   <Fragment slot="menu">
-    <BlogNav currentUrl="/contact.html" />
+    <SitemapNav currentUrl="/contact.html" />
   </Fragment>
 </BaseLayout>
index 25a87a30919350a209dae98d4f4c151e0acc3245..89e89b89a62539ab48d445173155a8b391345811 100644 (file)
@@ -1,7 +1,7 @@
 ---
 import BaseLayout from '../layouts/BaseLayout.astro';
 import Breadcrumb from '../components/Breadcrumb.astro';
-import BlogNav from '../components/BlogNav.astro';
+import SitemapNav from '../components/SitemapNav.astro';
 import { aboutSection, findAboutPath } from '../lib/about';
 
 const _path = findAboutPath(aboutSection, '/datenschutz.html')!;
@@ -107,7 +107,7 @@ const crumbs = [
   </Fragment>
 
   <Fragment slot="menu">
-    <BlogNav currentUrl="/datenschutz.html" />
+    <SitemapNav currentUrl="/datenschutz.html" />
   </Fragment>
 
   <Fragment slot="footerlinks">
index d505cd4351e6b8dc639f8e29877ff615d9bea014..fa577b89d0438be06e5c8350c71ecb8b5490f099 100644 (file)
@@ -1,7 +1,7 @@
 ---
 import BaseLayout from '../layouts/BaseLayout.astro';
 import Breadcrumb from '../components/Breadcrumb.astro';
-import BlogNav from '../components/BlogNav.astro';
+import SitemapNav from '../components/SitemapNav.astro';
 import { aboutSection, findAboutPath } from '../lib/about';
 
 const _path = findAboutPath(aboutSection, '/google-analytics.html')!;
@@ -50,6 +50,6 @@ const crumbs = [
   </Fragment>
 
   <Fragment slot="menu">
-    <BlogNav currentUrl="/google-analytics.html" />
+    <SitemapNav currentUrl="/google-analytics.html" />
   </Fragment>
 </BaseLayout>
index 04bb17ec55b11452ff56f103a137c98c269ac525..7fae5a7863812c7c7aadc0f52f4690c81e42ae8e 100644 (file)
@@ -1,7 +1,7 @@
 ---
 import BaseLayout from '../layouts/BaseLayout.astro';
 import Breadcrumb from '../components/Breadcrumb.astro';
-import BlogNav from '../components/BlogNav.astro';
+import SitemapNav from '../components/SitemapNav.astro';
 import { aboutSection, findAboutPath } from '../lib/about';
 
 const _path = findAboutPath(aboutSection, '/haftung-inhalte.html')!;
@@ -33,6 +33,6 @@ const crumbs = [
   </Fragment>
 
   <Fragment slot="menu">
-    <BlogNav currentUrl="/haftung-inhalte.html" />
+    <SitemapNav currentUrl="/haftung-inhalte.html" />
   </Fragment>
 </BaseLayout>
index 2b77c912f1d611085da923c8250bc8e2a9db6cc4..1dbc88af465d9c4e845895a3aa73599951ad9c9a 100644 (file)
@@ -1,7 +1,7 @@
 ---
 import BaseLayout from '../layouts/BaseLayout.astro';
 import Breadcrumb from '../components/Breadcrumb.astro';
-import BlogNav from '../components/BlogNav.astro';
+import SitemapNav from '../components/SitemapNav.astro';
 import { aboutSection, findAboutPath } from '../lib/about';
 
 const _path = findAboutPath(aboutSection, '/haftung-links.html')!;
@@ -31,6 +31,6 @@ const crumbs = [
   </Fragment>
 
   <Fragment slot="menu">
-    <BlogNav currentUrl="/haftung-links.html" />
+    <SitemapNav currentUrl="/haftung-links.html" />
   </Fragment>
 </BaseLayout>
index b78e652fb73972fd92cd46e6cdccf343843fb6cc..e0550535c702e63ba3f7c45e28999eb7f50c71e7 100644 (file)
@@ -1,7 +1,7 @@
 ---
 import BaseLayout from '../layouts/BaseLayout.astro';
 import Breadcrumb from '../components/Breadcrumb.astro';
-import BlogNav from '../components/BlogNav.astro';
+import SitemapNav from '../components/SitemapNav.astro';
 import { aboutSection, findAboutPath } from '../lib/about';
 
 const _path = findAboutPath(aboutSection, '/impressum.html')!;
@@ -40,7 +40,7 @@ const crumbs = [
   </Fragment>
 
   <Fragment slot="menu">
-    <BlogNav currentUrl="/impressum.html" />
+    <SitemapNav currentUrl="/impressum.html" />
   </Fragment>
 
   <Fragment slot="footerlinks">
index 8d49bc90f6f7c90d72fbcd620b00f5dc5ab1d98f..9eb13e3457968fa69e5405f439a2ff1d2cc3f285 100644 (file)
@@ -1,6 +1,6 @@
 ---
 import BaseLayout from '../layouts/BaseLayout.astro';
-import BlogNav from '../components/BlogNav.astro';
+import SitemapNav from '../components/SitemapNav.astro';
 import { getAllPosts, postUrl } from '../lib/posts';
 
 const currentUrl = '/';
@@ -55,6 +55,6 @@ const recentPosts = (await getAllPosts()).slice(0, 5);
   </Fragment>
 
   <Fragment slot="menu">
-    <BlogNav currentUrl={currentUrl} />
+    <SitemapNav currentUrl={currentUrl} />
   </Fragment>
 </BaseLayout>
index e7930bb64761f881cd84828ec5c68a4a9bf985d0..978332f48a3dbcfff63bd486ca390bb7dc8d8b73 100644 (file)
@@ -1,7 +1,7 @@
 ---
 import BaseLayout from '../../layouts/BaseLayout.astro';
 import Breadcrumb from '../../components/Breadcrumb.astro';
-import BlogNav from '../../components/BlogNav.astro';
+import SitemapNav from '../../components/SitemapNav.astro';
 import { getVisibleProjectNavTrees } from '../../lib/projects';
 
 const projects = await getVisibleProjectNavTrees();
@@ -26,6 +26,6 @@ const crumbs = [{ title: 'Home', url: '/' }, { title: 'Projects' }];
   </Fragment>
 
   <Fragment slot="menu">
-    <BlogNav currentUrl="/projects/" />
+    <SitemapNav currentUrl="/projects/" />
   </Fragment>
 </BaseLayout>
index e29b3bf0e7bb9951f5a27aae360d190a6ddfbf32..29f942da97218a8f1a124589d6ce7f61f7f2236a 100644 (file)
@@ -1,7 +1,7 @@
 ---
 import BaseLayout from '../../layouts/BaseLayout.astro';
 import Breadcrumb from '../../components/Breadcrumb.astro';
-import BlogNav from '../../components/BlogNav.astro';
+import SitemapNav from '../../components/SitemapNav.astro';
 import { getAllPosts, postUrl, wordCount } from '../../lib/posts';
 
 export async function getStaticPaths() {
@@ -78,6 +78,6 @@ const crumbs = [
   </Fragment>
 
   <Fragment slot="menu">
-    <BlogNav currentUrl={currentUrl} />
+    <SitemapNav currentUrl={currentUrl} />
   </Fragment>
 </BaseLayout>
index 8158ad9310dd27edfcb2f27c3ce043d2bd68cc4a..550ee21b05c68bbbe8fd4aa41208e40540256d79 100644 (file)
@@ -1,7 +1,7 @@
 ---
 import BaseLayout from '../layouts/BaseLayout.astro';
 import Breadcrumb from '../components/Breadcrumb.astro';
-import BlogNav from '../components/BlogNav.astro';
+import SitemapNav from '../components/SitemapNav.astro';
 import { aboutSection, findAboutPath } from '../lib/about';
 
 const _path = findAboutPath(aboutSection, '/urheberrechte.html')!;
@@ -33,6 +33,6 @@ const crumbs = [
   </Fragment>
 
   <Fragment slot="menu">
-    <BlogNav currentUrl="/urheberrechte.html" />
+    <SitemapNav currentUrl="/urheberrechte.html" />
   </Fragment>
 </BaseLayout>