hypertwist/website/src/components/seo/SiteMetadata.tsx
2026-07-03 10:59:04 +00:00

105 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 120cell 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
}