Add top and bottom navigation between sequential articles with hacker aesthetic:
- Top nav: [visitor@danix.xyz articles]$ cd
- Bottom nav: [visitor@danix.xyz articles]$ ls ../
- Missing link shows dimmed placeholder (beginning/end)
- Only renders on articles, not static pages
- New partial: article-nav.html
- Styling: monospace prompt in accent color, hover links with transition
Co-Authored-By: Claude Haiku 4.5 <redacted>
--- /dev/null
+# Prev/Next Article Navigation — Design Spec
+
+## Context
+
+danix.xyz articles currently have no way to navigate between sequential posts. The user wants shell-prompt-style "hacker" navigation at both the top and bottom of each article, consistent with the site's open-source/hacker soul.
+
+## Visual Design
+
+### Top nav (above breadcrumb)
+```
+[visitor@danix.xyz articles]$ cd
+◄ Previous Article Title Next Article Title ►
+```
+
+### Bottom nav (after tags section)
+```
+[visitor@danix.xyz articles]$ ls ../
+◄ Previous Article Title Next Article Title ►
+```
+
+- Two lines, full width of the content column (`md:col-span-2`)
+- Line 1: decorative shell prompt — monospace, accent purple, not a link
+- Line 2: prev on left (◄ title), next on right (title ►), both clickable
+- Missing side: dimmed placeholder `◄ (beginning)` or `(end) ►`
+- Both sides always rendered to keep layout balanced
+
+## Architecture
+
+Single reusable partial `article-nav.html` accepting:
+- `page` — current Hugo page context
+- `variant` — `"top"` or `"bottom"` (controls prompt command: `cd` vs `ls ../`)
+
+Hugo variables: `.PrevInSection` / `.NextInSection` (nil-safe).
+
+## Styling
+
+- Font: JetBrains Mono (`font-mono`)
+- Prompt color: `var(--accent)` (#a855f7 purple)
+- Link hover: `hover:text-accent transition-colors`
+- Placeholder: `text-text-dim opacity-40`
+- Container: `border-t border-border pt-6`
+- Titles truncated at `max-w-[45%]` with `truncate` to prevent collision
+
+## Files Affected
+
+- `layouts/partials/article-nav.html` — new partial (create)
+- `layouts/articles/single.html` — insert partial top + bottom
+- `layouts/_default/single.html` — insert partial top + bottom
+- `assets/css/main.css` — add `.article-nav*` component classes
+
+## Accessibility
+
+- `aria-hidden="true"` on prompt line (decorative)
+- `rel="prev"` / `rel="next"` on links (SEO + semantics)
+- `title` attribute on links for full title on hover (truncation)
+- `aria-label` on placeholder spans
.section-title {
font-size: clamp(1.5rem, 3vw + 0.5rem, 2.5rem);
}
+
+ /* ---- Article prev/next navigation ---- */
+ .article-nav {
+ @apply border-t border-border pt-6;
+ }
+
+ .article-nav-prompt {
+ @apply font-mono text-sm mb-2;
+ color: var(--accent);
+ }
+
+ .article-nav-links {
+ @apply flex justify-between items-center font-mono text-sm;
+ }
+
+ .article-nav-link {
+ @apply hover:text-accent transition-colors text-text;
+ }
+
+ .article-nav-placeholder {
+ @apply text-text-dim opacity-40;
+ }
}
/* Prose overrides for light theme */
/* Hero typography with fluid sizing */
+/* ---- Article prev/next navigation ---- */
+
+.article-nav {
+ border-color: var(--border);
+}
+
+
+ .frosted-bar.article-nav {
+ border-color: var(--border);
+}
+
+.article-nav {
+ border-top-width: 1px;
+ border-color: var(--border);
+ padding-top: 1.5rem;
+}
+
+.article-nav-prompt {
+ margin-bottom: 0.5rem;
+ font-family: JetBrains Mono, monospace;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ color: var(--accent);
+}
+
+.article-nav-links {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-family: JetBrains Mono, monospace;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+}
+
+.article-nav-link {
+ color: var(--text);
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
+.article-nav-link:hover {
+ color: var(--accent);
+}
+
+.article-nav-placeholder {
+ color: var(--text-dim);
+ opacity: 0.4;
+}
+
.sr-only {
position: absolute;
width: 1px;
max-width: 80rem;
}
+.max-w-\[45\%\] {
+ max-width: 45%;
+}
+
.max-w-lg {
max-width: 32rem;
}
<div class="grid md:grid-cols-3 gap-8 max-w-7xl mx-auto content-grid">
<!-- Article section -->
<div class="md:col-span-2">
+ <!-- Top article navigation (articles only) -->
+ {{ if eq .Section "articles" }}
+ {{ partial "article-nav.html" (dict "page" . "variant" "top") }}
+ {{ end }}
+
<!-- Breadcrumb -->
{{ partial "breadcrumb.html" . }}
</div>
</div>
{{ end }}
+
+ <!-- Bottom article navigation (articles only) -->
+ {{ if eq .Section "articles" }}
+ {{ partial "article-nav.html" (dict "page" . "variant" "bottom") }}
+ {{ end }}
</div>
<!-- Sidebar -->
<div class="grid md:grid-cols-3 gap-8 max-w-7xl mx-auto content-grid">
<!-- Article section -->
<div class="md:col-span-2">
+ <!-- Top article navigation -->
+ {{ partial "article-nav.html" (dict "page" . "variant" "top") }}
+
<!-- Breadcrumb -->
{{ partial "breadcrumb.html" . }}
</div>
</div>
{{ end }}
+
+ <!-- Bottom article navigation -->
+ {{ partial "article-nav.html" (dict "page" . "variant" "bottom") }}
</div>
<!-- Sidebar -->
--- /dev/null
+{{ $page := .page }}
+{{ $variant := .variant | default "bottom" }}
+{{ $prev := $page.PrevInSection }}
+{{ $next := $page.NextInSection }}
+
+{{/* Shell prompt command varies by position */}}
+{{ $cmd := "ls ../" }}
+{{ if eq $variant "top" }}
+ {{ $cmd = "cd" }}
+{{ end }}
+
+<nav class="article-nav {{ if eq $variant "bottom" }}mt-8{{ else }}mb-8{{ end }}"
+ aria-label="Article navigation">
+ <p class="article-nav-prompt" aria-hidden="true">
+ [visitor@danix.xyz articles]$ {{ $cmd }}
+ </p>
+ <div class="article-nav-links">
+ {{/* ---- Previous (left side) ---- */}}
+ {{ if $prev }}
+ <a href="{{ $prev.Permalink }}"
+ class="article-nav-link truncate max-w-[45%]"
+ rel="prev"
+ title="{{ $prev.Title }}">
+ ◄ {{ $prev.Title }}
+ </a>
+ {{ else }}
+ <span class="article-nav-placeholder" aria-label="Beginning of articles">
+ ◄ (beginning)
+ </span>
+ {{ end }}
+
+ {{/* ---- Next (right side) ---- */}}
+ {{ if $next }}
+ <a href="{{ $next.Permalink }}"
+ class="article-nav-link truncate max-w-[45%] text-right"
+ rel="next"
+ title="{{ $next.Title }}">
+ {{ $next.Title }} ►
+ </a>
+ {{ else }}
+ <span class="article-nav-placeholder text-right" aria-label="End of articles">
+ (end) ►
+ </span>
+ {{ end }}
+ </div>
+</nav>