Files
zeekay 70349b34c7 beta: a post about one topic, with that topic's page and card
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
2026-10-26 12:03:39 -07:00

86 lines
4.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}`)