Files
zeekayandClaude Code 2858061a21 Say the FAQ and say who we are, where a machine can read it
FAQPage is the one schema that changes what a search of a page returns
rather than only how it is described: the question is the index and the
answer is the quote. Both FAQ sources already rendered the pair; neither
handed it over. FaqJsonLd reads the node the page already draws down to
its words, so the quoted answer is the visible one and there is no second
copy to keep true — 3 on the home page, 25 on pricing.

The Organization node pointed at a 16-pixel favicon as its logo and named
three properties. It now carries brand/hanzo-logo.png at 696x696, the
2016 founding and Techstars '17 the about page states, the eight Hanzo
properties that resolve, and the three addresses /contact prints.

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

59 lines
2.2 KiB
TypeScript

import type { ReactNode } from 'react'
/**
* A page's FAQ, said once more as a machine can hold it.
*
* FAQPage is the one schema that changes what a search of the page returns
* rather than only how the page is described: the question is the index, and
* the answer is what gets quoted. A page that already renders a question and
* its answer has the whole entity; what it has never done is hand it over.
*
* The answer is taken from the node the page already draws, not retyped for
* the schema. A second copy of an answer is a second thing to keep true, and
* the day it is not is the day the quoted answer contradicts the visible one.
* `plain` reads a ReactNode down to its words, so a link inside an answer
* arrives as its text and nothing is lost that a reader would have seen.
*/
const SITE = 'https://hanzo.ai'
/** `</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 words a node says. Elements contribute their children and none of their chrome. */
export function plain(node: ReactNode): string {
if (node == null || typeof node === 'boolean') return ''
if (typeof node === 'string' || typeof node === 'number') return String(node)
if (Array.isArray(node)) return node.map(plain).join('')
const children = (node as { props?: { children?: ReactNode } })?.props?.children
return children === undefined ? '' : plain(children)
}
export function FaqJsonLd({ path, items }: { path: string; items: { name: string; answer: ReactNode }[] }) {
const main = items
.map((it) => ({ name: it.name.trim(), text: plain(it.answer).replace(/\s+/g, ' ').trim() }))
.filter((it) => it.name && it.text)
.map((it) => ({
'@type': 'Question',
name: it.name,
acceptedAnswer: { '@type': 'Answer', text: it.text },
}))
if (!main.length) return null
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: embed({
'@context': 'https://schema.org',
'@type': 'FAQPage',
'@id': `${SITE}${path}#faq`,
url: `${SITE}${path}`,
mainEntity: main,
}),
}}
/>
)
}