Skip to content

gads: CRO improvements to /gads/terraform/ + opt-in template fields#18766

Open
alexleventer wants to merge 4 commits intomasterfrom
cro-terraform-landing-page
Open

gads: CRO improvements to /gads/terraform/ + opt-in template fields#18766
alexleventer wants to merge 4 commits intomasterfrom
cro-terraform-landing-page

Conversation

@alexleventer
Copy link
Copy Markdown
Contributor

Summary

The terraform landing page (https://www.pulumi.com/gads/terraform/) wasn't converting. Root cause was a template bug masking the value prop above the fold; on top of that the page had no above-the-fold CTA, generic CTA copy, a wall-of-text overview, no competitor comparison, and no before/after code.

This PR fixes the underlying template bug for all 33 gads pages and adds opt-in template fields that the terraform page now uses as the pilot. The other 32 pages render unchanged until someone fills in the new frontmatter.

The bug

layouts/gads/gads-template.html calls the hero partial with only title, but layouts/partials/gads-hero.html reads subtitle. So the carefully-written subheading frontmatter on every gads page was silently dropped — every gads hero rendered with just an H1 and no value prop. (Compare gads-neo.html, which passes both correctly.)

Opt-in template additions

Field What it does
hero_cta Primary/secondary buttons + microcopy directly under the H1
cta_label / cta_href / cta_microcopy Per-page override for body CTAs (otherwise inherits site.Params.cta.primary)
social_proof_position: top Moves logo strip + customer quote directly under the hero
overview.bullets 3-bullet list instead of the paragraph in the overview section
comparison_table Pulumi-vs-competitor table section
key_features_above[].before_after Side-by-side language code comparison (e.g. HCL → TypeScript)
dki_template Extends Google Ads dynamic keyword insertion to the H1 (was previously body-only)

Pilot — terraform page

  • Hero now renders the subheading + two CTAs (`Convert your Terraform — free` / `Sign up free`) + `Free forever • Open source • No credit card required`
  • Body CTAs all changed from generic `Get Started` → migration-specific `Convert your Terraform — free` pointing at `/tf2pulumi/`
  • Social proof (logos + Snowflake "1.5 weeks → 1 day" quote) moved above the overview
  • Overview is now 3 bullets leading with the 500-resource cap pain point
  • New HCL → TypeScript before/after card replaces the generic IDE chooser in the first feature
  • New "Pulumi vs. Terraform at a glance" comparison table added between the first feature and the rest of the body
  • DKI now also swaps the H1 from `Terraform Alternative` → `{keyword} Alternative`

Test plan

  • `make build` succeeds (3,948 pages, no template errors)
  • Rendered `public/gads/terraform/index.html` contains all expected new sections (hero subtitle, comparison table, before/after, 7 microcopy renders)
  • No raw frontmatter keys leaked into the rendered HTML
  • `prettier --check` clean on all four touched files
  • Visual QA on a deployed preview — verify spacing/responsive behavior on mobile
  • Spot-check a few other gads pages (e.g. `/gads/aws/`, `/gads/cdk/`) to confirm their newly-rendered `subheading` reads well in the hero now that the bug is fixed
  • After merge: monitor conversion rate on `/gads/terraform/` for at least 1–2 weeks before generalizing the pattern to other competitor pages

🤖 Generated with Claude Code

alexleventer and others added 2 commits May 1, 2026 14:50
The terraform landing page wasn't converting. The biggest cause was a
template bug: gads-template.html passed `title` to the hero partial but
the partial reads `subtitle`, so the carefully-written `subheading`
frontmatter on all 33 gads pages was silently dropped. Hero rendered
with just an H1 and no value prop or CTA above the fold.

Fixes the bug for all gads pages, and adds opt-in template fields so
landing pages can render hero CTAs, a per-page CTA label/href override,
overview bullets, a competitor comparison table, side-by-side
before/after code, repositionable social proof, CTA microcopy, and DKI
of the H1. All new fields are opt-in — the other 32 pages render
unchanged until they fill in the new frontmatter.

Activates the new fields on /gads/terraform/ as the pilot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@alexleventer
Copy link
Copy Markdown
Contributor Author

@claude - pls review

@pulumi-bot
Copy link
Copy Markdown
Collaborator

pulumi-bot commented May 1, 2026

Claude finished @alexleventer's task in 2m 18s —— View job


PR Review: gads CRO improvements + opt-in template fields

  • Gather context and understand the PR
  • Read changed files
  • Review template changes (gads-template.html)
  • Review partial changes (gads-hero.html, gads-social-proof.html)
  • Review content changes (terraform/index.md)
  • Check for bugs, correctness, and best practices
  • Post review summary

Summary

This is a well-structured PR. The bug fix is real and the opt-in design is the right call — all 33 existing pages remain unchanged until someone opts in. Code quality is high. A few things worth discussing before merge:


Bug fix: confirmed correct

layouts/gads/gads-template.html:2 — the original call was:

{{ partial "gads-hero" (dict "title" .Params.heading) }}

Now correctly passes subtitle and dki_template. This matches how gads-neo.html:15 already called the partial, and the fix is minimal and correct.


Issues

Medium — Terraform-specific defaults in a shared template

layouts/gads/gads-template.html:78–82:

{{ $feature.before_after.before.title | default "Before — Terraform (HCL)" }}
{{ $feature.before_after.after.title | default "After — Pulumi" }}

These defaults are baked into the shared template. If another gads page (e.g., /gads/ansible/) ever adds a before_after block without explicitly setting titles, they'll get "Before — Terraform (HCL)" rendered — which is wrong. Suggest more neutral defaults like "Before" / "After — Pulumi". Fix this →

Medium — competitor_label default is also Terraform-specific

layouts/gads/gads-template.html:183:

{{ .competitor_label | default "Terraform" }}

Same concern as above. A page comparing against CDK or CloudFormation that adds a comparison table but forgets competitor_label will render "Terraform" as the column header. "Competitor" is a better neutral default.

Minor — Fragile DKI placeholder embedded in YAML frontmatter

content/gads/terraform/index.md:46:

description: "...with zero downtime. <span id=\"dki-placeholder\" style=\"display:none;\"></span>"

Raw HTML with a required JS-targeting ID inside a YAML string is easy to accidentally delete. It would be worth a short comment somewhere (frontmatter comment or a {{/* */}} in the template near the JS) explaining why this span needs to exist here. Not a blocker, but future editors will likely be confused.

Nit — Secondary CTA missing analytics attribute

layouts/gads/gads-template.html:9 — the primary CTA has data-role="cta-get-started", the secondary does not. Probably intentional (these are different CTA types), but worth confirming analytically whether the secondary "Sign up free" should also be tracked.


What looks good

  • XSS safe: All DKI keyword injection uses innerText (not innerHTML), so reading utm_term from the URL is safe.
  • DKI H1 logic is correct: JS checks heading.dataset.dkiTemplate before acting, so pages without dki_template in frontmatter are unaffected. The rainbow-text animation is properly handled by setting both data-text and innerText.
  • gads-social-proof.html extraction is clean. The conditional position logic ($proofTop) is simple and correct. The new "Read the case study →" link is properly gated with {{ with .link }}.
  • Bullets fallback in overview: the {{ with .Params.overview.bullets }} ... {{ else }} ... {{ end }} pattern is a clean opt-in that preserves existing behavior.
  • 33 pages unaffected: All new frontmatter fields are opt-in, confirmed by the | default fallbacks throughout the template.

The two medium items above are worth fixing before this becomes the pattern for other gads pages — a neutral default will prevent silent "Terraform" leaking into non-Terraform competitor pages. Everything else is ship-ready.
|

@pulumi-bot
Copy link
Copy Markdown
Collaborator

pulumi-bot commented May 1, 2026

- Drop Terraform-specific defaults from the shared template: the
  before/after card titles default to "Before"/"After" and the comparison
  table competitor column defaults to "Competitor". The terraform page
  sets explicit titles so it renders the same; future non-Terraform
  pages that opt in won't accidentally render "Terraform" as a header.
- Tag the hero's secondary CTA with data-role="cta-secondary" so it can
  be tracked independently from the primary "cta-get-started" event.
- Add a template comment near the DKI script explaining the dki-placeholder
  span and dki_template hooks, so future editors don't accidentally drop
  the span from frontmatter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pulumi-bot
Copy link
Copy Markdown
Collaborator

pulumi-bot commented May 1, 2026

Lighthouse Performance Report

Commit: ef4c35b | Metric definitions

Page Device Score FCP LCP TBT CLS SI
Homepage Mobile 🟡 69 2.5s 2.5s 706ms 0.018 7.3s
Homepage Desktop 🟡 89 0.8s 1.1s 21ms 0.086 2.8s
Install Pulumi Mobile 🔴 32 4.2s 7.1s 386ms 0.439 7.5s
Install Pulumi Desktop 🟡 89 1.1s 1.6s 22ms 0.024 1.7s
AWS Get Started Mobile 🟡 53 4.0s 7.0s 300ms 0.083 7.4s
AWS Get Started Desktop 🟡 89 1.1s 1.7s 21ms 0.024 1.7s

@alexleventer
Copy link
Copy Markdown
Contributor Author

@claude - another review pls

@pulumi-bot
Copy link
Copy Markdown
Collaborator

pulumi-bot commented May 1, 2026

Claude finished @alexleventer's task in 3m 19s —— View job


PR Review: gads CRO improvements + opt-in template fields (re-review)

  • Gather context
  • Read changed files
  • Review template, partial, and content changes
  • Post review summary

Previous review feedback — all addressed ✅

The latest commit (384655c) cleanly resolves everything flagged in the first pass:

Issue Status
before.title default was "Before — Terraform (HCL)" ✅ Changed to "Before"
after.title default was "After — Pulumi" ✅ Changed to "After"
competitor_label default was "Terraform" ✅ Changed to "Competitor"
Secondary CTA missing analytics attribute data-role="cta-secondary" added
DKI dki-placeholder span unexplained ✅ Template comment block added

New findings

Minor — rainbow-text class is unconditional but has no CSS rule in this context

layouts/partials/gads-hero.html:9 — the inner <span> now always gets class="rainbow-text":

<span class="rainbow-text">{{ .title }}</span>

main.scss:415 has a comment: // NOTE: .rainbow-text intentionally removed — meaning this class deliberately has no global styling. The class only exists as a JS selector hook for the DKI H1 replacement script. Adding a styling class name to serve as a DOM selector is a mild concern:

  1. All 33 gads pages (via gads-template.html) and gads-neo.html (which calls the same partial) now have rainbow-text on their H1 — even though only the terraform page uses DKI H1 replacement.
  2. The script already guards this correctly (if (heading && heading.dataset.dkiTemplate)), so there's no functional issue today.
  3. The class name implies visual styling; a data attribute (data-dki-heading) or a more descriptive selector class would be less ambiguous for future devs.

Not a blocker, but worth a follow-up comment if you intend to keep rainbow-text as a permanent hook here.

Minor — no trailing newline at end of gads-template.html

The diff still shows \ No newline at end of file. A one-character fix: add a newline after the final {{ end }}. Linters and editors will flag this.

Nit — before_after silently ignored for non-first key_features_above items

layouts/gads/gads-template.html:70 — the before_after block is inside {{ if eq $index 0 }}. If a future page adds before_after to a second or third key_features_above item, it will render nothing (not even a fallback). Consider a template comment warning, or move the check outside the index guard if before_after should work for all items.

Nit — markdownify inside <li> may inject a <p> wrapper

layouts/gads/gads-template.html:44:

<li class="text-lg"><strong>{{ .title }}</strong> {{ .description | markdownify }}</li>

Hugo's markdownify wraps single-line content in <p> tags, producing <li><strong>…</strong> <p>…</p></li>. This is technically valid HTML but <p> carries block-level margin/padding that could cause unexpected spacing inside the bullet. One of the description values also embeds raw <a> HTML — consider safeHTML (for the already-HTML content) or test that the rendered spacing is acceptable in the deployed preview.


What's solid

  • DKI logic is correct and safe: innerText throughout (no XSS); the early-exit guard if (!keyword) return is clean; H1 replacement only fires when dkiTemplate is present on the dataset.
  • Opt-in design holds: every new field degrades gracefully via | default fallbacks — all 32 non-terraform pages are genuinely untouched.
  • gads-social-proof.html extraction is clean; the $proofTop flag is minimal and readable; the new "Read the case study →" link is properly gated with {{ with .link }}.
  • Comparison table CTA is consistent with the rest of the page — uses $cta_label / $cta_href / $cta_microcopy variables like the other CTAs.
  • Bug fix confirmed: layouts/partials/gads-hero.html now receives subtitle and dki_template, matching how gads-neo.html already called the partial.

The two medium items from the first review are fixed and the PR is functionally correct. The remaining items above are minor/nits that could be tidied up before or after merge — nothing is a blocker. The trailing-newline issue is the easiest one-liner fix if you want a clean diff before landing.

- Hero primary CTA: "Convert your Terraform — free" → "Sign up free"
  (target: app.pulumi.com/signup instead of /tf2pulumi/)
- Hero secondary CTA: "Sign up free" → "Talk to an expert"
  (target: /contact/?form=sales)
- Drop per-page cta_label/cta_href overrides so body CTAs fall back to
  the global "Get Started" → app.pulumi.com/signup
- "Migrate on your timeline" bullet no longer mentions tf2pulumi; reads
  "Import existing state with `pulumi import` ..." instead

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants