diff --git a/apps/vdn-static/package.json b/apps/vdn-static/package.json index 5d35406..456b0af 100644 --- a/apps/vdn-static/package.json +++ b/apps/vdn-static/package.json @@ -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" diff --git a/apps/vdn-static/src/composables/useI18nMeta.ts b/apps/vdn-static/src/composables/useI18nMeta.ts index 1b17e40..00f7d1d 100644 --- a/apps/vdn-static/src/composables/useI18nMeta.ts +++ b/apps/vdn-static/src/composables/useI18nMeta.ts @@ -1,8 +1,8 @@ import { computed, type ComputedRef, type Ref, unref, watch } from 'vue' import { useRoute } from 'vue-router' -import { useMeta, type MetaConfig, type LocaleAwareMeta } from './useMeta' +import { useMeta, type MetaConfig, type LocaleAwareMeta, type OpenGraphConfig, type TwitterCardConfig } from './useMeta' import { useLocale, type LocaleId } from '../i18n' -import type { RouteMeta, RouteMetaValue } from '../types/route-meta.d' +import type { RouteMetaValue } from '../types/route-meta.d' /** * Composable for i18n-aware meta tag management @@ -49,7 +49,7 @@ export function useI18nMeta( routeValue: typeof route ): T | undefined => { if (value === undefined) return undefined - return typeof value === 'function' ? value(routeValue) : value + return typeof value === 'function' ? (value as Function)(routeValue) as T : value } /** @@ -66,8 +66,8 @@ export function useI18nMeta( keywords: resolveMetaValue(configValue.keywords, routeValue), author: resolveMetaValue(configValue.author, routeValue), canonical: resolveMetaValue(configValue.canonical, routeValue), - og: resolveMetaValue(configValue.og, routeValue), - twitter: resolveMetaValue(configValue.twitter, routeValue), + og: resolveI18nOgTwitter(resolveMetaValue(configValue.og, routeValue), currentLocale), + twitter: resolveI18nOgTwitter(resolveMetaValue(configValue.twitter, routeValue), currentLocale), custom: resolveI18nCustomTags(resolveMetaValue(configValue.custom, routeValue), currentLocale), } }) @@ -135,6 +135,28 @@ function resolveI18nContent( return content['en-US'] || Object.values(content)[0] } +/** + * Resolves locale-aware title/description fields in og/twitter config objects. + * After resolution, title/description are plain strings — compatible with MetaConfig. + */ +function resolveI18nOgTwitter( + config: I18nOpenGraphConfig | I18nTwitterCardConfig | undefined, + locale: LocaleId +): OpenGraphConfig | TwitterCardConfig | undefined { + if (!config) return config + const title = config.title + ? typeof config.title === 'object' + ? resolveI18nContent(config.title as LocaleAwareMeta, locale) + : config.title + : config.title + const description = config.description + ? typeof config.description === 'object' + ? resolveI18nContent(config.description as LocaleAwareMeta, locale) + : config.description + : config.description + return { ...config, title, description } as OpenGraphConfig | TwitterCardConfig +} + /** * Resolves custom meta tags with i18n support */ @@ -158,14 +180,26 @@ 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 + content: typeof tag.content === 'function' + ? '' + : typeof tag.content === 'object' + ? resolveI18nContent(tag.content as any, currentLocale) || '' : tag.content })) } +/** OpenGraph config with locale-aware title/description */ +type I18nOpenGraphConfig = Omit & { + title?: string | LocaleAwareMeta + description?: string | LocaleAwareMeta +} + +/** Twitter Card config with locale-aware title/description */ +type I18nTwitterCardConfig = Omit & { + title?: string | LocaleAwareMeta + description?: string | LocaleAwareMeta +} + /** * i18n-aware meta configuration interface */ @@ -180,12 +214,14 @@ export interface I18nMetaConfig { author?: string | ((route: any) => string) /** Canonical URL */ canonical?: string | ((route: any) => string) - /** OpenGraph configuration */ - og?: MetaConfig['og'] - /** Twitter Card configuration */ - twitter?: MetaConfig['twitter'] + /** OpenGraph configuration (locale-aware title/description) */ + og?: I18nOpenGraphConfig | ((route: any) => I18nOpenGraphConfig) + /** Twitter Card configuration (locale-aware title/description) */ + twitter?: I18nTwitterCardConfig | ((route: any) => I18nTwitterCardConfig) /** Custom meta tags */ custom?: MetaConfig['custom'] + /** Robots meta directive (e.g. 'index, follow') */ + robots?: string | ((route: any) => string) /** Hreflang configuration for international SEO */ hreflang?: { [locale: string]: string diff --git a/apps/vdn-static/src/viossa.net/apps/vdn-static/src/composables/useMeta.ts b/apps/vdn-static/src/composables/useMeta.ts similarity index 98% rename from apps/vdn-static/src/viossa.net/apps/vdn-static/src/composables/useMeta.ts rename to apps/vdn-static/src/composables/useMeta.ts index 4d5766c..89cfff9 100644 --- a/apps/vdn-static/src/viossa.net/apps/vdn-static/src/composables/useMeta.ts +++ b/apps/vdn-static/src/composables/useMeta.ts @@ -33,6 +33,8 @@ export interface MetaConfig { appendLocale?: boolean /** Title template for formatting */ titleTemplate?: (title: string) => string + /** Robots meta directive (e.g. 'index, follow') */ + robots?: string | ((route: RouteLocationNormalizedLoaded) => string) } /** @@ -235,7 +237,7 @@ export function useMeta( routeValue: RouteLocationNormalizedLoaded ): T | undefined => { if (value === undefined) return undefined - return typeof value === 'function' ? value(routeValue) : value + return typeof value === 'function' ? (value as Function)(routeValue) as T : value } /** diff --git a/apps/vdn-static/src/composables/useRouteMeta.ts b/apps/vdn-static/src/composables/useRouteMeta.ts index 2b7e3ec..d530137 100644 --- a/apps/vdn-static/src/composables/useRouteMeta.ts +++ b/apps/vdn-static/src/composables/useRouteMeta.ts @@ -1,7 +1,8 @@ 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 } from 'vue-router' +import type { RouteMetaValue } from '../types/route-meta' /** * Composable for automatic meta tag management based on route configuration @@ -45,7 +46,7 @@ export function useRouteMeta() { routeValue: typeof route ): T | undefined => { if (value === undefined) return undefined - return typeof value === 'function' ? value(routeValue) : value + return typeof value === 'function' ? (value as Function)(routeValue) as T : value } /** diff --git a/apps/vdn-static/src/config/meta-examples.ts b/apps/vdn-static/src/config/meta-examples.ts index 713fd4d..9ccd319 100644 --- a/apps/vdn-static/src/config/meta-examples.ts +++ b/apps/vdn-static/src/config/meta-examples.ts @@ -5,7 +5,8 @@ * in the Viossa application. */ -import type { MetaConfig, I18nMetaConfig } from '../composables/useI18nMeta' +import type { MetaConfig } from '../composables/useMeta' +import type { I18nMetaConfig } from '../composables/useI18nMeta' import { createRouteMeta } from '../composables/useMeta' /** diff --git a/apps/vdn-static/src/pages/admin/index.vue b/apps/vdn-static/src/pages/admin/index.vue index 0323773..183703e 100644 --- a/apps/vdn-static/src/pages/admin/index.vue +++ b/apps/vdn-static/src/pages/admin/index.vue @@ -1,5 +1,5 @@