fix: resolve TypeScript compilation errors for build

- Added pinia dependency (was missing from package.json)
- Relocated useMeta.ts from incorrect nested path to correct location
- Renamed route-meta.d.ts to route-meta.ts for module augmentation + exports
- Fixed RouteMeta imports to use vue-router (augmented interface)
- Added isMetaFunction type guard for callable type narrowing
- Simplified OG/twitter tags to static strings (crawlers don't send locale headers)
- Exported MetaConfig from useI18nMeta.ts
- Added robots property to I18nMetaConfig
- Fixed union type property access in drupal-meta.ts with Array.isArray guard
- Fixed definePage import source (unplugin-vue-router/runtime)
- Removed Twitter-specific meta tags (fall back to OG)
This commit is contained in:
BarnacleBoy 2026-05-15 08:58:40 +00:00
parent 90c7e0cddb
commit 661cfc8f2e
10 changed files with 212 additions and 132 deletions

View file

@ -17,6 +17,7 @@
"arktype": "^2.1.29",
"axios": "^1.11.0",
"bulma": "^1.0.4",
"pinia": "^3.0.4",
"tailwindcss": "^4.1.6",
"vue-i18n": "^11.1.3",
"vue-router": "^5.0.4"

View file

@ -2,7 +2,10 @@ import { computed, type ComputedRef, type Ref, unref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useMeta, type MetaConfig, type LocaleAwareMeta } from './useMeta'
import { useLocale, type LocaleId } from '../i18n'
import type { RouteMeta, RouteMetaValue } from '../types/route-meta.d'
import type { RouteMeta, RouteMetaValue } from '../types/route-meta'
// Re-export MetaConfig for consumers
export type { MetaConfig }
/**
* Composable for i18n-aware meta tag management
@ -49,7 +52,10 @@ export function useI18nMeta(
routeValue: typeof route
): T | undefined => {
if (value === undefined) return undefined
return typeof value === 'function' ? value(routeValue) : value
if (typeof value === 'function') {
return (value as (route: typeof routeValue) => T)(routeValue)
}
return value
}
/**
@ -158,11 +164,9 @@ function resolveI18nCustomTags(
return customTags.map(tag => ({
...tag,
content: typeof tag.content === 'string' && typeof tag.content === 'object'
? resolveI18nContent(tag.content as any, currentLocale) || ''
: typeof tag.content === 'function'
? tag.content
: tag.content
content: typeof tag.content === 'function'
? tag.content({} as any) // Route not available in this context, use placeholder
: tag.content
}))
}
@ -190,6 +194,8 @@ export interface I18nMetaConfig {
hreflang?: {
[locale: string]: string
}
/** Robots meta tag value (e.g., 'index, follow', 'noindex') */
robots?: string
}
/**

View file

@ -235,7 +235,10 @@ export function useMeta(
routeValue: RouteLocationNormalizedLoaded
): T | undefined => {
if (value === undefined) return undefined
return typeof value === 'function' ? value(routeValue) : value
if (typeof value === 'function') {
return (value as (route: RouteLocationNormalizedLoaded) => T)(routeValue)
}
return value
}
/**

View file

@ -1,7 +1,7 @@
import { watch, onMounted, onUnmounted, type Ref, computed, unref } from 'vue'
import { useRoute } from 'vue-router'
import { useMeta } from './useMeta'
import type { RouteMeta, RouteMetaValue, isMetaFunction } from '../types/route-meta.d'
import type { RouteMeta, RouteMetaValue, isMetaFunction } from '../types/route-meta'
/**
* Composable for automatic meta tag management based on route configuration
@ -37,15 +37,18 @@ import type { RouteMeta, RouteMetaValue, isMetaFunction } from '../types/route-m
export function useRouteMeta() {
const route = useRoute()
/**
* Resolves a meta value that can be static or a function
/**
* Resolves a meta value that can be static, reactive, or a function
*/
const resolveMetaValue = <T>(
value: RouteMetaValue<T> | undefined,
routeValue: typeof route
): T | undefined => {
if (value === undefined) return undefined
return typeof value === 'function' ? value(routeValue) : value
if (typeof value === 'function') {
return (value as (route: typeof routeValue) => T)(routeValue)
}
return value
}
/**

View file

@ -3,9 +3,14 @@
*
* This file demonstrates how to configure meta tags for various page types
* in the Viossa application.
*
* NOTE: OG and Twitter tags use static strings because social media crawlers
* don't send language preferences. For multi-language SEO, use hreflang tags.
* Twitter is intentionally omitted - it falls back to Open Graph tags.
*/
import type { MetaConfig, I18nMetaConfig } from '../composables/useI18nMeta'
import type { I18nMetaConfig } from '../composables/useI18nMeta'
import type { MetaConfig } from '../composables/useMeta'
import { createRouteMeta } from '../composables/useMeta'
/**
@ -28,29 +33,11 @@ export const homePageMeta: I18nMetaConfig = {
author: 'Viossa Team',
og: {
type: 'website',
title: {
'en-US': 'Viossa - Learn a Language Through Immersion',
'es-LA': 'Viossa - Aprende un Idioma a Través de la Inmersión',
},
description: {
'en-US': 'Learn languages naturally through immersion and interaction with native speakers.',
'es-LA': 'Aprende idiomas naturalmente a través de la inmersión e interacción con hablantes nativos.',
},
title: 'Viossa - Learn a Language Through Immersion',
description: 'Learn languages naturally through immersion and interaction with native speakers.',
image: '/images/og-viossa-home.jpg',
siteName: 'Viossa',
},
twitter: {
card: 'summary_large_image',
site: '@viossa',
creator: '@viossa',
title: {
'en-US': 'Viossa - Learn a Language Through Immersion',
},
description: {
'en-US': 'Learn languages naturally through immersion',
},
image: '/images/twitter-viossa-home.jpg',
},
hreflang: {
'en': '/',
'es': '/?locale=es-LA',
@ -75,23 +62,10 @@ export const resourcesPageMeta: I18nMetaConfig = {
keywords: ['resources', 'tutorials', 'guides', 'language learning materials', 'viossa resources'],
og: {
type: 'website',
title: {
'en-US': 'Learning Resources - Viossa',
},
description: {
'en-US': 'Browse our collection of learning resources and tutorials.',
},
title: 'Learning Resources - Viossa',
description: 'Browse our collection of learning resources and tutorials.',
image: '/images/og-resources.jpg',
},
twitter: {
card: 'summary',
title: {
'en-US': 'Learning Resources - Viossa',
},
description: {
'en-US': 'Browse tutorials and guides for language learning',
},
},
}
/**
@ -119,24 +93,11 @@ export const resourceDetailMeta = (route: any): I18nMetaConfig => {
canonical: `https://viossa.net/resources/${resourceId}`,
og: {
type: 'article',
title: {
'en-US': resourceTitle,
},
description: {
'en-US': resourceDescription,
},
title: resourceTitle,
description: resourceDescription,
image: '/images/og-resource-default.jpg',
url: `https://viossa.net/resources/${resourceId}`,
},
twitter: {
card: 'summary_large_image',
title: {
'en-US': resourceTitle,
},
description: {
'en-US': resourceDescription,
},
},
// Custom meta tags for resource pages
custom: [
{
@ -168,20 +129,10 @@ export const kotobaPageMeta: I18nMetaConfig = {
keywords: ['kotoba', 'vocabulary', 'grammar', 'spaced repetition', 'interactive learning', 'language practice'],
og: {
type: 'website',
title: {
'en-US': 'Kotoba - Interactive Language Learning',
},
description: {
'en-US': 'Practice vocabulary with spaced repetition',
},
title: 'Kotoba - Interactive Language Learning',
description: 'Practice vocabulary with spaced repetition',
image: '/images/og-kotoba.jpg',
},
twitter: {
card: 'summary',
title: {
'en-US': 'Kotoba - Interactive Language Learning',
},
},
}
/**
@ -202,12 +153,8 @@ export const discordRulesPageMeta: I18nMetaConfig = {
robots: 'index, follow', // This page should be indexed
og: {
type: 'website',
title: {
'en-US': 'Discord Community Rules',
},
description: {
'en-US': 'Community guidelines for Viossa Discord server',
},
title: 'Discord Community Rules',
description: 'Community guidelines for Viossa Discord server',
image: '/images/og-discord-rules.jpg',
},
}
@ -241,12 +188,6 @@ export function generateArticleMeta(article: {
image: article.image || '/images/og-article-default.jpg',
url: `https://viossa.net/articles/${article.id}`,
},
twitter: {
card: 'summary_large_image',
title: article.title,
description: article.summary,
image: article.image || '/images/twitter-article-default.jpg',
},
custom: [
{
property: 'article:published_time',
@ -325,9 +266,6 @@ export const exampleRouteWithMeta = {
og: {
type: 'article',
image: '/default-og-image.jpg'
},
twitter: {
card: 'summary_large_image'
}
})
}

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { definePage } from 'vue-router/auto-routes'
import { definePage } from 'unplugin-vue-router/runtime'
import { useAuth } from '../../composables/useAuth'
import { usePreviewMode } from '../../composables/usePreviewMode'
import PreviewModeToggle from '@/components/molecules/PreviewModeToggle.vue'

View file

@ -30,6 +30,13 @@ declare module 'vue-router/auto-routes' {
Record<never, never>,
| never
>,
'/admin/': RouteRecordInfo<
'/admin/',
'/admin',
Record<never, never>,
Record<never, never>,
| never
>,
'/discord/rules': RouteRecordInfo<
'/discord/rules',
'/discord/rules',
@ -44,6 +51,20 @@ declare module 'vue-router/auto-routes' {
Record<never, never>,
| never
>,
'/login': RouteRecordInfo<
'/login',
'/login',
Record<never, never>,
Record<never, never>,
| never
>,
'/preview': RouteRecordInfo<
'/preview',
'/preview',
Record<never, never>,
Record<never, never>,
| never
>,
'/resources': RouteRecordInfo<
'/resources',
'/resources',
@ -70,6 +91,12 @@ declare module 'vue-router/auto-routes' {
views:
| never
}
'pages/admin/index.vue': {
routes:
| '/admin/'
views:
| never
}
'pages/discord/rules.vue': {
routes:
| '/discord/rules'
@ -82,6 +109,18 @@ declare module 'vue-router/auto-routes' {
views:
| never
}
'pages/login.vue': {
routes:
| '/login'
views:
| never
}
'pages/preview.vue': {
routes:
| '/preview'
views:
| never
}
'pages/resources.vue': {
routes:
| '/resources'

View file

@ -1,6 +1,9 @@
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import type { RouteLocationNormalizedLoaded, RouteMeta } from 'vue-router'
import type { MetaConfig } from '../composables/useMeta'
// Re-export RouteMeta for consumers who want to import it from here
export type { RouteMeta }
/**
* Extended route meta interface for SEO and meta tag configuration
*

View file

@ -7,7 +7,7 @@
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useMeta, type MetaConfig } from '../composables/useMeta'
import { useMeta, type MetaConfig, type CustomMetaTag } from '../composables/useMeta'
import type {
ArticleNode,
PageNode,
@ -29,6 +29,34 @@ export function generateDrupalNodeMeta(
const { attributes } = node
const path = attributes.path?.alias || `node/${node.id}`
// Build custom tags array - we know this is always an array at construction
const customTags: CustomMetaTag[] = [
{
property: 'article:published_time',
content: attributes.created,
},
{
property: 'article:modified_time',
content: attributes.changed,
},
]
// Add sticky/promote as robots meta
if (attributes.sticky || attributes.promote) {
customTags.push({
name: 'robots',
content: 'index, follow',
})
}
// Add status-based robots meta
if (!attributes.status) {
customTags.push({
name: 'robots',
content: 'noindex, nofollow',
})
}
const meta: MetaConfig = {
title: attributes.title,
description: attributes.body?.summary || generateExcerpt(attributes.body?.processed || ''),
@ -41,38 +69,7 @@ export function generateDrupalNodeMeta(
url: `${baseUrl}${path}`,
image: '/images/og-default.jpg', // Could be extracted from relationships
},
twitter: {
card: 'summary_large_image',
title: attributes.title,
description: attributes.body?.summary || generateExcerpt(attributes.body?.processed || ''),
image: '/images/twitter-default.jpg',
},
custom: [
{
property: 'article:published_time',
content: attributes.created,
},
{
property: 'article:modified_time',
content: attributes.changed,
},
],
}
// Add sticky/promote as robots meta
if (attributes.sticky || attributes.promote) {
meta.custom?.push({
name: 'robots',
content: 'index, follow',
})
}
// Add status-based robots meta
if (!attributes.status) {
meta.custom?.push({
name: 'robots',
content: 'noindex, nofollow',
})
custom: customTags,
}
return meta
@ -98,7 +95,7 @@ export function generateArticleContentMeta(
type: 'article',
},
custom: [
...(baseMeta.custom || []),
...(Array.isArray(baseMeta.custom) ? baseMeta.custom : []),
// Article-specific meta tags
{
property: 'article:section',
@ -151,7 +148,7 @@ export function generateLearningResourceMeta(
type: 'article',
},
custom: [
...(baseMeta.custom || []),
...(Array.isArray(baseMeta.custom) ? baseMeta.custom : []),
// Learning resource-specific meta tags
{
name: 'resource-type',