Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions __tests__/nextConfigRedirects.test.ts
Original file line number Diff line number Diff line change
@@ -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<unknown[]> };
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\//);
}
});
});
10 changes: 0 additions & 10 deletions app/onboarding/first-task/page.tsx

This file was deleted.

5 changes: 0 additions & 5 deletions app/onboarding/roster/page.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion app/setup/tasks/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => <FirstTaskStep />;

Expand Down
8 changes: 7 additions & 1 deletion components/Catalog/report/CatalogReportCta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -20,7 +26,7 @@ const CatalogReportCta = () => {
re-measures your catalog and emails you the trend.
</p>
<Link
href="/onboarding/first-task"
href="/setup/tasks"
className="mt-4 inline-flex items-center justify-center rounded-xl bg-primary px-5 py-2.5 font-heading text-sm font-semibold text-primary-foreground transition-colors duration-200 hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
Set up your weekly report
Expand Down
31 changes: 31 additions & 0 deletions components/Catalog/report/__tests__/CatalogReportCta.test.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => (
<a href={href} {...rest}>
{children}
</a>
),
}));

describe("CatalogReportCta", () => {
it("routes to the canonical /setup/tasks step, not a retired /onboarding mount", () => {
render(<CatalogReportCta />);
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");
});
});
8 changes: 4 additions & 4 deletions components/Onboarding/RosterSocialsFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading