mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
102 lines
4.0 KiB
TypeScript
102 lines
4.0 KiB
TypeScript
import { getPrimitive } from '@/lib/data/cloud-primitives'
|
|
import type { Product } from '@/lib/product'
|
|
|
|
/**
|
|
* What a product page says to a machine.
|
|
*
|
|
* The root layout declares one `SoftwareApplication` for the whole platform, so
|
|
* every product page is currently distinguishable from the homepage only by its
|
|
* `<title>`. An answer engine asked "what does Hanzo Base do" finds a page and
|
|
* no entity: nothing in the structured data names Base, states what it does, or
|
|
* links the source that proves it.
|
|
*
|
|
* So this emits the product as its own node, built entirely out of facts the
|
|
* catalog already owns — the name, the blurb, the feature list, the repo, the
|
|
* API path that proves the product exists. Nothing here is written for the
|
|
* schema; a field with no stated value is left off rather than filled in, which
|
|
* is the difference between a description and an advertisement.
|
|
*
|
|
* `@id` is the page plus a fragment, so the node is addressable without being
|
|
* the page, and `isPartOf` points back at the platform node the root already
|
|
* declares instead of describing the platform a second time.
|
|
*
|
|
* Rendered by `ProductLanding`, so all fifty pages that go through the kit get
|
|
* it and none of them can state it differently.
|
|
*/
|
|
|
|
const SITE = 'https://hanzo.ai'
|
|
const ORG = `${SITE}/#organization`
|
|
const PLATFORM = `${SITE}/#platform`
|
|
|
|
/** `</script>` inside a JSON string ends the block and the rest of the page becomes markup. */
|
|
const embed = (value: unknown) => JSON.stringify(value).replace(/</g, '\\u003c')
|
|
|
|
/** The first stated sentence about a product, in the order the catalog says it. */
|
|
function describe(p: Product): string | undefined {
|
|
const listed = getPrimitive(p.slug)
|
|
return listed?.description ?? listed?.blurb ?? listed?.tagline
|
|
}
|
|
|
|
/**
|
|
* `description` and `features` are overrides, not additions. The catalog's
|
|
* blurb for a flagship is a menu line — "Instant app backend" — and a page
|
|
* that already states a fuller sentence in its own metadata should be the one
|
|
* that answers, because that is the sentence a reader saw. Where the catalog
|
|
* carries the richer prose it wins by default and nothing is passed.
|
|
*/
|
|
export function ProductJsonLd({
|
|
product,
|
|
path,
|
|
description,
|
|
features,
|
|
}: {
|
|
product: Product
|
|
path: string
|
|
description?: string
|
|
features?: string[]
|
|
}) {
|
|
const url = `${SITE}${path}`
|
|
const listed = getPrimitive(product.slug)
|
|
|
|
const app: Record<string, unknown> = {
|
|
'@type': 'SoftwareApplication',
|
|
'@id': `${url}#product`,
|
|
name: `Hanzo ${product.name}`,
|
|
url,
|
|
applicationCategory: 'DeveloperApplication',
|
|
applicationSubCategory: product.kind === 'client' ? 'Client library' : 'Cloud service',
|
|
publisher: { '@id': ORG },
|
|
isPartOf: { '@id': PLATFORM },
|
|
}
|
|
|
|
const blurb = description ?? describe(product)
|
|
if (blurb) app.description = blurb
|
|
// The API namespace is a feature line: schema.org gives a SoftwareApplication
|
|
// no `additionalProperty` (that is a Product's or an Offer's).
|
|
const feats = [...(features ?? listed?.features ?? []), ...(product.api ? [`API namespace: ${product.api}`] : [])]
|
|
if (feats.length) app.featureList = feats
|
|
if (product.cli) app.installUrl = `https://hanzo.sh`
|
|
if (product.docs) app.sameAs = [product.docs]
|
|
|
|
// The repository is the source code's, a node of its own: `codeRepository`
|
|
// belongs to SoftwareSourceCode, and `targetProduct` ties it to the app.
|
|
const source = listed?.github
|
|
? [{ '@type': 'SoftwareSourceCode', '@id': `${url}#source`, codeRepository: listed.github, targetProduct: { '@id': `${url}#product` } }]
|
|
: []
|
|
|
|
const graph = [
|
|
app,
|
|
...source,
|
|
{
|
|
'@type': 'BreadcrumbList',
|
|
'@id': `${url}#breadcrumb`,
|
|
itemListElement: [
|
|
{ '@type': 'ListItem', position: 1, name: 'Hanzo', item: SITE },
|
|
{ '@type': 'ListItem', position: 2, name: product.name, item: url },
|
|
],
|
|
},
|
|
]
|
|
|
|
return <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: embed({ '@context': 'https://schema.org', '@graph': graph }) }} />
|
|
}
|