This commit is contained in:
76
frontend/app/composables/seo.ts
Normal file
76
frontend/app/composables/seo.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useHead, useSeoMeta, useRoute } from '#imports'
|
||||
|
||||
interface SeoOptions {
|
||||
title?: string
|
||||
description?: string
|
||||
ogImage?: string
|
||||
canonicalUrl?: string
|
||||
articleDate?: string
|
||||
structuredData?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export function useSeo(options: SeoOptions = {}) {
|
||||
const { locale } = useI18n()
|
||||
const route = useRoute()
|
||||
const baseURL = 'https://aranroig.com'
|
||||
|
||||
// Map locale codes to BCP 47 tags
|
||||
const localeToIso: Record<string, string> = {
|
||||
en: 'en-US',
|
||||
es: 'es-ES',
|
||||
ca: 'ca-ES'
|
||||
}
|
||||
|
||||
const canonicalPath = computed(() => {
|
||||
if (!options.canonicalUrl) return ''
|
||||
return options.canonicalUrl.startsWith('http') ? options.canonicalUrl : `${baseURL}${options.canonicalUrl}`
|
||||
})
|
||||
|
||||
// Hreflang alternate links for all locales
|
||||
const hreflangs = computed(() => {
|
||||
const currentPath = typeof route.path === 'string' ? route.path : ''
|
||||
const base = currentPath.replace(/^(\/en|\/es|\/ca)?\//, '/')
|
||||
|
||||
return [
|
||||
{ rel: 'alternate', hreflang: 'en', href: `${baseURL}/en${base}` },
|
||||
{ rel: 'alternate', hreflang: 'es', href: `${baseURL}/es${base}` },
|
||||
{ rel: 'alternate', hreflang: 'ca', href: `${baseURL}/ca${base}` }
|
||||
] as Array<{rel: string; hreflang: string; href: string}>
|
||||
})
|
||||
|
||||
const seoTitle = computed(() => {
|
||||
if (options.title) return `${options.title} | Aran Roig`
|
||||
return 'Aran Roig — Developer, Artist & Designer'
|
||||
})
|
||||
|
||||
const seoDescription = options.description || 'Personal website of Aran Roig — developer, artist, and designer. Explore projects, blog posts, art gallery, and more.'
|
||||
|
||||
useSeoMeta({
|
||||
title: seoTitle.value,
|
||||
description: seoDescription,
|
||||
ogTitle: seoTitle.value,
|
||||
ogDescription: seoDescription,
|
||||
ogImage: options.ogImage || `${baseURL}/og-image.png`,
|
||||
ogUrl: canonicalPath.value,
|
||||
ogType: 'website',
|
||||
twitterCard: 'summary_large_image',
|
||||
twitterTitle: seoTitle.value,
|
||||
twitterDescription: seoDescription,
|
||||
twitterImage: options.ogImage || `${baseURL}/og-image.png`,
|
||||
canonical: canonicalPath.value
|
||||
})
|
||||
|
||||
useHead({
|
||||
htmlAttrs: { lang: computed(() => localeToIso[locale.value] || 'en-US') },
|
||||
link: [
|
||||
...hreflangs.value,
|
||||
...(canonicalPath.value ? [{ rel: 'canonical', href: canonicalPath.value }] : [])
|
||||
],
|
||||
script: options.structuredData ? [
|
||||
{
|
||||
type: 'application/ld+json',
|
||||
innerHTML: JSON.stringify(options.structuredData)
|
||||
}
|
||||
] : []
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user