mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
The share card offers Hanzo, the products and the betas the member asked for, the rest under More by group. A topic picks the posts Shuffle offers, the link (/beta/for/<topic>, tagged utm_content) and the card it unfurls to: that page carries the topic's title, line and card, a canonical naming itself and the one noindex (lib/publish SHARES), and sends a person on to /beta with the query kept, so ref and the campaign land as before. lib/hanzo/topics is the list, its words the site's and BETAS's own. generate-og draws a card per topic from it, palette-quantized with sharp: 49 cards, 2.9 MB. The cards gate reads every card's rendered words. Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
86 lines
4.4 KiB
TypeScript
86 lines
4.4 KiB
TypeScript
import { phrase } from '@hanzo/i18n'
|
||
import { BETAS, type Group } from '@hanzo/app/lib/betas'
|
||
import { MESSAGES } from './messages.ts'
|
||
|
||
/**
|
||
* What a member may share about: Hanzo as a whole, one of its products, or one
|
||
* app or model in beta (BETAS). Each topic is a page of its own,
|
||
* /beta/for/<id> (app/beta/for/[topic]), with a card of its own that
|
||
* scripts/generate-og.mjs draws from this list, so the link unfurls to what the
|
||
* post says. Every word is one the site already says: a beta's `line` is
|
||
* BETAS's own, and a post fills `{name}` and `{line}` from its topic, so it is
|
||
* true by construction. A `name` is never translated.
|
||
*/
|
||
|
||
/** A post: authored English through phrase(), drawn as `t(text, { name, line: t(line) })`. */
|
||
export interface Post {
|
||
id: string
|
||
text: string
|
||
}
|
||
|
||
export interface Topic {
|
||
/** The path segment, /beta/for/<id>, and the link's `utm_content`. */
|
||
id: string
|
||
/** The product's name, as it ships. */
|
||
name: string
|
||
/** The share page's title: the beta page's own for Hanzo, the name for the rest. */
|
||
title: string
|
||
/** What it is, in the site's words: the share page's description and the card's lede. */
|
||
line: string
|
||
/** `beta` is a BETAS entry; `product` is one everyone can open; `hanzo` is the whole of it. */
|
||
kind: 'hanzo' | 'product' | 'beta'
|
||
/** A beta's group in BETAS. */
|
||
group?: Group
|
||
/** What Shuffle offers. */
|
||
posts: readonly Post[]
|
||
}
|
||
|
||
/** The topic a member shares when they have picked none. */
|
||
export const HANZO = 'hanzo'
|
||
|
||
const said = (id: (typeof MESSAGES)[number]['id']): Post => MESSAGES.find((m) => m.id === id)!
|
||
|
||
const ON: Post = { id: 'on', text: phrase('{name} on Hanzo: {line}. I’m in the beta.') }
|
||
const LINK: Post = { id: 'link', text: phrase('{name} is in beta on Hanzo. {line}. Get early access with my link.') }
|
||
const ASK: Post = { id: 'ask', text: phrase('Ask for early access to {name} on Hanzo. {line}.') }
|
||
// The beta page's own description (app/beta/layout.tsx), after the product's line.
|
||
const FIVE: Post = { id: 'five', text: phrase('{name}: {line}. Early access to Dev, Studio, Kai, Enso and Zen.') }
|
||
|
||
/**
|
||
* The products the beta page names ("Early access to Dev, Studio, Kai, Enso and
|
||
* Zen.", app/beta/layout.tsx) and Tabs, which the beta's own posts name. Each
|
||
* line is the first sentence of its page's own description, its period dropped
|
||
* to read like a BETAS line.
|
||
*/
|
||
const PRODUCTS: Topic[] = [
|
||
// app/dev/page.tsx DESCRIPTION.
|
||
{ id: 'dev', name: 'Dev', line: phrase('Autonomous AI coding agent for your terminal, IDE, and cloud'), posts: [ON, FIVE, said('dev')] },
|
||
// app/studio/page.tsx metadata.description.
|
||
{ id: 'studio', name: 'Studio', line: phrase('Visual node-graph builder for generative media'), posts: [ON, FIVE, LINK] },
|
||
// app/kai/page.tsx DESCRIPTION.
|
||
{ id: 'kai', name: 'Kai', line: phrase('Calibrated probability decision model for discrete choices'), posts: [ON, FIVE, LINK] },
|
||
// app/enso/page.tsx DESCRIPTION. Its second sentence names latency, which no
|
||
// card says (e2e/unit/cards.spec.ts, UNBACKED), so the line is the first.
|
||
{ id: 'enso', name: 'Enso', line: phrase('Intelligent model routing for frontier AI'), posts: [ON, FIVE, LINK] },
|
||
// app/zen/page.tsx DESCRIPTION.
|
||
{ id: 'zen', name: 'Zen', line: phrase('Open-weight foundation models for agentic coding'), posts: [ON, FIVE, LINK] },
|
||
// app/(marketing)/tabs/layout.tsx title, the page's <h1>.
|
||
{ id: 'tabs', name: 'Tabs', line: phrase('Keep tabs on your agents'), posts: [ON, said('tabs'), said('one-app')] },
|
||
].map((p) => ({ ...p, title: p.name, kind: 'product' as const }))
|
||
|
||
export const TOPICS: readonly Topic[] = [
|
||
// The beta page itself (app/beta/layout.tsx): its title, its description, and the posts about all of it.
|
||
{ id: HANZO, name: 'Hanzo', title: phrase('Get there first'), line: phrase('Early access to Dev, Studio, Kai, Enso and Zen.'), kind: 'hanzo', posts: MESSAGES },
|
||
...PRODUCTS,
|
||
...BETAS.map((b) => ({ id: b.id, name: b.name, title: b.name, line: b.line, kind: 'beta' as const, group: b.group, posts: [ON, LINK, ASK] })),
|
||
]
|
||
|
||
/** The topic named `id`, or none. */
|
||
export const topic = (id: string): Topic | undefined => TOPICS.find((t) => t.id === id)
|
||
|
||
/** A topic's share page. */
|
||
export const page = (id: string) => `/beta/for/${id}`
|
||
|
||
/** The name of a topic's card in public/og/: Hanzo's is the beta page's own. */
|
||
export const picture = (id: string) => (id === HANZO ? 'beta' : `beta-${id}`)
|