diff --git a/apps/web/src/app/(landing)/status/[id]/[component]/component-detail.tsx b/apps/web/src/app/(landing)/status/[id]/[component]/component-detail.tsx index ccb634118..6023623c2 100644 --- a/apps/web/src/app/(landing)/status/[id]/[component]/component-detail.tsx +++ b/apps/web/src/app/(landing)/status/[id]/[component]/component-detail.tsx @@ -18,7 +18,7 @@ import { ContentBoxTitle, } from "../../../content-box"; import { ExternalServicePill } from "../../external-service-pill"; -import { formatRelative } from "../../utils"; +import { formatRelative, getComponentFullName } from "../../utils"; import { HistoryBars } from "../history-bars"; import { ReportIssue } from "../report-issue"; @@ -77,18 +77,19 @@ function formatTimestamp(value: string | null | undefined): string | null { function jsonLd(args: { serviceName: string; componentName: string; + fullName: string; serviceUrl: string; componentUrl: string; answer: string; }) { return createJsonLDGraph([ getJsonLDWebPage({ - name: `${args.serviceName} ${args.componentName} Status`, + name: `${args.fullName} Status`, url: args.componentUrl, }), getJsonLDFAQPage([ { - question: `Is ${args.serviceName} ${args.componentName} down?`, + question: `Is ${args.fullName} down?`, answer: args.answer, }, ]), @@ -154,7 +155,7 @@ export function ComponentDetail({ if (!data.found || !data.service || !data.component) return null; const { service, component, history, incidents, overlayIncidents } = data; - const fullName = `${service.name} ${component.name}`; + const fullName = getComponentFullName(service.name, component.name); const answer = answerFor({ fullName, indicator: component.indicator, @@ -167,6 +168,7 @@ export function ComponentDetail({ const ld = jsonLd({ serviceName: service.name, componentName: component.name, + fullName, serviceUrl, componentUrl, answer, @@ -176,9 +178,7 @@ export function ComponentDetail({
-

- Is {service.name} {component.name} down? -

+

Is {fullName} down?

{answer} Below you'll find the live {component.name} status, uptime over the last {days} days, and recent incidents affecting {component.name}. @@ -214,7 +214,7 @@ export function ComponentDetail({

{component.reporters > 0 ? ( diff --git a/apps/web/src/app/(landing)/status/[id]/[component]/page.tsx b/apps/web/src/app/(landing)/status/[id]/[component]/page.tsx index 5e9e139ac..fbffb6dcf 100644 --- a/apps/web/src/app/(landing)/status/[id]/[component]/page.tsx +++ b/apps/web/src/app/(landing)/status/[id]/[component]/page.tsx @@ -18,6 +18,7 @@ import { ContentBoxDescription, ContentBoxTitle, } from "../../../content-box"; +import { getComponentFullName } from "../../utils"; import { ComponentDetail } from "./component-detail"; export const dynamic = "force-dynamic"; @@ -38,7 +39,7 @@ export async function generateMetadata(args: { return { ...defaultMetadata, title: "Not Found" }; } - const fullName = `${service.name} ${component.name}`; + const fullName = getComponentFullName(service.name, component.name); const title = `Is ${fullName} Down? ${fullName} Status & History`; const description = `Is ${component.name} (${service.name}) down right now? Check the live status, uptime over the last ${HISTORY_DAYS} days, and recent incidents for ${component.name} tracked by OpenStatus.`; const canonicalUrl = `${BASE_URL}/status/${service.slug}/${component.slug}`; @@ -77,6 +78,8 @@ export default async function Page(args: { params: Promise }) { permanentRedirect(`/status/${service.slug}/${component.slug}`); } + const fullName = getComponentFullName(service.name, component.name); + await api.externalService.component.prefetch({ serviceSlug: service.slug, componentSlug: component.slug, @@ -100,7 +103,7 @@ export default async function Page(args: { params: Promise }) { fallback={

- Loading {service.name} {component.name} status… + Loading {fullName} status…

} diff --git a/apps/web/src/app/(landing)/status/utils.test.ts b/apps/web/src/app/(landing)/status/utils.test.ts new file mode 100644 index 000000000..7339b146e --- /dev/null +++ b/apps/web/src/app/(landing)/status/utils.test.ts @@ -0,0 +1,52 @@ +import { expect } from "@std/expect"; +import { describe, test } from "@std/testing/bdd"; + +import { getComponentFullName } from "./utils"; + +describe("getComponentFullName", () => { + test("does not repeat the provider already in the component name", () => { + expect(getComponentFullName("Zoom", "Zoom AI")).toBe("Zoom AI"); + }); + + test("prefixes the provider when the component name lacks it", () => { + expect(getComponentFullName("Zoom", "Web SDK")).toBe("Zoom Web SDK"); + }); + + test("matches the provider prefix case-insensitively", () => { + expect(getComponentFullName("Zoom", "zoom Chat")).toBe("zoom Chat"); + }); + + test("component equal to the provider name", () => { + expect(getComponentFullName("Zoom", "Zoom")).toBe("Zoom"); + }); + + test("requires a word boundary after the provider name", () => { + expect(getComponentFullName("Zoom", "Zoomify")).toBe("Zoom Zoomify"); + }); + + test("keeps the provider name when it appears later", () => { + expect(getComponentFullName("Zoom", "Rooms for Zoom")).toBe( + "Zoom Rooms for Zoom", + ); + }); + + test("trims surrounding whitespace", () => { + expect(getComponentFullName(" Zoom ", " Zoom AI ")).toBe("Zoom AI"); + expect(getComponentFullName(" Zoom", "Web SDK ")).toBe("Zoom Web SDK"); + }); + + test("collapses inner whitespace runs", () => { + expect(getComponentFullName("Zoom", "Zoom AI")).toBe("Zoom AI"); + expect(getComponentFullName("Zoom", "Zoom\tAI")).toBe("Zoom AI"); + expect(getComponentFullName("Zoom Video", "Web SDK")).toBe( + "Zoom Video Web SDK", + ); + }); + + test("falls back to the non-empty name", () => { + expect(getComponentFullName("Zoom", "")).toBe("Zoom"); + expect(getComponentFullName("Zoom", " ")).toBe("Zoom"); + expect(getComponentFullName("", "Zoom AI")).toBe("Zoom AI"); + expect(getComponentFullName(" ", "")).toBe(""); + }); +}); diff --git a/apps/web/src/app/(landing)/status/utils.ts b/apps/web/src/app/(landing)/status/utils.ts index bdcfd5d25..c433d6d41 100644 --- a/apps/web/src/app/(landing)/status/utils.ts +++ b/apps/web/src/app/(landing)/status/utils.ts @@ -17,6 +17,22 @@ export function isStale(fetchedAtMs: number): boolean { return Date.now() - fetchedAtMs > STALE_THRESHOLD_MS; } +// Upstream component names often already carry the provider ("Zoom AI" on +// Zoom), so prefixing blindly yields "Zoom Zoom AI". +export function getComponentFullName( + serviceName: string, + componentName: string, +): string { + const service = serviceName.trim().replace(/\s+/g, " "); + const component = componentName.trim().replace(/\s+/g, " "); + if (!component) return service; + if (!service) return component; + const lower = component.toLowerCase(); + const prefix = service.toLowerCase(); + if (lower === prefix || lower.startsWith(`${prefix} `)) return component; + return `${service} ${component}`; +} + // Natural-language answer to "Is down?", used both as on-page lead copy // and as the FAQPage answer in JSON-LD. Mirrors getPillStyle semantics. export function getStatusAnswer(args: { diff --git a/apps/web/src/app/api/og/external-service/route.tsx b/apps/web/src/app/api/og/external-service/route.tsx index d188b7790..986ac0ab2 100644 --- a/apps/web/src/app/api/og/external-service/route.tsx +++ b/apps/web/src/app/api/og/external-service/route.tsx @@ -2,7 +2,7 @@ import { readFile } from "node:fs/promises"; import { ImageResponse } from "next/og"; -import { isStale } from "../../../(landing)/status/utils"; +import { getComponentFullName, isStale } from "../../../(landing)/status/utils"; import { getComponentEscalation, getServiceEscalation, @@ -133,6 +133,7 @@ export async function GET(req: Request) { if (componentResult?.service && componentResult.component) { const { service, component } = componentResult; isDetail = true; + const fullName = getComponentFullName(service.name, component.name); const esc = await getComponentEscalation({ serviceId: service.id, componentId: component.id, @@ -145,8 +146,8 @@ export async function GET(req: Request) { category = content.label; categoryDot = content.bg; title = esc.escalated - ? `Users reporting issues with ${service.name} ${component.name}` - : `Is ${service.name} ${component.name} down?`; + ? `Users reporting issues with ${fullName}` + : `Is ${fullName} down?`; description = ""; footer = `${FOOTER}/${service.slug}/${component.slug}`; } else {