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
4 changes: 2 additions & 2 deletions apps/cms/emails/contact-confirmation.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
<body style="margin:0;padding:0;width:100%;background-color:#f1f5f9;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<!-- Preheader: shown in the inbox list, never in the open email. The trailing
entities stop clients padding the preview with body text. -->
<div style="display:none;max-height:0;overflow:hidden;mso-hide:all;font-size:1px;line-height:1px;color:#f1f5f9;opacity:0;">Your message reached the CleanStart team. We reply within one business day.&#847;&zwnj;&nbsp;&#847;&zwnj;&nbsp;&#847;&zwnj;&nbsp;&#847;&zwnj;&nbsp;&#847;&zwnj;&nbsp;</div>
<div style="display:none;max-height:0;overflow:hidden;mso-hide:all;font-size:1px;line-height:1px;color:#f1f5f9;opacity:0;">Your message reached the CleanStart team and someone will get back to you.&#847;&zwnj;&nbsp;&#847;&zwnj;&nbsp;&#847;&zwnj;&nbsp;&#847;&zwnj;&nbsp;&#847;&zwnj;&nbsp;</div>

<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="background-color:#f1f5f9;">
<tr><td align="center" style="padding:32px 12px;">
Expand All @@ -58,7 +58,7 @@
<tr><td class="cs-pad" style="padding:0 32px 28px;">
<p style="margin:0 0 10px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:#3960f9;">Contact</p>
<h1 class="cs-heading" style="margin:0 0 18px;font-family:Arial,Helvetica,sans-serif;font-size:22px;line-height:1.35;font-weight:700;color:#0f172a;">Thanks, Priya</h1>
<p class="cs-text" style="margin:0 0 16px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.6;color:#334155;">We&#39;ve received your message and someone from the team will reply within one business day.</p>
<p class="cs-text" style="margin:0 0 16px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.6;color:#334155;">We&#39;ve received your message and someone from the team will get back to you soon.</p>
<p class="cs-text" style="margin:0 0 16px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.6;color:#334155;">If it is urgent, reply to this email and it will reach us directly.</p>
<p class="cs-muted" style="margin:0 0 16px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.6;color:#64748b;">The CleanStart team</p>
</td></tr>
Expand Down
2 changes: 1 addition & 1 deletion apps/cms/emails/deal-registration-confirmation.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
<tr><td class="cs-pad" style="padding:0 32px 28px;">
<p style="margin:0 0 10px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:#3960f9;">Deal registration</p>
<h1 class="cs-heading" style="margin:0 0 18px;font-family:Arial,Helvetica,sans-serif;font-size:22px;line-height:1.35;font-weight:700;color:#0f172a;">Thanks, Marcus</h1>
<p class="cs-text" style="margin:0 0 16px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.6;color:#334155;">We&#39;ve received your deal registration for Elena Vasquez — Northgate Security. Our partner team will review it and confirm next steps within one business day.</p>
<p class="cs-text" style="margin:0 0 16px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.6;color:#334155;">We&#39;ve received your deal registration for Elena Vasquez — Northgate Security. Our partner team will review it and confirm next steps with you.</p>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;margin:0 0 20px;width:100%;"><tr>
<td style="padding:8px 16px 8px 0;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:1.5;color:#64748b;white-space:nowrap;vertical-align:top;">Deal</td>
<td style="padding:8px 0;font-family:Arial,Helvetica,sans-serif;font-size:15px;line-height:1.5;color:#334155;vertical-align:top;">Elena Vasquez — Northgate Security</td>
Expand Down
4 changes: 2 additions & 2 deletions apps/cms/emails/demo-confirmation.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
<body style="margin:0;padding:0;width:100%;background-color:#f1f5f9;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<!-- Preheader: shown in the inbox list, never in the open email. The trailing
entities stop clients padding the preview with body text. -->
<div style="display:none;max-height:0;overflow:hidden;mso-hide:all;font-size:1px;line-height:1px;color:#f1f5f9;opacity:0;">We have your demo request. A specialist will reach out within one business day.&#847;&zwnj;&nbsp;&#847;&zwnj;&nbsp;&#847;&zwnj;&nbsp;&#847;&zwnj;&nbsp;&#847;&zwnj;&nbsp;</div>
<div style="display:none;max-height:0;overflow:hidden;mso-hide:all;font-size:1px;line-height:1px;color:#f1f5f9;opacity:0;">We have your demo request. A specialist will reach out to arrange a time.&#847;&zwnj;&nbsp;&#847;&zwnj;&nbsp;&#847;&zwnj;&nbsp;&#847;&zwnj;&nbsp;&#847;&zwnj;&nbsp;</div>

<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="background-color:#f1f5f9;">
<tr><td align="center" style="padding:32px 12px;">
Expand All @@ -58,7 +58,7 @@
<tr><td class="cs-pad" style="padding:0 32px 28px;">
<p style="margin:0 0 10px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:#3960f9;">Demo request</p>
<h1 class="cs-heading" style="margin:0 0 18px;font-family:Arial,Helvetica,sans-serif;font-size:22px;line-height:1.35;font-weight:700;color:#0f172a;">Thanks, Priya</h1>
<p class="cs-text" style="margin:0 0 16px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.6;color:#334155;">We&#39;ve received your demo request. One of our solutions specialists will reach out within one business day to arrange a time that suits you.</p>
<p class="cs-text" style="margin:0 0 16px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.6;color:#334155;">We&#39;ve received your demo request. One of our solutions specialists will reach out to arrange a time that suits you.</p>
<p class="cs-text" style="margin:0 0 16px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.6;color:#334155;">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.</p>
<p class="cs-muted" style="margin:0 0 16px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.6;color:#64748b;">The CleanStart team</p>
</td></tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
8 changes: 4 additions & 4 deletions apps/cms/src/payload/lib/email/lead-emails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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,
Expand Down
13 changes: 13 additions & 0 deletions apps/web/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/app/(legal)/privacy-policy/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down Expand Up @@ -60,7 +62,7 @@ export default async function PrivacyPolicyPage(): Promise<React.ReactElement> {
const graph = await getPageGraph("/privacy-policy", [
breadcrumbSchema([
{ name: "Home", path: "/" },
{ name: "Legal", path: "/legal" },
{ name: "Legal", path: sectionIndexHref("/legal") },
{ name: doc.title },
]),
]);
Expand Down
13 changes: 11 additions & 2 deletions apps/web/src/components/sections/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down Expand Up @@ -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" },
];
Expand Down Expand Up @@ -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
href={link.href}
prefetch={false}
className="text-xs leading-[1.75] text-white/70 transition-colors duration-200 hover:text-white cursor-pointer"
className="relative inline-flex items-center text-xs leading-[1.75] text-white/70 transition-colors duration-200 hover:text-white cursor-pointer before:absolute before:inset-x-0 before:-inset-y-[3px] before:content-['']"
>
{link.label}
</Link>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/sections/contact/ContactForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/sections/forms/BookDemoForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -88,7 +90,7 @@ function Breadcrumb({ category }: { category?: string | null | undefined }): Rea
</li>
<li>
<Link
href="/knowledge-hub"
href={sectionIndexHref("/knowledge-hub")}
className="transition-colors hover:text-cs-purple-1"
style={{ color: '#5A5F75' }}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/lib/nav-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <span> to <Link> in MegaMenu / MobileNav.

import { sectionIndexHref } from "@/lib/nav/section-index";

export type NavLeaf = {
label: string;
href: string;
Expand Down Expand Up @@ -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" },
],
},
{
Expand Down
28 changes: 28 additions & 0 deletions apps/web/src/lib/nav/section-index.test.ts
Original file line number Diff line number Diff line change
@@ -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();
}
});
});
22 changes: 22 additions & 0 deletions apps/web/src/lib/nav/section-index.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
"/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;
}
8 changes: 3 additions & 5 deletions apps/web/src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<string, string> = {
"/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" =
Expand Down
11 changes: 11 additions & 0 deletions docs/operations/PRODUCTION-ROLLOUT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 |

---

Expand Down
Loading
Loading