Files
hanzo.ai/components/product/ProductJsonLd.tsx
zeekayandClaude Code c0aec98240 Name the three flagships in the structured data too
Enso, Kai and Zen sit outside the ProductLanding kit, so the emitter added
for the fifty catalog pages reached none of them. Each now declares itself
with its own page sentence and the features that sentence already states.
The kit also passes a page's lede ahead of the catalog's menu blurb, so
Base answers as "a single Go binary that serves your data" rather than
"instant app backend".

Co-Authored-By: Claude Code <[email protected]>
2026-10-04 03:03:00 -07:00

95 lines
3.6 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
const feats = features ?? listed?.features
if (feats?.length) app.featureList = feats
if (product.cli) app.installUrl = `https://hanzo.sh`
if (product.api) app.additionalProperty = { '@type': 'PropertyValue', name: 'API namespace', value: product.api }
if (listed?.github) app.codeRepository = listed.github
if (product.docs) app.sameAs = [product.docs]
const graph = [
app,
{
'@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 }) }} />
}