mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
Calculator, captable, careers, cloud, compare, contact and contact-sales, customers, dashboards, dataroom, datastore, dev, download, enso, enterprise, extension, family, integrations, investors, kai, learn, legal and their components say their words through t() in client code, <T> in server components and phrase() where copy is held as data; one sentence is one key, figures and links its arguments. Rebased on main: in every conflict main's file stands (its copy, layout and styling) and only the wrapping is added back, including copy main added since. Claude-Session: https://claude.ai/code/session_015XYKLjZtDTJEnMo4eK3AFH
104 lines
4.4 KiB
TypeScript
104 lines
4.4 KiB
TypeScript
import { notFound } from 'next/navigation'
|
|
import { Page, PageHero, Prose, Section } from '@/components/marketing/page-kit'
|
|
import { legalDoc } from '@/lib/legal'
|
|
import { pageMeta } from '@/lib/page-meta'
|
|
import { AUP_VERSION, TERMS_VERSION, versionDate } from '@hanzo/app/lib/terms'
|
|
import { phrase } from '@hanzo/i18n'
|
|
import { T } from '@hanzo/i18n/react'
|
|
|
|
/** The two documents a person agrees to at sign-up take their version from `@hanzo/app/lib/terms`, not their own front matter. */
|
|
const AGREED: Record<string, string> = { terms: TERMS_VERSION, 'acceptable-use': AUP_VERSION }
|
|
|
|
/**
|
|
* A legal document, rendered — the ONE place any of them is drawn.
|
|
*
|
|
* `content/legal/*.md` is the one authoritative version of each document and
|
|
* this is the one thing that renders it. Four routes reach it: `/legal/<slug>`
|
|
* for all ten, and `/privacy`, `/terms` and `/cookies` for the three whose
|
|
* short addresses were minted before the markdown stack existed.
|
|
*
|
|
* Those three used to be HAND-WRITTEN TSX, and they were not stale renderings
|
|
* of the same text — they were DIFFERENT DOCUMENTS. The TSX privacy policy ran
|
|
* thirteen generic sections against the markdown's nineteen, and the two
|
|
* disagreed about the minimum age and about whether research participation is
|
|
* on by default. A site cannot serve two answers to "what are your terms", so
|
|
* the bodies are gone and both addresses now read one file.
|
|
*
|
|
* The status line is not decoration. Every one of these is a DRAFT pending
|
|
* counsel, and the page says so above the text, from the document's own front
|
|
* matter. A reviewer relying on unexecuted terms because a page implied
|
|
* otherwise is the exact failure /trust exists to avoid.
|
|
*/
|
|
export function LegalDocument({ slug }: { slug: string }) {
|
|
const doc = legalDoc(slug)
|
|
if (!doc) notFound()
|
|
const effective = doc.status.toLowerCase() === 'effective'
|
|
const version = AGREED[slug] ?? doc.version
|
|
const date = AGREED[slug] ? versionDate(version) : doc.updated
|
|
return (
|
|
<Page>
|
|
<PageHero eyebrow={phrase('Legal')} title={doc.title} lede={doc.summary} />
|
|
<Section>
|
|
<Prose>
|
|
{effective ? null : (
|
|
<p>
|
|
<strong><T>Draft.</T></strong>{' '}<T>This document is not yet in force.</T>
|
|
</p>
|
|
)}
|
|
<p>
|
|
<T values={{ date, version }}>{'Effective {date} · Version {version} ·'}</T>{' '}
|
|
{doc.previous ? <a href={doc.previous}><T>Previous version</T></a> : <T>Previous version: none, this is the first.</T>}
|
|
</p>
|
|
{doc.toc.length > 1 ? (
|
|
<nav aria-label="Contents">
|
|
<h2><T>Contents</T></h2>
|
|
<ul>
|
|
{doc.toc.map((row) => (
|
|
<li key={row.id}>
|
|
<a href={`#${row.id}`}>{row.text}</a>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</nav>
|
|
) : null}
|
|
</Prose>
|
|
</Section>
|
|
<Section>
|
|
<Prose html={doc.html} />
|
|
</Section>
|
|
</Page>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* The metadata for a document served at its own short address, `/<slug>`.
|
|
*
|
|
* `/legal/<slug>` is the address, and this is what says so: the canonical
|
|
* points there, so search consolidates two URLs onto one document rather than
|
|
* ranking them against each other. `pageMeta` already carries that option and
|
|
* already states the reason — "left to point at itself it is a second, empty
|
|
* URL competing with the page that holds the words".
|
|
*
|
|
* A redirect stub was the alternative and it is not available here. This is a
|
|
* static export, so Next's `redirects()` emits nothing; the repo's substitute
|
|
* is a shell listed in `lib/publish`'s `EMPTY`, which makes the route
|
|
* `noindex` — and `shown()` in `components/home/shell.tsx` links a page in the
|
|
* chrome only while `policy()` calls it public. `@hanzogui/shell`'s footer
|
|
* registry points at `https://hanzo.ai/privacy` and `https://hanzo.ai/terms`,
|
|
* so a stub would delete both links from the footer of every page on the site,
|
|
* in a package this repo cannot edit.
|
|
*
|
|
* Title and description come from the document's own front matter, so nothing
|
|
* about it is typed twice.
|
|
*/
|
|
export function legalMeta(slug: string, path = `/${slug}`) {
|
|
const doc = legalDoc(slug)
|
|
if (!doc) throw new Error(`no legal document '${slug}' in content/legal`)
|
|
return pageMeta({
|
|
title: doc.title,
|
|
description: doc.summary,
|
|
path,
|
|
canonical: `/legal/${slug}`,
|
|
})
|
|
}
|