commit c987982bb2a6ffcfe719aaff7f7b8d167c0fc43b
parent a7b97f3252ce9d255657c9066dc66b6ade2e6e94
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Mon, 22 Jun 2026 18:39:56 -0400
Fix homepage export build when no docs pages are authored
`output: "export"` rejects the `[slug]` dynamic route when
generateStaticParams() is empty — which happens for a hub with no docs
pages, or one with only a home (`index`) page. Add a built-in fallback
doc (`about`) so homepageDocSlugs() always yields ≥1 param; it's
superseded as soon as any real non-index page is authored. The hub nav
now derives from the same slug set so the built route always has a link.
Verified: export build passes for an empty data dir (fallback /about)
and for authored pages (real /about body); common + homepage typecheck.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat:
3 files changed, 62 insertions(+), 16 deletions(-)
diff --git a/common/lib/homepagePages.ts b/common/lib/homepagePages.ts
@@ -1,7 +1,7 @@
import fs from "node:fs";
import path from "node:path";
import { getPaths, type Paths } from "./paths";
-import { isValidHomepageSlug } from "./homepage";
+import { HOMEPAGE_INDEX_SLUG, isValidHomepageSlug } from "./homepage";
// On-disk store for the hub's Markdown page bodies, one file per page at
// homepagePagesDir/<slug>.md. Page ORDER and link LABEL live in the homepage
@@ -18,6 +18,18 @@ export type HomepagePage = {
body: string;
};
+// Built-in fallback doc. `output: "export"` rejects the `[slug]` dynamic route
+// when generateStaticParams() is empty, which happens for a hub with no docs
+// pages OR one with only a home (`index`) page. To keep the route valid we
+// always build at least this one doc; it's superseded the moment any real non-
+// index page is authored. See homepageDocSlugs / getHomepageDoc below.
+export const DEFAULT_HOMEPAGE_DOC: HomepagePage = {
+ slug: "about",
+ body:
+ "# About\n\nThis is the Archilyzer hub. Edit this page or add new ones in the " +
+ "editor under **Manage → Homepage**.\n",
+};
+
function pageFile(paths: Paths, slug: string): string {
return path.join(paths.homepagePagesDir, `${slug}${PAGE_EXT}`);
}
@@ -59,6 +71,33 @@ export function listHomepagePages(paths: Paths = getPaths()): HomepagePage[] {
.filter((p): p is HomepagePage => p !== null);
}
+// Real, authored non-index doc slugs (the ones that get a /<slug> route).
+function realDocSlugs(paths: Paths): string[] {
+ return listHomepagePageSlugs(paths).filter((s) => s !== HOMEPAGE_INDEX_SLUG);
+}
+
+// Doc slugs to route/build: the authored non-index pages, or the single built-in
+// fallback when there are none (so the static `[slug]` route always has ≥1
+// param). Use for generateStaticParams and the hub nav.
+export function homepageDocSlugs(paths: Paths = getPaths()): string[] {
+ const real = realDocSlugs(paths);
+ return real.length ? real : [DEFAULT_HOMEPAGE_DOC.slug];
+}
+
+// Resolve a doc for rendering: the authored file if present, else the built-in
+// fallback when its slug is requested and no real docs exist. Null otherwise.
+export function getHomepageDoc(
+ slug: string,
+ paths: Paths = getPaths(),
+): HomepagePage | null {
+ const real = getHomepagePage(slug, paths);
+ if (real) return real;
+ if (slug === DEFAULT_HOMEPAGE_DOC.slug && realDocSlugs(paths).length === 0) {
+ return DEFAULT_HOMEPAGE_DOC;
+ }
+ return null;
+}
+
export async function writeHomepagePage(
slug: string,
body: string,
diff --git a/homepage/app/[slug]/page.tsx b/homepage/app/[slug]/page.tsx
@@ -1,23 +1,22 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getPaths } from "yt-dlp-transcript-common/lib/paths";
-import { HOMEPAGE_INDEX_SLUG } from "yt-dlp-transcript-common/lib/homepage";
import {
- getHomepagePage,
- listHomepagePageSlugs,
+ getHomepageDoc,
+ homepageDocSlugs,
} from "yt-dlp-transcript-common/lib/homepagePages";
import { currentHomepage } from "../lib/homepage";
import PageBody from "../components/PageBody";
// Every Markdown page except "index" (which is the home route) gets a static
// "/<slug>" route. With output: "export" the full set is enumerated here at
-// build; unknown slugs 404.
+// build; unknown slugs 404. homepageDocSlugs() always returns ≥1 slug (a built-in
+// fallback when no docs are authored) so the export build never sees an empty
+// dynamic route.
export const dynamicParams = false;
export function generateStaticParams(): { slug: string }[] {
- return listHomepagePageSlugs(getPaths())
- .filter((slug) => slug !== HOMEPAGE_INDEX_SLUG)
- .map((slug) => ({ slug }));
+ return homepageDocSlugs(getPaths()).map((slug) => ({ slug }));
}
function labelFor(slug: string): string {
@@ -40,8 +39,7 @@ export default async function HomepageDocPage({
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
- if (slug === HOMEPAGE_INDEX_SLUG) notFound();
- const page = getHomepagePage(slug, getPaths());
+ const page = getHomepageDoc(slug, getPaths());
if (!page) notFound();
return (
<div className="flex flex-col gap-2">
diff --git a/homepage/app/components/Header.tsx b/homepage/app/components/Header.tsx
@@ -1,14 +1,23 @@
import Link from "next/link";
-import { HOMEPAGE_INDEX_SLUG } from "yt-dlp-transcript-common/lib/homepage";
+import { getPaths } from "yt-dlp-transcript-common/lib/paths";
+import { homepageDocSlugs } from "yt-dlp-transcript-common/lib/homepagePages";
import { currentHomepage } from "../lib/homepage";
-// Top nav for the hub. The header title links home; nav links come from the
-// config's ordered `nav` array (the "index" page is the home route, so it's not
-// repeated as a nav link), with a trailing always-on "Stats" link to the
-// cross-site dashboard.
+function titleCase(slug: string): string {
+ return slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
+}
+
+// Top nav for the hub. The header title links home; doc links come from the
+// built doc routes (homepageDocSlugs — the same set the [slug] route builds,
+// including the built-in fallback when nothing is authored), labeled via the
+// config nav, with a trailing always-on "Stats" link to the cross-site board.
export default function Header() {
const config = currentHomepage();
- const links = config.nav.filter((n) => n.slug !== HOMEPAGE_INDEX_SLUG);
+ const labelFor = new Map(config.nav.map((n) => [n.slug, n.label]));
+ const links = homepageDocSlugs(getPaths()).map((slug) => ({
+ slug,
+ label: labelFor.get(slug) ?? titleCase(slug),
+ }));
return (
<header className="border-b border-zinc-200 dark:border-zinc-800 bg-white/70 dark:bg-zinc-900/70 backdrop-blur sticky top-0 z-20">
<div className="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between gap-3">