[object Object]

← back to Nineoh Guide

episode detail: prev/next adjacent-episode nav (row-comparison, crosses season boundaries) — deeper internal linking + crawl depth for SEO

7a343d78c5c69a36027fc29ad13bb1f727564176 · 2026-07-25 13:15:28 -0700 · Steve

Files touched

Diff

commit 7a343d78c5c69a36027fc29ad13bb1f727564176
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Jul 25 13:15:28 2026 -0700

    episode detail: prev/next adjacent-episode nav (row-comparison, crosses season boundaries) — deeper internal linking + crawl depth for SEO
---
 apps/web/app/episodes/[season]/[episode]/page.tsx | 51 +++++++++++++++++++++++
 1 file changed, 51 insertions(+)

diff --git a/apps/web/app/episodes/[season]/[episode]/page.tsx b/apps/web/app/episodes/[season]/[episode]/page.tsx
index c4774c8..d7f781c 100644
--- a/apps/web/app/episodes/[season]/[episode]/page.tsx
+++ b/apps/web/app/episodes/[season]/[episode]/page.tsx
@@ -21,6 +21,26 @@ async function getEpisode(season: number, episode: number) {
   }
 }
 
+async function getAdjacent(season: number, episode: number) {
+  try {
+    const next = await pool.query(
+      `select season_number, episode_number, title from episodes
+        where (season_number, episode_number) > ($1,$2)
+        order by season_number, episode_number limit 1`,
+      [season, episode]
+    );
+    const prev = await pool.query(
+      `select season_number, episode_number, title from episodes
+        where (season_number, episode_number) < ($1,$2)
+        order by season_number desc, episode_number desc limit 1`,
+      [season, episode]
+    );
+    return { prev: prev.rows[0] ?? null, next: next.rows[0] ?? null };
+  } catch {
+    return { prev: null, next: null };
+  }
+}
+
 export async function generateMetadata({
   params,
 }: {
@@ -49,6 +69,7 @@ export default async function EpisodePage({
   const { season, episode } = await params;
   const ep = await getEpisode(Number(season), Number(episode));
   if (!ep) notFound();
+  const { prev, next } = await getAdjacent(ep.season_number, ep.episode_number);
 
   const jsonLd = {
     "@context": "https://schema.org",
@@ -96,6 +117,36 @@ export default async function EpisodePage({
       ) : (
         <p style={{ color: "var(--muted)" }}>An original recap for this episode is coming soon.</p>
       )}
+
+      <nav
+        style={{
+          display: "flex",
+          justifyContent: "space-between",
+          gap: 12,
+          marginTop: 30,
+          paddingTop: 16,
+          borderTop: "1px solid var(--line)",
+          fontSize: 13,
+        }}
+      >
+        {prev ? (
+          <a href={`/episodes/${prev.season_number}/${prev.episode_number}`}>
+            ← S{prev.season_number}E{prev.episode_number} · {prev.title}
+          </a>
+        ) : (
+          <span />
+        )}
+        {next ? (
+          <a
+            href={`/episodes/${next.season_number}/${next.episode_number}`}
+            style={{ textAlign: "right", marginLeft: "auto" }}
+          >
+            S{next.season_number}E{next.episode_number} · {next.title} →
+          </a>
+        ) : (
+          <span />
+        )}
+      </nav>
     </article>
   );
 }

← 99ec1b5 design-consistency pass: interior pages (episodes/cast/searc  ·  back to Nineoh Guide  ·  2026 graphics: Photoshop-baked grainy ombre hero (413KB jpg) 3b392c3 →