]> danix's work - danix.xyz-2.git/commitdiff
feat: add prev/next article navigation with shell prompt style
authorDanilo M. <redacted>
Sat, 18 Apr 2026 18:54:50 +0000 (20:54 +0200)
committerDanilo M. <redacted>
Sat, 18 Apr 2026 18:54:50 +0000 (20:54 +0200)
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>
docs/superpowers/specs/2026-04-18-prev-next-navigation-design.md [new file with mode: 0644]
themes/danix-xyz-hacker/assets/css/main.css
themes/danix-xyz-hacker/assets/css/main.min.css
themes/danix-xyz-hacker/layouts/_default/single.html
themes/danix-xyz-hacker/layouts/articles/single.html
themes/danix-xyz-hacker/layouts/partials/article-nav.html [new file with mode: 0644]

diff --git a/docs/superpowers/specs/2026-04-18-prev-next-navigation-design.md b/docs/superpowers/specs/2026-04-18-prev-next-navigation-design.md
new file mode 100644 (file)
index 0000000..2fa22a7
--- /dev/null
@@ -0,0 +1,56 @@
+# 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
index f618f2de4556686397d649acac26b0f2ee22802a..298c38b3c688f83c60a7251e6782f8b05106b4b3 100644 (file)
@@ -574,6 +574,28 @@ html.theme-light picture img[src="/images/default_thumbnail_dark.png"] {
   .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 */
index d928b7ab84947cd21580a8cac7027500e7b4b89f..942d4019e0209434ed9b852634fb10a2eb68aee7 100644 (file)
@@ -1632,6 +1632,56 @@ article.border.border-border\/30.rounded-lg.card.group.bg-bg {
 
 /* 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;
@@ -1935,6 +1985,10 @@ article.border.border-border\/30.rounded-lg.card.group.bg-bg {
   max-width: 80rem;
 }
 
+.max-w-\[45\%\] {
+  max-width: 45%;
+}
+
 .max-w-lg {
   max-width: 32rem;
 }
index 0d3c6fae5e60af3363e5f754aa77e9e55a5fc56c..62e4a644dd85e8f0a163141591efd38fd9f79d8f 100644 (file)
@@ -3,6 +3,11 @@
   <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 -->
index 4e97fb3d30d7c9ee0d702ed592cf20f413f882bf..e64663916bc0abc8085a9f7a8a99889f739e1706 100644 (file)
@@ -5,6 +5,9 @@
   <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" . }}
 
@@ -35,6 +38,9 @@
         </div>
       </div>
       {{ end }}
+
+      <!-- Bottom article navigation -->
+      {{ partial "article-nav.html" (dict "page" . "variant" "bottom") }}
     </div>
 
     <!-- Sidebar -->
diff --git a/themes/danix-xyz-hacker/layouts/partials/article-nav.html b/themes/danix-xyz-hacker/layouts/partials/article-nav.html
new file mode 100644 (file)
index 0000000..d7f8ca3
--- /dev/null
@@ -0,0 +1,46 @@
+{{ $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>