Files
hanzo.ai/app/(marketing)/integrations/[slug]/page.tsx

295 lines
10 KiB
TypeScript

/**
* /integrations/<slug> — one integration, its attribution, and its code.
*
* NO TAILWIND, and it is the same idiom as the index it hangs off
* (`app/(marketing)/integrations/page.tsx`): a SERVER page, `Box` for the
* frame, @hanzo/ui's marketing components for the type and the controls — `Line`
* for every run of small print — and gui props over @hanzo/design's tokens.
*
* The two controls at the foot are `Action`, the site's ONE button shape at
* its two weights — the loud one keeps the brand ground it was drawn with,
* because the `data-fill` hover is an OPACITY and an inline colour does not
* fight it. The chips beneath are `Chip`, which owns its own hover; padding is
* the caller's, which is the split that rule states.
*
* Two ink lifts on hover are deliberately gone rather than moved, and both were
* on a link inside a `<p>`: @hanzo/design's base already underlines those on
* approach, so the affordance stays and only the colour change is dropped. A
* role would have brought its own size and weight with it, which is a bigger
* change than the one it repairs.
*/
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import Link from 'next/link'
import { KEYS } from '@/components/home/nav-data'
import { INTEGRATIONS, getIntegration, INTEGRATION_CATEGORIES } from '@/lib/integrations'
import { Models } from '@/components/api/count'
import { Box } from '@/components/ui'
import { Code } from '@/components/code'
import { Action, Chip, Display, Lede, Line, type LineProps } from '@hanzo/ui/marketing'
interface Props {
params: Promise<{ slug: string }>
}
export function generateStaticParams() {
return INTEGRATIONS.map((i) => ({ slug: i.slug }))
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
const integration = getIntegration(slug)
if (!integration) {
return { title: 'Integration Not Found — Hanzo AI' }
}
const title = `Use Hanzo AI with ${integration.name}`
const description = integration.description
return {
title: `${title} — Hanzo AI`,
description,
openGraph: {
title,
description,
url: `https://hanzo.ai/integrations/${slug}`,
siteName: 'Hanzo AI',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title,
description,
},
}
}
/** The two quiet greys this page sets its small print in, and full ink. */
const QUIET = { fontSize: '$2', color: 'var(--muted-foreground)' } as const
const SMALL = { fontSize: '$1', color: 'var(--muted-foreground)' } as const
const INK = 'var(--foreground)'
/**
* A heading beneath the page's own. These label a sample and a shelf rather
* than making a section's argument, so they state their rung instead of taking
* a role and overriding two thirds of it — the same call the index page makes.
*/
const Sub = (p: LineProps) => (
<Line render="h2" display="block" fontSize="$6" lineHeight="var(--leading-snug)" fontWeight="600" color="var(--foreground)" {...p} />
)
/** A sample under its language, highlighted at build (components/code). */
function CodeBlock({ code, lang }: { code: string; lang: string }) {
return (
<Box rounded="var(--radius-md)" borderWidth={1} borderColor="var(--border)" overflow="hidden">
<Box
display="flex"
flexDirection="row"
items="center"
justify="space-between"
px={16}
py={8}
borderBottomWidth={1}
borderColor="var(--border)"
>
<Line fontFamily="$mono" {...SMALL}>
{lang}
</Line>
</Box>
<Code code={code} lang={lang} />
</Box>
)
}
export default async function IntegrationPage({ params }: Props) {
const { slug } = await params
const integration = getIntegration(slug)
if (!integration) {
notFound()
}
const jsonLd = {
'@context': 'https://schema.org',
'@type': ['HowTo', 'SoftwareApplication'],
name: `Use Hanzo AI with ${integration.name}`,
description: integration.description,
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Any',
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
author: { '@type': 'Organization', name: integration.creator, url: integration.creatorUrl },
license: integration.upstream,
step: integration.examples.map((ex, i) => ({
'@type': 'HowToStep',
position: i + 1,
name: ex.label,
text: ex.code,
})),
}
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<Box minH="100vh">
<Box
width="100%"
maxW={896}
mx="auto"
px={16}
py={80}
>
{/* Breadcrumb */}
<Line render="nav" display="flex" flexDirection="row" items="center" gap={8} mb={40} {...QUIET}>
<Link href="/integrations">Integrations</Link>
<span>/</span>
<Line color={INK}>{integration.name}</Line>
</Line>
{/* Header */}
<Box mb={48}>
<Box display="flex" flexDirection="row" items="center" gap={16} mb={16}>
<Line fontSize="$10" color="inherit">
{integration.icon}
</Line>
<div>
<Line {...SMALL} fontWeight="500" textTransform="uppercase" letterSpacing="0.05em">
{INTEGRATION_CATEGORIES[integration.category]}
</Line>
<Display>Use Hanzo AI with {integration.name}</Display>
</div>
</Box>
<Lede maxW={672}>{integration.description}</Lede>
</Box>
{/* API base info */}
<Box
mb={40}
rounded="var(--radius-lg)"
borderWidth={1}
p={20}
/* A `color-mix()` ink is not a value gui's colour props type, so the
brand wash rides `$platform-web`, which gui still writes as a class. */
$platform-web={{ borderColor: 'color-mix(in oklab, var(--brand) 20%, transparent)', backgroundColor: 'color-mix(in oklab, var(--brand) 5%, transparent)' }}
>
<Line render="p" display="block" mb={8} {...QUIET}>
<Line render="strong" color={INK}>Base URL:</Line>{' '}
<Line render="code" color="var(--brand)">https://api.hanzo.ai/v1</Line>
</Line>
<Line render="p" display="block" {...QUIET}>
<Line render="strong" color={INK}>API Key:</Line> Get yours at{' '}
<Line href={KEYS} color="var(--brand)">
platform.hanzo.ai/api-keys
</Line>{' '}
· Fully OpenAI-compatible · <Models /> available
</Line>
</Box>
{/* Attribution */}
<Box
mb={40}
rounded="var(--radius-lg)"
borderWidth={1}
borderColor="var(--border)"
bg="$panel"
p={20}
display="flex"
flexDirection="row"
items="flex-start"
gap={16}
>
<Line render="div" display="block" fontSize="$7" color="inherit">
{integration.icon}
</Line>
<Box grow={1} shrink={1} flexBasis="0%" minW={0}>
<Line render="p" display="block" mb={2} fontSize="$2" fontWeight="500" color="var(--foreground)">
Created by{' '}
<Line href={integration.creatorUrl} target="_blank" rel="noopener noreferrer" color="var(--state-error)">
{integration.creator}
</Line>
</Line>
<Line render="p" display="block" mb={8} {...SMALL}>
License:{' '}
<Line fontFamily="$mono" color={INK} bg="var(--muted)" px={6} py={2} rounded="var(--radius)">
{integration.license}
</Line>
{' · '}
<a href={integration.upstream} target="_blank" rel="noopener noreferrer">
View source on GitHub →
</a>
</Line>
<Line render="p" display="block" {...SMALL}>
Hanzo AI is OpenAI-compatible, so existing {integration.name} code works with zero refactoring. We deeply appreciate the {integration.creator} team for building and maintaining this open-source project.
</Line>
</Box>
</Box>
{/* Code examples */}
<Box display="flex" flexDirection="column" gap={40}>
{integration.examples.map((example, i) => (
<div key={i}>
<Sub mb={16}>{example.label}</Sub>
<CodeBlock code={example.code} lang={example.lang} />
</div>
))}
</Box>
{/* Footer CTA */}
<Box
mt={64}
rounded="var(--radius-lg)"
borderWidth={1}
borderColor="var(--border)"
bg="var(--card)"
p={32}
$platform-web={{ textAlign: 'center' }}
>
<Sub mb={12} fontSize="$7" fontWeight="700">
Ready to get started?
</Sub>
<Line render="p" display="block" mb={24} color="var(--muted-foreground)">
Create a free account and get your API key. The Free plan includes 50 free requests a day, no card.
</Line>
<Box display="flex" flexDirection="row" items="center" justify="center" gap={16}>
{/* The brand ground is a prop and the fill's hover is an
OPACITY, so the two do not fight. */}
<Action
href={KEYS}
fill
bg="var(--brand)"
color="var(--brand-foreground)"
>
Get API key — Free
</Action>
<Action href="/models">
Browse Models
</Action>
</Box>
</Box>
{/* Other integrations */}
<Box mt={48}>
<Sub mb={16}>More integrations</Sub>
<Box display="flex" flexDirection="row" flexWrap="wrap" gap={8}>
{INTEGRATIONS.filter((i) => i.slug !== slug).map((i) => (
<Chip
href={`/integrations/${i.slug}`}
key={i.slug}
px={12}
py={6}
>
{i.name}
</Chip>
))}
</Box>
</Box>
</Box>
</Box>
</>
)
}