]> juplo.de Git - website/commitdiff
BlogNav + projects: Code bereinigt und vereinfacht
authorKai Moritz <kai.milan.moritz@googlemail.com>
Sun, 14 Jun 2026 14:35:54 +0000 (14:35 +0000)
committerKai Moritz <kai.milan.moritz@googlemail.com>
Sun, 14 Jun 2026 14:35:54 +0000 (14:35 +0000)
Totes Code entfernt:
- activeProjectUrl, isReports (nie benutzt)
- dirname, basename in content.config.ts (unused imports)
- visibleRoot-Branch in getActiveProject (identisch mit visiblePage)

Duplikate zusammengeführt:
- inSubtree + inProjectSubtree → generische Funktion (funktioniert für
  NavPage und ProjectNavNode dank struktureller Typisierung)
- aboutPageOff + projectNodeOff → nodeOff(nodeUrl, parentUrl, inPath,
  currentParentUrl, currentIsNode); Partial Applications aboutNodeOff
  und projNodeOff binden den section-spezifischen Kontext
- findProjectNode + findProjectParent → findNodeAndParent (ein DFS-Pass
  statt zwei); _activeProjectRoot-Alias dadurch obsolet

Wartbarkeit:
- aboutParentOf Map + aboutSectionIndexUrls Set durch findInTree ersetzt
  (leitet parentUrl und isNode direkt aus dem aboutSection-Baum ab statt
  beide separat zu pflegen)
- inPath einmal pro Node berechnet, für off-Logik und selected-Klasse
  wiederverwendet (war vorher doppelter Traversal)
- versionPrefix in buildNavTree aus makeNode herausgezogen

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

index 5c05a463b879a00c7dbe0fed59e621a05fba80a9..5cde6a2253e510debd34904cc608a84e3d7cdbd8 100644 (file)
@@ -46,118 +46,104 @@ const tags = [...tagSet.entries()].map(([name, count]) => ({
   count,
 })).sort((a, b) => a.displayName.localeCompare(b.displayName));
 
-// ── About section: static tree matching Hugo content/about/ structure ──────────
-// Sorted by weight (Hugo default), then by title.
-// isNode = true means the page is a section index (has children in Hugo).
+// ── 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.
+function nodeOff(
+  nodeUrl: string,
+  parentUrl: string,
+  inPath: boolean,
+  currentParentUrl: string,
+  currentIsNode: boolean,
+): boolean {
+  const isCurrent  = nodeUrl === currentUrl;
+  const isAncestor = inPath && !isCurrent;
+  const isSibling  = parentUrl === currentParentUrl && !currentIsNode;
+  const isChild    = parentUrl === currentUrl;
+  return !(isCurrent || isAncestor || isSibling || isChild);
+}
+
+// ── About section: static tree matching content/about/ structure ──────────────
+// isNode = true means the page is a section index (has children).
 interface NavPage { url: string; title: string; isNode?: boolean; children?: NavPage[] }
 
 const aboutSection: NavPage = {
   url: '/about.html', title: 'About', isNode: true,
   children: [
-    { url: '/contact.html',   title: 'Contact' },   // weight 10
-    {                                                 // weight 10 (alpha: C < I)
+    { url: '/contact.html',   title: 'Contact' },          // weight 10
+    {                                                        // weight 10 (alpha: C < I)
       url: '/impressum.html', title: 'Impressum', isNode: true,
       children: [
-        { url: '/urheberrechte.html',   title: 'Urheberrecht' },    // weight 10
-        { url: '/datenschutz.html',     title: 'Datenschutz' },     // weight 20
-        { url: '/haftung-inhalte.html', title: 'Haftung für Inhalte' }, // weight 30
-        { url: '/haftung-links.html',   title: 'Haftung für Links' },   // weight 40
-        { url: '/google-analytics.html', title: 'Google Analytics' },   // no weight → last
+        { url: '/urheberrechte.html',    title: 'Urheberrecht' },         // weight 10
+        { url: '/datenschutz.html',      title: 'Datenschutz' },          // weight 20
+        { url: '/haftung-inhalte.html',  title: 'Haftung für Inhalte' },  // weight 30
+        { url: '/haftung-links.html',    title: 'Haftung für Links' },    // weight 40
+        { url: '/google-analytics.html', title: 'Google Analytics' },     // no weight → last
       ]
     },
-    { url: '/agb.html', title: 'AGB' },             // weight 30
+    { url: '/agb.html', title: 'AGB' },                    // weight 30
   ]
 };
 
-// True if `url` is anywhere inside `node`'s subtree (inclusive)
-function inSubtree(node: NavPage, url: string): boolean {
-  if (node.url === url) return true;
-  return (node.children ?? []).some(c => inSubtree(c, url));
+// Walk the About tree to find currentUrl — returns parent URL + isNode flag.
+// Replaces the separate aboutParentOf Map and aboutSectionIndexUrls Set.
+function findInTree(node: NavPage, url: string, parentUrl = ''): { parentUrl: string; isNode: boolean } | null {
+  if (node.url === url) return { parentUrl, isNode: node.isNode ?? false };
+  for (const c of node.children ?? []) {
+    const r = findInTree(c, url, node.url);
+    if (r) return r;
+  }
+  return null;
 }
 
-const onAboutSection = inSubtree(aboutSection, currentUrl);
-
-// Parent URL of each About-section page (needed for sibling/child off-logic)
-const aboutParentOf = new Map<string, string>([
-  ['/about.html',           ''],
-  ['/contact.html',         '/about.html'],
-  ['/impressum.html',       '/about.html'],
-  ['/agb.html',             '/about.html'],
-  ['/urheberrechte.html',   '/impressum.html'],
-  ['/datenschutz.html',     '/impressum.html'],
-  ['/haftung-inhalte.html', '/impressum.html'],
-  ['/haftung-links.html',   '/impressum.html'],
-  ['/google-analytics.html','/impressum.html'],
-]);
+const _currentAbout      = findInTree(aboutSection, currentUrl);
+const onAboutSection     = _currentAbout !== null;
+const currentAboutParent = _currentAbout?.parentUrl ?? '';
+const currentAboutIsNode = _currentAbout?.isNode ?? false;
 
-// Section-index pages within About (= have children in Hugo)
-const aboutSectionIndexUrls = new Set(['/about.html', '/impressum.html']);
-const currentAboutIsNode  = aboutSectionIndexUrls.has(currentUrl);
-const currentAboutParent  = aboutParentOf.get(currentUrl) ?? '';
-
-// off-logic mirroring Hugo tree.html for a page inside the About subtree.
-// A page is visible (not off) if it is current, ancestor-of-current,
-// sibling-of-current (only when current is not a section-index), or child-of-current.
-function aboutPageOff(page: NavPage, parentUrl: string): boolean {
-  const isCurrent        = page.url === currentUrl;
-  const isAncestor       = !isCurrent && inSubtree(page, currentUrl);
-  const isSibling        = parentUrl === currentAboutParent && !currentAboutIsNode;
-  const isChild          = parentUrl === currentUrl;
-  return !(isCurrent || isAncestor || isSibling || isChild);
-}
+// 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;
+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();
 
-// Nav helpers for project trees
-function inProjectSubtree(node: ProjectNavNode, url: string): boolean {
-  if (node.url === url) return true;
-  return node.children.some(c => inProjectSubtree(c, url));
-}
-
-// Find the parent node of a URL within a tree
-function findProjectParent(node: ProjectNavNode, url: string): ProjectNavNode | null {
-  for (const c of node.children) {
-    if (c.url === url) return node;
-    const found = findProjectParent(c, url);
-    if (found) return found;
-  }
-  return null;
-}
-
-// Find a node by URL
-function findProjectNode(node: ProjectNavNode, url: string): ProjectNavNode | null {
-  if (node.url === url) return node;
+// 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 found = findProjectNode(c, url);
-    if (found) return found;
+    const r = findNodeAndParent(c, url, node);
+    if (r) return r;
   }
   return null;
 }
 
-// Determine the "active" project URL — either from visible page or SNAPSHOT page
-const activeProjectUrl = activeProject?.navRoot.url ?? null;
-// The active project's current node and its parent (for off-logic)
-const _activeProjectRoot = activeProject?.navRoot ?? null;
-const activeProjectCurrentNode = _activeProjectRoot ? findProjectNode(_activeProjectRoot, currentUrl) : null;
-const activeProjectCurrentParent = _activeProjectRoot ? findProjectParent(_activeProjectRoot, currentUrl) : 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;
 
-// off-logic mirroring tree.html for a node within a project tree
-function projectNodeOff(node: ProjectNavNode, parentUrl: string): boolean {
-  const isCurrent  = node.url === currentUrl;
-  const isAncestor = !isCurrent && inProjectSubtree(node, currentUrl);
-  // Sibling: same parent as current page, and current is NOT a section node
-  const isSibling  = parentUrl === (activeProjectCurrentParent?.url ?? '') && !activeProjectCurrentIsNode;
-  // Child: this node's parent is the current page
-  const isChild    = parentUrl === currentUrl;
-  return !(isCurrent || isAncestor || isSibling || isChild);
-}
+// 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);
 
 // ── Blog URL classification ────────────────────────────────────────────────────
 const mainSections = [
@@ -332,9 +318,9 @@ const activeTagSlug  = onTagTerm      ? currentUrl.replace(/^\/tags\//, '').repl
       </a>
       <ul class={`s${onProjectsSection ? ' active' : ''}`}>
         {projectTrees.map(info => {
-          const root = info.navRoot;
-          const rootOff = projectNodeOff(root, '/projects/');
-          const rootInPath = inProjectSubtree(root, currentUrl);
+          const root       = info.navRoot;
+          const rootInPath = inSubtree(root, currentUrl);
+          const rootOff    = projNodeOff(root.url, '/projects/', rootInPath);
           return (
             <li class={`s sub${rootOff ? ' off' : ''}`}>
               <a href={root.url} class={rootInPath ? 's selected' : 's'}>
@@ -342,9 +328,9 @@ const activeTagSlug  = onTagTerm      ? currentUrl.replace(/^\/tags\//, '').repl
               </a>
               <ul class={`s${rootInPath ? ' active' : ''}`}>
                 {root.children.map(child => {
-                  const childOff     = projectNodeOff(child, root.url);
-                  const childInPath  = inProjectSubtree(child, currentUrl);
-                  const childIsNode  = child.isNode;
+                  const childInPath = inSubtree(child, currentUrl);
+                  const childOff    = projNodeOff(child.url, root.url, childInPath);
+                  const childIsNode = child.isNode;
                   return (
                     <li class={`s${childIsNode ? ' sub' : ''}${childOff ? ' off' : ''}`}>
                       <a href={child.url} class={childInPath ? 's selected' : 's'}>
@@ -353,8 +339,8 @@ const activeTagSlug  = onTagTerm      ? currentUrl.replace(/^\/tags\//, '').repl
                       {childIsNode && child.children.length > 0 && (
                         <ul class={`s${childInPath ? ' active' : ''}`}>
                           {child.children.map(grand => {
-                            const grandOff    = projectNodeOff(grand, child.url);
-                            const grandInPath = inProjectSubtree(grand, currentUrl);
+                            const grandInPath = inSubtree(grand, currentUrl);
+                            const grandOff    = projNodeOff(grand.url, child.url, grandInPath);
                             const grandIsNode = grand.isNode;
                             return (
                               <li class={`s${grandIsNode ? ' sub' : ''}${grandOff ? ' off' : ''}`}>
@@ -364,12 +350,12 @@ const activeTagSlug  = onTagTerm      ? currentUrl.replace(/^\/tags\//, '').repl
                                 {grandIsNode && grand.children.length > 0 && (
                                   <ul class={`s${grandInPath ? ' active' : ''}`}>
                                     {grand.children.map(ggrand => {
-                                      const ggOff    = projectNodeOff(ggrand, grand.url);
-                                      const ggActive = ggrand.url === currentUrl;
+                                      const ggInPath = ggrand.url === currentUrl;
+                                      const ggOff    = projNodeOff(ggrand.url, grand.url, ggInPath);
                                       return (
                                         <li class={`s${ggOff ? ' off' : ''}`}>
-                                          <a href={ggrand.url} class={ggActive ? 's selected' : 's'}>
-                                            {ggActive ? <strong>{ggrand.title}</strong> : ggrand.title}
+                                          <a href={ggrand.url} class={ggInPath ? 's selected' : 's'}>
+                                            {ggInPath ? <strong>{ggrand.title}</strong> : ggrand.title}
                                           </a>
                                         </li>
                                       );
@@ -398,9 +384,9 @@ const activeTagSlug  = onTagTerm      ? currentUrl.replace(/^\/tags\//, '').repl
       </a>
       <ul class={`s${onAboutSection ? ' active' : ''}`}>
         {aboutSection.children!.map(child => {
-          const childOff      = aboutPageOff(child, '/about.html');
-          const childInPath   = inSubtree(child, currentUrl);
-          const childHasSubs  = (child.children ?? []).length > 0;
+          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' : ''}`}>
               <a href={child.url} class={childInPath ? 's selected' : 's'}>
@@ -409,8 +395,8 @@ const activeTagSlug  = onTagTerm      ? currentUrl.replace(/^\/tags\//, '').repl
               {childHasSubs && (
                 <ul class={`s${childInPath ? ' active' : ''}`}>
                   {child.children!.map(grand => {
-                    const grandOff    = aboutPageOff(grand, child.url);
                     const grandActive = grand.url === currentUrl;
+                    const grandOff    = aboutNodeOff(grand.url, child.url, grandActive);
                     return (
                       <li class={`s${grandOff ? ' off' : ''}`}>
                         <a href={grand.url} class={grandActive ? 's selected' : 's'}>
index 3896f88a19e679fe451d4257f0b34802f6b106d0..88f2f0031c28defdf84c49d6659dd3cf5d7bd4d2 100644 (file)
@@ -2,7 +2,7 @@ import { defineCollection, z } from 'astro:content';
 import { glob } from 'astro/loaders';
 import { load as yamlLoad } from 'js-yaml';
 import { readFileSync, readdirSync } from 'node:fs';
-import { resolve, relative, dirname, basename } from 'node:path';
+import { resolve, relative } from 'node:path';
 
 const blog = defineCollection({
   loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
index 79abcf9b66de852907221129ce4c3d8213709c4d..00f74e3932ca9dcf1e9774cc89ead986d6b8d397 100644 (file)
@@ -52,14 +52,12 @@ function buildNavTree(
   projectSlug: string,
   version: string,
 ): ProjectNavNode | null {
-  // Find the root entry (_index.html at version level)
-  const rootEntry = pages.find(p => p.id === `${projectSlug}/${version}/_index.html`);
+  const versionPrefix = `${projectSlug}/${version}/`;
+  const rootEntry = pages.find(p => p.id === `${versionPrefix}_index.html`);
   if (!rootEntry) return null;
 
   function makeNode(entry: ProjectPage): ProjectNavNode {
-    const idPath = entry.id; // e.g. "hibernate-maven-plugin/2.1.1/project-reports/plugin-info/_index.html"
-    const versionPrefix = `${projectSlug}/${version}/`;
-    const rel = idPath.slice(versionPrefix.length); // e.g. "project-reports/plugin-info/_index.html"
+    const rel = entry.id.slice(versionPrefix.length);
     const isNode = rel.endsWith('/_index.html') || rel === '_index.html';
 
     // Find direct children: entries whose path is one level deeper inside the same dir
@@ -82,8 +80,6 @@ function buildNavTree(
           children.push(makeNode(child));
         }
       }
-      // Add generated-content nodes under root or under project-reports
-      const isReports = rel === 'project-reports/_index.html' || rel === '_index.html';
       if (rel === 'project-reports/_index.html') {
         children.push(...generatedDocNodes(projectSlug, version, entry.data.params.current));
       }
@@ -126,19 +122,7 @@ export async function getActiveProject(currentUrl: string): Promise<ProjectInfo
     return { slug: projectSlug, version, isCurrent: false, navRoot };
   }
 
-  // Visible: currentUrl matches the url of a current: true page
-  const visibleRoot = pages.find(
-    p => p.data.params.current && p.id.endsWith('/_index.html') && p.data.url === currentUrl
-  );
-  if (visibleRoot) {
-    const [projectSlug, version] = visibleRoot.id.split('/');
-    const versionPages = pages.filter(p => p.id.startsWith(`${projectSlug}/${version}/`));
-    const navRoot = buildNavTree(versionPages, projectSlug, version);
-    if (!navRoot) return null;
-    return { slug: projectSlug, version, isCurrent: true, navRoot };
-  }
-
-  // Also handle sub-pages of a visible project
+  // Visible: currentUrl matches the url of a current: true page (root or sub-page)
   const visiblePage = pages.find(p => p.data.params.current && p.data.url === currentUrl);
   if (visiblePage) {
     const [projectSlug, version] = visiblePage.id.split('/');