Files
zeekay 8b9517791a A product page's foot is one band: its name, licence and repository on the left, Deploy to Cloud on the right
ProductFooter stacked "Open source" with its pills, then "Get <name>"
with its own tagline and buttons, each in a band of its own, so the 68
pages that end on it showed three headings in a row counting the closing
call. It is one band now: Get <name>, the product's line, the licence and
repository pills under them, and Deploy to Cloud and Self-host beside
them from 768px. The parts keep the test ids the products audit reads.
DeployBlock no longer falls back to "Deploy in seconds", a claim
e2e/unit/cards.spec.ts keeps off the share cards for the same reason.
2026-10-18 17:00:25 -07:00

82 lines
2.7 KiB
TypeScript

'use client'
/**
* Deploy block.
*
* Primary CTA → platform.hanzo.ai/deploy/<slug> (managed cloud deploy).
* Secondary CTA → docs.hanzo.ai/docs/<docs_slug> (self-host docs), shown only
* when docs actually publishes a page for the product — `docsUrl` returns null
* otherwise, and a Self-host button that lands on a 404 is worse than no
* button.
*
* Both URLs derive from products-metadata so there is exactly one mapping
* from slug to deploy URL and one from slug to docs URL.
*
* The two controls are `Action`, this site's one button shape in its two
* weights. They had each written that shape out as ten utilities, and the
* filled one reached for `bg-primary`, which is a token gui redeclares as a
* self-reference — the rule states `--pure-white` and cannot be poisoned that
* way.
*
* The name lost its `hz-` prefix when this document's rules did, and this call
* site did not follow — so "Deploy to Cloud", the primary action of ~78 product
* pages, was shipping unfilled and unoutlined. `Start.tsx` carries the
* measurement.
*/
import React from 'react'
import { Rocket, BookOpen, ArrowRight } from 'lucide-react'
import { deployUrl, docsUrl } from '@/lib/constants/products-metadata'
import { YStack } from '@hanzo/ui'
import { Action } from '@hanzo/ui/marketing'
type Props = {
slug: string
className?: string
}
/** Below `sm` the pair is a column and each control stretches, so a label with
* nothing centring it sits at the left edge of a full-width button. */
const CENTRED = { justify: "center" } as const
export function DeployBlock({ slug, className = '' }: Props) {
const deploy = deployUrl(slug)
const docs = docsUrl(slug)
return (
// A plain element carries the test id the products audit locates this
// block by; a data attribute handed to a gui stack is not something this
// repo has measured reaching the DOM.
<div data-testid="deploy-block" className={className || undefined}>
<YStack gap={12} justify="center" $sm={{ flexDirection: 'row' }}>
<Action
data-testid="deploy-cta"
href={deploy}
target="_blank"
rel="noopener noreferrer"
fill
{...CENTRED}
>
<Rocket width={16} height={16} />
Deploy to Cloud
<ArrowRight width={16} height={16} />
</Action>
{docs && (
<Action
data-testid="self-host-cta"
href={docs}
target="_blank"
rel="noopener noreferrer"
{...CENTRED}
>
<BookOpen width={16} height={16} />
Self-host
</Action>
)}
</YStack>
</div>
)
}
export default DeployBlock