]> juplo.de Git - website/commitdiff
BlogNav: vollständige About-Subtree mit korrekter off-Logik
authorKai Moritz <kai.milan.moritz@googlemail.com>
Sun, 14 Jun 2026 09:12:36 +0000 (09:12 +0000)
committerKai Moritz <kai.milan.moritz@googlemail.com>
Sun, 14 Jun 2026 09:12:36 +0000 (09:12 +0000)
Statischer About-Seitenbaum aus Hugo content/about/-Struktur:
  /about.html → /contact.html, /impressum.html (mit 5 Unterseiten), /agb.html

aboutPageOff() implementiert Hugo tree.html-Logik:
  sichtbar wenn isCurrent || isAncestor || isSibling(nicht section-index) || isChild

Korrekturen gegenüber Vorgängerversion:
- onAboutSection (inSubtree) statt onAbout (nur /about.html)
  → activeSection 'about' auch auf Impressum/Datenschutz
  → blogSectionOff = onAboutSection (Blog versteckt auf allen About-Seiten)
- About-Link zeigt selected auf allen About-Section-Seiten
- About-li hat jetzt class 'sub' (hat Kinder)

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

index a75a4631b071a8dd2d51f5566096b1d8b20a681a..926d7f7cc0b1609d5bfbe8f625b8d3179b945976 100644 (file)
@@ -45,59 +45,110 @@ const tags = [...tagSet.entries()].map(([name, count]) => ({
   count,
 })).sort((a, b) => a.displayName.localeCompare(b.displayName));
 
-const mainSections = [
-  { name: 'blog', url: '/blog/', title: 'Blog' },
-  { name: 'projects', url: '/projects/', title: 'Projects' },
-  { name: 'about', url: '/about.html', title: 'About' },
-];
+// ── 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).
+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: '/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: '/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));
+}
+
+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'],
+]);
+
+// 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) ?? '';
 
-// URL classification
-const onBlog = currentUrl === '/blog/';
-const isOnBlogPost = allPosts.some(p => (p.data.url ?? `/${p.id}/`) === currentUrl);
-const onArchive = currentUrl.startsWith('/blog/archive/') || isOnBlogPost;
+// 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);
+}
 
-const onCategoriesIndex = currentUrl === '/blog/categories/';
-const onCategoryTerm = currentUrl.startsWith('/categories/');
-const onTagsIndex = currentUrl === '/blog/tags/';
-const onTagTerm = currentUrl.startsWith('/tags/');
+// ── 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 blogSelected = onBlog || onArchive || onCategoriesIndex || onCategoryTerm || onTagsIndex || onTagTerm;
-const onAbout = currentUrl === '/about.html';
+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;
 
-// Active state flags
 const categoriesSelected = onCategoriesIndex || onCategoryTerm;
-const tagsSelected = onTagsIndex || onTagTerm;
+const tagsSelected       = onTagsIndex || onTagTerm;
 
-// "off" class logic derived from Hugo nav output:
-// Archive/Categories/Tags are all off when not in blog context (e.g. homepage, About)
-// Within blog context: Archive is off on taxonomy term pages only
-//                      Categories/Tags are off on archive pages and the sibling taxonomy term
-const archiveOff = !blogSelected || onCategoryTerm || onTagTerm;
-const categoriesOff = !blogSelected || onArchive || onTagTerm;
-const tagsOff = !blogSelected || onArchive || onCategoryTerm;
+// off-class logic for Blog subtree items (derived from Hugo blog.html + tree.html)
+const archiveOff    = !blogSelected || onCategoryTerm || onTagTerm;
+const categoriesOff = !blogSelected || onArchive      || onTagTerm;
+const tagsOff       = !blogSelected || onArchive      || onCategoryTerm;
 
-// Determine active section
+// Active section for #menu highlight
 const activeSection = mainSections.find(s => {
-  if (s.name === 'blog') return blogSelected;
+  if (s.name === 'blog')  return blogSelected;
+  if (s.name === 'about') return onAboutSection;
   return currentUrl.startsWith(s.url);
 })?.name ?? '';
 
-// Hugo blog.html: Blog always visible on blog pages, all other sections always off.
-// Hugo default.html + tree.html: on Homepage/regular-root-pages all sections visible
-// (child/sibling logic); on About section-index only About visible, Blog off.
-// When Projects is implemented, extend both: blogSectionOff |= onProjects, aboutOff |= onProjects.
-const blogSectionOff = onAbout;
-const aboutOff = blogSelected;
+// Top-level section off-logic (Hugo blog.html vs default.html + tree.html):
+// Blog section hidden when viewing any About-section page (and later: Projects pages).
+// About section hidden only when on Blog pages.
+const blogSectionOff = onAboutSection;
+const aboutOff       = blogSelected;
 
-// Determine active year from currentUrl
+// Active year / taxonomy term helpers
 function isActiveYear(year: string) {
   return currentUrl === `/blog/archive/${year}/` ||
     postsByYear[year]?.some(p => postUrl(p) === currentUrl) === true;
 }
-
-// Active taxonomy term from URL
-const activeCatSlug = onCategoryTerm ? currentUrl.replace(/^\/categories\//, '').replace(/\/$/, '') : '';
-const activeTagSlug = onTagTerm ? currentUrl.replace(/^\/tags\//, '').replace(/\/$/, '') : '';
+const activeCatSlug  = onCategoryTerm ? currentUrl.replace(/^\/categories\//, '').replace(/\/$/, '') : '';
+const activeTagSlug  = onTagTerm      ? currentUrl.replace(/^\/tags\//, '').replace(/\/$/, '') : '';
 ---
 
 <nav id="nav">
@@ -116,13 +167,14 @@ const activeTagSlug = onTagTerm ? currentUrl.replace(/^\/tags\//, '').replace(/\
     <a class="s selected" href="/">Home</a>
   </h2>
   <ul id="submenu" class="s active">
-    {/* Blog subtree */}
+
+    {/* ── Blog subtree ─────────────────────────────────────────────────────── */}
     <li class={`s sub${blogSectionOff ? ' off' : ''}`}>
       <a href="/blog/" class={blogSelected ? 's selected' : 's'}>
         {blogSelected ? <strong>Blog</strong> : 'Blog'}
       </a>
       <ul class={blogSelected ? 's active' : 's'}>
-        {/* Archive — only render subtree on blog/archive pages */}
+        {/* Archive — only render year/post subtree on blog/archive pages */}
         <li class={`s sub${archiveOff ? ' off' : ''}`}>
           <a href="/blog/archive/" class={onArchive ? 's selected' : 's'}>
             {onArchive ? <strong>Archive</strong> : 'Archive'}
@@ -219,12 +271,43 @@ const activeTagSlug = onTagTerm ? currentUrl.replace(/^\/tags\//, '').replace(/\
         )}
       </ul>
     </li>
-    {/* About entry */}
-    <li class={`s${aboutOff ? ' off' : ''}`}>
-      <a href="/about.html" class={onAbout ? 's selected' : 's'}>
-        {onAbout ? <strong>About</strong> : 'About'}
+
+    {/* ── About subtree ────────────────────────────────────────────────────── */}
+    <li class={`s sub${aboutOff ? ' off' : ''}`}>
+      <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 childOff      = aboutPageOff(child, '/about.html');
+          const childInPath   = inSubtree(child, currentUrl);
+          const childHasSubs  = (child.children ?? []).length > 0;
+          return (
+            <li class={`s${childHasSubs ? ' sub' : ''}${childOff ? ' off' : ''}`}>
+              <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 grandOff    = aboutPageOff(grand, child.url);
+                    const grandActive = grand.url === currentUrl;
+                    return (
+                      <li class={`s${grandOff ? ' off' : ''}`}>
+                        <a href={grand.url} class={grandActive ? 's selected' : 's'}>
+                          {grandActive ? <strong>{grand.title}</strong> : grand.title}
+                        </a>
+                      </li>
+                    );
+                  })}
+                </ul>
+              )}
+            </li>
+          );
+        })}
+      </ul>
     </li>
+
   </ul>
   <hr class="n"/>
 </nav>