From 5d79d4bc03318f815c84342af8dc40959a4673fd Mon Sep 17 00:00:00 2001 From: Matt Gowie Date: Mon, 2 Nov 2026 14:32:49 -0700 Subject: [PATCH 1/3] feat(testimonials): redesign the /testimonials/ index page The section had no layout of its own, so it fell back to the generic blog list template and rendered every testimonial as a broken post card ("Published: 1.01.0001", a missing image, an empty summary). Nothing links to the page, but it and the six per-person pages are in the sitemap, which is how people were finding it. - Add a testimonials list layout and card partial: glow banner, then a two-column grid of quote cards (logo, quote, headshot, name/title, optional case-study link) reading the same front matter as the homepage slider, logo size/color overrides included. - Stop rendering the per-person pages (thin, single-quote pages) via a section cascade. They stay in page collections, so the slider is unchanged. Redirect their old URLs to /testimonials/ and drop the section's RSS feed, whose items would have no links or dates. - Add weights so the list leads with the strongest quotes and pairs similar lengths per row. The slider shuffles client-side, so its order is unaffected. - Let the careers glow-banner mixin take a background image so the testimonials banner can reuse it with the homepage "our word" art. Co-Authored-By: Claude Opus 5.5 --- assets/css/careers.scss | 7 +- assets/css/custom.scss | 2 + assets/css/testimonials.scss | 266 +++++++++++++++++++++++++ content/testimonials/_index.md | 20 ++ content/testimonials/adam-pallin.md | 1 + content/testimonials/alec-baker.md | 1 + content/testimonials/gunter-pfau.md | 1 + content/testimonials/liam-egan.md | 1 + content/testimonials/nathan-wade.md | 1 + content/testimonials/travis-mcpeak.md | 1 + layouts/partials/testimonial-card.html | 52 +++++ layouts/testimonials/list.html | 67 +++++++ netlify.toml | 8 + 13 files changed, 425 insertions(+), 3 deletions(-) create mode 100644 assets/css/testimonials.scss create mode 100644 content/testimonials/_index.md create mode 100644 layouts/partials/testimonial-card.html create mode 100644 layouts/testimonials/list.html diff --git a/assets/css/careers.scss b/assets/css/careers.scss index afc2b2a1..838ed2e0 100644 --- a/assets/css/careers.scss +++ b/assets/css/careers.scss @@ -26,8 +26,9 @@ $hire-pine-deep: #07262a; // Shared glow banner (copy of #caseStudiesPage #banner so the two sections match). // padding-top is inherited from the global #banner (9.9rem) — it clears the -// absolutely-positioned header; don't override it. -@mixin hire-glow-banner { +// absolutely-positioned header; don't override it. Also used by the +// testimonials index (testimonials.scss), which passes its own $image. +@mixin hire-glow-banner($image: "/img/bg_our_team.jpg") { position: relative; overflow: hidden; padding-bottom: 3.5rem; @@ -48,7 +49,7 @@ $hire-pine-deep: #07262a; rgba(9, 42, 44, 0.62) 45%, $pine 100% ), - url("/img/bg_our_team.jpg") center / cover no-repeat, + url($image) center / cover no-repeat, $pine; &::after { diff --git a/assets/css/custom.scss b/assets/css/custom.scss index 341d7465..9a77d351 100644 --- a/assets/css/custom.scss +++ b/assets/css/custom.scss @@ -5092,3 +5092,5 @@ font-size: 1.55rem; @import "case-studies.scss"; @import "careers.scss"; + +@import "testimonials.scss"; diff --git a/assets/css/testimonials.scss b/assets/css/testimonials.scss new file mode 100644 index 00000000..e605c7e5 --- /dev/null +++ b/assets/css/testimonials.scss @@ -0,0 +1,266 @@ +// Testimonials index — /testimonials/ (#testimonialsPage). +// Imported at the end of custom.scss, after careers.scss: it reuses that file's +// glow banner and brand-gradient mixins, and its dark rounded card language. +// Cards read the same front matter as the homepage slider (.quoteBx in +// custom.scss), including the --quote-logo-h / --quote-logo-w logo overrides. + +#testimonialsPage { + #banner { + // the homepage "our word" testimonial art, behind the same pine scrim + @include hire-glow-banner("/img/bg_our_word.jpg"); + } + + main { + background: $pine; + padding-top: 0.5rem; + + .container { + max-width: 1080px; + } + } + + .testimonials-intro { + color: rgba(255, 255, 255, 0.8); + text-align: center; + max-width: 760px; + margin: 0 auto 2.4rem; + font-size: 1.05rem; + line-height: 1.6; + } + + .testimonial-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1.5rem; + + // an odd card out spans the full row instead of leaving a hole + > :last-child:nth-child(odd) { + grid-column: 1 / -1; + } + + @media (max-width: 767px) { + grid-template-columns: minmax(0, 1fr); + } + } + + // ── Testimonial card ────────────────────────────────────────────── + .testimonial-card { + display: flex; + flex-direction: column; + margin: 0; + padding: 2.2rem 2.4rem 2.1rem; + border-radius: 18px; + color: #fff; + background: + radial-gradient( + ellipse at 100% 0%, + rgba(42, 217, 194, 0.12), + transparent 45% + ), + linear-gradient(120deg, #0c3133 0%, $hire-pine-deep 100%); + border: 1px solid rgba(42, 217, 194, 0.16); + box-shadow: 0 14px 32px -22px rgba(0, 0, 0, 0.7); + // clears the fixed header when deep-linked by #slug + scroll-margin-top: 7rem; + transition: border-color 0.24s ease; + + &:hover, + &:target { + border-color: rgba(42, 217, 194, 0.45); + } + + @media (max-width: 575px) { + padding: 1.6rem 1.4rem 1.5rem; + } + } + + .testimonial-card__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + min-height: 54px; + margin-bottom: 1.4rem; + } + + // Monochrome white by default (the source files are dark marks), opt out + // with `logo_full_color: true` — same rules as the slider's .quoteLogo. + .testimonial-card__logo { + min-width: 0; + + // A fixed height (not max-height) so SVGs without intrinsic dimensions + // still size from their viewBox instead of collapsing in the flex row; + // object-fit keeps wide marks undistorted when max-width kicks in. + img { + display: block; + height: var(--quote-logo-h, 34px); + width: auto; + // unquote() keeps Sass from trying to evaluate CSS min() itself + max-width: unquote("min(var(--quote-logo-w, 220px), 100%)"); + object-fit: contain; + object-position: left center; + filter: brightness(0) invert(1); + opacity: 0.92; + } + + &--color img { + filter: none; + opacity: 1; + } + + @media (max-width: 575px) { + img { + height: calc(var(--quote-logo-h, 34px) * 0.8); + } + } + } + + // big gradient opening quote, tucked into the top-right corner + .testimonial-card__mark { + flex: 0 0 auto; + margin-left: auto; + height: 54px; + font-family: Georgia, "Times New Roman", serif; + font-size: 5.5rem; + line-height: 1; + @include hire-brand-gradient-text; + opacity: 0.85; + } + + // Grows to fill the card so every attribution sits on the bottom edge, + // lined up with its neighbour in the row whether or not either card has a + // case-study link. + .testimonial-card__body { + display: flex; + flex: 1 1 auto; + flex-direction: column; + align-items: flex-start; + margin-bottom: 1.8rem; + } + + .testimonial-card__quote { + margin: 0; + padding: 0; + border: 0; + font-style: normal; + font-size: 1.08rem; + line-height: 1.7; + letter-spacing: 0.01em; + color: rgba(255, 255, 255, 0.74); + + p { + font-size: inherit; + line-height: inherit; + margin: 0 0 0.8em; + + &:last-child { + margin-bottom: 0; + } + } + + // the bolded phrases carry the quote: lift them to full white + strong { + color: #fff; + font-weight: 700; + } + + @media (max-width: 575px) { + font-size: 1rem; + } + } + + .testimonial-card__by { + display: flex; + align-items: center; + gap: 1rem; + padding-top: 1.4rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); + } + + // headshot with the brand-gradient ring (as in the slider) + .testimonial-card__avatar { + flex: 0 0 auto; + padding: 1px; + border-radius: 50%; + background: linear-gradient(40deg, #2ad9c2 0%, #55c1b4 42%, #d191bf 100%); + box-shadow: 0 8px 22px rgba(0, 0, 0, 0.35); + + img { + display: block; + width: 60px; + height: 60px; + border-radius: 50%; + object-fit: cover; + } + } + + .testimonial-card__byText { + min-width: 0; + } + + .testimonial-card__name { + font-weight: 800; + font-size: 1.05rem; + line-height: 1.2; + letter-spacing: 0.04em; + text-transform: uppercase; + } + + .testimonial-card__position { + margin-top: 0.3rem; + font-size: 0.85rem; + line-height: 1.45; + color: rgba(255, 255, 255, 0.6); + } + + .testimonial-card__cta { + display: inline-flex; + align-items: center; + gap: 0.45rem; + margin-top: 1.3rem; + font-size: 0.72rem; + font-weight: 800; + letter-spacing: 0.12em; + text-transform: uppercase; + text-decoration: none; + @include hire-brand-gradient-text; + + &:hover, + &:focus-visible { + text-decoration: none; + + .testimonial-card__arrow { + transform: translateX(5px); + } + } + } + + .testimonial-card__arrow { + font-size: 1.05rem; + -webkit-text-fill-color: $hire-mint; + color: $hire-mint; + transition: transform 0.28s ease; + } + + // ── "See the work" links ────────────────────────────────────────── + .testimonials-links { + margin: 3.2rem 0 4rem; + text-align: center; + + &__label { + display: block; + margin-bottom: 1rem; + font-size: 0.75rem; + font-weight: 800; + letter-spacing: 0.24em; + text-transform: uppercase; + color: rgba(255, 255, 255, 0.55); + } + &__row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.8rem; + } + } +} diff --git a/content/testimonials/_index.md b/content/testimonials/_index.md new file mode 100644 index 00000000..e722c03b --- /dev/null +++ b/content/testimonials/_index.md @@ -0,0 +1,20 @@ +--- +title: Testimonials +banner_title: "Don't take our word for it" +banner_tagline: IN THEIR WORDS +banner_text: Engineering leaders, from fast-moving startups to Fortune 500 enterprises, on what it's like to work with Masterpoint. +description: What engineering leaders at companies like Cursor, Staples, and Stuzo say about working with Masterpoint on Terraform, OpenTofu, and Infrastructure as Code. +id: testimonialsPage +# No RSS feed: its items would have no links (see below) and no dates. +outputs: + - HTML +# Each testimonial is a single quote, so it only renders as a card on this page +# and in the testimonials slider. Individual pages would be thin, near-empty +# pages; their old URLs redirect here (see netlify.toml). `list` stays at its +# default (always), so the slider's .Site.RegularPages query still finds them. +cascade: + - target: + kind: page + build: + render: never +--- diff --git a/content/testimonials/adam-pallin.md b/content/testimonials/adam-pallin.md index abc987b2..aaae0f8c 100644 --- a/content/testimonials/adam-pallin.md +++ b/content/testimonials/adam-pallin.md @@ -1,5 +1,6 @@ --- title: Adam Pallin +weight: 3 position: Director of Software Engineering, MarketSpark photo: /img/testimonials/adam-pallin.jpeg company: MarketSpark diff --git a/content/testimonials/alec-baker.md b/content/testimonials/alec-baker.md index acab5cfe..dc1f3621 100644 --- a/content/testimonials/alec-baker.md +++ b/content/testimonials/alec-baker.md @@ -1,5 +1,6 @@ --- title: Alec Baker +weight: 5 position: Principal Solutions Architect, Leadership Analytics Group photo: /img/testimonials/alec-baker.jpeg company: Leadership Analytics Group diff --git a/content/testimonials/gunter-pfau.md b/content/testimonials/gunter-pfau.md index 3f4e85ac..eabfdaf3 100644 --- a/content/testimonials/gunter-pfau.md +++ b/content/testimonials/gunter-pfau.md @@ -1,5 +1,6 @@ --- title: Gunter Pfau +weight: 2 position: CEO, Stuzo photo: /img/testimonials/gunter-pfau.jpg company: Stuzo diff --git a/content/testimonials/liam-egan.md b/content/testimonials/liam-egan.md index cfa545a1..41a5e4d0 100644 --- a/content/testimonials/liam-egan.md +++ b/content/testimonials/liam-egan.md @@ -1,5 +1,6 @@ --- title: Liam Egan +weight: 6 position: Director of Technology, We The Collective photo: /img/testimonials/liam-egan.jpeg company: We The Collective diff --git a/content/testimonials/nathan-wade.md b/content/testimonials/nathan-wade.md index 00240a34..8511c210 100644 --- a/content/testimonials/nathan-wade.md +++ b/content/testimonials/nathan-wade.md @@ -1,5 +1,6 @@ --- title: Nathan Wade +weight: 4 position: Solutions Architect, Staples photo: /img/testimonials/nathan-wade.jpeg company: Staples diff --git a/content/testimonials/travis-mcpeak.md b/content/testimonials/travis-mcpeak.md index a37f1fb0..5ae19899 100644 --- a/content/testimonials/travis-mcpeak.md +++ b/content/testimonials/travis-mcpeak.md @@ -1,5 +1,6 @@ --- title: Travis McPeak +weight: 1 position: Security Lead, Cursor photo: /img/testimonials/travis-mcpeak.jpeg company: Cursor diff --git a/layouts/partials/testimonial-card.html b/layouts/partials/testimonial-card.html new file mode 100644 index 00000000..85160bfe --- /dev/null +++ b/layouts/partials/testimonial-card.html @@ -0,0 +1,52 @@ +{{- /* One testimonial on /testimonials/. Reads the same front matter as the +slider (layouts/partials/testimonials-slider.html), including the per-logo size +and color overrides. */ -}} {{- $company := .Params.company | default .Title -}} +{{- $logoStyle := "" -}} {{- with .Params.logo_height }}{{ $logoStyle = printf +"--quote-logo-h:%dpx;" (int .) }}{{ end -}} {{- with .Params.logo_max_width }}{{ +$logoStyle = printf "%s--quote-logo-w:%dpx;" $logoStyle (int .) }}{{ end -}} {{- +/* Some quotes are wrapped in quote marks in their source and some aren't. The +card draws its own opening mark, so drop the wrapping pair for a consistent +grid. (\x3c is "<", spelled out so Prettier doesn't parse the patterns as HTML +tags.) */ -}} {{- $quote := .Content | replaceRE `^(\s*\x3cp>)“` "$1" | +replaceRE `”(\x3c/p>\s*)$` "$1" -}} +
+
+ {{ with .Params.logo }} +
+ {{ $company }} +
+ {{ end }} + +
+
+
+ {{ $quote | safeHTML }} +
+ {{ with .Params.case_study }} + Read the {{ $company }} case study + + {{ end }} +
+
+ {{ with .Params.photo }} +
+ {{ $.Title }} +
+ {{ end }} +
+
{{ .Title }}
+
{{ .Params.position }}
+
+
+
diff --git a/layouts/testimonials/list.html b/layouts/testimonials/list.html new file mode 100644 index 00000000..ee258f60 --- /dev/null +++ b/layouts/testimonials/list.html @@ -0,0 +1,67 @@ +{{ partial "head.html" . }} + + {{ partial "header.html" . }} + + +
+
+ {{ with .Content }} +
+
{{ . }}
+
+ {{ end }} + +
+ {{ range .Pages.ByWeight }} {{ partial "testimonial-card.html" . }} {{ + end }} +
+ + +
+
+ {{ partial "schedule-assessment.html" . }} {{ partial "footer.html" . }} {{ + partial "scripts.html" . }} + +{{- /* The html end tag is intentionally omitted (it is optional in HTML). +Prettier lints this file on its own and rejects closing an element that +head.html opened. */ -}} diff --git a/netlify.toml b/netlify.toml index 20ad18d6..db5e519b 100644 --- a/netlify.toml +++ b/netlify.toml @@ -66,6 +66,14 @@ status = 301 force = true ######################################################## +# Individual testimonial pages are no longer rendered (content/testimonials/_index.md), +# so send their old URLs to the index. Not forced: /testimonials/ itself is a +# real file, which shadows this rule and keeps it from looping. +[[redirects]] +from = "/testimonials/*" +to = "/testimonials/" +status = 301 + ######################################################## # Plausible Analytics proxy. Serves the tracker first-party so ad blockers # don't strip it. These MUST stay above the catch-all 404 below, and the paths From d14c6656fcc4731d90eb3989fc0b17ab215411e8 Mon Sep 17 00:00:00 2001 From: Matt Gowie Date: Mon, 2 Nov 2026 15:18:29 -0700 Subject: [PATCH 2/3] fix(testimonials): use explicit forced redirects for old testimonial URLs On the deploy preview the non-forced /testimonials/* splat never fired: the old per-person URLs returned 404 while every forced rule in the file redirected correctly. Replace it with one forced 301 per old page. Each matches with or without the trailing slash, and none can match /testimonials/ itself, so there's no loop. Co-Authored-By: Claude Opus 5.5 --- netlify.toml | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/netlify.toml b/netlify.toml index db5e519b..10fc7174 100644 --- a/netlify.toml +++ b/netlify.toml @@ -67,12 +67,44 @@ force = true ######################################################## # Individual testimonial pages are no longer rendered (content/testimonials/_index.md), -# so send their old URLs to the index. Not forced: /testimonials/ itself is a -# real file, which shadows this rule and keeps it from looping. +# so send their old URLs to the index. One explicit rule per old page: a +# non-forced /testimonials/* splat didn't fire on Netlify (404s instead), and a +# forced splat would also catch /testimonials/ itself and loop. [[redirects]] -from = "/testimonials/*" +from = "/testimonials/adam-pallin/" to = "/testimonials/" status = 301 +force = true + +[[redirects]] +from = "/testimonials/alec-baker/" +to = "/testimonials/" +status = 301 +force = true + +[[redirects]] +from = "/testimonials/gunter-pfau/" +to = "/testimonials/" +status = 301 +force = true + +[[redirects]] +from = "/testimonials/liam-egan/" +to = "/testimonials/" +status = 301 +force = true + +[[redirects]] +from = "/testimonials/nathan-wade/" +to = "/testimonials/" +status = 301 +force = true + +[[redirects]] +from = "/testimonials/travis-mcpeak/" +to = "/testimonials/" +status = 301 +force = true ######################################################## # Plausible Analytics proxy. Serves the tracker first-party so ad blockers From fda92cf238e3382aaf7d297139831535313554e2 Mon Sep 17 00:00:00 2001 From: Matt Gowie Date: Mon, 2 Nov 2026 15:57:53 -0700 Subject: [PATCH 3/3] chore(faqs): raise minimum project size to $100k The pricing FAQ (rendered on /services/) is the only place the site states the minimum engagement size. The $15k audit/workshop starting price is unchanged. Co-Authored-By: Claude Opus 5.5 --- content/faqs/4-prices.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/faqs/4-prices.md b/content/faqs/4-prices.md index 89b289e8..e8f4312a 100644 --- a/content/faqs/4-prices.md +++ b/content/faqs/4-prices.md @@ -3,4 +3,4 @@ title: What are your prices? weight: 4 --- -Every project is a little different, but in general you can expect our pricing to start at about $15k for an audit or workshop. For more complicated projects, our minimum project size is $80k. +Every project is a little different, but in general you can expect our pricing to start at about $15k for an audit or workshop. For more complicated projects, our minimum project size is $100k.