diff --git a/__tests__/nextConfigRedirects.test.ts b/__tests__/nextConfigRedirects.test.ts
new file mode 100644
index 000000000..7632986a5
--- /dev/null
+++ b/__tests__/nextConfigRedirects.test.ts
@@ -0,0 +1,58 @@
+import { describe, expect, it } from "vitest";
+import nextConfig from "@/next.config.mjs";
+
+/**
+ * The interim `/onboarding/*` mounts (chat#1880) were dev scaffolding for
+ * testing the steps before the sequence container existed. They are retired
+ * into the canonical `/setup/*` sequence (chat#1889).
+ *
+ * These live in `redirects()` rather than as `redirect()` page components so
+ * they resolve as real 308s at the edge — a prerendered page shipping the
+ * redirect in its RSC payload returns HTTP 200 to any non-JS client (crawler,
+ * link previewer, curl), which defeats the point of keeping the old URL alive.
+ */
+describe("next.config redirects", () => {
+ const getRedirects = async () => {
+ const config = nextConfig as { redirects?: () => Promise };
+ expect(typeof config.redirects).toBe("function");
+ return (await config.redirects!()) as Array<{
+ source: string;
+ destination: string;
+ permanent: boolean;
+ }>;
+ };
+
+ it("retires /onboarding/first-task into /setup/tasks", async () => {
+ const rule = (await getRedirects()).find(
+ (r) => r.source === "/onboarding/first-task",
+ );
+
+ expect(rule).toBeDefined();
+ expect(rule?.destination).toBe("/setup/tasks");
+ expect(rule?.permanent).toBe(true);
+ });
+
+ it("retires /onboarding/roster into /setup/artists", async () => {
+ const rule = (await getRedirects()).find(
+ (r) => r.source === "/onboarding/roster",
+ );
+
+ expect(rule).toBeDefined();
+ expect(rule?.destination).toBe("/setup/artists");
+ expect(rule?.permanent).toBe(true);
+ });
+
+ it("never sends a retired onboarding mount to the home surface", async () => {
+ // Home renders the placeholder step cards while onboarding is incomplete,
+ // so routing a funnel arrival there drops them out of the sequence.
+ const onboardingRules = (await getRedirects()).filter((r) =>
+ r.source.startsWith("/onboarding"),
+ );
+
+ expect(onboardingRules.length).toBeGreaterThan(0);
+ for (const rule of onboardingRules) {
+ expect(rule.destination).not.toBe("/");
+ expect(rule.destination).toMatch(/^\/setup\//);
+ }
+ });
+});
diff --git a/app/onboarding/first-task/page.tsx b/app/onboarding/first-task/page.tsx
deleted file mode 100644
index 2db228f60..000000000
--- a/app/onboarding/first-task/page.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import FirstTaskStep from "@/components/Onboarding/FirstTaskStep";
-
-/**
- * Standalone mount for the onboarding first-task step (chat#1867) so
- * the step is user-testable before the OnboardingSequence container
- * exists. The sequence PR will mount directly.
- */
-const FirstTaskOnboardingPage = () => ;
-
-export default FirstTaskOnboardingPage;
diff --git a/app/onboarding/roster/page.tsx b/app/onboarding/roster/page.tsx
deleted file mode 100644
index 93d2abfc0..000000000
--- a/app/onboarding/roster/page.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import RosterSocialsFlow from "@/components/Onboarding/RosterSocialsFlow";
-
-const OnboardingRoster = () => ;
-
-export default OnboardingRoster;
diff --git a/app/setup/tasks/page.tsx b/app/setup/tasks/page.tsx
index a2a6e4a31..4722438bf 100644
--- a/app/setup/tasks/page.tsx
+++ b/app/setup/tasks/page.tsx
@@ -2,7 +2,7 @@ import FirstTaskStep from "@/components/Onboarding/FirstTaskStep";
/**
* `/setup/tasks` — welcome email step 5 ("Automate with tasks"). Mounts the
- * self-contained first-task step (same as /onboarding/first-task).
+ * self-contained first-task step.
*/
const SetupTasksPage = () => ;
diff --git a/components/Catalog/report/CatalogReportCta.tsx b/components/Catalog/report/CatalogReportCta.tsx
index ab7d05f26..1a71d22f0 100644
--- a/components/Catalog/report/CatalogReportCta.tsx
+++ b/components/Catalog/report/CatalogReportCta.tsx
@@ -5,6 +5,12 @@ import Link from "next/link";
* this valuation measured (onboarding sequence advance, chat#1867). Routes to
* the one-click first-task step — pre-runs the first weekly report, then
* confirms the Monday schedule — instead of a blank /tasks page.
+ *
+ * Points at the canonical `/setup/tasks` route rather than the deleted
+ * `/onboarding/first-task` mount (chat#1889, now a 308 in `next.config.mjs`).
+ * This CTA is the landing page for both the marketing valuation funnel and the
+ * valuation email, so it must route straight at the canonical step rather than
+ * bouncing through a retired one.
*/
const CatalogReportCta = () => {
return (
@@ -20,7 +26,7 @@ const CatalogReportCta = () => {
re-measures your catalog and emails you the trend.
Set up your weekly report
diff --git a/components/Catalog/report/__tests__/CatalogReportCta.test.tsx b/components/Catalog/report/__tests__/CatalogReportCta.test.tsx
new file mode 100644
index 000000000..643900e05
--- /dev/null
+++ b/components/Catalog/report/__tests__/CatalogReportCta.test.tsx
@@ -0,0 +1,31 @@
+// @vitest-environment jsdom
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+import CatalogReportCta from "@/components/Catalog/report/CatalogReportCta";
+
+vi.mock("next/link", () => ({
+ default: ({
+ href,
+ children,
+ ...rest
+ }: React.ComponentProps<"a"> & { href: string }) => (
+
+ {children}
+
+ ),
+}));
+
+describe("CatalogReportCta", () => {
+ it("routes to the canonical /setup/tasks step, not a retired /onboarding mount", () => {
+ render();
+ const cta = screen.getByRole("link", { name: /set up your weekly report/i });
+
+ // This CTA is the landing page for both the marketing valuation funnel and
+ // the valuation email, so it must point at the canonical setup sequence
+ // (chat#1889) — never at `/onboarding/*`, and never at `/`, which would
+ // drop a funnel signup on the home surface instead of the step.
+ expect(cta.getAttribute("href")).toBe("/setup/tasks");
+ expect(cta.getAttribute("href")).not.toContain("/onboarding");
+ });
+});
diff --git a/components/Onboarding/RosterSocialsFlow.tsx b/components/Onboarding/RosterSocialsFlow.tsx
index 825decedd..366af3495 100644
--- a/components/Onboarding/RosterSocialsFlow.tsx
+++ b/components/Onboarding/RosterSocialsFlow.tsx
@@ -8,10 +8,10 @@ import VerifySocialsStep from "./VerifySocialsStep";
type FlowStep = "roster" | "socials" | "done";
/**
- * Standalone container for the roster + socials onboarding steps so the
- * slice is user-testable at /onboarding/roster today. The sibling
- * onboarding-sequence router (chat#1867) mounts `ConfirmRosterStep` and
- * `VerifySocialsStep` directly with its own state-derived stepping.
+ * Standalone container for the roster + socials onboarding steps, mounted at
+ * `/setup/artists` and `/setup/socials`. The sibling onboarding-sequence
+ * router (chat#1867) mounts `ConfirmRosterStep` and `VerifySocialsStep`
+ * directly with its own state-derived stepping.
*
* `initialStep` lets a deep link open the flow directly at a given step (e.g.
* the welcome email's "verify socials" link → `/setup/socials`); it defaults to
diff --git a/next.config.mjs b/next.config.mjs
index c5387a5b5..0ed00b309 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -7,6 +7,29 @@ const nextConfig = {
reactStrictMode: true,
transpilePackages: ["geist"],
serverExternalPackages: ['@browserbasehq/stagehand', 'playwright'],
+ // The interim `/onboarding/*` mounts (chat#1880) were scaffolding so each
+ // step was user-testable before the sequence container existed. `/setup/*`
+ // is now the canonical sequence (chat#1889), so they are retired here.
+ //
+ // Config redirects rather than `redirect()` page components: Next prerenders
+ // such a page and ships the redirect in its RSC payload, so the old URL
+ // answers HTTP 200 to any non-JS client (crawler, link previewer, curl) and
+ // only redirects once React hydrates. These are real 308s at the edge, and
+ // they leave no page files behind to forget to delete.
+ redirects() {
+ return [
+ {
+ source: "/onboarding/first-task",
+ destination: "/setup/tasks",
+ permanent: true,
+ },
+ {
+ source: "/onboarding/roster",
+ destination: "/setup/artists",
+ permanent: true,
+ },
+ ];
+ },
experimental: {
optimizePackageImports: [
'lucide-react',