From 76620ed31b5a3af4e779ed1cf98e67c0f1bbf847 Mon Sep 17 00:00:00 2001 From: Gaurav Jadhav Date: Wed, 9 Sep 2026 18:06:04 +0530 Subject: [PATCH 1/3] fix(web,schema): stop linking at redirects, and always attribute Article schema Three findings from the 2026-08 SEO audit, verified against the live site, the droplet and the Payload DB on 2026-09-09. Internal links to 3XX (523 pages flagged, 543 measured): /knowledge-hub and /legal sit in the nav and footer on every page and both 308 to their first document. The redirect map moves out of proxy.ts into lib/nav/section-index.ts so the proxy and every link site read the same pairs, and hrefs resolve through sectionIndexHref(). The proxy still 308s anyone arriving on the old path. Article author (319 of 457 nodes): articleSchema, blogPostingSchema and newsArticleSchema dropped `author` when a document had no named contributor, leaving knowledge-hub, news and resources failing Google's Article requirements. They now fall back to the Organization node, which is what the house byline and the case studies already resolve to. Named contributors still win where the CMS has them. Duplicate H1 across pages: the triam-security-rebrands `-2` slug now 308s to its unsuffixed canonical, matching the precedent set for why-containers. Both CMS documents stay in the sitemap until an editor sets seo.indexable=noindex. Also lifts the three footer legal links from a 21px to a 27px touch target, the last ones under the WCAG 2.5.8 AA floor; the nav columns already cleared it. --- apps/web/next.config.ts | 13 ++++++ .../src/app/(legal)/privacy-policy/page.tsx | 4 +- apps/web/src/components/sections/Footer.tsx | 13 +++++- .../knowledge-hub/KnowledgeHubArticle.tsx | 4 +- apps/web/src/lib/nav-config.ts | 4 +- apps/web/src/lib/nav/section-index.test.ts | 28 ++++++++++++ apps/web/src/lib/nav/section-index.ts | 22 +++++++++ apps/web/src/proxy.ts | 8 ++-- packages/schema/src/builders/jsonld.test.ts | 45 +++++++++++++++++++ packages/schema/src/builders/jsonld.tsx | 34 +++++++------- 10 files changed, 150 insertions(+), 25 deletions(-) create mode 100644 apps/web/src/lib/nav/section-index.test.ts create mode 100644 apps/web/src/lib/nav/section-index.ts diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index 52fa6492c..7233d6270 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -217,6 +217,19 @@ const nextConfig: NextConfig = { destination: "/news/why-containers-drive-supply-chain-breaches", permanent: true, }, + // Second duplicate news article, same collision-suffix pattern as above. + // Both documents were created 2025-08-26 and render the same story with + // the same 104-character H1 (617 vs 616 words); both returned 200 with + // their own self-canonical. The unsuffixed slug is kept, matching the + // precedent set directly above. The CMS document still exists and is + // still in the sitemap until its `seo.indexable` is set to noindex. + { + source: + "/news/triam-security-rebrands-as-cleanstart-to-reflect-product-led-focus-on-securing-the-software-supply-chain-2", + destination: + "/news/triam-security-rebrands-as-cleanstart-to-reflect-product-led-focus-on-securing-the-software-supply-chain", + permanent: true, + }, ]; }, images: { diff --git a/apps/web/src/app/(legal)/privacy-policy/page.tsx b/apps/web/src/app/(legal)/privacy-policy/page.tsx index 9f46b9aad..c101025a2 100644 --- a/apps/web/src/app/(legal)/privacy-policy/page.tsx +++ b/apps/web/src/app/(legal)/privacy-policy/page.tsx @@ -8,6 +8,8 @@ import { buildPageMetadata } from "@/lib/seo/canonical"; import { resolveCmsSeo } from "@/lib/seo/cms-seo"; import { breadcrumbSchema } from "@/lib/seo/jsonld"; import { JsonLdGraph } from "@/components/JsonLdGraph"; +import { sectionIndexHref } from "@/lib/nav/section-index"; + import { getPageGraph } from "@/lib/seo/compose-page"; /** @@ -60,7 +62,7 @@ export default async function PrivacyPolicyPage(): Promise { const graph = await getPageGraph("/privacy-policy", [ breadcrumbSchema([ { name: "Home", path: "/" }, - { name: "Legal", path: "/legal" }, + { name: "Legal", path: sectionIndexHref("/legal") }, { name: doc.title }, ]), ]); diff --git a/apps/web/src/components/sections/Footer.tsx b/apps/web/src/components/sections/Footer.tsx index bafae9c49..efd1daebb 100644 --- a/apps/web/src/components/sections/Footer.tsx +++ b/apps/web/src/components/sections/Footer.tsx @@ -2,6 +2,8 @@ import React from "react"; import Link from "next/link"; import { CookiePreferencesButton } from "@/components/consent"; +import { sectionIndexHref } from "@/lib/nav/section-index"; + import { FooterSignature } from "@/components/sections/FooterSignature"; /** @@ -74,7 +76,7 @@ const CREDENTIALS: Badge[] = [ ]; const LEGAL_LINKS = [ - { label: "Legal", href: "/legal" }, + { label: "Legal", href: sectionIndexHref("/legal") }, { label: "Privacy Policy", href: "/privacy-policy" }, { label: "Acceptable Use Policy", href: "/legal/acceptable-use-policy" }, ]; @@ -300,10 +302,17 @@ export function Footer({ page, so default in-viewport prefetch fires an RSC (`?_rsc=`) request site-wide — pure crawl-budget waste for low-traffic policy pages. Click nav is unaffected. */} + {/* `before:` lifts the touch target from 21px (12px x + 1.75, no padding) to 27px, clearing the 24x24 WCAG + 2.5.8 AA floor — same pseudo-element trick as the nav + columns, so the utility row's visual rhythm is + untouched. The row uses gap-y-1 (4px) when it wraps, so + 3px of overhang each side still leaves the wrapped + lines from overlapping. */} {link.label} diff --git a/apps/web/src/components/sections/knowledge-hub/KnowledgeHubArticle.tsx b/apps/web/src/components/sections/knowledge-hub/KnowledgeHubArticle.tsx index 04737846f..d10589c65 100644 --- a/apps/web/src/components/sections/knowledge-hub/KnowledgeHubArticle.tsx +++ b/apps/web/src/components/sections/knowledge-hub/KnowledgeHubArticle.tsx @@ -2,6 +2,8 @@ import { HeroReveal, Reveal } from '@/components/ui/Reveal'; import type { KhArticle } from '@/lib/knowledge-hub'; import { RenderLexical } from '@/lib/renderLexical'; import { KnowledgeHubArticleFAQ } from './KnowledgeHubArticleFAQ'; +import { sectionIndexHref } from "@/lib/nav/section-index"; + import Link from 'next/link'; export function KnowledgeHubArticle({ @@ -88,7 +90,7 @@ function Breadcrumb({ category }: { category?: string | null | undefined }): Rea
  • diff --git a/apps/web/src/lib/nav-config.ts b/apps/web/src/lib/nav-config.ts index f1398c077..b71c8576f 100644 --- a/apps/web/src/lib/nav-config.ts +++ b/apps/web/src/lib/nav-config.ts @@ -6,6 +6,8 @@ // the href is data-only until the page exists. When a page is marked ✅ in WEB-PAGES.md, // switch its renderer from to in MegaMenu / MobileNav. +import { sectionIndexHref } from "@/lib/nav/section-index"; + export type NavLeaf = { label: string; href: string; @@ -195,7 +197,7 @@ export const NAV_TREE: NavItem[] = [ { label: "Resource Center", href: "/resource-center", icon: "folder" }, { label: "Case Studies", href: "/case-studies", icon: "doc-signed" }, { label: "Newsroom", href: "/news", icon: "newspaper" }, - { label: "Knowledge Hub", href: "/knowledge-hub", icon: "book-open" }, + { label: "Knowledge Hub", href: sectionIndexHref("/knowledge-hub"), icon: "book-open" }, ], }, { diff --git a/apps/web/src/lib/nav/section-index.test.ts b/apps/web/src/lib/nav/section-index.test.ts new file mode 100644 index 000000000..6f735ade1 --- /dev/null +++ b/apps/web/src/lib/nav/section-index.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; + +import { + SECTION_INDEX_REDIRECTS, + sectionIndexHref, +} from "./section-index"; + +describe("sectionIndexHref", () => { + it("resolves a section landing path to its canonical document", () => { + expect(sectionIndexHref("/knowledge-hub")).toBe( + "/knowledge-hub/vex-documents", + ); + expect(sectionIndexHref("/legal")).toBe( + "/legal/additional-third-party-terms", + ); + }); + + it("returns any other path unchanged", () => { + expect(sectionIndexHref("/blogs")).toBe("/blogs"); + expect(sectionIndexHref("/")).toBe("/"); + }); + + it("never maps a path to another redirecting path", () => { + for (const target of Object.values(SECTION_INDEX_REDIRECTS)) { + expect(SECTION_INDEX_REDIRECTS[target]).toBeUndefined(); + } + }); +}); diff --git a/apps/web/src/lib/nav/section-index.ts b/apps/web/src/lib/nav/section-index.ts new file mode 100644 index 000000000..e892b991c --- /dev/null +++ b/apps/web/src/lib/nav/section-index.ts @@ -0,0 +1,22 @@ +/** + * Section landing paths that have no standalone index page. Each permanently + * redirects to its canonical first document, issued in `proxy.ts` before any + * rendering so crawlers receive a hard 308. + * + * This is the single source of truth for those pairs. `proxy.ts` reads it to + * issue the redirect; link sites read it through `sectionIndexHref` so no + * internal link ever points at a URL that redirects. Keeping both sides on one + * map is what stops the two drifting apart. + */ +export const SECTION_INDEX_REDIRECTS: Record = { + "/knowledge-hub": "/knowledge-hub/vex-documents", + "/legal": "/legal/additional-third-party-terms", +}; + +/** + * Resolves an internal path to the URL that actually returns 200. Paths with no + * entry in the map are returned unchanged, so this is safe to wrap any href in. + */ +export function sectionIndexHref(path: string): string { + return SECTION_INDEX_REDIRECTS[path] ?? path; +} diff --git a/apps/web/src/proxy.ts b/apps/web/src/proxy.ts index 499d97fbf..e1c9df0ba 100644 --- a/apps/web/src/proxy.ts +++ b/apps/web/src/proxy.ts @@ -23,6 +23,7 @@ import { import { detailRouteExists } from "@/lib/detail-route-not-found-guard"; import { isIndexingAllowed } from "@/lib/seo/indexing"; import { stripLegacyPaginationParams } from "@/lib/seo/legacy-params"; +import { SECTION_INDEX_REDIRECTS } from "@/lib/nav/section-index"; const PRODUCTION_HOST = "www.cleanstart.com"; const APEX_HOST = "cleanstart.com"; @@ -36,11 +37,8 @@ const DRAFT_BYPASS_COOKIE = "__prerender_bypass"; // async Header fetches the CMS), Next downgrades the redirect to a 200 + meta // refresh, which search engines read as a duplicate page (it inherited the // default/home canonical). Targets are displayOrder-pinned, so they're stable -// across re-seeds; the owning pages keep the same slugs as their fallback. -const SECTION_INDEX_REDIRECTS: Record = { - "/knowledge-hub": "/knowledge-hub/vex-documents", - "/legal": "/legal/additional-third-party-terms", -}; +// across re-seeds; the owning pages keep the same slugs as their fallback. The +// map lives in lib/nav so link sites resolve hrefs through the same pairs. // Stays report-only until the CSP burn-in is complete; set CSP_ENFORCE=1 to flip. const CSP_MODE: "report-only" | "enforce" = diff --git a/packages/schema/src/builders/jsonld.test.ts b/packages/schema/src/builders/jsonld.test.ts index 6803b00f7..dbfdcfa7c 100644 --- a/packages/schema/src/builders/jsonld.test.ts +++ b/packages/schema/src/builders/jsonld.test.ts @@ -1,11 +1,13 @@ import { describe, expect, it } from "vitest"; import { + articleSchema, blogPostingSchema, breadcrumbSchema, eventSchema, jobPostingSchema, caseStudyListSchema, organizationSchema, + newsArticleSchema, reviewSchema, videoObjectSchema, } from "./jsonld"; @@ -193,3 +195,46 @@ describe("reviewSchema", () => { expect(reviewSchema([])).toEqual([]); }); }); + +describe("author fallback (Google Article requirement)", () => { + const ORG = { "@id": "https://www.cleanstart.com/#organization" }; + + it("attributes an Article with no named author to the Organization", () => { + const node = articleSchema({ title: "T", path: "/knowledge-hub/x" }); + expect(node.author).toEqual([ORG]); + }); + + it("attributes a BlogPosting with no named author to the Organization", () => { + const node = blogPostingSchema({ title: "T", path: "/blogs/x" }); + expect(node.author).toEqual([ORG]); + }); + + it("attributes a NewsArticle with no named author to the Organization", () => { + const node = newsArticleSchema({ title: "T", path: "/news/x" }); + expect(node.author).toEqual([ORG]); + }); + + it("prefers named contributors over the Organization fallback", () => { + const node = articleSchema({ + title: "T", + path: "/blogs/x", + authors: [{ name: "Dhanush VM", slug: "dhanush-vm" }], + }); + expect(node.author).toEqual([ + { + "@type": "Person", + name: "Dhanush VM", + url: "https://www.cleanstart.com/author/dhanush-vm", + }, + ]); + }); + + it("still collapses the house byline onto the Organization node", () => { + const node = articleSchema({ + title: "T", + path: "/blogs/x", + authors: [{ name: "CleanStart Security", slug: "cleanstart-security" }], + }); + expect(node.author).toEqual([ORG]); + }); +}); diff --git a/packages/schema/src/builders/jsonld.tsx b/packages/schema/src/builders/jsonld.tsx index 8e622c5da..90a1a37ef 100644 --- a/packages/schema/src/builders/jsonld.tsx +++ b/packages/schema/src/builders/jsonld.tsx @@ -53,6 +53,22 @@ const authorNode = (a: { name: string; slug?: string | undefined }) => name: a.name, ...(a.slug ? { url: absoluteUrl(`/author/${a.slug}`) } : {}), }; + +/** + * `author` is required on Article and its subtypes. Named contributors win when + * the document has them; otherwise the publishing Organization is the honest + * attribution, which is what the house byline already resolves to. Omitting the + * property instead leaves the node failing Google's Article requirements, which + * is what knowledge-hub, news and resources documents used to do. + */ +const authorProperty = ( + authors?: Array<{ name: string; slug?: string | undefined }> | undefined, +) => ({ + author: + authors && authors.length > 0 + ? authors.map(authorNode) + : [{ "@id": ORGANIZATION_ID }], +}); const WEBSITE_ID = `${SITE_URL}/#website`; /** Shared schema.org event-status IRIs (Event + webinar ItemList). */ @@ -361,11 +377,7 @@ export function blogPostingSchema({ ...(imageUrl ? { image: [imageUrl] } : {}), ...(publishedAt ? { datePublished: publishedAt } : {}), ...(lastModified ? { dateModified: lastModified } : {}), - ...(authors && authors.length > 0 - ? { - author: authors.map(authorNode), - } - : {}), + ...authorProperty(authors), ...(category ? { articleSection: category } : {}), ...(cleanRelatedLinks ? { relatedLink: cleanRelatedLinks } : {}), publisher: { "@id": ORGANIZATION_ID }, @@ -404,11 +416,7 @@ export function articleSchema({ ...(imageUrl ? { image: [imageUrl] } : {}), ...(publishedAt ? { datePublished: publishedAt } : {}), ...(lastModified ? { dateModified: lastModified } : {}), - ...(authors && authors.length > 0 - ? { - author: authors.map(authorNode), - } - : {}), + ...authorProperty(authors), ...(type ? { genre: type } : {}), publisher: { "@id": ORGANIZATION_ID }, }; @@ -454,11 +462,7 @@ export function newsArticleSchema({ ...(publishedAt ? { datePublished: publishedAt } : {}), ...(lastModified ? { dateModified: lastModified } : {}), ...(section ? { articleSection: section } : {}), - ...(authors && authors.length > 0 - ? { - author: authors.map(authorNode), - } - : {}), + ...authorProperty(authors), publisher: { "@id": ORGANIZATION_ID }, }; } From 63ba49a2ca193fd6db078e565c592c7c25f10c3b Mon Sep 17 00:00:00 2001 From: Gaurav Jadhav Date: Wed, 9 Sep 2026 18:15:14 +0530 Subject: [PATCH 2/3] docs(ops): record the CMS/web same-branch deploy hazard Both apps deploy from `main`, so one merge fires the Docker build and the Vercel build together and Vercel normally wins by several minutes. Any change where the web app stops sending a field the CMS still marks required will 400 every submission for the length of that gap. Hit while shipping the forms overhaul. The fix is to relax the constraint in prod before merging rather than trying to sequence two deploys off one branch. --- docs/operations/PRODUCTION-ROLLOUT.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/operations/PRODUCTION-ROLLOUT.md b/docs/operations/PRODUCTION-ROLLOUT.md index e00d3c926..de21316e7 100644 --- a/docs/operations/PRODUCTION-ROLLOUT.md +++ b/docs/operations/PRODUCTION-ROLLOUT.md @@ -6,6 +6,16 @@ **Run mechanism:** prod CMS image lacks `scripts/`; `docker cp` the script into `cleanstart-cms-1` and run via `pnpm exec tsx` (no `--env-file` — env is via container vars). See memory note `prod-backfill-script-run-mechanism`. +**Deploy-order hazard:** `apps/cms` and `apps/web` both deploy from `main`, so one +merge fires the Docker build and the Vercel build together, and Vercel normally +wins by several minutes. Any change where the web app stops sending a field that +the CMS `forms` row still marks required will 400 every submission for the length +of that gap. There is no way to sequence the two from a single merge, so relax the +constraint in prod *before* merging: make the field optional, deploy, then run the +script that finishes the job. Field defs are additive-safe in the other direction, +because `validateFields` iterates the form definition and ignores submitted fields +it does not list. + ### /industries path rename follow-up (2026-09-08) Script: `scripts/repoint-industry-paths.ts` — repoints the CMS rows that key on @@ -38,6 +48,7 @@ docker exec -w /app/apps/cms cleanstart-cms-1 pnpm exec tsx \ | 11 | Webflow media filename cleanup | DONE | 2026-06-09 | | 14 | Legacy Webflow 301 redirects seed | DONE | 2026-06-11 | | 15 | SEO keywords backfill (guides) | DONE | 2026-06-29 | +| 17 | Forms overhaul field changes (`apply-form-field-changes.ts`) | DONE | 2026-09-09 | --- From 9f0defc2eb2fc3f32e8beb995538a33b1ca1196e Mon Sep 17 00:00:00 2001 From: Gaurav Jadhav Date: Wed, 9 Sep 2026 18:25:45 +0530 Subject: [PATCH 3/3] fix(web,cms): drop the response-time promise, and stop signing lead mail as Careers Every form told the visitor we would respond within 24 hours or one business day, and the confirmation emails repeated it. On screen that is a transient toast; in an inbox it is durable, forwardable, and reads as a commitment the team has not agreed to. Softened to what is actually true: someone will get back to you. Changed in both places, so the site and the email cannot promise different things. Also stops the lead confirmations going out signed "CleanStart Careers". BREVO_SENDER_NAME is the global fallback and was set to the careers identity, so demo requests, contact replies, newsletter welcomes and resource downloads were all branded as careers mail. The droplet value is now the generic brand; careers and partnerships keep their own names through CAREERS_SENDER_NAME and PARTNERS_SENDER_NAME. --- apps/cms/emails/contact-confirmation.html | 4 ++-- apps/cms/emails/deal-registration-confirmation.html | 2 +- apps/cms/emails/demo-confirmation.html | 4 ++-- .../payload/lib/deal-registrations/notification-email.ts | 2 +- apps/cms/src/payload/lib/email/lead-emails.ts | 8 ++++---- apps/web/src/components/sections/contact/ContactForm.tsx | 2 +- apps/web/src/components/sections/forms/BookDemoForm.tsx | 2 +- .../components/sections/forms/DealRegistrationForm.tsx | 2 +- .../src/components/sections/partners/BecomePartnerCta.tsx | 2 +- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/cms/emails/contact-confirmation.html b/apps/cms/emails/contact-confirmation.html index 60a632577..77fc67081 100644 --- a/apps/cms/emails/contact-confirmation.html +++ b/apps/cms/emails/contact-confirmation.html @@ -39,7 +39,7 @@ -
    Your message reached the CleanStart team. We reply within one business day.͏‌ ͏‌ ͏‌ ͏‌ ͏‌ 
    +
    Your message reached the CleanStart team and someone will get back to you.͏‌ ͏‌ ͏‌ ͏‌ ͏‌ 
    diff --git a/apps/cms/emails/deal-registration-confirmation.html b/apps/cms/emails/deal-registration-confirmation.html index 1bce8e88a..17ea2dc2c 100644 --- a/apps/cms/emails/deal-registration-confirmation.html +++ b/apps/cms/emails/deal-registration-confirmation.html @@ -58,7 +58,7 @@
    @@ -58,7 +58,7 @@

    Contact

    Thanks, Priya

    -

    We've received your message and someone from the team will reply within one business day.

    +

    We've received your message and someone from the team will get back to you soon.

    If it is urgent, reply to this email and it will reach us directly.

    The CleanStart team

    Deal registration

    Thanks, Marcus

    -

    We've received your deal registration for Elena Vasquez — Northgate Security. Our partner team will review it and confirm next steps within one business day.

    +

    We've received your deal registration for Elena Vasquez — Northgate Security. Our partner team will review it and confirm next steps with you.

    diff --git a/apps/cms/emails/demo-confirmation.html b/apps/cms/emails/demo-confirmation.html index 5a746f00e..7e99ec853 100644 --- a/apps/cms/emails/demo-confirmation.html +++ b/apps/cms/emails/demo-confirmation.html @@ -39,7 +39,7 @@ -
    We have your demo request. A specialist will reach out within one business day.͏‌ ͏‌ ͏‌ ͏‌ ͏‌ 
    +
    We have your demo request. A specialist will reach out to arrange a time.͏‌ ͏‌ ͏‌ ͏‌ ͏‌ 
    Deal Elena Vasquez — Northgate Security
    diff --git a/apps/cms/src/payload/lib/deal-registrations/notification-email.ts b/apps/cms/src/payload/lib/deal-registrations/notification-email.ts index 847ecdec9..af23344d3 100644 --- a/apps/cms/src/payload/lib/deal-registrations/notification-email.ts +++ b/apps/cms/src/payload/lib/deal-registrations/notification-email.ts @@ -80,7 +80,7 @@ export const buildDealRegistrationConfirmationEmail = ( blocks: [ { kind: 'paragraph', - text: `We've received your deal registration for ${dealName}. Our partner team will review it and confirm next steps within one business day.`, + text: `We've received your deal registration for ${dealName}. Our partner team will review it and confirm next steps with you.`, }, { kind: 'details', diff --git a/apps/cms/src/payload/lib/email/lead-emails.ts b/apps/cms/src/payload/lib/email/lead-emails.ts index 311d6fbd3..d87d32400 100644 --- a/apps/cms/src/payload/lib/email/lead-emails.ts +++ b/apps/cms/src/payload/lib/email/lead-emails.ts @@ -24,13 +24,13 @@ export const buildDemoConfirmationEmail = ( ): { subject: string; htmlContent: string } => ({ subject: 'Your CleanStart demo request', htmlContent: renderEmail({ - preheader: 'We have your demo request. A specialist will reach out within one business day.', + preheader: 'We have your demo request. A specialist will reach out to arrange a time.', eyebrow: 'Demo request', heading: greeting(input.firstName), blocks: [ { kind: 'paragraph', - text: "We've received your demo request. One of our solutions specialists will reach out within one business day to arrange a time that suits you.", + text: "We've received your demo request. One of our solutions specialists will reach out to arrange a time that suits you.", }, { kind: 'paragraph', @@ -47,13 +47,13 @@ export const buildContactConfirmationEmail = ( ): { subject: string; htmlContent: string } => ({ subject: "We've received your message", htmlContent: renderEmail({ - preheader: 'Your message reached the CleanStart team. We reply within one business day.', + preheader: 'Your message reached the CleanStart team and someone will get back to you.', eyebrow: 'Contact', heading: greeting(input.firstName), blocks: [ { kind: 'paragraph', - text: "We've received your message and someone from the team will reply within one business day.", + text: "We've received your message and someone from the team will get back to you soon.", }, { kind: 'paragraph', text: 'If it is urgent, reply to this email and it will reach us directly.' }, signOff, diff --git a/apps/web/src/components/sections/contact/ContactForm.tsx b/apps/web/src/components/sections/contact/ContactForm.tsx index a7d2e8e4d..05ba53cc5 100644 --- a/apps/web/src/components/sections/contact/ContactForm.tsx +++ b/apps/web/src/components/sections/contact/ContactForm.tsx @@ -162,7 +162,7 @@ export function ContactForm() { tone: "success", title: "Message sent", message: - "Thanks, we've received your message and will reply within 24 hours.", + "Thanks, we've received your message and will get back to you soon.", }); window.setTimeout(() => { setSubmitted(false); diff --git a/apps/web/src/components/sections/forms/BookDemoForm.tsx b/apps/web/src/components/sections/forms/BookDemoForm.tsx index ef77f41b3..2dc15ee15 100644 --- a/apps/web/src/components/sections/forms/BookDemoForm.tsx +++ b/apps/web/src/components/sections/forms/BookDemoForm.tsx @@ -150,7 +150,7 @@ export function BookDemoForm(): React.ReactElement { tone: "success", title: "Demo request received", message: - "Thanks, your demo request has been received. Our team will reach out within 24 hours.", + "Thanks, your demo request has been received. Our team will reach out to arrange a time.", }); window.setTimeout(() => setStatus(null), 5000); return; diff --git a/apps/web/src/components/sections/forms/DealRegistrationForm.tsx b/apps/web/src/components/sections/forms/DealRegistrationForm.tsx index a3e43383a..529671013 100644 --- a/apps/web/src/components/sections/forms/DealRegistrationForm.tsx +++ b/apps/web/src/components/sections/forms/DealRegistrationForm.tsx @@ -171,7 +171,7 @@ export function DealRegistrationForm(): React.ReactElement { tone: "success", title: "Deal registration received", message: - "Thanks, your deal registration has been received. We'll be in touch within one business day.", + "Thanks, your deal registration has been received. We'll be in touch about next steps.", }); window.setTimeout(() => setStatus(null), 6000); return; diff --git a/apps/web/src/components/sections/partners/BecomePartnerCta.tsx b/apps/web/src/components/sections/partners/BecomePartnerCta.tsx index 58acb08b9..0dfb1d9fc 100644 --- a/apps/web/src/components/sections/partners/BecomePartnerCta.tsx +++ b/apps/web/src/components/sections/partners/BecomePartnerCta.tsx @@ -227,7 +227,7 @@ function PartnerModal({ open, onClose }: PartnerModalProps): React.ReactElement tone: "success", title: "Request received", message: - "Thanks, your partnership request has been received. Our team will be in touch within one business day.", + "Thanks, your partnership request has been received. Our team will be in touch soon.", }); // Modal UX: show the confirmation briefly, then close the popup. if (closeTimerRef.current != null) window.clearTimeout(closeTimerRef.current);
    @@ -58,7 +58,7 @@

    Demo request

    Thanks, Priya

    -

    We've received your demo request. One of our solutions specialists will reach out within one business day to arrange a time that suits you.

    +

    We've received your demo request. One of our solutions specialists will reach out to arrange a time that suits you.

    It helps to know what you would like to see: your current base images, the CVE burden you are carrying, or the audit you are preparing for. Reply to this email with anything you want covered.

    The CleanStart team