diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index c4265a7..7f6796d 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -60,17 +60,17 @@ The goal is to have a browsable, usable site where users can find and play songs ### 4. Discovery & Search Engine *Helping users find specific content or explore new music.* -- [ ] **Search Page (`pages/discovery.vue` or `/search`)**: +- [x] **Search Page (`pages/discovery.vue` or `/search`)**: - Accept query parameters (`?q=`, `?genre=`, `?key=`). - Implement a layout with Filters Sidebar (Desktop) / Drawer (Mobile) and Results Grid. -- [ ] **Filtering Logic**: +- [x] **Filtering Logic**: - Filter by Genre (Pop, Folklore, etc.), Key (Cm, Am...), and Rhythm. - Sort options (Newest, Most Viewed, A-Z). -- [ ] **Results Grid**: +- [x] **Results Grid**: - Efficient pagination or Infinite Scroll for large result sets. - Re-use `SongCard` with optimized props for list views. - "No Results" empty state with a "Request Song" CTA. -- [ ] **Artists Index (`pages/artists/index.vue`)**: +- [x] **Artists Index (`pages/artists/index.vue`)**: - A simple, paginated grid of all available artists, sortable by name or popularity. --- diff --git a/end_user/components/widget/LanguageSwitcher.vue b/end_user/components/widget/LanguageSwitcher.vue index eed4beb..cae0297 100644 --- a/end_user/components/widget/LanguageSwitcher.vue +++ b/end_user/components/widget/LanguageSwitcher.vue @@ -4,7 +4,6 @@ import { Languages } from 'lucide-vue-next' import type { UILanguageOption } from '~/stores/contentLanguage' import { useAppConfigStore } from '~/stores/appConfig' -const { locale } = useI18n() const appConfig = useAppConfigStore() const languages: { code: UILanguageOption; label: string; nativeLabel: string }[] = [ diff --git a/end_user/components/widget/song/LanguageSwitcher.vue b/end_user/components/widget/song/LanguageSwitcher.vue index 11b21b5..1895ec0 100644 --- a/end_user/components/widget/song/LanguageSwitcher.vue +++ b/end_user/components/widget/song/LanguageSwitcher.vue @@ -1,29 +1,17 @@ - diff --git a/end_user/composables/useBaseUrl.ts b/end_user/composables/useBaseUrl.ts new file mode 100644 index 0000000..e114290 --- /dev/null +++ b/end_user/composables/useBaseUrl.ts @@ -0,0 +1,57 @@ +/** + * Composable to get the base URL dynamically based on the request + * Works for both server-side (SSR) and client-side rendering + * Handles Nginx proxy headers (x-forwarded-*) and direct requests + * Returns a computed ref that updates reactively + */ +export const useBaseUrl = () => { + // Lazy initialization - only access Nuxt composables when computed is accessed + return computed(() => { + // Client-side: Use window.location + if (import.meta.client && typeof window !== 'undefined') { + return window.location.origin + } + + // Server-side: Try to get from headers + if (import.meta.server) { + try { + // useRequestHeaders() is safer and handles context checking + const headers = useRequestHeaders(['host', 'x-forwarded-host', 'x-forwarded-proto']) + + // Check for Nginx proxy headers first + const protocol = headers['x-forwarded-proto'] || 'https' + const host = headers['x-forwarded-host'] || + headers['host'] || + 'goranee.ir' + + // Remove port if it's standard (80 for http, 443 for https) + const hostWithoutPort = host.split(':')[0] + const port = host.includes(':') ? host.split(':')[1] : null + + // Only include port if it's non-standard + let baseUrl = `${protocol}://${hostWithoutPort}` + if (port && port !== '80' && port !== '443') { + baseUrl = `${protocol}://${hostWithoutPort}:${port}` + } + + return baseUrl + } catch (e) { + // Headers not available in this context, will use fallback + } + } + + // Fallback: Try runtime config + try { + const config = useRuntimeConfig() + const envBaseUrl = config.public.baseUrl as string | undefined + if (envBaseUrl) { + return envBaseUrl + } + } catch (e) { + // Runtime config not available, use default + } + + // Final fallback + return 'https://goranee.ir' + }) +} diff --git a/end_user/composables/useImageUrl.ts b/end_user/composables/useImageUrl.ts index f7aa453..9726adc 100644 --- a/end_user/composables/useImageUrl.ts +++ b/end_user/composables/useImageUrl.ts @@ -6,6 +6,35 @@ import { fileProvider } from '@modular-rest/client' * (not internal Docker URLs like http://server:8081) */ export const useImageUrl = () => { + // Get runtime config at composable initialization (valid Nuxt context) + let ssrBaseUrl: string | undefined = undefined + + try { + if (!process.client) { + // Server-side (SSR): Use ssrApiBaseUrl from runtime config + // This is set at build time via NUXT_SSR_API_BASE_URL build arg in Dockerfile + const config = useRuntimeConfig() + const configBaseUrl = config.public.ssrApiBaseUrl + + if (configBaseUrl && typeof configBaseUrl === 'string' && configBaseUrl.trim()) { + ssrBaseUrl = configBaseUrl.trim().replace(/\/$/, '') + } + } + } catch (e) { + // Runtime config not available, will use fallback + } + + // Fallback: Try reading from process.env (for development or if runtime config isn't set) + if (!ssrBaseUrl && !process.client) { + const envBaseUrl = process.env.NUXT_SSR_API_BASE_URL || process.env.VITE_SSR_API_BASE_URL + if (envBaseUrl && typeof envBaseUrl === 'string' && envBaseUrl.trim()) { + ssrBaseUrl = envBaseUrl.trim().replace(/\/$/, '') + console.warn('[useImageUrl] Using SSR base URL from process.env (fallback):', ssrBaseUrl) + } else { + console.error('[useImageUrl] NUXT_SSR_API_BASE_URL is not configured. Image URLs will use internal Docker URL which is not accessible to clients.') + } + } + /** * Get a public-facing image URL for client-side use * During SSR, uses NUXT_SSR_API_BASE_URL to generate URLs accessible to browsers @@ -14,30 +43,10 @@ export const useImageUrl = () => { const getImageUrl = (file: any): string | undefined => { if (!file) return undefined - let baseUrl: string | undefined = undefined - - if (!process.client) { - // Server-side (SSR): Use ssrApiBaseUrl from runtime config - // This is set at build time via NUXT_SSR_API_BASE_URL build arg in Dockerfile - // The value is baked into the build, so it's available via runtime config - const config = useRuntimeConfig() - const ssrBaseUrl = config.public.ssrApiBaseUrl - - if (ssrBaseUrl && typeof ssrBaseUrl === 'string' && ssrBaseUrl.trim()) { - baseUrl = ssrBaseUrl.trim().replace(/\/$/, '') - } else { - // Fallback: Try reading from process.env (for development or if runtime config isn't set) - const envBaseUrl = process.env.NUXT_SSR_API_BASE_URL || process.env.VITE_SSR_API_BASE_URL - if (envBaseUrl && typeof envBaseUrl === 'string' && envBaseUrl.trim()) { - baseUrl = envBaseUrl.trim().replace(/\/$/, '') - console.warn('[useImageUrl] Using SSR base URL from process.env (fallback):', baseUrl) - } else { - console.error('[useImageUrl] NUXT_SSR_API_BASE_URL is not configured. Image URLs will use internal Docker URL which is not accessible to clients.') - } - } - } + // Server-side: Use pre-fetched ssrBaseUrl // Client-side: baseUrl is undefined, so fileProvider will use GlobalOptions.host // which is set to window.location.origin + /api/ in the plugin + const baseUrl = !process.client ? ssrBaseUrl : undefined return fileProvider.getFileLink(file, baseUrl) } diff --git a/end_user/composables/useSchema.ts b/end_user/composables/useSchema.ts new file mode 100644 index 0000000..66c314e --- /dev/null +++ b/end_user/composables/useSchema.ts @@ -0,0 +1,250 @@ +import type { LanguageCode } from '~/constants/routes' +import type { ContentLanguageCode } from '~/types/song.type' +import type { Artist } from '~/types/song.type' + +/** + * Composable for generating JSON-LD structured data (Schema.org) + * Provides utilities for creating, cleaning, and validating schema markup + */ +export const useSchema = () => { + + /** + * Convert language code to BCP 47 format for schema.org + * Schema.org's inLanguage property requires BCP 47 language tags (IETF standard) + * BCP 47 combines: ISO 639 (language) + ISO 15924 (script) + ISO 3166-1 (region) + */ + const getLanguageCode = (lang: LanguageCode): string => { + const langMap: Record = { + 'ckb-IR': 'ckb-Arab-IR', // Sorani Kurdish in Arabic script, Iran region (BCP 47) + 'ckb-Latn': 'ckb-Latn', // Sorani Kurdish in Latin script (BCP 47) + 'kmr': 'kmr', // Kurmanji Kurdish (BCP 47) + 'en': 'en', // English (BCP 47) + } + return langMap[lang] || 'ckb-Arab-IR' // Default to Sorani Kurdish + } + + /** + * Type-safe helper to extract artist name from Artist object + * Respects current language and falls back to other languages if needed + */ + const getArtistName = (artist: Artist | string, currentLang: LanguageCode): string | null => { + if (typeof artist === 'string') return null + + // Try to get name from content based on current language + // Only use languages that exist in Artist.content type (exclude 'en') + if (currentLang !== 'en' && currentLang in artist.content) { + const langContent = artist.content[currentLang as ContentLanguageCode] + if (langContent?.name) { + return langContent.name + } + } + + // Fallback: try other languages (only ContentLanguageCode, not 'en') + const fallbackLangs: ContentLanguageCode[] = ['ckb-IR', 'ckb-Latn', 'kmr'] + for (const lang of fallbackLangs) { + const content = artist.content?.[lang] + if (content?.name) { + return content.name + } + } + + // Last resort: check if backend added a direct name property (for backward compatibility) + if ('name' in artist && typeof (artist as any).name === 'string') { + return (artist as any).name + } + + return null + } + + /** + * Clean object from MongoDB internal fields and ensure valid JSON-LD + * This function preserves @type fields and removes only MongoDB internal fields + */ + const cleanObject = (obj: unknown): unknown => { + if (obj === null || obj === undefined) return null + if (typeof obj !== 'object') return obj + if (Array.isArray(obj)) { + return obj.map(cleanObject).filter(item => item !== null) + } + + const cleaned: Record = {} + for (const [key, value] of Object.entries(obj)) { + // Skip MongoDB internal fields (starting with _) + // Note: @type is a valid JSON-LD field and doesn't start with _, so it's preserved + if (key.startsWith('_')) continue + // Skip undefined values + if (value === undefined) continue + // Recursively clean nested objects + if (typeof value === 'object' && value !== null) { + const cleanedValue = cleanObject(value) + if (cleanedValue !== null) { + cleaned[key] = cleanedValue + } + } else { + cleaned[key] = value + } + } + return cleaned + } + + /** + * Validate and stringify structured data for JSON-LD script tag + * Returns empty array if validation fails + */ + const validateAndStringifySchema = (structuredData: unknown): Array<{ type: string; innerHTML: string }> => { + const cleaned = cleanObject(structuredData) + + // Validate that cleaned data is an object + if (!cleaned || typeof cleaned !== 'object') { + console.error('Invalid structured data after cleaning:', cleaned) + return [] + } + + // Validate that @type is present and is a string + if (!('@type' in cleaned) || typeof cleaned['@type'] !== 'string') { + console.error('Invalid @type in structured data:', cleaned) + return [] + } + + // Validate that @context is present + if (!('@context' in cleaned) || typeof cleaned['@context'] !== 'string') { + console.error('Invalid @context in structured data:', cleaned) + return [] + } + + try { + const jsonString = JSON.stringify(cleaned) + return [{ + type: 'application/ld+json', + innerHTML: jsonString, + }] + } catch (error) { + console.error('Error stringifying structured data:', error) + return [] + } + } + + /** + * Format date for schema.org (ISO 8601 format) + * Ensures dateModified is not earlier than dateCreated + */ + const formatDates = (createdAt?: Date | string, updatedAt?: Date | string): { dateCreated?: string; dateModified?: string } => { + const result: { dateCreated?: string; dateModified?: string } = {} + + if (createdAt) { + try { + const createdDate = new Date(createdAt) + if (!isNaN(createdDate.getTime())) { + result.dateCreated = createdDate.toISOString() + + // Set dateModified - ensure it's not earlier than dateCreated + if (updatedAt) { + const updatedDate = new Date(updatedAt) + if (!isNaN(updatedDate.getTime())) { + // If updatedAt is earlier than createdAt, use createdAt as dateModified + result.dateModified = updatedDate >= createdDate + ? updatedDate.toISOString() + : createdDate.toISOString() + } else { + result.dateModified = createdDate.toISOString() + } + } else { + // If no updatedAt, use createdAt as dateModified + result.dateModified = createdDate.toISOString() + } + } + } catch (error) { + console.warn('Error parsing dates for schema:', error) + } + } + + return result + } + + /** + * Create MusicComposition schema for songs + */ + const createMusicCompositionSchema = (params: { + name: string + language: LanguageCode + composers: Array<{ '@type': 'Person'; name: string; url?: string }> + image?: string + dateCreated?: Date | string + dateModified?: Date | string + }) => { + const { name, language, composers, image, dateCreated, dateModified } = params + + type Composer = { + '@type': 'Person' + name: string + url?: string + } + + // Ensure composer is always defined (required field) + let composerValue: Composer | Composer[] + if (composers.length === 0) { + composerValue = { + '@type': 'Person', + name: 'Unknown Artist', + } + } else if (composers.length === 1) { + composerValue = composers[0]! + } else { + composerValue = composers + } + + const structuredData = { + '@context': 'https://schema.org', + '@type': 'MusicComposition', + name: String(name || 'Untitled Song'), + inLanguage: getLanguageCode(language), + composer: composerValue, + inAlbum: { + '@type': 'MusicAlbum', + name: 'Goranee Kurdish Chords', + }, + ...(image ? { image: String(image) } : {}), + ...formatDates(dateCreated, dateModified), + } + + return structuredData + } + + /** + * Create Person schema for artists + */ + const createPersonSchema = (params: { + name: string + language: LanguageCode + url: string + description?: string + image?: string + dateCreated?: Date | string + dateModified?: Date | string + }) => { + const { name, language, url, description, image, dateCreated, dateModified } = params + + const structuredData = { + '@context': 'https://schema.org', + '@type': 'Person', + name: String(name), + inLanguage: getLanguageCode(language), + url: String(url), + ...(description ? { description: String(description) } : {}), + ...(image ? { image: String(image) } : {}), + ...formatDates(dateCreated, dateModified), + } + + return structuredData + } + + return { + getLanguageCode, + getArtistName, + cleanObject, + validateAndStringifySchema, + formatDates, + createMusicCompositionSchema, + createPersonSchema, + } +} diff --git a/end_user/i18n/locales/english.json b/end_user/i18n/locales/english.json index 5820914..4d70e3d 100644 --- a/end_user/i18n/locales/english.json +++ b/end_user/i18n/locales/english.json @@ -241,6 +241,12 @@ "of": "of" }, "song": { + "language": "Language", + "languages": { + "ckb-IR": "سورانی (ئێران)", + "ckb-Latn": "Soranî (Latînî)", + "kmr": "Kurmancî" + }, "metadata": { "key": "Key", "rhythm": "Rhythm", diff --git a/end_user/i18n/locales/farsi.json b/end_user/i18n/locales/farsi.json index da093fd..4d040df 100644 --- a/end_user/i18n/locales/farsi.json +++ b/end_user/i18n/locales/farsi.json @@ -241,6 +241,12 @@ "of": "از" }, "song": { + "language": "زبان", + "languages": { + "ckb-IR": "سورانی (ئێران)", + "ckb-Latn": "Soranî (Latînî)", + "kmr": "Kurmancî" + }, "metadata": { "key": "کلید", "rhythm": "ریتم", diff --git a/end_user/i18n/locales/kmr.json b/end_user/i18n/locales/kmr.json index 62f689c..5d45398 100644 --- a/end_user/i18n/locales/kmr.json +++ b/end_user/i18n/locales/kmr.json @@ -241,6 +241,12 @@ "of": "ji" }, "song": { + "language": "Ziman", + "languages": { + "ckb-IR": "سورانی (ئێران)", + "ckb-Latn": "Soranî (Latînî)", + "kmr": "Kurmancî" + }, "metadata": { "key": "Kî", "rhythm": "Rîtm", diff --git a/end_user/i18n/locales/sorani-farsi.json b/end_user/i18n/locales/sorani-farsi.json index ca503ad..7179f11 100644 --- a/end_user/i18n/locales/sorani-farsi.json +++ b/end_user/i18n/locales/sorani-farsi.json @@ -241,6 +241,12 @@ "of": "لە" }, "song": { + "language": "زمان", + "languages": { + "ckb-IR": "سورانی (ئێران)", + "ckb-Latn": "Soranî (Latînî)", + "kmr": "Kurmancî" + }, "metadata": { "key": "پلی دەنگ", "rhythm": "ڕیتم", diff --git a/end_user/i18n/locales/sorani-latin.json b/end_user/i18n/locales/sorani-latin.json index 612bcb4..5c4a317 100644 --- a/end_user/i18n/locales/sorani-latin.json +++ b/end_user/i18n/locales/sorani-latin.json @@ -241,6 +241,12 @@ "of": "le" }, "song": { + "language": "Ziman", + "languages": { + "ckb-IR": "سورانی (ئێران)", + "ckb-Latn": "Soranî (Latînî)", + "kmr": "Kurmancî" + }, "metadata": { "key": "Pley Deng", "rhythm": "Rîtm", diff --git a/end_user/nuxt.config.ts b/end_user/nuxt.config.ts index 9d2c9b5..013a53e 100644 --- a/end_user/nuxt.config.ts +++ b/end_user/nuxt.config.ts @@ -23,7 +23,7 @@ export default defineNuxtConfig({ // @ts-ignore apiBaseUrl: process.env.NUXT_API_BASE_URL || process.env.VITE_API_BASE_URL || '/api/', // @ts-ignore - ssrApiBaseUrl: process.env.NUXT_SSR_API_BASE_URL || process.env.VITE_SSR_API_BASE_URL, + baseUrl: process.env.NUXT_PUBLIC_BASE_URL || process.env.BASE_URL || 'https://goranee.ir', }, }, diff --git a/end_user/pages/artist/[id]/[[lang]].vue b/end_user/pages/artist/[id]/[[lang]].vue index 4533514..ac91ddd 100644 --- a/end_user/pages/artist/[id]/[[lang]].vue +++ b/end_user/pages/artist/[id]/[[lang]].vue @@ -5,7 +5,9 @@ import { useI18n } from 'vue-i18n'; import { Play, Heart } from 'lucide-vue-next'; import { useTabService } from '~/composables/useTabService'; import { useContentLanguageStore } from '~/stores/contentLanguage'; -import type { Artist, SongWithPopulatedRefs } from '~/types/song.type'; +import { useSchema } from '~/composables/useSchema'; +import type { Artist, SongWithPopulatedRefs, ContentLanguageCode } from '~/types/song.type'; +import { getAvailableLangsForArtist } from '~/types/song.type'; import type { LanguageCode } from '~/constants/routes'; import SongCard from '~/components/widget/SongCard.vue'; import ArtistCard from '~/components/widget/ArtistCard.vue'; @@ -17,6 +19,7 @@ const route = useRoute(); const { t } = useI18n(); const contentLanguageStore = useContentLanguageStore(); const { fetchArtist, fetchSongsByArtist, fetchFeaturedArtists, getImageUrl } = useTabService(); +const { createPersonSchema, validateAndStringifySchema } = useSchema(); const artistId = computed(() => route.params.id as string); const langCode = computed(() => { @@ -150,30 +153,74 @@ const navigateToHome = () => { // Get artist name from current language content const artistName = computed(() => { if (!artist.value) return '' - const langContent = artist.value.content?.[langCode.value] - return langContent?.name || artist.value.content?.['ckb-IR']?.name || '' + // Only use languages that exist in Artist.content type (exclude 'en') + const currentLang = langCode.value + if (currentLang !== 'en' && currentLang in artist.value.content) { + const langContent = artist.value.content[currentLang as ContentLanguageCode] + if (langContent?.name) { + return langContent.name + } + } + // Fallback to default language + return artist.value.content?.['ckb-IR']?.name || '' }); -// SEO -useHead({ +// SEO: Meta tags +const baseUrl = useBaseUrl() +const artistDescription = computed(() => { + if (!artist.value) return 'Goranee - Kurdish Chords Platform' + // Only use languages that exist in Artist.content type (exclude 'en') + const currentLang = langCode.value + let langContent = artist.value.content?.['ckb-IR'] + if (currentLang !== 'en' && currentLang in artist.value.content) { + langContent = artist.value.content[currentLang as ContentLanguageCode] + } + const bio = langContent?.bio || '' + const songCount = songsCount.value + return bio + ? `${bio} | ${songCount} ${t('common.songs')} | Goranee` + : `${artistName.value} - ${songCount} ${t('common.songs')} | Goranee` +}) +const artistImageUrl = computed(() => { + if (!artistImage.value) return `${baseUrl.value}/favicon.ico` + return artistImage.value +}) +const canonicalUrl = computed(() => { + return langCode.value === 'ckb-IR' + ? `${baseUrl.value}/artist/${artistId.value}` + : `${baseUrl.value}/artist/${artistId.value}/${langCode.value}` +}) + +useSeoMeta({ title: computed(() => artistName.value ? `${artistName.value} - Goranee` : t('common.artist')), + description: artistDescription, + ogTitle: artistName, + ogDescription: artistDescription, + ogImage: artistImageUrl, + ogType: 'profile', + ogUrl: canonicalUrl, + twitterCard: 'summary_large_image', + twitterTitle: artistName, + twitterDescription: artistDescription, + twitterImage: artistImageUrl, +}) + +// SEO: Structured Data (JSON-LD) and hreflang/canonical +useHead({ link: computed(() => { if (!artist.value) return [] - const baseUrl = 'https://goranee.ir' const links: any[] = [] - // Get available languages for artist - const availableLangs = (Object.keys(artist.value.content || {}) as LanguageCode[]).filter( - lang => artist.value?.content?.[lang]?.name - ) + // Get available languages for artist (only ContentLanguageCode, not 'en') + const availableLangs = getAvailableLangsForArtist(artist.value) if (availableLangs.length > 1) { // Add hreflang for each available language - availableLangs.forEach(lang => { + availableLangs.forEach((lang: ContentLanguageCode) => { const url = lang === 'ckb-IR' - ? `${baseUrl}/artist/${artistId.value}` - : `${baseUrl}/artist/${artistId.value}/${lang}` + ? `${baseUrl.value}/artist/${artistId.value}` + : `${baseUrl.value}/artist/${artistId.value}/${lang}` links.push({ rel: 'alternate', @@ -186,22 +233,46 @@ useHead({ links.push({ rel: 'alternate', hreflang: 'x-default', - href: `${baseUrl}/artist/${artistId.value}`, + href: `${baseUrl.value}/artist/${artistId.value}`, }) } // Add canonical URL - const canonicalUrl = langCode.value === 'ckb-IR' - ? `${baseUrl}/artist/${artistId.value}` - : `${baseUrl}/artist/${artistId.value}/${langCode.value}` - links.push({ rel: 'canonical', - href: canonicalUrl, + href: canonicalUrl.value, }) return links }), + script: computed(() => { + if (!artist.value) return [] + + // Only use languages that exist in Artist.content type (exclude 'en') + const currentLang = langCode.value + let langContent = artist.value.content?.['ckb-IR'] + if (currentLang !== 'en' && currentLang in artist.value.content) { + langContent = artist.value.content[currentLang as ContentLanguageCode] + } + const bio = langContent?.bio || '' + + // Get timestamps if available + const artistWithTimestamps = artist.value as any + + // Create structured data using the shared composable + const structuredData = createPersonSchema({ + name: artistName.value, + language: langCode.value, + url: canonicalUrl.value, + description: bio || undefined, + image: artistImageUrl.value || undefined, + dateCreated: artistWithTimestamps.createdAt, + dateModified: artistWithTimestamps.updatedAt, + }) + + // Validate and stringify using the shared composable + return validateAndStringifySchema(structuredData) + }), }); diff --git a/end_user/pages/discovery.vue b/end_user/pages/discovery.vue index bbfc7b4..f816e18 100644 --- a/end_user/pages/discovery.vue +++ b/end_user/pages/discovery.vue @@ -224,6 +224,35 @@ const navigateToSong = (id: string) => { router.push(ROUTES.TAB.DETAIL(id)) } +// SEO: Meta tags +const baseUrl = useBaseUrl() +const discoveryTitle = computed(() => { + if (hasQuery.value) { + return `${t('pages.discovery.searchResults')} "${searchQuery.value}" - Goranee` + } + return `${t('pages.discovery.title')} - Goranee` +}) +const discoveryDescription = computed(() => { + if (hasQuery.value) { + return `Search results for "${searchQuery.value}" on Goranee. Find Kurdish chord sheets and songs.` + } + return 'Discover and search Kurdish songs with chord sheets. Filter by genre, key, rhythm and find the perfect song to learn.' +}) + +useSeoMeta({ + title: discoveryTitle, + description: discoveryDescription, + ogTitle: discoveryTitle, + ogDescription: discoveryDescription, + ogImage: `${baseUrl.value}/favicon.ico`, + ogType: 'website', + ogUrl: computed(() => `${baseUrl.value}/discovery${route.query.q ? `?q=${route.query.q}` : ''}`), + twitterCard: 'summary_large_image', + twitterTitle: discoveryTitle, + twitterDescription: discoveryDescription, + twitterImage: `${baseUrl.value}/favicon.ico`, +}) +