Files
zeekay ac64d5d7dd Folds say one line: every first screen keeps its heading and a single lede
The owner's spec for a landing page is the models bento's: monochrome, the
words left and the art right, no extra prose. The folds carried two to four
sentences under each heading, and some a claim, a lede and a list besides.
Each now says one line: ProductLanding the tagline where a page states one
(the lede still describes it to search), a claim alone where the claim is
the line (/payments, /treasury, /zap, the extension), and elsewhere the
sentence that says what the thing is. /philosophy's two points, /dns' claim
that repeated its title, /calculator's capabilities line and the
Spare clauses on /research and /learn go.
2026-10-30 06:42:40 -07:00

490 lines
18 KiB
TypeScript

"use client"
/**
* /dns — one control plane over every DNS provider.
*
* NO TAILWIND. The five section frames are `components/marketing/band`'s `Band`
* and `Head` — the rule, the rhythm, the gutter, the measure and the centred
* head this page wrote out by hand at each of them — the roles carry the type
* and the controls (`Display`, `Claim`, `Lede`, `Title`, `Chip`, `Action`,
* `Leaf`, `Line`), and everything left is gui props over @hanzo/design's
* tokens.
*
* Three things the swap moves, each deliberate:
*
* - the rules take `Band`'s `--neutral-900` hairline rather than this page's
* `--border`, which is `rgb(255 255 255 / .10)` — the same line to within a
* value of grey, and now the one line the whole site draws.
* - `Claim` and `Lede` step at 640 where `md:` stepped at 768, and each sits
* on its role's rung rather than the one this page typed.
* - `Action` is the one button shape at its two weights, and `Leaf` is the
* card whose edge lifts, its hover a prop beside its resting edge.
*
* The hero is NOT a Band: it is a 90vh centred stage rather than a section of
* rhythm, and `Band` has neither the height nor the centring to say so.
*/
import type { ReactNode } from "react"
import { phrase } from "@hanzo/i18n"
import { useI18n } from "@hanzo/i18n/react"
import { motion } from "@/components/motion"
import { ProductFooter } from "@/components/products/ProductFooter"
import {
Globe,
ArrowRight,
Layers,
Zap,
Terminal,
Code2,
Shield,
Server,
Lock,
Cloud,
Network,
Rocket,
Check,
} from "lucide-react"
import { Box, View, XStack, YStack } from '@hanzo/ui'
import { Grid } from '@hanzo/ui/grid'
import { Band, Head } from '@/components/marketing/band'
import { AgentPrompt } from '@/components/product/AgentPrompt'
import { product } from '@/lib/product'
import { Action, Lede, Title, Leaf, Line, type LineProps } from '@hanzo/ui/marketing'
import { Fold } from '@/components/marketing/fold'
import { Win } from '@/components/home/chrome'
/** The prompt's product, off the one table that holds a product's name, route,
* audited command and docs page. `ProductFooter` closes the page. */
const PRODUCT = product('dns')
/** How narrow a column may get before a grid drops one, per shape. */
const DECK = { min: 304, max: 3 } as const
const PAIR = { min: 400, max: 2 } as const
const TIERS = { min: 280, max: 3 } as const
/** The inks this page draws body copy and marks in. */
const WASH = 'color-mix(in oklab, var(--primary) 10%, transparent)'
const TILE = 'color-mix(in oklab, var(--secondary) 50%, transparent)'
/** The card whose edge lifts on approach — `Leaf`, at this page's two ends. */
const LEAF = { p: 24, borderColor: "var(--border)", bg: TILE, hoverStyle: { borderColor: "var(--neutral-600)" } } as const
/** The hairline tile a stat and a plan sit on. A `color-mix()` ink is not a
* value gui's colour props type, so it rides `$platform-web`, which gui still
* writes as an atomic class. */
const TILE_BOX = { rounded: 'var(--radius-xl)', borderWidth: 1, borderColor: 'var(--border)', '$platform-web': { backgroundColor: TILE } } as const
/** A card's heading and the paragraph under it. */
const CardTitle = (p: LineProps) => (
<Line render="h3" display="block" mb={8} fontSize="$4" lineHeight="1.5556" fontWeight="600" color="var(--foreground)" {...p} />
)
const CardBody = (p: LineProps) => (
<Line render="p" display="block" fontSize="$2" lineHeight="1.4286" color="var(--muted-foreground)" {...p} />
)
/** The mark's plate, at the two sizes this page draws it. */
const Plate = ({ size, children, ...p }: { size: number; children: ReactNode; mb?: number }) => (
<XStack items="center" justify="center" width={size} height={size} shrink={0} rounded="var(--radius-lg)" {...p} $platform-web={{ backgroundColor: WASH }}>
{children}
</XStack>
)
/** A named box in the architecture diagram, its heading, and a cell inside it. */
const GROUP = { p: 16, rounded: 'var(--radius-lg)', borderWidth: 1, borderColor: 'var(--border)', bg: 'var(--secondary)' } as const
const GROUP_LIT = {
...GROUP,
'$platform-web': {
borderColor: 'color-mix(in oklab, var(--primary) 20%, transparent)',
backgroundColor: 'color-mix(in oklab, var(--primary) 5%, transparent)',
},
} as const
const CHIP = {
display: 'block',
p: 8,
rounded: 'var(--radius)',
borderWidth: 1,
borderColor: 'var(--border)',
bg: 'color-mix(in oklab, var(--background) 50%, transparent)',
fontSize: '$1',
lineHeight: '1.3333',
text: 'center',
color: 'var(--muted-foreground)',
} as const
/** The two controls this page draws, everywhere it draws them. */
const FILL = { px: 32 } as const
const OUTLINE = { px: 32, borderColor: "var(--border)" } as const
const controlPlane = [
{ title: phrase("Your Applications"), columns: 3, accent: false, cells: ["CI/CD", "CLI", "REST API"] },
{ title: phrase("Hanzo DNS Control Plane"), columns: 2, accent: true, cells: [phrase("Record Manager"), phrase("DNSSEC Engine"), phrase("Parallel Query"), phrase("Audit Log")] },
{ title: phrase("DNS Providers"), columns: 3, accent: false, cells: ["Cloudflare", "Route53", "CoreDNS", "GoDaddy", "DigitalOcean", phrase("+ More")] },
] as const
const guarantees = [
{ icon: Server, text: phrase("Provider-agnostic record management with automatic conflict resolution") },
{ icon: Lock, text: phrase("Audit logging for every DNS change with rollback support") },
{ icon: Zap, text: phrase("Sub-second propagation checks across global resolvers") },
{ icon: Shield, text: phrase("DNSSEC key management with automated rotation schedules") },
] as const
const features = [
{
icon: Layers,
title: phrase("Multi-Provider"),
description:
phrase("Unified API across Cloudflare, CoreDNS, Route53, GoDaddy, and DigitalOcean. Manage all your DNS from one interface."),
},
{
icon: Zap,
title: phrase("Parallel Queries"),
description:
phrase("Query all providers simultaneously and merge results. Get a unified view of your DNS records across every provider."),
},
{
icon: Terminal,
title: phrase("CLI Native"),
description:
phrase("Full CLI control with hanzo dns zones list, hanzo dns zones records create, and more. Script and automate your DNS workflows natively."),
},
{
icon: Code2,
title: phrase("API First"),
description:
phrase("REST API for programmatic DNS management. Integrate DNS operations into your CI/CD pipelines and infrastructure code."),
},
{
icon: Shield,
title: "DNSSEC",
description:
phrase("Built-in DNSSEC signing and automatic key rotation. Protect your domains from spoofing and cache poisoning attacks."),
},
{
icon: Globe,
title: phrase("Edge DNS"),
description:
phrase("CoreDNS at the edge with Hanzo plugins. Low-latency resolution with custom middleware for advanced routing logic."),
},
]
const useCases = [
{
icon: Rocket,
title: phrase("DevOps Automation"),
description:
phrase("Automate DNS record management as part of your deployment pipeline. Create, update, and clean up records programmatically."),
},
{
icon: Cloud,
title: phrase("Multi-Cloud DNS"),
description:
phrase("Manage DNS across AWS, Cloudflare, and DigitalOcean from a single API. No more switching consoles."),
},
{
icon: Network,
title: phrase("Domain Portfolio Management"),
description:
phrase("Manage hundreds of domains across multiple registrars and DNS providers from one unified interface."),
},
]
// PRICING IS CUSTOM HERE, deliberately.
//
// These tiers used to publish $20 Pro, $25 Team, $200 Enterprise and "$0.10 per
// 10K queries". Commerce — GET /v1/billing/plans, category `dns`, the rows that
// actually charge — carries three rows at $0 / $5 / $25, no Team row at any
// price, and no per-query rate anywhere. So Pro was advertised at 4x what it
// bills, Enterprise at 8x, and one tier did not exist.
//
// Rather than republish the charged numbers, the owner decision is that the only
// prices Hanzo publishes are personal 20/100/200 and team $25/seat. Everything
// else is a conversation, because an enterprise deal can be shaped many ways and
// a number on a page forecloses that. The feature comparison stays — it is what
// a reader is actually here for.
const pricingTiers = [
{
name: "Starter",
price: phrase("Custom"),
period: "",
description: phrase("Usage-based pricing for getting started"),
features: [
phrase("Up to 5 zones"),
phrase("2 DNS providers"),
phrase("CLI and API access"),
phrase("Volume-based query pricing"),
],
cta: phrase("Talk to us"),
ctaHref: "/contact-sales",
highlight: false,
},
{
name: "Pro",
price: phrase("Custom"),
period: phrase("/month"),
description: phrase("For teams managing production DNS"),
features: [
phrase("Unlimited zones"),
phrase("All DNS providers"),
phrase("DNSSEC management"),
phrase("Audit logging"),
phrase("Priority support"),
],
cta: phrase("Talk to us"),
ctaHref: "/contact-sales",
highlight: true,
},
{
name: "Team",
price: phrase("Custom"),
period: phrase("/month"),
description: phrase("For teams with shared DNS management"),
features: [
phrase("Everything in Pro"),
phrase("Team collaboration"),
phrase("Role-based access"),
phrase("Shared audit log"),
phrase("Priority support"),
],
cta: phrase("Talk to us"),
ctaHref: "/contact-sales",
highlight: false,
},
{
name: "Enterprise",
price: phrase("Custom"),
period: phrase("/month"),
description: phrase("For organizations with advanced needs"),
features: [
phrase("Everything in Team"),
phrase("Edge DNS (CoreDNS)"),
phrase("Custom plugins"),
phrase("SLA guarantee"),
phrase("Dedicated support"),
],
cta: phrase("Contact Sales"),
ctaHref: "/contact-sales",
highlight: false,
},
]
export default function DNSPage({ cli }: { cli: ReactNode }) {
const { t } = useI18n()
return (
<>
{/* docs publishes no DNS page: `/docs/services/dns` is a 404 and
`/docs/dns` is a `Moved` stub whose canonical 404s too. The way in is
the key, which is what every other product page offers. */}
<Fold
label="Programmable DNS Infrastructure"
mark={<Globe width={14} height={14} color="currentColor" aria-hidden />}
title="Hanzo DNS"
lede="One API for Cloudflare, CoreDNS, Route53, and more."
actions={
<>
<Action href="/login" fill {...FILL}>
{t('Try Hanzo')}
<ArrowRight width={16} height={16} />
</Action>
<Action href="https://github.com/hanzoai" {...OUTLINE}>
GitHub
</Action>
</>
}
preview={
<Win title="terminal" icon={<Globe size={12} color="currentColor" />}>
{cli}
</Win>
}
/>
{PRODUCT && <AgentPrompt product={PRODUCT} />}
{/* Features Grid */}
<Band pad="$11" measure={1280}>
<Head
title={t('One API. Every Provider.')}
lede={t('Stop juggling provider consoles. Every zone you own, in one place.')}
/>
<Grid columns={DECK} gap={24}>
{features.map((feature, index) => (
<Leaf
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: index * 0.05 }} />}
key={feature.title}
{...LEAF}
hoverStyle={{ borderColor: "var(--neutral-600)" }}
>
<Plate size={48} mb={16}>
<feature.icon width={24} height={24} color="var(--foreground)" />
</Plate>
<CardTitle>{t(feature.title)}</CardTitle>
<CardBody>{t(feature.description)}</CardBody>
</Leaf>
))}
</Grid>
</Band>
{/* Architecture Section */}
<Band pad="$11" measure={1280}>
<Grid columns={PAIR} gap={48} items="center">
<motion.div
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
>
<Title quiet mb={24}>
{t('Built for Infrastructure Teams')}
</Title>
<Lede mb={24} mt="$3.5">
{t('Hanzo DNS sits between your applications and DNS providers, giving you a single control plane for all DNS operations.')}
</Lede>
<YStack render="ul" gap={16}>
{guarantees.map(({ icon: Icon, text }) => (
<XStack key={text} render="li" items="flex-start" gap={12}>
<View mt={2}>
<Icon width={20} height={20} color="var(--foreground)" />
</View>
<Line color="var(--muted-foreground)">{t(text)}</Line>
</XStack>
))}
</YStack>
</motion.div>
{/* Architecture Diagram */}
<Box
render={<motion.div initial={{ opacity: 0, x: 20 }} whileInView={{ opacity: 1, x: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: 0.1 }} />}
p={32}
{...TILE_BOX}
>
<YStack gap={16}>
{controlPlane.map((group, index) => (
<YStack key={group.title} gap={16}>
{index > 0 ? (
<XStack justify="center">
<Box width={1} height={24} bg="var(--border)" />
</XStack>
) : null}
<Box {...(group.accent ? GROUP_LIT : GROUP)}>
<Line render="h4" display="block" mb={8} fontSize="$2" lineHeight="1.4286" fontWeight="600" color="var(--foreground)" text={group.accent ? 'center' : undefined}>
{t(group.title)}
</Line>
<Grid columns={group.columns} gap={8}>
{group.cells.map((cell) => (
<Line key={cell} render="div" {...CHIP}>
{t(cell)}
</Line>
))}
</Grid>
</Box>
</YStack>
))}
</YStack>
</Box>
</Grid>
</Band>
{/* Use Cases */}
<Band pad="$11" measure={896}>
<Head
title={t('Use Cases')}
lede={t('From startups to enterprise, Hanzo DNS scales with your infrastructure')}
/>
<YStack gap={16}>
{useCases.map((useCase, index) => (
<Leaf
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: index * 0.05 }} />}
key={useCase.title}
{...LEAF}
hoverStyle={{ borderColor: "var(--neutral-600)" }}
>
<XStack items="flex-start" gap={16}>
<Plate size={40}>
<useCase.icon width={20} height={20} color="var(--foreground)" />
</Plate>
<Box>
<CardTitle>{t(useCase.title)}</CardTitle>
<CardBody>{t(useCase.description)}</CardBody>
</Box>
</XStack>
</Leaf>
))}
</YStack>
</Band>
{/* Pricing */}
<Band pad="$11" measure={1024}>
<Head
title={t('Pricing')}
lede={t('Start free. Scale as you grow.')}
/>
<Grid columns={TIERS} gap={24}>
{pricingTiers.map((tier, index) => (
<Box
key={tier.name}
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: index * 0.05 }} />}
position="relative"
p={24}
{...TILE_BOX}
borderColor={tier.highlight ? 'var(--primary)' : 'var(--border)'}
>
{tier.highlight && (
<Line
render="div"
display="block"
position="absolute"
t={-12}
l="50%"
x="-50%"
px={12}
py={2}
rounded="var(--radius-full)"
bg="var(--primary)"
color="var(--primary-foreground)"
fontSize="$1"
lineHeight="1.3333"
fontWeight="500"
>
{t('Popular')}
</Line>
)}
<CardTitle>{tier.name}</CardTitle>
<Line render="p" display="block" mb={4} fontSize="$8" lineHeight="1.2" fontWeight="700" color="var(--foreground)">
{t(tier.price)}
<Line fontSize="$2" lineHeight="1.4286" fontWeight="400" color="var(--muted-foreground)">
{t(tier.period)}
</Line>
</Line>
<CardBody mb={24}>{t(tier.description)}</CardBody>
<YStack render="ul" gap={8} mb={24}>
{tier.features.map((feature) => (
<XStack key={feature} render="li" items="center" gap={8}>
<Check width={16} height={16} color="var(--foreground)" />
<CardBody render="span" display="inline">
{t(feature)}
</CardBody>
</XStack>
))}
</YStack>
<Action
href={tier.ctaHref}
fill={tier.highlight}
width="100%"
justify="center"
px={24}
{...(tier.highlight ? null : { borderColor: 'var(--border)' })}
>
{t(tier.cta)}
</Action>
</Box>
))}
</Grid>
</Band>
<ProductFooter slug="dns" name="DNS" />
</>
)
}