English stays at /. Each other locale is a static directory with its own lang and dir, hreflang, and a translated catalog. A missing key still renders the English sentence. Co-authored-by: z <[email protected]>
5.3 KiB
Hanzo Help
Overview
Hanzo Help — Guides, docs, and support for Hanzo AI. A Next.js static export
(output: "export"), one page.
Tech Stack
- TypeScript, Next 16 (App Router, static export)
- UI is
@hanzo/uion@hanzo/gui. No Tailwind, no Radix, nocn(), no CSS file of our own. Import from the@hanzo/uiroot barrel only — reaching past it to@hanzo/guiresolves a second copy of the runtime. lucide-reactfor glyphs (sizeprop, never a class; they strokecurrentColor, so colouring the parentTextcolours the glyph)
Build & Run
pnpm install && pnpm build # writes out/
Structure
app/
document.tsx <html lang dir>, the font, <Hanzo>, analytics
(default)/ English, at /
(locale)/[locale]/ every other language
help-page.tsx the page; data arrays + composition, Band
language.tsx language switcher (a disclosure of links)
analytics.tsx event client, tag manager, consent banner
link.tsx Link / Pill / Topic
lib/i18n/ catalogs, fallback, hreflang
messages/ one JSON catalog per locale; see messages/README.md
next.config.ts
Languages
English is the source and the default, at /. The other locales are path
prefixes: es, fr, de, it, pt-BR, ru, tr, ar, hi, id, vi,
th, ja, ko, zh-CN, zh-TW. Copy is authored with phrase(); t()
reads messages/<locale>.json and a missing key renders the English.
@hanzo/i18n supplies that fallback, dir, and the hreflang set. Arabic is
dir="rtl". There is no article tree in this repo.
pnpm check:i18n # missing keys, placeholders, protected names
pnpm translate # fill gaps; HANZO_API_KEY, see messages/README.md
What was learned the hard way
Resolver. gui renders through react-native-web, so next.config.ts must
alias react-native → react-native-web and put .web.js ahead of .js in
the extension order (react-native-svg, under the icon set, ships its browser
implementation that way). Both are stated for webpack AND turbopack so dev
and build resolve identically. @react-native/assets-registry is pinned to
0.86.0 deliberately: 0.87 re-exports from react-native, which the alias
sends to react-native-web, and the build warns on every run.
Tokens are silently dropped. A style prop naming a token that does not
exist emits NO class — no error, no type failure, just an unstyled element.
$colorSubtle cost an hour that way. The real names are $color,
$background, $borderColor(+Hover), $accentBackground/$accentColor, and
the surface ladder $sunken $panel $hover $edge $raised $rim $bound $dim $faint $soft $quiet $ink. Muted text is $soft. Verify by reading the built CSS: every
_-prefixed class in the output must have a rule.
color is not a prop on a stack. XStack/YStack reject it; Text owns
it. Wrap, don't guess.
Breakpoints are MIN-width — $sm 640, $md 768, $lg 1024 — so write
mobile-first: the base is the phone's value and $md steps it up. Section's
own source comment claims max-width; the shipped CSS disagrees and the shipped
CSS is what renders. A $md object passed to a component REPLACES that
component's own rather than merging, which is how Section's rhythm is
overridden.
The font hooks do not work as documented. --font-sans-provided lives
inside @layer hanzo.font and loses to an unlayered :root{--font-sans:"Zen"};
the root element's --font-sans is owned by @hanzo/appearance, which clears
it on mount. Set it on <body> — managed by neither. "Zen" ships no
@font-face here, so without this the page renders system-ui.
Verify in a browser, not in the markup. The static export carries gui's
SSR-extracted styles in an inline <style>, so it is fully styled with JS off —
but only a real render catches a card whose text runs out of its frame.
Analytics and deploy
app/analytics.tsxis the docs.hanzo.ai pattern:@hanzo/event's provider sends the pageview toPOST api.hanzo.ai/v1/event(always: it is first party) with this site's own publishable key (cloud projecthanzo-help),startTagsloads GA4 and the Meta pixel from cloud (GET /v1/project/tags) once the visitor consents, and@hanzo/ui/consentis the banner. No tag id lives in this repo; the key is stated because @hanzo/event's SITE_KEY table does not list hanzo.help.@hanzo/ui/styles.cssis imported in the layout. Without it the export ships the atomic classes and none of the base sheet, and the page renders unstyled.@hanzo/uitypes against the v5 shorthands (items,justify,px,bg,rounded,maxW…); a longhand is a type error, though it still renders.pnpm-workspace.yamloverrides hold one@hanzo/guiand one@hanzo/eventin the tree. Two guis are two configs; two event clients are two consent stores.- Deploy: push to main.
.github/workflows/cicd.ymlruns hanzoai/ci, which type-checks, buildsout/and publishes it as Sites projecthanzo-help(s3://hanzo-sites/hanzo/hanzo-help, live athanzo-help.hanzo.app). app/socials.tsxis every Hanzo channel, the set hanzo.ai's footer and JSON-LD name, drawn in the footer as marks with accessible names. Discord is discord.gg/XthHQQj; discord.gg/hanzo is another server.