From 0d88ea618a7aa985a7d4828c2a602411d892c6e9 Mon Sep 17 00:00:00 2001 From: AArslane Date: Fri, 2 Oct 2026 09:22:36 +0200 Subject: [PATCH] fix(web): don't repeat the provider in external component names (#2815) Component pages built their display name as `${service.name} ${component.name}`, so a Zoom component already named "Zoom AI" rendered as "Zoom Zoom AI" in the page title, og/twitter title, H1, lead answer, report-issue block, JSON-LD (WebPage name, FAQ question) and the OG image. Add getComponentFullName() and use it in all of those places. The provider is only skipped when the component name already starts with it followed by a space (or equals it), case-insensitively. "Zoom" + "Web SDK" still yields "Zoom Web SDK", and "Zoomify" / "Rooms for Zoom" keep the prefix. Names are trimmed and inner whitespace collapsed; an empty name falls back to the other. URLs, canonicals, slugs and source data are unchanged. Co-authored-by: Claude Sonnet 5.5 --- .../[id]/[component]/component-detail.tsx | 16 +++--- .../status/[id]/[component]/page.tsx | 7 ++- .../src/app/(landing)/status/utils.test.ts | 52 +++++++++++++++++++ apps/web/src/app/(landing)/status/utils.ts | 16 ++++++ .../src/app/api/og/external-service/route.tsx | 7 +-- 5 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 apps/web/src/app/(landing)/status/utils.test.ts 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 ccb63411..6023623c 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 5e9e139a..fbffb6dc 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 00000000..7339b146 --- /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 bdcfd5d2..c433d6d4 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 d188b779..986ac0ab 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 { -- 2.51.2