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
6 changes: 6 additions & 0 deletions .changeset/refine-search-workflows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@workspace/web": patch
"@workspace/lib": patch
---

Separate free search diagnosis from explicit paid GEO recommendations and clarify localized analysis, evidence and action workflows across screen sizes.
11 changes: 11 additions & 0 deletions apps/web/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ const config: StorybookConfig = {
find: /^@\/server\/opportunities-fn(\.ts)?$/,
replacement: path.resolve(mocksDir, "server-opportunities.ts"),
},
...[
"analysis-fn",
"monitoring-fn",
"content-draft-fn",
"workspace-fn",
"evidence-summary",
"site-audit-fn",
].map((module) => ({
find: new RegExp(`^@/server/${module}(\\.ts)?$`),
replacement: path.resolve(mocksDir, "server-workflow.ts"),
})),
{ find: /^@tanstack\/react-router$/, replacement: path.resolve(mocksDir, "tanstack-router.tsx") },
{ find: /^@tanstack\/react-start\/server$/, replacement: path.resolve(mocksDir, "tanstack-start.ts") },
{ find: /^@tanstack\/react-start$/, replacement: path.resolve(mocksDir, "tanstack-start.ts") },
Expand Down
98 changes: 98 additions & 0 deletions apps/web/src/components/__tests__/monitoring-loop.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it, vi } from "vitest";
import { type ActionKindFilter, MonitoringLoop } from "@/components/monitoring-loop";
import { LocaleProvider } from "@/lib/i18n";

vi.mock("@/server/monitoring-fn", () => ({
getMonitoringFn: vi.fn(),
applyOptimizationActionFn: vi.fn(),
retestOptimizationActionFn: vi.fn(),
}));
vi.mock("@/components/collection-check", () => ({ CollectionCheck: () => <div>Question collection controls</div> }));
vi.mock("@/components/workspace-control", () => ({ WorkspaceControl: () => <div>GEO runtime controls</div> }));
vi.mock("@/components/content-draft", () => ({ ContentDraft: () => <div>Draft editor</div> }));

const seoAction = {
id: "seo-1",
kind: "seo",
title: "Improve the pricing snippet",
description: "Reviewed pricing page",
status: "applied",
implementationNote: "Updated title",
comparison: null,
seoComparison: {
status: "waiting",
reasonCode: "waiting_period",
reason: "Wait for an equally long, non-overlapping full period after implementation (Search Console timezone).",
windows: {
baseline: { startDate: "2026-10-01", endDate: "2026-10-28" },
current: { startDate: "2026-10-03", endDate: "2026-10-30" },
},
},
};
const geoAction = {
...seoAction,
id: "geo-1",
kind: "geo",
title: "Improve citation coverage",
seoComparison: null,
};

function render(kind: ActionKindFilter, actions = [seoAction, geoAction], locale: "en" | "zh" = "en") {
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
client.setQueryData(["monitoring", "brand-1"], {
actions,
attempts: [],
questions: [],
runtime: { worker: "ready" },
});
return renderToStaticMarkup(
<LocaleProvider initialLocale={locale}>
<QueryClientProvider client={client}>
<MonitoringLoop brandId="brand-1" initialKind={kind} />
</QueryClientProvider>
</LocaleProvider>,
);
}

describe("SEO and GEO action journeys", () => {
it("keeps SEO actions focused on search periods and gives a route back to diagnosis", () => {
const html = render("seo");
expect(html).toContain("Improve the pricing snippet");
expect(html).not.toContain("Improve citation coverage");
expect(html).not.toContain("Question collection controls");
expect(html).not.toContain("GEO runtime controls");
expect(html).not.toContain("Retest · uses configured providers");
expect(html).toContain("2026-10-01");
expect(html).toContain("2026-10-30");
expect(html).toContain("non-overlapping full period");
expect(html).toContain('href="/opportunities#search"');
expect(html).toContain('id="optimization-action-seo-1"');
});

it("preserves question collection and retesting for GEO", () => {
const html = render("geo");
expect(html).toContain("Question collection controls");
expect(html).toContain("Retest · uses configured providers");
expect(html).toContain("Improve citation coverage");
expect(html).not.toContain("Improve the pricing snippet");
});

it("shows both types when all actions are selected", () => {
const html = render("all");
expect(html).toContain("Improve the pricing snippet");
expect(html).toContain("Improve citation coverage");
});

it("directs an empty SEO list to diagnosis rather than question collection", () => {
const html = render("seo", []);
expect(html).toContain("No SEO actions yet. Review search diagnosis and save a candidate.");
expect(html).toContain('href="/opportunities#search"');
expect(html).not.toContain("Collect a baseline");
});

it("localizes persisted SEO comparison reasons", () => {
expect(render("seo", [seoAction], "zh")).toContain("请等待实施后的完整等长周期");
});
});
33 changes: 31 additions & 2 deletions apps/web/src/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,33 @@ function brandGroups(organization: OrganizationSummary, brand: SidebarBrand, seo
matchLinks: INSIGHT_PAGES.slice(1).map((page) => ({ to: page.to, params })),
icon: IconChartBar,
},
{ title: "AI Traffic & Crawls", link: { to: "/app/org/$org/brand/$brand/traffic", params }, icon: IconChartBar },
{ title: "Opportunities", link: { to: "/app/org/$org/brand/$brand/opportunities", params }, icon: IconTarget },
{
title: "GEO recommendations",
link: { to: "/app/org/$org/brand/$brand/opportunities", params },
icon: IconTarget,
},
{
title: "GEO actions",
link: { to: "/app/org/$org/brand/$brand/opportunities", params, hash: "actions" },
icon: IconFileText,
},
);

const groups: NavGroup[] = [];
if (seoEnabled)
groups.push({
label: "SEO",
items: [
{
title: "Search diagnosis",
link: { to: "/app/org/$org/brand/$brand/opportunities", params, hash: "search" },
icon: IconTarget,
},
{
title: "SEO actions",
link: { to: "/app/org/$org/brand/$brand/opportunities", params, hash: "seo-actions" },
icon: IconFileText,
},
...SEO_PAGES.map(
(page): NavItem => ({
title: page.title,
Expand All @@ -93,6 +111,17 @@ function brandGroups(organization: OrganizationSummary, brand: SidebarBrand, seo
],
});
groups.push({ label: "GEO", items: dashboard });
groups.push({
label: "Analytics",
items: [
{ title: "Traffic analysis", link: { to: "/app/org/$org/brand/$brand/traffic", params }, icon: IconChartBar },
{
title: "All improvement actions",
link: { to: "/app/org/$org/brand/$brand/opportunities", params, hash: "all-actions" },
icon: IconFileText,
},
],
});

groups.push({
label: "Settings",
Expand Down
Loading
Loading