@@ -13,6 +13,21 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/app/components/content/ArtSelector.vue b/frontend/app/components/content/ArtSelector.vue
new file mode 100644
index 0000000..bacac48
--- /dev/null
+++ b/frontend/app/components/content/ArtSelector.vue
@@ -0,0 +1,105 @@
+
+
+
+
+
────────
+
+
────────
+
{{ item.title }}
+
+
+
+
diff --git a/frontend/app/components/content/BlogEntryList.vue b/frontend/app/components/content/BlogEntryList.vue
new file mode 100644
index 0000000..c78dfb8
--- /dev/null
+++ b/frontend/app/components/content/BlogEntryList.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+ -
+
+ {{ post.title }}
+
+
+ {{ post.title }}
+
+ [{{ post.date }}] {{ post.description }}
+
+
+
+
+
+
diff --git a/frontend/app/composables/seo.ts b/frontend/app/composables/seo.ts
new file mode 100644
index 0000000..674c009
--- /dev/null
+++ b/frontend/app/composables/seo.ts
@@ -0,0 +1,76 @@
+import { useHead, useSeoMeta, useRoute } from '#imports'
+
+interface SeoOptions {
+ title?: string
+ description?: string
+ ogImage?: string
+ canonicalUrl?: string
+ articleDate?: string
+ structuredData?: Record
+}
+
+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 = {
+ 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)
+ }
+ ] : []
+ })
+}
diff --git a/frontend/app/pages/art/[slug].vue b/frontend/app/pages/art/[slug].vue
index 5e9b208..0770afb 100644
--- a/frontend/app/pages/art/[slug].vue
+++ b/frontend/app/pages/art/[slug].vue
@@ -1,5 +1,6 @@
-
-
+
{{ post.title }}
@@ -26,6 +50,44 @@ const { data: post } = await useAsyncData(`art-${slug}`, () =>
margin-top: 32px;
}
+.art {
+ h2 {
+ a {
+ text-decoration: none;
+ color: var(--color-text);
+ }
+ }
+
+ img {
+ margin: auto;
+ display: flex;
+ max-height: 77vh;
+ max-width: 100%;
+ }
+}
+
+.art-title {
+ font-family: 'Hurmit', monospace;
+ font-size: 1.8rem;
+ color: var(--color-text);
+ margin-bottom: 16px;
+ line-height: 1.3;
+}
+
+
+
+
+
+
+
+