commit 5b777de29efa14b36d1fe834855ff6ca5fad1b68
parent 15fa8be09a1fddd89e519e661d03dd360b40b32d
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Wed, 30 Sep 2026 18:57:59 -0400
export: the Use with AI page is removed; the header's, the menu's, the footer's and Ask AI's Use with AI go to the homepage's AI and MCP doc
Same label, same tab, a plain <a> to AI_DOC_URL, on every site and the hub. The specs that used
the page as a route outside the workspace (first-search, restore-no-refire) leave for /changelog/
through the app router, a client navigation as before; responsive checks /changelog/. New
use-with-ai-link specs (site and hub): every link carries the doc URL, /use-with-ai/ is a 404.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
9 files changed, 184 insertions(+), 41 deletions(-)
diff --git a/export/app/(workspace)/ask/page.tsx b/export/app/(workspace)/ask/page.tsx
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
-import Link from "next/link";
+import { AI_DOC_URL } from "yt-dlp-transcript-common/lib/project";
import { currentSite } from "../../lib/site";
import { instanceMode } from "../../lib/mode";
import AskHub from "../../ask/AskHub";
@@ -34,9 +34,9 @@ export default function AskPage() {
browser, sends the relevant excerpts to your chosen AI, and answers with
citations. Nothing is hosted here — your key and the requests stay
between your browser and the provider. See{" "}
- <Link href="/use-with-ai" className="text-brand hover:underline">
+ <a href={AI_DOC_URL} className="text-brand hover:underline">
Use with AI
- </Link>{" "}
+ </a>{" "}
for other ways to use {isHub ? "every archive on this hub" : "this archive"}.
</p>
</header>
diff --git a/export/app/components/Footer.tsx b/export/app/components/Footer.tsx
@@ -5,6 +5,7 @@ import {
resolveSocialLinks,
} from "yt-dlp-transcript-common/lib/site";
import {
+ AI_DOC_URL,
PROJECT_NAME,
PROJECT_URL,
} from "yt-dlp-transcript-common/lib/project";
@@ -68,8 +69,9 @@ export default function Footer() {
·
</span>
)}
+ {/* The homepage's AI and MCP doc, in the same tab (release 16). */}
<a
- href="/use-with-ai"
+ href={AI_DOC_URL}
className="underline underline-offset-2 hover:text-foreground transition-colors"
>
Use with AI
diff --git a/export/app/components/Header.tsx b/export/app/components/Header.tsx
@@ -2,7 +2,7 @@ import Link from "next/link";
import { getSettings } from "yt-dlp-transcript-common/lib/settings";
import { resolveSocialLinks } from "yt-dlp-transcript-common/lib/site";
import { headerSocialLinks } from "yt-dlp-transcript-common/lib/socialLinks";
-import { INSTANCES_URL } from "yt-dlp-transcript-common/lib/project";
+import { AI_DOC_URL, INSTANCES_URL } from "yt-dlp-transcript-common/lib/project";
import { ThemeToggle } from "yt-dlp-transcript-common/components/ThemeToggle";
import { SocialLinks } from "yt-dlp-transcript-common/components/SocialLinks";
import { SocialScroll } from "yt-dlp-transcript-common/components/SocialScroll";
@@ -82,12 +82,14 @@ export default function Header() {
// The inline nav from lg. Ask AI is new: the chat was only reachable from
// the workspace control or from Use with AI, which is not where anyone looks
// for it.
- const navLinks = [
+ // Use with AI is the homepage's AI and MCP doc (release 16 slice DX): off
+ // this site, so a plain <a> in the same tab, not a client navigation.
+ const navLinks: { href: string; label: string; external?: boolean }[] = [
{ href: "/", label: "Search" },
{ href: "/ask/", label: "Ask AI" },
...(showDuplicates ? [{ href: "/duplicates", label: "Duplicates" }] : []),
...(showDownloads ? [{ href: "/downloads", label: "Downloads" }] : []),
- { href: "/use-with-ai", label: "Use with AI" },
+ { href: AI_DOC_URL, label: "Use with AI", external: true },
];
// The sheet also takes Offline on a PWA-shipping site — gated exactly as
// Footer.tsx gates it, so the two never disagree about whether this instance
@@ -121,15 +123,25 @@ export default function Header() {
</Link>
<nav className="hidden lg:flex shrink-0 items-center gap-4 text-sm font-medium">
- {navLinks.map((l) => (
- <Link
- key={l.href}
- href={l.href}
- className="text-foreground hover:text-brand transition-colors"
- >
- {l.label}
- </Link>
- ))}
+ {navLinks.map((l) =>
+ l.external ? (
+ <a
+ key={l.href}
+ href={l.href}
+ className="text-foreground hover:text-brand transition-colors"
+ >
+ {l.label}
+ </a>
+ ) : (
+ <Link
+ key={l.href}
+ href={l.href}
+ className="text-foreground hover:text-brand transition-colors"
+ >
+ {l.label}
+ </Link>
+ ),
+ )}
</nav>
<div className="flex min-w-0 items-center gap-5 lg:ml-auto">
diff --git a/export/app/components/MobileMenu.tsx b/export/app/components/MobileMenu.tsx
@@ -26,7 +26,9 @@ export default function MobileMenu({
links,
instancesUrl,
}: {
- links: { href: string; label: string }[];
+ // `external`: off this site (Use with AI, the homepage's AI and MCP doc), so
+ // a plain <a> in the same tab rather than a client navigation.
+ links: { href: string; label: string; external?: boolean }[];
// The Archilyzer home's Official Instances (common/lib/project.ts
// INSTANCES_URL); absent on the hub, which lists the instances itself.
instancesUrl?: string;
@@ -60,12 +62,21 @@ export default function MobileMenu({
<nav className="mt-2 flex flex-col px-2">
{links.map((l) => (
<SheetClose asChild key={l.href}>
- <Link
- href={l.href}
- className="rounded-md px-2 py-3 text-base font-medium text-foreground transition-colors hover:bg-accent"
- >
- {l.label}
- </Link>
+ {l.external ? (
+ <a
+ href={l.href}
+ className="rounded-md px-2 py-3 text-base font-medium text-foreground transition-colors hover:bg-accent"
+ >
+ {l.label}
+ </a>
+ ) : (
+ <Link
+ href={l.href}
+ className="rounded-md px-2 py-3 text-base font-medium text-foreground transition-colors hover:bg-accent"
+ >
+ {l.label}
+ </Link>
+ )}
</SheetClose>
))}
{instancesUrl && (
diff --git a/export/e2e-hub/use-with-ai-link.spec.ts b/export/e2e-hub/use-with-ai-link.spec.ts
@@ -0,0 +1,48 @@
+import { expect, test, type Locator, type Route } from "@playwright/test";
+import { AI_DOC_URL } from "../../common/lib/project";
+
+// Release 16 slice DX, on the hub: no /use-with-ai page; the header's, the
+// footer's and Ask AI's "Use with AI" go to the homepage's AI and MCP doc
+// (AI_DOC_URL), in the same tab, as plain anchors — the same links a site has.
+
+async function fulfillJson(route: Route, body: unknown) {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ headers: { "access-control-allow-origin": "*" },
+ body: JSON.stringify(body),
+ });
+}
+
+async function expectDocLink(link: Locator) {
+ await expect(link).toHaveAttribute("href", AI_DOC_URL);
+ await expect(link).not.toHaveAttribute("target", /./);
+}
+
+test.beforeEach(async ({ page }) => {
+ // No members: the shelf stays empty and nothing reaches a real origin.
+ await page.route("**/hub-sites.json", (r) => fulfillJson(r, []));
+ await page.route("**/hub-summary.json", (r) => r.fulfill({ status: 404, body: "" }));
+});
+
+test("the hub's header and footer Use with AI go to the homepage doc", async ({ page }) => {
+ await page.goto("/");
+ await expectDocLink(
+ page.getByRole("banner").getByRole("link", { name: "Use with AI", exact: true }),
+ );
+ await expectDocLink(
+ page.getByRole("contentinfo").getByRole("link", { name: "Use with AI", exact: true }),
+ );
+});
+
+test("the hub's Ask AI links Use with AI to the homepage doc", async ({ page }) => {
+ await page.goto("/ask/");
+ await expectDocLink(
+ page.getByRole("main").getByRole("link", { name: "Use with AI", exact: true }),
+ );
+});
+
+test("the hub has no /use-with-ai page", async ({ page }) => {
+ const res = await page.request.get("/use-with-ai/");
+ expect(res.status()).toBe(404);
+});
diff --git a/export/e2e/first-search.spec.ts b/export/e2e/first-search.spec.ts
@@ -47,6 +47,24 @@ async function expectClearScreen(page: Page) {
await expect(page.getByText(HINT)).toBeVisible();
}
+// A route outside the workspace, reached as a client navigation in the same
+// page life: the search session unmounts with the workspace and mounts again on
+// Back. Until release 16 the header's Use with AI link made that navigation;
+// it now leaves the site, and the footer's Changelog is a plain <a> (a new page
+// life), so the spec asks the app router itself — `window.next.router`, which
+// Next sets for debugging in development and production alike.
+async function leaveForChangelog(page: Page) {
+ await page.evaluate(() => {
+ (
+ window as unknown as { next: { router: { push(href: string): void } } }
+ ).next.router.push("/changelog/");
+ });
+ // A dev server compiles /changelog on its first visit.
+ await expect(page).toHaveURL(/\/changelog\/?$/, { timeout: 20_000 });
+ await expect(page.getByRole("heading", { level: 1, name: "Changelog" })).toBeVisible();
+ await expect(page.getByTestId("query-builder")).toHaveCount(0);
+}
+
async function expectAllVideos(page: Page) {
await expect(page.getByTestId("results-summary")).toHaveText("All videos (3)");
await expect(page.getByTestId("browse-hint")).toBeVisible();
@@ -124,13 +142,9 @@ test.describe("a clear screen until the first Search", () => {
await showAll(page);
await expectAllVideos(page);
// A route outside the workspace: the search session unmounts with it.
- await page
- .getByRole("banner")
- .getByRole("link", { name: "Use with AI", exact: true })
- .click();
- await expect(page).toHaveURL(/\/use-with-ai\/?$/);
+ await leaveForChangelog(page);
await page.goBack();
- await expect(page).not.toHaveURL(/use-with-ai/);
+ await expect(page).not.toHaveURL(/changelog/);
await expectAllVideos(page);
});
diff --git a/export/e2e/responsive.spec.ts b/export/e2e/responsive.spec.ts
@@ -74,7 +74,7 @@ test.describe("phone layout", () => {
"/ask/",
"/downloads/",
"/duplicates/",
- "/use-with-ai/",
+ "/changelog/",
]) {
test(`no horizontal overflow on ${route}`, async ({ page }) => {
await page.goto(route);
diff --git a/export/e2e/restore-no-refire.spec.ts b/export/e2e/restore-no-refire.spec.ts
@@ -57,13 +57,20 @@ async function seedStoredSearch(page: Page) {
}
// A route outside the workspace unmounts the search session; coming back
-// mounts it again in the same page life.
-async function leaveForUseWithAi(page: Page) {
- await page
- .getByRole("banner")
- .getByRole("link", { name: "Use with AI", exact: true })
- .click();
- await expect(page).toHaveURL(/use-with-ai/, { timeout: 20_000 });
+// mounts it again in the same page life. Until release 16 the header's Use
+// with AI link made that client navigation; it now leaves the site, and the
+// footer's Changelog is a plain <a> (a new page life), so the spec asks the app
+// router itself — `window.next.router`, which Next sets for debugging in
+// development and production alike.
+async function leaveForChangelog(page: Page) {
+ await page.evaluate(() => {
+ (
+ window as unknown as { next: { router: { push(href: string): void } } }
+ ).next.router.push("/changelog/");
+ });
+ await expect(page).toHaveURL(/\/changelog\/?$/, { timeout: 20_000 });
+ await expect(page.getByRole("heading", { level: 1, name: "Changelog" })).toBeVisible();
+ await expect(page.getByTestId("query-builder")).toHaveCount(0);
}
async function expectHeld(page: Page) {
@@ -160,9 +167,9 @@ test.describe("restored search waits for the visitor", () => {
// …and the stored query is in the box, held.
await expectHeld(page);
- await leaveForUseWithAi(page);
+ await leaveForChangelog(page);
await page.goBack();
- await expect(page).not.toHaveURL(/use-with-ai/);
+ await expect(page).not.toHaveURL(/changelog/);
// A second mount in the same visit: still held, still the listing.
await expectHeld(page);
await expect(page.getByTestId("results-summary")).toHaveText("All videos (1)");
@@ -182,12 +189,12 @@ test.describe("restored search waits for the visitor", () => {
await expect(page.getByTestId("results-summary")).toContainText("All videos");
await expect(leafInput(page)).toHaveValue("");
- await leaveForUseWithAi(page);
+ await leaveForChangelog(page);
await page
.getByRole("banner")
.getByRole("link", { name: "Search", exact: true })
.click();
- await expect(page).not.toHaveURL(/use-with-ai/, { timeout: 20_000 });
+ await expect(page).not.toHaveURL(/changelog/, { timeout: 20_000 });
// The plain search page reads the stored session: the query is held, and
// the listing shows because the visitor asked earlier in this visit.
await expectHeld(page);
diff --git a/export/e2e/use-with-ai-link.spec.ts b/export/e2e/use-with-ai-link.spec.ts
@@ -0,0 +1,49 @@
+import { expect, test, type Locator } from "@playwright/test";
+import { AI_DOC_URL } from "../../common/lib/project";
+import { installRoutes } from "./helpers";
+
+// Release 16 slice DX: a site has no /use-with-ai page. Its "Use with AI"
+// links — the header's nav (inline from `lg`, in the slide-out menu below it),
+// the footer's and the one on Ask AI — go to the homepage's AI and MCP doc
+// (AI_DOC_URL), in the same tab, as plain anchors: the doc is on another
+// origin, so there is no client navigation to make. The label is unchanged.
+
+async function expectDocLink(link: Locator) {
+ await expect(link).toHaveAttribute("href", AI_DOC_URL);
+ await expect(link).not.toHaveAttribute("target", /./);
+}
+
+test.beforeEach(async ({ page }) => {
+ await installRoutes(page);
+});
+
+test("the header's and the footer's Use with AI go to the homepage doc", async ({ page }) => {
+ await page.goto("/");
+ await expectDocLink(
+ page.getByRole("banner").getByRole("link", { name: "Use with AI", exact: true }),
+ );
+ await expectDocLink(
+ page.getByRole("contentinfo").getByRole("link", { name: "Use with AI", exact: true }),
+ );
+});
+
+test("the slide-out menu's Use with AI goes to the homepage doc", async ({ page }) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ await page.goto("/");
+ await page.getByRole("button", { name: "Open menu" }).click();
+ await expectDocLink(
+ page.getByRole("dialog").getByRole("link", { name: "Use with AI", exact: true }),
+ );
+});
+
+test("Ask AI's Use with AI goes to the homepage doc", async ({ page }) => {
+ await page.goto("/ask/");
+ await expectDocLink(
+ page.getByRole("main").getByRole("link", { name: "Use with AI", exact: true }),
+ );
+});
+
+test("the page is gone", async ({ page }) => {
+ const res = await page.request.get("/use-with-ai/");
+ expect(res.status()).toBe(404);
+});