{ name: 'about', url: '/about.html', title: 'About' },
];
+// URL classification
const onBlog = currentUrl === '/blog/';
-// A blog post URL (flat, not /blog/...) still counts as "in archive"
const isOnBlogPost = allPosts.some(p => (p.data.url ?? `/${p.id}/`) === currentUrl);
const onArchive = currentUrl.startsWith('/blog/archive/') || isOnBlogPost;
-const blogSelected = onBlog || onArchive;
+
+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;
+
+// "off" class logic derived from Hugo nav output:
+// On archive pages: Categories and Tags are off; Archive is normal (selected)
+// On category term: Archive and Tags are off
+// On tag term: Archive and Categories are off
+const archiveOff = onCategoryTerm || onTagTerm;
+const categoriesOff = onArchive || onTagTerm;
+const tagsOff = onArchive || onCategoryTerm;
// Determine active section
const activeSection = mainSections.find(s => {
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(/\/$/, '') : '';
---
<nav id="nav">
{blogSelected ? <strong>Blog</strong> : 'Blog'}
</a>
<ul class={blogSelected ? 's active' : 's'}>
- <li class="s sub">
+ {/* Archive — only render 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'}
</a>
- <ul class={onArchive ? 's active' : 's'}>
- {years.map(year => {
- const active = isActiveYear(year);
- return (
- <li class={`s${onBlog ? ' off' : ''}`}>
- <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">
- <a href={pUrl} class={current ? 's selected' : 's'}>
- {current ? <strong>{post.data.title}</strong> : post.data.title}
- </a>
- </li>
- );
- })}
- </ul>
- </li>
- );
- })}
- </ul>
+ {(onArchive || onBlog) && (
+ <ul class={onArchive ? 's active' : 's'}>
+ {years.map(year => {
+ const active = isActiveYear(year);
+ return (
+ <li class={`s${onBlog ? ' off' : ''}`}>
+ <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">
+ <a href={pUrl} class={current ? 's selected' : 's'}>
+ {current ? <strong>{post.data.title}</strong> : post.data.title}
+ </a>
+ </li>
+ );
+ })}
+ </ul>
+ </li>
+ );
+ })}
+ </ul>
+ )}
</li>
{/* Categories subtree */}
{categories.length > 0 && (
- <li class={`s${onBlog ? '' : ' off'}`}>
- <a href="/blog/categories/" class="s">Categories</a>
- <ul class="s">
- {categories.map(cat => (
- <li class="s nav-leaf">
- <a href={cat.url} class="s">{cat.displayName}</a>
- </li>
- ))}
- </ul>
+ <li class={`s${categoriesOff ? ' off' : ''}${onCategoryTerm ? ' sub' : ''}`}>
+ <a href="/blog/categories/" class={categoriesSelected ? 's selected' : 's'}>
+ {categoriesSelected ? <strong>Categories</strong> : 'Categories'}
+ </a>
+ {onCategoryTerm && (
+ <ul class="s active">
+ {categories.map(cat => {
+ const active = cat.name === activeCatSlug;
+ return (
+ <li class="s">
+ <a href={cat.url} class={active ? 's selected' : 's'}>
+ {active ? <strong>{cat.displayName}</strong> : cat.displayName}
+ </a>
+ </li>
+ );
+ })}
+ </ul>
+ )}
+ {!onCategoriesIndex && !onCategoryTerm && (
+ <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${onBlog ? '' : ' off'}`}>
- <a href="/blog/tags/" class="s">Tags</a>
- <ul class="s">
- {tags.map(tag => (
- <li class="s nav-leaf">
- <a href={tag.url} class="s">{tag.displayName}</a>
- </li>
- ))}
- </ul>
+ <li class={`s${tagsOff ? ' off' : ''}${onTagTerm ? ' sub' : ''}`}>
+ <a href="/blog/tags/" class={tagsSelected ? 's selected' : 's'}>
+ {tagsSelected ? <strong>Tags</strong> : 'Tags'}
+ </a>
+ {onTagTerm && (
+ <ul class="s active">
+ {tags.map(tag => {
+ const active = tag.name === activeTagSlug;
+ return (
+ <li class="s">
+ <a href={tag.url} class={active ? 's selected' : 's'}>
+ {active ? <strong>{tag.displayName}</strong> : tag.displayName}
+ </a>
+ </li>
+ );
+ })}
+ </ul>
+ )}
+ {!onTagsIndex && !onTagTerm && (
+ <ul class="s">
+ {tags.map(tag => (
+ <li class="s nav-leaf">
+ <a href={tag.url} class="s">{tag.displayName}</a>
+ </li>
+ ))}
+ </ul>
+ )}
</li>
)}
</ul>
--- /dev/null
+---
+import { getCollection } from 'astro:content';
+import BaseLayout from '../../../layouts/BaseLayout.astro';
+import Breadcrumb from '../../../components/Breadcrumb.astro';
+import BlogNav from '../../../components/BlogNav.astro';
+
+const currentUrl = '/blog/categories/';
+
+const posts = await getCollection('blog', ({ data }) => !data.draft);
+
+const categoryMap = new Map<string, number>();
+for (const post of posts) {
+ for (const c of post.data.categories ?? []) {
+ categoryMap.set(c, (categoryMap.get(c) ?? 0) + 1);
+ }
+}
+const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
+const categories = [...categoryMap.entries()]
+ .map(([name, count]) => ({ name, displayName: capitalize(name), count }))
+ .sort((a, b) => a.displayName.localeCompare(b.displayName));
+
+const crumbs = [
+ { title: 'Home', url: '/' },
+ { title: 'Blog', url: '/blog/' },
+ { title: 'Categories' },
+];
+---
+
+<BaseLayout title="Blog - Categories">
+ <Fragment slot="breadcrumb">
+ <Breadcrumb crumbs={crumbs} />
+ </Fragment>
+
+ <Fragment slot="article">
+ <header>
+ <h1>Blog - Categories</h1>
+ </header>
+ <ul class="t">
+ {categories.map(cat => (
+ <li>
+ <a href={`/categories/${cat.name}/`}>
+ <span class="title">{cat.displayName}</span><span class="sep"> | </span><span class="count">{cat.count}</span>
+ </a>
+ </li>
+ ))}
+ </ul>
+ </Fragment>
+
+ <Fragment slot="menu">
+ <BlogNav currentUrl={currentUrl} />
+ </Fragment>
+</BaseLayout>
--- /dev/null
+---
+import { getCollection } from 'astro:content';
+import BaseLayout from '../../../layouts/BaseLayout.astro';
+import Breadcrumb from '../../../components/Breadcrumb.astro';
+import BlogNav from '../../../components/BlogNav.astro';
+
+const currentUrl = '/blog/tags/';
+
+const posts = await getCollection('blog', ({ data }) => !data.draft);
+
+const tagMap = new Map<string, number>();
+for (const post of posts) {
+ for (const t of post.data.tags ?? []) {
+ tagMap.set(t, (tagMap.get(t) ?? 0) + 1);
+ }
+}
+const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
+const tags = [...tagMap.entries()]
+ .map(([name, count]) => ({ name, displayName: capitalize(name), count }))
+ .sort((a, b) => a.displayName.localeCompare(b.displayName));
+
+const crumbs = [
+ { title: 'Home', url: '/' },
+ { title: 'Blog', url: '/blog/' },
+ { title: 'Tags' },
+];
+---
+
+<BaseLayout title="Blog - Tags">
+ <Fragment slot="breadcrumb">
+ <Breadcrumb crumbs={crumbs} />
+ </Fragment>
+
+ <Fragment slot="article">
+ <header>
+ <h1>Blog - Tags</h1>
+ </header>
+ <ul class="t">
+ {tags.map(tag => (
+ <li>
+ <a href={`/tags/${tag.name}/`}>
+ <span class="title">{tag.displayName}</span><span class="sep"> | </span><span class="count">{tag.count}</span>
+ </a>
+ </li>
+ ))}
+ </ul>
+ </Fragment>
+
+ <Fragment slot="menu">
+ <BlogNav currentUrl={currentUrl} />
+ </Fragment>
+</BaseLayout>
--- /dev/null
+---
+import { getCollection } from 'astro:content';
+import BaseLayout from '../../layouts/BaseLayout.astro';
+import Breadcrumb from '../../components/Breadcrumb.astro';
+import BlogNav from '../../components/BlogNav.astro';
+
+export async function getStaticPaths() {
+ const allPosts = await getCollection('blog', ({ data }) => !data.draft);
+ const termMap = new Map<string, typeof allPosts>();
+ for (const post of allPosts) {
+ for (const cat of post.data.categories ?? []) {
+ if (!termMap.has(cat)) termMap.set(cat, []);
+ termMap.get(cat)!.push(post);
+ }
+ }
+ return [...termMap.entries()].map(([term, posts]) => ({
+ params: { term },
+ props: {
+ posts: posts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()),
+ },
+ }));
+}
+
+const { term } = Astro.params;
+const { posts } = Astro.props;
+
+const currentUrl = `/categories/${term}/`;
+const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
+const termDisplay = capitalize(term);
+
+function postUrl(post: any): string {
+ return post.data.url ?? `/${post.id}/`;
+}
+
+function wordCount(body: string): number {
+ return body.replace(/```[\s\S]*?```/g, '').split(/\s+/).filter(w => w.length > 0).length;
+}
+
+const crumbs = [
+ { title: 'Home', url: '/' },
+ { title: 'Blog', url: '/blog/' },
+ { title: 'Categories', url: '/blog/categories/' },
+ { title: termDisplay },
+];
+---
+
+<BaseLayout title={termDisplay}>
+ <Fragment slot="breadcrumb">
+ <Breadcrumb crumbs={crumbs} />
+ </Fragment>
+
+ <Fragment slot="article">
+ <header>
+ <h1>
+ <a href={`/categories/${term}/index.xml`} title="RSS" aria-label="RSS">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4"
+ stroke-linecap="round" stroke-linejoin="round" height="23">
+ <path d="M4 11a9 9 0 0 1 9 9" />
+ <path d="M4 4a16 16 0 0 1 16 16" />
+ <circle cx="5" cy="19" r="1" />
+ </svg>
+ Categories: {termDisplay}
+ </a>
+ </h1>
+ </header>
+ {posts.map(post => {
+ const url = postUrl(post);
+ const date = post.data.date;
+ const dateStr = date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
+ const dateIso = date.toISOString();
+ const wc = wordCount(post.body ?? '');
+ const readTime = Math.ceil(wc / 200);
+ return (
+ <div class="archive-entry">
+ <h3 class="archive-entry-title entry-hint-parent">
+ <a class="entry-link" aria-label={`post link to ${post.data.title}`} href={url}>{post.data.title}</a>
+ </h3>
+ <div class="archive-meta">
+ <p><em>
+ <span title={dateIso}>Posted on {dateStr}</span>
+ · <span>{wc} words</span>
+ · <span>{readTime} min</span>
+ </em></p>
+ </div>
+ </div>
+ );
+ })}
+ </Fragment>
+
+ <Fragment slot="menu">
+ <BlogNav currentUrl={currentUrl} />
+ </Fragment>
+</BaseLayout>
--- /dev/null
+---
+import { getCollection } from 'astro:content';
+import BaseLayout from '../../layouts/BaseLayout.astro';
+import Breadcrumb from '../../components/Breadcrumb.astro';
+import BlogNav from '../../components/BlogNav.astro';
+
+export async function getStaticPaths() {
+ const allPosts = await getCollection('blog', ({ data }) => !data.draft);
+ const termMap = new Map<string, typeof allPosts>();
+ for (const post of allPosts) {
+ for (const tag of post.data.tags ?? []) {
+ if (!termMap.has(tag)) termMap.set(tag, []);
+ termMap.get(tag)!.push(post);
+ }
+ }
+ return [...termMap.entries()].map(([term, posts]) => ({
+ params: { term },
+ props: {
+ posts: posts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()),
+ },
+ }));
+}
+
+const { term } = Astro.params;
+const { posts } = Astro.props;
+
+const currentUrl = `/tags/${term}/`;
+const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
+const termDisplay = capitalize(term);
+
+function postUrl(post: any): string {
+ return post.data.url ?? `/${post.id}/`;
+}
+
+function wordCount(body: string): number {
+ return body.replace(/```[\s\S]*?```/g, '').split(/\s+/).filter(w => w.length > 0).length;
+}
+
+const crumbs = [
+ { title: 'Home', url: '/' },
+ { title: 'Blog', url: '/blog/' },
+ { title: 'Tags', url: '/blog/tags/' },
+ { title: termDisplay },
+];
+---
+
+<BaseLayout title={termDisplay}>
+ <Fragment slot="breadcrumb">
+ <Breadcrumb crumbs={crumbs} />
+ </Fragment>
+
+ <Fragment slot="article">
+ <header>
+ <h1>
+ <a href={`/tags/${term}/index.xml`} title="RSS" aria-label="RSS">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4"
+ stroke-linecap="round" stroke-linejoin="round" height="23">
+ <path d="M4 11a9 9 0 0 1 9 9" />
+ <path d="M4 4a16 16 0 0 1 16 16" />
+ <circle cx="5" cy="19" r="1" />
+ </svg>
+ Tags: {termDisplay}
+ </a>
+ </h1>
+ </header>
+ {posts.map(post => {
+ const url = postUrl(post);
+ const date = post.data.date;
+ const dateStr = date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
+ const dateIso = date.toISOString();
+ const wc = wordCount(post.body ?? '');
+ const readTime = Math.ceil(wc / 200);
+ return (
+ <div class="archive-entry">
+ <h3 class="archive-entry-title entry-hint-parent">
+ <a class="entry-link" aria-label={`post link to ${post.data.title}`} href={url}>{post.data.title}</a>
+ </h3>
+ <div class="archive-meta">
+ <p><em>
+ <span title={dateIso}>Posted on {dateStr}</span>
+ · <span>{wc} words</span>
+ · <span>{readTime} min</span>
+ </em></p>
+ </div>
+ </div>
+ );
+ })}
+ </Fragment>
+
+ <Fragment slot="menu">
+ <BlogNav currentUrl={currentUrl} />
+ </Fragment>
+</BaseLayout>