Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
a0268b6
docs(web): specify SaaS verified core redesign
jadhavgaurav Sep 1, 2026
fe4f441
docs(web): plan SaaS verified core implementation
jadhavgaurav Sep 1, 2026
4ea68aa
feat(web): redesign SaaS verified core pipeline
jadhavgaurav Sep 1, 2026
f3bd0b0
feat(web): rebuild the SaaS hero artifact from real catalogue data
jadhavgaurav Sep 1, 2026
5c3d9ba
docs(web): specify SaaS pipeline stage icons
jadhavgaurav Sep 1, 2026
513ffef
feat(web): make the SaaS hero artifact a CleanStart product panel
jadhavgaurav Sep 1, 2026
7c753de
feat(web): distinguish SaaS pipeline stages with icons
jadhavgaurav Sep 1, 2026
c26fd86
feat(web): make the SaaS hero the attack surface, shown as size
jadhavgaurav Sep 1, 2026
332aa8b
feat(web): seat the SaaS hero app on a verified container image
jadhavgaurav Sep 1, 2026
28511e5
docs(web): specify SaaS stage paint order
jadhavgaurav Sep 1, 2026
8bc3991
fix(web): show the SaaS hero artifact from tablet up, not just xl
jadhavgaurav Sep 1, 2026
132887b
feat(web): fill out the SaaS hero artifact and close the middle gap
jadhavgaurav Sep 1, 2026
308d371
test(web): lock SaaS stage icon paint order
jadhavgaurav Sep 1, 2026
adfd08a
docs(web): specify SaaS scanner mask and grid fade
jadhavgaurav Sep 1, 2026
498a32d
feat(web): give the SaaS hero container a real surface
jadhavgaurav Sep 1, 2026
2412e21
fix(web): make the SaaS hero base read as an image, not as freight
jadhavgaurav Sep 1, 2026
1e2f58d
feat(web): make the SaaS hero seal a shield with a checkmark
jadhavgaurav Sep 1, 2026
705bbe5
test(web): lock SaaS scanner and grid layering
jadhavgaurav Sep 1, 2026
af78d09
copy(web): change the SaaS H1 to the client's headline
jadhavgaurav Sep 1, 2026
4617bcf
docs(web): specify SaaS late route removal
jadhavgaurav Sep 1, 2026
8ac85dc
refactor(web): remove SaaS late review route
jadhavgaurav Sep 1, 2026
66c5e6b
feat(web): rebuild the SaaS hero as an application and its components
jadhavgaurav Sep 1, 2026
c74db81
feat(web): push the SaaS hero artifact closer to the client reference
jadhavgaurav Sep 1, 2026
8c7480c
docs(web): specify SaaS 3D process deck
jadhavgaurav Sep 1, 2026
265e286
feat(web): make the SaaS hero dashboard read as a product, not a wire…
jadhavgaurav Sep 1, 2026
8d06899
feat(web): add depth and density to the SaaS hero artifact
jadhavgaurav Sep 1, 2026
c20efd4
fix(web): bring the SaaS hero package card to the front layer
jadhavgaurav Sep 1, 2026
700db7c
fix(web): mark only the supply-chain pieces in the SaaS hero
jadhavgaurav Sep 1, 2026
0bfcfda
feat(web): add two structural cards to the SaaS hero and enrich two more
jadhavgaurav Sep 1, 2026
80b229c
feat(web): drift two pieces of the SaaS hero artifact
jadhavgaurav Sep 1, 2026
b683c3b
fix(cms): validate hand-typed internal links in the broken-link scan
jadhavgaurav Sep 1, 2026
8f45831
fix(web): make the SaaS hero drift actually perceptible
jadhavgaurav Sep 1, 2026
4cd2148
feat(web): cursor parallax on the SaaS hero artifact
jadhavgaurav Sep 1, 2026
79d3bed
feat(web): rebuild the Move Beyond Shift Left pipeline
jadhavgaurav Sep 1, 2026
38238d1
docs(web): design spec for hero Verified/Hardened correction animation
jadhavgaurav Sep 1, 2026
d2800a1
docs(web): finalize hero Verified/Hardened design (shared gradient, s…
jadhavgaurav Sep 1, 2026
933b5be
fix(web): rebuild the Shift Left diagram around the merge
jadhavgaurav Sep 1, 2026
db14ee2
docs(web): implementation plan for hero Verified/Hardened animation
jadhavgaurav Sep 1, 2026
b4b0e74
feat(web): restructure hero H1 into Verified/Hardened three-line markup
jadhavgaurav Sep 1, 2026
265c09e
feat(web): animate hero H1 Hardened-strikethrough to Verified correction
jadhavgaurav Sep 1, 2026
358142b
fix(web): balance the Shift Left card height
jadhavgaurav Sep 1, 2026
24490ab
fix(web): soften hero strikethrough red to a muted coral
jadhavgaurav Sep 1, 2026
bf60729
fix(web): sync the Shift Left rail fill with the stage halos
jadhavgaurav Sep 1, 2026
ea3b2b6
fix(web): make the Shift Left rail visible before the fill reaches it
jadhavgaurav Sep 1, 2026
0f6d776
fix(web): stop the Shift Left rail growing past its declared width
jadhavgaurav Sep 1, 2026
6fe1301
fix(web): drop baked-glow card background on the desktop pipeline cards
jadhavgaurav Sep 1, 2026
5ca550a
feat(web): extend the Shift Left pipeline to its release, and reseal …
jadhavgaurav Sep 1, 2026
ced9d34
fix(web): remove lavender outline stroke, keep cards' original glow art
jadhavgaurav Sep 1, 2026
f46ef92
fix(web): land the merge curve on the rail instead of beside it
jadhavgaurav Sep 1, 2026
fa590f9
feat(web): 301 the short SaaS industry path to its canonical URL
jadhavgaurav Sep 2, 2026
46af574
feat(web): 301 the short finance industry path to its canonical URL
jadhavgaurav Sep 2, 2026
c5b2e68
copy(web): rename the fourth stack item to Golden Images
jadhavgaurav Sep 2, 2026
4d08594
fix(web): slow down the hero H1 reveal sequence (~2.5s to ~3.5s)
jadhavgaurav Sep 2, 2026
48c82a4
fix(web): lighten struck 'Hardened' to 500 and thin its strike to 2px
jadhavgaurav Sep 2, 2026
e094158
copy(web): terminate the hero's 'Verified' line with a period
jadhavgaurav Sep 2, 2026
2c7957d
fix(web): left-align the requirement bullets on mobile, equalise the …
jadhavgaurav Sep 2, 2026
9491d48
feat(web): rebuild the SaaS Shift Left diagram as a verified-componen…
jadhavgaurav Sep 2, 2026
27bfc2b
style(web): thicken the hero strike line to 2.5px
jadhavgaurav Sep 2, 2026
05c9fdd
style(web): thicken the hero strike line to 3px
jadhavgaurav Sep 2, 2026
ff7bb79
style(web): saturate the hero strike to vermillion
jadhavgaurav Sep 2, 2026
b01a1ce
fix(web): stop the hero strike overshooting past 'Hardened.'
jadhavgaurav Sep 2, 2026
d7ef69b
fix(web): exclude the period from the hero strike
jadhavgaurav Sep 2, 2026
3c2f91e
fix(web): refine the SaaS Shift Left scene choreography
jadhavgaurav Sep 2, 2026
c107d2b
copy(web): trim the fourth SaaS foundation caption to one line
jadhavgaurav Sep 2, 2026
6a75632
feat(web): launch the SaaS industry page
jadhavgaurav Sep 2, 2026
cc65dd7
copy(web): change the SaaS hero H1 to "Applications Move Faster. Secu…
jadhavgaurav Sep 2, 2026
6003ae1
feat(web): rename the SaaS industry page to /industries/modern-techno…
jadhavgaurav Sep 2, 2026
7129767
feat(web): move the SaaS page to /modern-applications with the SEO te…
jadhavgaurav Sep 2, 2026
c8cfa63
feat(web): settle the SaaS page at /industries/modern-applications
jadhavgaurav Sep 2, 2026
0dcb7ce
feat(web): give the Modern Applications nav row the stacked-layers glyph
jadhavgaurav Sep 2, 2026
e737c0f
feat(web): rename the ROI calculator to Impact Estimator and rebuild …
jadhavgaurav Sep 2, 2026
559a098
feat(web): rebuild the Docker Hardened Images comparison page
jadhavgaurav Sep 2, 2026
c4d998a
docs(web): restore the Impact Estimator row in WEB-PAGES.md
jadhavgaurav Sep 2, 2026
c525b75
fix(web): make the estimator inputs card actually stick, tighten the …
jadhavgaurav Sep 2, 2026
924c330
feat(web): link the Impact Estimator from the Solutions mega menu
jadhavgaurav Sep 2, 2026
d61c7ce
style(web): give the Impact Estimator nav row a gauge glyph
jadhavgaurav Sep 2, 2026
cb7448b
feat(web): remove the unfinished CleanStart Platform page
jadhavgaurav Sep 2, 2026
b0c544e
fix(web): stop mobile drawer links turning dark on hover
jadhavgaurav Sep 2, 2026
508866e
feat(web): move the estimator's copy-link out of the hours card
jadhavgaurav Sep 2, 2026
d0fa7c9
feat(web): make the Impact Estimator indexable and tighten its on-pag…
jadhavgaurav Sep 2, 2026
7ddcad6
feat(cms): seed the pageRegistry row for /impact-estimator
jadhavgaurav Sep 2, 2026
492b04d
feat(web): explain the Impact Estimator's method and answer the obvio…
jadhavgaurav Sep 2, 2026
b443075
revert(web): drop the "How the estimate works" section from the Impac…
jadhavgaurav Sep 2, 2026
3bf97d5
style(web): compact the Impact Estimator FAQ
jadhavgaurav Sep 2, 2026
fefdca3
fix(web): drop the trust-line demo link from the Impact Estimator
jadhavgaurav Sep 2, 2026
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
68 changes: 66 additions & 2 deletions apps/cms/src/payload/lib/broken-links/extract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,72 @@ describe('extractAllLinks', () => {
);
});

it('drops site-relative URLs and non-string scalar fields', () => {
const links = extractAllLinks({ body: null, applyUrl: '/internal-path', registrationUrl: null, atsUrl: 42 });
it('resolves site-relative URLs against the public origin, and drops non-string scalars', () => {
// Reverses the previous contract, which dropped these. Nothing else
// validates a hand-typed internal path: it carries no relationship for the
// slug-change hook, so `/guide/orchestration` shipped as a live 404.
const links = extractAllLinks(
{ body: null, applyUrl: '/internal-path', registrationUrl: null, atsUrl: 42 },
'https://www.cleanstart.com',
);
expect(links).toEqual([
{ url: 'https://www.cleanstart.com/internal-path', anchorText: null, location: 'Apply URL' },
]);
});

it('resolves site-relative body links too, keeping the anchor text', () => {
const links = extractAllLinks(
{
body: wrap([
{ type: 'link', fields: { url: '/guide/orchestration' }, children: [{ type: 'text', text: 'orchestration guide' }] },
]),
},
'https://www.cleanstart.com',
);
expect(links).toEqual([
{ url: 'https://www.cleanstart.com/guide/orchestration', anchorText: 'orchestration guide', location: 'Body' },
]);
});

it('never rewrites a protocol-relative URL into one of ours', () => {
// `//other.example/path` is another host, not a site path. Prefixing the
// origin would silently retarget it at ourselves and report a foreign
// link as healthy. It has no scheme, so the SSRF guard drops it instead —
// the property under test is that it is not absorbed into our origin.
const links = extractAllLinks(
{ body: null, applyUrl: '//other.example/path' },
'https://www.cleanstart.com',
);
expect(links).toEqual([]);
expect(JSON.stringify(links)).not.toContain('cleanstart.com');
});

it('collapses a relative and an absolute reference to the same page into one check', () => {
const links = extractAllLinks(
{
body: wrap([
{ type: 'link', fields: { url: '/pricing' }, children: [{ type: 'text', text: 'pricing' }] },
]),
applyUrl: 'https://www.cleanstart.com/pricing',
},
'https://www.cleanstart.com',
);
expect(links).toEqual([
{ url: 'https://www.cleanstart.com/pricing', anchorText: 'pricing', location: 'Body' },
]);
});

it('does not resolve anchors, mailto or tel into page URLs', () => {
const links = extractAllLinks(
{
body: wrap([
{ type: 'link', fields: { url: '#section-2' } },
{ type: 'link', fields: { url: 'mailto:hi@cleanstart.com' } },
{ type: 'link', fields: { url: 'tel:+911234567890' } },
]),
},
'https://www.cleanstart.com',
);
expect(links).toEqual([]);
});

Expand Down
30 changes: 27 additions & 3 deletions apps/cms/src/payload/lib/broken-links/extract.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { resolveSiteUrl } from '../site-url';
import { isSafePublicHttpUrl } from '../url-safety/ssrf-guard';

/**
* Walk a Lexical body + adjacent doc fields and return every external
* URL the editor referenced, with the visible anchor text and a
* human-readable location. Used by the nightly broken-link scanner.
*
* Internal-doc relationships (`linkType === 'internal'`, `doc != null`)
* Internal-doc *relationships* (`linkType === 'internal'`, `doc != null`)
* are skipped — Payload's slug-change hook keeps those resolvable.
*
* Hand-typed site-relative paths are NOT skipped. They carry no relationship
* for the slug-change hook to follow, so nothing else in the system validates
* them: `/guide/orchestration` and `/images/redis/details` both shipped in
* published bodies as 404s and went unnoticed until a manual crawl. They are
* resolved against the public origin so the scanner HEAD-checks them like any
* other link.
*
* SSRF defence: every emitted URL passes `isSafePublicHttpUrl`.
*/

Expand Down Expand Up @@ -71,6 +79,17 @@ export const extractLinksFromLexical = (body: unknown): LexicalLink[] => {

const isFetchSafeHttpUrl = (raw: string): boolean => isSafePublicHttpUrl(raw).ok;

/**
* Resolve a root-relative editor link against the public origin.
*
* Only `/path` is rewritten. `//host/path` is protocol-relative and points at
* another origin, so prefixing it would silently retarget the link; anything
* else (absolute URLs, `#anchor`, `mailto:`, `tel:`) is returned untouched and
* falls to the SSRF guard to accept or drop.
*/
const absolutiseInternal = (raw: string, origin: string): string =>
raw.startsWith('/') && !raw.startsWith('//') ? `${origin}${raw}` : raw;

const SCALAR_URL_FIELDS: ReadonlyArray<readonly [key: string, label: string]> = [
['applyUrl', 'Apply URL'],
['atsUrl', 'ATS URL'],
Expand All @@ -86,9 +105,14 @@ const SCALAR_URL_FIELDS: ReadonlyArray<readonly [key: string, label: string]> =
* by field label). Returns absolute http(s) URLs that pass the SSRF
* guard; first occurrence of a URL wins (body before typed fields).
*/
export const extractAllLinks = (doc: Record<string, unknown>): ExtractedLink[] => {
export const extractAllLinks = (
doc: Record<string, unknown>,
siteOrigin: string = resolveSiteUrl(),
): ExtractedLink[] => {
const byUrl = new Map<string, ExtractedLink>();
const add = (url: string, anchorText: string | null, location: string): void => {
const add = (raw: string, anchorText: string | null, location: string): void => {
// Dedupe on the resolved URL so `/x` and `https://site/x` collapse to one check.
const url = absolutiseInternal(raw, siteOrigin);
if (isFetchSafeHttpUrl(url) && !byUrl.has(url)) {
byUrl.set(url, { url, anchorText, location });
}
Expand Down
3 changes: 3 additions & 0 deletions apps/cms/src/payload/lib/page-registry-seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ export const PAGE_REGISTRY_SEED: readonly PageRegistrySeedRow[] = [
{ path: '/software-bill-materials', title: 'Software Bill of Materials', kind: 'static', order: 7, webPageType: 'WebPage' },
{ path: '/for-developers', title: 'For Developers', kind: 'static', order: 8, webPageType: 'WebPage' },
{ path: '/for-ciso', title: 'For CISO', kind: 'static', order: 9, webPageType: 'WebPage' },
// Solutions › Capability tool. Order sits after every seeded row so existing
// dashboard positions keep their numbers.
{ path: '/impact-estimator', title: 'Impact Estimator', kind: 'static', order: 49, webPageType: 'WebPage' },

// Resources (mega-menu)
{ path: '/blogs', title: 'Blogs', kind: 'cms-listing', order: 10, backingCollection: 'blogs' },
Expand Down
18 changes: 18 additions & 0 deletions apps/web/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,24 @@ const nextConfig: NextConfig = {
destination: "/guide/:slug*",
permanent: true,
},
// Industry pages live under `/industries/`, but the slug already names
// the industry, so the shorter path is the natural guess and returns a
// hard 404. Same courtesy 301 as `/guides` above: it was never a live
// URL, just one worth catching. The sibling /industries/modern-applications
// was never indexed or linked under its earlier slugs, so it carries none.
{
source: "/financial-services-container-security",
destination: "/industries/financial-services-container-security",
permanent: true,
},
// The operational-impact estimator launched at `/roi-calculator` and was
// renamed while still noindex,nofollow, so nothing is indexed under the
// old path. It is live in the client's review links, though, so 308 it.
{
source: "/roi-calculator",
destination: "/impact-estimator",
permanent: true,
},
// Canonical detail routes are singular `/event/[slug]` and `/job/[slug]`
// (matching the indexed Webflow URLs). The redesign also shipped plural
// aliases that rendered the same content and self-canonicalled to
Expand Down
4 changes: 2 additions & 2 deletions apps/web/public/images/ciso/enterprise-icon-devsecops.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions apps/web/public/images/ciso/enterprise-icon-security-ops.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading