105 lines
3.9 KiB
TypeScript
105 lines
3.9 KiB
TypeScript
import { useEffect } from 'react'
|
||
import { brandConfig } from '../../site-config'
|
||
|
||
const DEFAULT_DESCRIPTION =
|
||
'HyperTwist is the desktop-first cube and hypercube simulator for guided practice, replay review, coaching, and serious 120‑cell and 5D study.'
|
||
const DEFAULT_IMAGE_PATH = '/branding/hypertwist-3d-symbol.png'
|
||
|
||
function ensureMetaTag(attributeName: 'name' | 'property', attributeValue: string) {
|
||
let element = document.head.querySelector<HTMLMetaElement>(`meta[${attributeName}="${attributeValue}"]`)
|
||
if (!element) {
|
||
element = document.createElement('meta')
|
||
element.setAttribute(attributeName, attributeValue)
|
||
document.head.appendChild(element)
|
||
}
|
||
return element
|
||
}
|
||
|
||
function ensureCanonicalLink() {
|
||
let element = document.head.querySelector<HTMLLinkElement>('link[rel="canonical"]')
|
||
if (!element) {
|
||
element = document.createElement('link')
|
||
element.setAttribute('rel', 'canonical')
|
||
document.head.appendChild(element)
|
||
}
|
||
return element
|
||
}
|
||
|
||
function normalizeSiteOrigin() {
|
||
const explicitOrigin = String((brandConfig as { origin?: string }).origin || '').trim()
|
||
if (explicitOrigin) {
|
||
return explicitOrigin.replace(/\/$/, '')
|
||
}
|
||
|
||
const domain = String(brandConfig.domain || 'hypertwist.app').trim()
|
||
if (!domain) {
|
||
return 'https://hypertwist.app'
|
||
}
|
||
|
||
return (domain.startsWith('http://') || domain.startsWith('https://') ? domain : `https://${domain}`).replace(/\/$/, '')
|
||
}
|
||
|
||
function resolveAbsoluteUrl(siteOrigin: string, candidatePath: string) {
|
||
const trimmed = String(candidatePath || '').trim()
|
||
if (!trimmed) {
|
||
return `${siteOrigin}/`
|
||
}
|
||
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
||
return trimmed
|
||
}
|
||
return `${siteOrigin}${trimmed.startsWith('/') ? trimmed : `/${trimmed}`}`
|
||
}
|
||
|
||
function buildDocumentTitle(title: string) {
|
||
const trimmed = String(title || '').trim()
|
||
if (!trimmed || trimmed === brandConfig.brandName) {
|
||
return brandConfig.brandName
|
||
}
|
||
return `${trimmed} | ${brandConfig.brandName}`
|
||
}
|
||
|
||
function readBrowserPathname() {
|
||
if (typeof window === 'undefined') {
|
||
return '/'
|
||
}
|
||
return window.location.pathname || '/'
|
||
}
|
||
|
||
export function SiteMetadata({
|
||
title,
|
||
description = DEFAULT_DESCRIPTION,
|
||
canonicalPath,
|
||
robots = 'index,follow',
|
||
imagePath = DEFAULT_IMAGE_PATH,
|
||
}: {
|
||
title: string
|
||
description?: string
|
||
canonicalPath?: string
|
||
robots?: string
|
||
imagePath?: string
|
||
}) {
|
||
useEffect(() => {
|
||
const nextTitle = buildDocumentTitle(title)
|
||
const nextDescription = String(description || DEFAULT_DESCRIPTION).trim() || DEFAULT_DESCRIPTION
|
||
const siteOrigin = normalizeSiteOrigin()
|
||
const canonicalUrl = resolveAbsoluteUrl(siteOrigin, canonicalPath || readBrowserPathname())
|
||
const imageUrl = resolveAbsoluteUrl(siteOrigin, imagePath)
|
||
|
||
document.title = nextTitle
|
||
ensureMetaTag('name', 'description').setAttribute('content', nextDescription)
|
||
ensureMetaTag('name', 'robots').setAttribute('content', robots)
|
||
ensureMetaTag('property', 'og:site_name').setAttribute('content', brandConfig.brandName)
|
||
ensureMetaTag('property', 'og:type').setAttribute('content', 'website')
|
||
ensureMetaTag('property', 'og:title').setAttribute('content', nextTitle)
|
||
ensureMetaTag('property', 'og:description').setAttribute('content', nextDescription)
|
||
ensureMetaTag('property', 'og:url').setAttribute('content', canonicalUrl)
|
||
ensureMetaTag('property', 'og:image').setAttribute('content', imageUrl)
|
||
ensureMetaTag('name', 'twitter:card').setAttribute('content', 'summary_large_image')
|
||
ensureMetaTag('name', 'twitter:title').setAttribute('content', nextTitle)
|
||
ensureMetaTag('name', 'twitter:description').setAttribute('content', nextDescription)
|
||
ensureMetaTag('name', 'twitter:image').setAttribute('content', imageUrl)
|
||
ensureCanonicalLink().setAttribute('href', canonicalUrl)
|
||
}, [canonicalPath, description, imagePath, robots, title])
|
||
|
||
return null
|
||
}
|