import { useHead, useRequestURL, useSeoMeta, useRuntimeConfig } from '#app' import type { CmsSiteInfo } from '~/types' type SeoInput = { title: string description?: string keywords?: string path?: string image?: string type?: 'website' | 'article' | 'product' siteName?: string publishedTime?: string modifiedTime?: string } function getSiteOrigin() { if (import.meta.client) return window.location.origin try { return useRequestURL().origin } catch { return '' } } /** * 页面 SEO 设置 * 设置 TDK、Open Graph、Twitter Card、Canonical URL */ export function usePageSeo(input: SeoInput, site?: CmsSiteInfo | null) { const origin = getSiteOrigin() const url = input.path && origin ? new URL(input.path, origin).toString() : undefined const overrideSiteName = (useRuntimeConfig().public.siteName as string) || '' const siteName = input.siteName || overrideSiteName || site?.websiteName || '' const description = input.description || site?.comments || site?.content || '' const keywords = input.keywords || site?.keywords || '' const image = input.image || site?.websiteLogo || '' const fullTitle = siteName ? `${input.title} - ${siteName}` : input.title useSeoMeta({ title: fullTitle, description, keywords, ogTitle: input.title, ogDescription: description, ogType: input.type || 'website', ogSiteName: siteName, ...(image ? { ogImage: image } : {}), ...(url ? { ogUrl: url } : {}), twitterCard: 'summary_large_image', twitterTitle: input.title, twitterDescription: description, ...(image ? { twitterImage: image } : {}), ...(input.publishedTime ? { articlePublishedTime: input.publishedTime } : {}), ...(input.modifiedTime ? { articleModifiedTime: input.modifiedTime } : {}) }) if (url) { useHead({ link: [{ rel: 'canonical', href: url }] }) } } /** * 注入 JSON-LD 结构化数据 */ export function useJsonLd(data: Record | Record[]) { const items = Array.isArray(data) ? data : [data] useHead({ script: items.map((item) => ({ type: 'application/ld+json', innerHTML: JSON.stringify(item) })) }) } /** * 注入企业组织结构化数据 */ export function useOrganizationSeo(site?: CmsSiteInfo | null) { if (!site) return const origin = getSiteOrigin() const rawPhone = site.phone || '' const phone = rawPhone && !rawPhone.includes('*') ? rawPhone : (useRuntimeConfig().public.phone as string) || site.config?.tel || rawPhone || '' const email = site.config?.email || site.email || '' const address = site.address || site.config?.address || '' const logo = site.websiteLogo || '' const overrideSiteName = (useRuntimeConfig().public.siteName as string) || '' useJsonLd({ '@context': 'https://schema.org', '@type': 'Organization', name: overrideSiteName || site.websiteName || '', url: origin, ...(logo ? { logo: new URL(logo, origin).toString() } : {}), ...(phone ? { telephone: phone } : {}), ...(email ? { email: email } : {}), ...(address ? { address: { '@type': 'PostalAddress', address } } : {}) }) } /** * 注入面包屑结构化数据 */ export function useBreadcrumbSeo(items: { name: string; url: string }[]) { const origin = getSiteOrigin() useJsonLd({ '@context': 'https://schema.org', '@type': 'BreadcrumbList', itemListElement: items.map((item, index) => ({ '@type': 'ListItem', position: index + 1, name: item.name, item: new URL(item.url, origin).toString() })) }) }