Gated downloads, HubSpot marketing opt-ins, and the Tricorder page (on hold) - #269
Merged
Merged
Conversation
Build /tricorder from "The Intelligence Layer" copy doc on the site's own design language: the product-hero shell with a scan console drawn in code (signal meters, version chain, verdict) on a radar sweep; three evidence cards for "Not Every Threat Has a CVE" (version timeline, behaviour diff, relationship graph); a dark context scene where history, behavior and relationships converge on the component and resolve to a verdict; the Analyze → Compare → Correlate → Enrich rail ending in a verdict terminal; and the substrate scene fanning the Tricorder core out to Clean Images, Clean Libraries and CleanSight. Footer CTA card with a verdict ledger. Every scene is SVG/CSS on the --fs-* tokens; motion reuses the cs-lep-* keyframes plus a small cs-tri-* set for the console, all off under prefers-reduced-motion. Registered in the Products nav (new `lens` glyph), the footer Product column, STATIC_ROUTES, llms.txt and WEB-PAGES.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0198mhURhZhARAmAHoDNoCmQ
Replace the orb-and-branches diagram with the layer drawn literally: the three products stand on glass pedestals carrying the site's own hexagonal product art (shared with the homepage factory), each dropping a flowing current into a gridded floor that recedes in perspective, with a flare where it lands. The Tricorder emblem — a bevelled hex in the product-art palette with the lens mark — is set into the floor, with the four analysis stages laid along it. Decision chips now carry lucide glyphs and align to the card bottoms. Below lg the pedestals stack and the floor becomes the homepage-style hatched layer panel. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0198mhURhZhARAmAHoDNoCmQ
The section diagrammed the claim instead of making it. Replace the floating tiles and dotted beams with one panel holding the same package twice: a flat, desaturated "seen in isolation" readout where every fact is reassuring (signed, zero CVEs, license OK, nothing to flag), then a lit "seen in context" half where three lenses each carry real evidence — a version timeline with the fresh release flagged, a capability profile with network and shell live, a relationship graph sharing one host — and their currents converge on a Malicious verdict. The two halves are separated by tone rather than a rule, so the eye travels from a dead readout into a live one. The left column drops the gradient divider bar under the heading and becomes a quiet accent-bar legend, since the panel opposite now carries the density. Below sm the identity row stacks (no truncated package name), the connectors are dropped and the verdict reads as an icon-led status banner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0198mhURhZhARAmAHoDNoCmQ
An audit diffing every rendered text run against the copy doc turned up four gaps, all now closed: - The doc sets the pipeline out as `Analyze → Compare → Correlate → Enrich → Verdict`. Only the last hop carried its arrow; every hop carries one now, and the arrow columns collapse below lg where the cards stack. - The hero console labelled two of its rows "Correlation" and "Enrichment", near-misses of the doc's own "Correlate" and "Enrich". All four rows now use the doc's stage names and the pipeline's accent colours, so the hero shows the same sequence the page explains further down. - The hero carried a second CTA, "How It Powers CleanStart", that appears nowhere in the doc. Removed; "Talk to an Expert" is the doc's only call to action and now the page's only one. The substrate anchor stays. - The rail keyframes were injected through a <style> tag inside the section, which put raw CSS into the page's text content. Moved to globals.css alongside the page's other keyframes. Every headline, sub-head and body line now matches the doc verbatim. The sample data inside the visualisations is illustrative and is flagged as such in the page inventory. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0198mhURhZhARAmAHoDNoCmQ
Take the structure and the vocabulary straight from the copy doc's own mock: three signal groups, each showing what it actually reads, their currents converging into one component that resolves into the verdict. Every label is the doc's — the 1.4.1 → 1.4.2 → 1.4.3 version chain, Network / Shell / File System / Code, Maintainer → Packages → Infrastructure → Endpoints, package-name@1.4.3, and "Evidence-backed verdict." That replaces the invented sample data this section carried. Three things are translated rather than copied, because the doc's mock is styled for a white page: - The group panels become glass on the section gradient, the way the Clean Libraries scenes are built. The section sits between two light sections, so building it light would run three pale bands together and flatten the page's alternation. - The doc's outline-in-a-circle icons become GlassIcon gem tiles, the icon vocabulary the rest of the site uses. Keeping the circles would put two icon languages on one page. - The doc's green becomes the teal already sanctioned as a card accent. Plain green is not in the CleanStart palette. The verdict card reuses the homepage Intelligence Center treatment, which is the site's existing way of saying "this is the Tricorder layer". The heading stays left-aligned so the section still reads differently from the centre-headed sections either side of it. Fixed 1320px canvas scaled to fit; below lg the groups stack and the four-across rows wrap to 2x2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0198mhURhZhARAmAHoDNoCmQ
…design-p9wsvc feat(web): add the Tricorder product page
Drop the outline rings on every card and build depth from fill, a top sheen and shadow. Move each group header inside its panel, set the item glyphs in accent tiles, turn History into a release timeline and join the Relationships chain with dashed links. The verdict card becomes the brightest object in the scene and loses the hatch that pooled at its bottom edge.
The elbow currents from History/Behavior/Relationships into the bus were visual noise the panels didn't need. Tighten the vertical rhythm now that they're gone and keep the single Component to Verdict drop as the one line in the scene.
…er diagram" This reverts commit 003bca1.
Replace the History/Behavior/Relationships GlassIcon gems with a small accent dot next to the label. The colour identity stays; the icon that was competing with the item tiles below it is gone.
Match the site's standard CTA layout (heading left, body + button right, as in DeveloperCTA/VulnCTA) instead of the two-column heading/ledger split. Removes the now-unused VERDICT palette import and MONO const.
items-start left the title and body/button block pinned to the top of the taller footer slot with dead space below. Center them as one row instead, and drop the stray justify-center that was fighting the left-aligned two-column layout.
Marking a resource gated in the CMS has never gated anything on the live site. Three independent faults, none noticed because no resource was gated until today. The popup never opened. The page loaded the gate form with getFormById, but fetchCMS only sends the CMS API key in draft mode, and the forms collection is readable by admins and editors only, so a visitor's render got a 403, the error was swallowed, and the button fell back to an open download link. It worked in preview, where the key is sent, which is how it hid. Sending the key would not have fixed it either: the key belongs to preview-bot, an author. Had it opened, every submit would have failed. Server validation reads a required consent field as `fields.consent === true`, and FormRenderer never puts consent into `fields`. And the file was public regardless. The listing query selected `asset`, which serialised every resource's file URL into the client payload on every page that lists resources; the detail page used the PDF as og:image, twitter:image and JSON-LD image; and the download button received the URL as a prop. The popup is now a coded GatedDownloadForm, like Book a Demo and Contact, on the shared fields with inline errors. It posts formSlug content-gated with the resource id as context and a boolean consent, through submitLead, which now carries context and returns the signed download link made absolute to the CMS. The HubSpot relay drops non-string fields, so that boolean never reaches the CRM form. Its field names must match the CMS form, because the API validates against that stored definition. The listing no longer selects `asset`, share and structured-data images use `heroImage` (a PDF is not an image for any resource; the three without a hero fall back to the generated share card), and a gated button gets "#" instead of the file URL. A gated page now contains the file URL zero times. getFormById had no other caller and is removed. The popup title wraps instead of truncating mid-word.
…flow gates
The gated-download email only handed over a file ("Here is your copy of..."),
and it arrives after the download has already started in the browser. It now
leads with thanks and keeps the signed link as the way back for anyone who
closed the tab. No follow-up promise, per 9f0defc. The registry named
resource-capture as its trigger; gated downloads go through content-gated, the
only form the Resources gate picker allows.
gate-webflow-gated-resources.ts restores the two gates the migration lost, and
is kept as the record of that production change. Webflow gated in its page
template by resource type, not with a CMS field, so the export had nothing to
read and the import wrote gated:false for all 27. Wayback snapshots of the
live Webflow pages show exactly two gated: the only ebook and the only report,
whose PDF links sat inside the form's success block. Every other resource page
exposed an open link. The script sets gated, gateForm and accessLevel through
payload.update, is idempotent, and was run in production on 2026-09-17.
Pre-migration URL /busybox-container-security-risk (76 referring domains, no /blogs/ prefix) 404s. 301 it to the live post.
Rebuild the page on the Financial Services / SaaS design system, with every word taken from the copy doc. - Hero: the doc's brain-in-a-glass-cube render replaces the scan console and its invented findings. - Not Every Threat Has a CVE: one joined white panel with blue icon spheres; the invented mini illustrations are gone. - From Signals to Verdicts: a scan line through four stations into a Verdict card that selects an outcome each pass. Static and fully lit before hydration and under reduced motion. - One Intelligence Layer: the Tricorder cube on top, branching into Clean Images, Clean Libraries and CleanSight, as the doc draws it. The page is noindex,nofollow and out of the sitemap until the redesign is signed off.
A visitor who ticked "Keep me updated" on book-a-demo, contact or the gated download form was never subscribed. The relay sent a marketing subscription whenever the form named a subscription type, and ignored the visitor's `consent.categories` entirely, so the only safe setting was to leave the type empty on every form that has an optional marketing box. The opt-in reached the server and was stored, and was then dropped on the way to HubSpot. The consent block is now a pure `hubspotLegalConsent`, which always records consent to process and adds the subscription only when the categories include `marketing` and the form names a type. The newsletter is unchanged, because signing up is itself the opt-in and it always sends that category. The old test asserted a subscription for a consent with no categories at all, which is the behaviour being removed. set-form-marketing-subscriptions.ts then points book-a-demo and content-gated at Marketing Information (2258674941). It must run after this change is live, since a type set against the old relay would subscribe everyone, so it imports the new helper and checks an unticked visitor gets nothing before writing. Against the current production relay it refuses with "does not provide an export named 'hubspotLegalConsent'" and writes nothing. IDs were verified in HubSpot rather than read off the forms. The portal has three subscription types: Marketing Information 2258674941, One to One 2258674944 and Newsletter 3005083821. The gated form's own HubSpot checkbox is wired to One to One, HubSpot's type for 1:1 sales email, so copying its ID would have filed marketing opt-ins under the wrong subscription. Contact is left out because its HubSpot form has no consent configuration.
The financial-services header still said its sibling stayed noindex,nofollow and unlisted. That page has launched: production serves index,follow and the sitemap lists it.
The leads list printed the syncedTo row count under the plural label, so a fully delivered lead read as "4 Sync attempts", which looks like retries. The column now shows "4/4 synced", counting skipped steps as done, turns red when a step failed, and lists each handler on hover.
FormRenderer rendered a form from its CMS definition. Its only caller, the gated resource download popup, now uses the coded GatedDownloadForm: the CMS `forms` collection is readable by admins and editors only, so a public page could never load a definition to render, and the gate fell back to an open link. Nothing else imports it. lib/forms.ts held the form definition types and went with it; FormRenderer was its only importer. The CSP comment on the Turnstile frame source now names the forms that render the widget. CLAUDE.md's 16px input rule now points at the shared field surface, which sets it through --fs-input (1rem), instead of FormRenderer's style.
Successful signups from the newsletter forms now emit newsletter_signup with form_name newsletter. It is a separate event rather than generate_lead so subscribers do not inflate the sales-lead count.
Replace the old five-section layout with Lifecycle, Build With What You Can Verify and Verification You Can Trust. The hero moves to the split layout of the other product heroes, with an animated SVG scene on the right: CVE chips dissolve at a CleanStart gate and feed a verified image stack. The trust section becomes a source-to-signature chain using the site's 3D icon set. The footer CTA now points to CleanStart Images.
Add a light variant to NewsletterConsent and use it on the Blog Detail and Guides CTAs, where the light-on-dark copy was unreadable.
New headings, card copy and CTA text across the hero, Bloated, Approach, Modern and CTA sections. The hero CTA now opens the product video.
gsc-url-audit.ts reports index status and 91-day search performance for a fixed URL list; gsc-single-inspect.ts inspects one URL. Both run inside the prod CMS container and write nothing back.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Eighteen commits from three workstreams.
1. Gated resource downloads (2 commits)
Marking a resource gated in the CMS has never gated anything on the live site. This makes it work, and restores the two gates lost in the Webflow migration.
Restored gates. Webflow gated by resource type in its page template, not with a CMS field, so the import wrote
gated: falsefor all 27. Wayback snapshots of the live Webflow pages show exactly two were gated, the only ebook and the only report:containing-vulnerabilities-in-your-containerssecuring-the-software-supply-chain-in-2026Both are already gated in production. Until this merges, they still download freely.
Why the gate never worked:
formsis readable by admins and editors only, so visitors got a 403 and the button fell back to an open link.fields.consent === true, which the old renderer never sent.Fix. A coded popup (
GatedDownloadForm) on the shared fields. The listing no longer selectsasset; share and structured-data images useheroImageinstead of the PDF (for every resource). A gated page now contains the file URL zero times.Flow: Download → popup → HubSpot → download starts → thank-you email with the link as a backup.
The popup's field names must match the CMS
content-gatedform, because the API validates against that stored definition.2. HubSpot marketing opt-ins (1 commit)
Ticking "Keep me updated" on book-a-demo, contact or the gate form never subscribed anyone. The relay sent a subscription whenever the form had a type, and ignored the visitor's choice, so every such form was left without a type. It now subscribes only when the visitor opted in. The newsletter is unchanged, since signing up is the opt-in.
Subscription types, verified in HubSpot: Marketing Information
2258674941, One to One2258674944, Newsletter3005083821. The gate form's own HubSpot checkbox is wired to One to One (HubSpot's type for 1:1 sales email), which is wrong for a marketing opt-in.After this is live, run in the production CMS container:
It points book-a-demo and content-gated at Marketing Information. It must not run before the deploy, because against the old relay it would subscribe everyone. It enforces this itself: run against the current production relay, it refused with
does not provide an export named 'hubspotLegalConsent'and wrote nothing. Contact is excluded because its HubSpot form has no consent configuration.3. Other sessions' work
fix(web): redirect bare busybox blog URL to /blogs/ postVerification
apps/web: lint, typecheck, 477 tests, build (against production CMS data).apps/cms: lint, typecheck, 2,023 tests, build, and theemails:checkdrift gate.Not yet tested: a real submission end to end (HubSpot, download, email). The production CMS rejects submissions from any origin but
www.cleanstart.com, so that test has to wait for the deploy.Deploy
This touches
apps/cms, so CMS CI and Deploy CMS run. There is no migration.