mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
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]>
59 lines
2.2 KiB
TypeScript
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,
|
|
}),
|
|
}}
|
|
/>
|
|
)
|
|
}
|