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',