diff --git a/apps/vdn-static/.env.example b/apps/vdn-static/.env.example deleted file mode 100644 index 6256a2a..0000000 --- a/apps/vdn-static/.env.example +++ /dev/null @@ -1,6 +0,0 @@ -# Drupal API Configuration -# Use the CORS proxy for local development -VITE_DRUPAL_API_URL=/api/drupal-proxy - -# For direct access (requires CORS on Drupal) -# VITE_DRUPAL_API_URL=https://cms.viossa.net/jsonapi \ No newline at end of file diff --git a/apps/vdn-static/docs/META_TAG_IMPLEMENTATION.md b/apps/vdn-static/docs/META_TAG_IMPLEMENTATION.md deleted file mode 100644 index 68d9613..0000000 --- a/apps/vdn-static/docs/META_TAG_IMPLEMENTATION.md +++ /dev/null @@ -1,307 +0,0 @@ -# Meta Tag Management System - Implementation Summary - -## Overview - -Successfully implemented a comprehensive meta tag management system for the Viossa Vue.js application that supports SEO optimization, social sharing, and multi-language content. - -## Files Created - -### Core Implementation - -1. **`viossa.net/apps/vdn-static/src/composables/useMeta.ts`** (12.8 KB) - - Core meta tag management composable - - Handles standard, OpenGraph, Twitter Card, and custom meta tags - - SSR-ready architecture - - Type-safe with full TypeScript support - - Reactive updates with efficient DOM manipulation - -2. **`viossa.net/apps/vdn-static/src/composables/useRouteMeta.ts`** (3.3 KB) - - Automatic route-based meta tag integration - - Vue Router integration - - Plugin system for easy setup - - Dynamic meta tag resolution - -3. **`viossa.net/apps/vdn-static/src/composables/useI18nMeta.ts`** (6.4 KB) - - i18n-aware meta tag management - - Automatic locale detection - - hreflang tag generation for international SEO - - Fallback locale support - -4. **`viossa.net/apps/vdn-static/src/types/route-meta.d.ts`** (3.3 KB) - - TypeScript type definitions for route meta - - Extended Vue Router RouteMeta interface - - Type guards and helper types - - Full type safety - -5. **`viossa.net/apps/vdn-static/src/utils/drupal-meta.ts`** (6.5 KB) - - Drupal content type integration - - Automatic meta tag generation from JSON:API nodes - - Support for Article, Page, and Learning Resource types - - Excerpt generation from HTML content - -### Configuration & Examples - -6. **`viossa.net/apps/vdn-static/src/config/meta-examples.ts`** (10.1 KB) - - Example configurations for all page types - - Home, resources, articles, Kotoba, Discord rules pages - - Dynamic resource detail example - - Helper functions for common patterns - -7. **`viossa.net/apps/vdn-static/src/composables/index.ts`** (1.0 KB) - - Public API exports - - Easy importing of all composables and types - - Unified entry point - -### Documentation - -8. **`viossa.net/apps/vdn-static/docs/META_TAG_SYSTEM.md`** (13.4 KB) - - Comprehensive system documentation - - API reference - - Usage examples - - Best practices - - Troubleshooting guide - - Migration guide - -9. **`viossa.net/apps/vdn-static/docs/META_TAG_PERFORMANCE.md`** (7.0 KB) - - Performance benchmarks - - Memory usage analysis - - SSR performance metrics - - Benchmarking methodology - -### Integration - -10. **`viossa.net/apps/vdn-static/src/App.vue`** (Modified) - - Integrated `useRouteMeta()` for automatic route-based meta - - Added import statement - - Ready for immediate use - -## Features Implemented - -### ✅ Core Features - -1. **Dynamic Meta Tag Generation** - - Automatic route-based meta tag updates - - Reactive meta tag management - - Support for dynamic values from route parameters - -2. **OpenGraph Tags** - - Complete OpenGraph implementation - - og:title, og:description, og:image, og:url - - og:type, og:site_name, og:locale - - Video and audio support - -3. **Twitter Card Tags** - - Full Twitter Card support - - Summary and summary_large_image cards - - Player and app card support - - Image alt text support - -4. **Multi-Language Support** - - i18n integration with existing system - - Locale-aware meta tags - - hreflang tag generation - - Automatic fallback locales - -5. **Custom Meta Tags** - - Flexible custom tag support - - Support for name, property, charset, httpEquiv attributes - - Dynamic content functions - -### ✅ Integration Features - -6. **Vue Router Integration** - - Automatic meta tag updates on route change - - Route meta configuration support - - Type-safe route meta interface - -7. **Drupal Content Integration** - - Automatic meta generation from Drupal nodes - - Support for Article, Page, Learning Resource types - - JSON:API compatible - - Excerpt generation from HTML - -8. **SSR Compatibility** - - Designed for server-side rendering - - State-less operation - - Hydration-friendly - -### ✅ Developer Experience - -9. **TypeScript Support** - - Full type safety - - Auto-completion - - Type guards - - Interface extensions - -10. **Documentation** - - Comprehensive API documentation - - Usage examples - - Best practices guide - - Performance benchmarks - - Migration guide - -## Technical Highlights - -### Performance -- Initial meta application: **<3ms** -- Route navigation: **<3.5ms** -- Locale switching: **<3ms** -- Memory overhead: **~2KB per instance** -- No memory leaks detected - -### Type Safety -- TypeScript strict mode compatible -- Extended Vue Router types -- Runtime type guards -- Comprehensive interfaces - -### Architecture -- Composable-based design -- Stateless operations -- Automatic cleanup -- SSR-ready from day one - -## Acceptance Criteria Status - -All acceptance criteria met: - -✅ **Dynamic meta tag generation based on route and content** -- Implemented via useRouteMeta and useI18nMeta -- Route-based and content-based meta supported - -✅ **OpenGraph and Twitter Card meta tags implemented** -- Full implementation in useMeta.ts -- All standard properties supported - -✅ **Multi-language meta tags supported via i18n integration** -- useI18nMeta composable -- hreflang automatic generation -- Locale fallback system - -✅ **Custom meta tags for different content types** -- Custom meta tag array support -- Dynamic content functions -- Drupal node integration - -✅ **Meta tags update dynamically when content changes** -- Reactive computed properties -- Watch-based updates -- Automatic cleanup - -✅ **All meta tags render correctly in HTML head** -- Tested during implementation -- DOM manipulation verified -- Type-safe rendering - -## Usage Examples - -### Basic Usage -```typescript -import { useMeta } from '@/composables/useMeta' - -useMeta({ - title: 'Page Title', - description: 'Page description', - og: { - type: 'website', - image: '/og.jpg' - } -}) -``` - -### Route-Based -```typescript -// In route configuration -meta: { - title: (route) => `Resource: ${route.params.id}`, - description: 'Learning resources' -} -``` - -### i18n-Aware -```typescript -import { useI18nMeta } from '@/composables/useI18nMeta' - -useI18nMeta({ - title: { - 'en-US': 'Welcome', - 'es-LA': 'Bienvenido' - } -}) -``` - -### Drupal Integration -```typescript -import { useDrupalNodeMeta } from '@/utils/drupal-meta' - -const node = ref(articleNode) -useDrupalNodeMeta(node) -``` - -## Dependencies Met - -✅ **SSR implementation (t_3d28c7b3)** -- System is SSR-ready -- Compatible with future SSR implementation -- State-less design - -✅ **Existing i18n system** -- Full integration with current i18n -- Locale-aware composables -- Compatible with all 5 locales - -✅ **Vue router configuration** -- Extended route meta interface -- Automatic route-based updates -- Type-safe route configuration - -## Deliverables Status - -✅ **useMeta.ts composable with full documentation** -- Implemented with comprehensive JSDoc comments -- Full TypeScript types -- Examples in code - -✅ **Route meta type definitions** -- Extended RouteMeta interface -- Type guards included -- Fully typed system - -✅ **Example configurations for different page types** -- Home, resources, articles examples -- Drupal content type helpers -- Dynamic route examples - -✅ **Integration with existing Vue components** -- App.vue integration complete -- Ready for immediate use -- No breaking changes - -✅ **Performance benchmarks for meta tag generation** -- Comprehensive benchmarking documented -- Performance metrics included -- Memory analysis complete - -## Next Steps - -The system is production-ready. Recommended next steps: - -1. **Testing**: Add unit tests for all composables -2. **SSR Integration**: When SSR is implemented, test server-side rendering -3. **Monitoring**: Add performance monitoring for meta operations -4. **SEO Validation**: Test with SEO tools (Google Search Console, etc.) - -## Summary - -Successfully delivered a complete, production-ready meta tag management system that: - -- Supports all required meta tag types (standard, OpenGraph, Twitter Card) -- Integrates seamlessly with Vue Router for automatic route-based updates -- Provides full i18n support with hreflang generation -- Includes Drupal content type integration -- Offers excellent performance (<3ms operations) -- Maintains full TypeScript type safety -- Includes comprehensive documentation and examples -- Is SSR-ready for future server-side rendering implementation - -All acceptance criteria met, all deliverables completed, and the system is ready for production use. \ No newline at end of file diff --git a/apps/vdn-static/docs/META_TAG_PERFORMANCE.md b/apps/vdn-static/docs/META_TAG_PERFORMANCE.md deleted file mode 100644 index a1fb638..0000000 --- a/apps/vdn-static/docs/META_TAG_PERFORMANCE.md +++ /dev/null @@ -1,258 +0,0 @@ -# Meta Tag Performance Benchmarks - -This document provides performance benchmarks for the meta tag management system. - -## Test Environment - -- **Test Device:** Development machine -- **Browser:** Chrome/120+ (latest) -- **Vue Version:** 3.5.32 -- **Node Version:** 20.x LTS - -## Benchmark Scenarios - -### Scenario 1: Initial Page Load - -Measures the time to apply meta tags on initial page load. - -**Test Setup:** -```typescript -const config = { - title: 'Test Page', - description: 'Test description', - keywords: ['test', 'benchmark', 'performance'], - og: { - type: 'website', - title: 'Test Page', - description: 'Test description', - image: '/test.jpg' - }, - twitter: { - card: 'summary_large_image', - title: 'Test Page' - } -} -``` - -**Results:** -- Average time: **~2.5ms** -- Median time: **~2.1ms** -- 95th percentile: **~4.2ms** - -**Analysis:** Initial meta tag application is very fast, adding negligible overhead to page load. - -### Scenario 2: Route Navigation - -Measures the time to update meta tags when navigating between routes. - -**Test Setup:** -- Navigate between 10 different routes -- Each route has unique meta configuration -- Measure time from route change to DOM update - -**Results:** -- Average time per navigation: **~3.1ms** -- Median time: **~2.8ms** -- 95th percentile: **~5.5ms** - -**Analysis:** Route-based meta tag updates add minimal overhead to navigation. - -### Scenario 3: i18n Locale Switch - -Measures the time to update meta tags when switching locales. - -**Test Setup:** -- Switch between 5 different locales -- Each locale has translated meta content -- Measure time from locale change to DOM update - -**Results:** -- Average time per locale switch: **~2.8ms** -- Median time: **~2.5ms** -- 95th percentile: **~4.8ms** - -**Analysis:** Locale switching is efficient with minimal performance impact. - -### Scenario 4: Dynamic Content Updates - -Measures the time to update meta tags when content changes reactively. - -**Test Setup:** -- Reactive meta configuration that updates every 100ms -- 100 consecutive updates -- Measure time for each update - -**Results:** -- Average time per update: **~1.8ms** -- Median time: **~1.5ms** -- 95th percentile: **~3.2ms** - -**Analysis:** Reactive updates are highly efficient even with frequent changes. - -### Scenario 5: Complex Meta Configuration - -Measures performance with maximum meta tag complexity. - -**Test Setup:** -```typescript -const complexConfig = { - title: 'Complex Page', - description: 'Complex description', - keywords: ['tag1', 'tag2', /* ... 50 tags */], - og: { - type: 'article', - title: 'Complex OG Title', - description: 'Complex OG Description', - image: '/complex.jpg', - url: 'https://example.com/complex', - siteName: 'Example Site', - locale: 'en_US', - localeAlternate: ['es_LA', 'fr_FR', 'de_DE'] - }, - twitter: { - card: 'summary_large_image', - site: '@example', - creator: '@creator', - title: 'Complex Twitter Title', - description: 'Complex Twitter Description', - image: '/twitter-complex.jpg', - imageAlt: 'Complex image description' - }, - custom: [ - { name: 'author', content: 'John Doe' }, - { name: 'robots', content: 'index, follow' }, - { property: 'article:published_time', content: '2024-01-01' }, - /* ... 20 more custom tags */ - ] -} -``` - -**Results:** -- Average time: **~4.2ms** -- Median time: **~3.8ms** -- 95th percentile: **~6.1ms** - -**Analysis:** Even with complex configurations, performance remains excellent. - -### Scenario 6: Drupal Node Meta Generation - -Measures the time to generate meta tags from Drupal content nodes. - -**Test Setup:** -- 100 Drupal nodes loaded from JSON:API -- Generate meta configuration for each node -- Apply to DOM - -**Results:** -- Average generation time: **~1.2ms per node** -- Median time: **~1.0ms per node** -- 95th percentile: **~1.9ms per node** - -**Analysis:** Drupal meta tag generation adds minimal overhead to content loading. - -## Memory Usage - -### Initial Memory Footprint - -- **Code:** ~15 KB (minified + gzipped) -- **Runtime:** ~2 KB heap allocation per instance -- **DOM:** ~500 bytes per meta tag set - -### Memory During Operation - -- **Stable with frequent updates:** Memory allocation remains constant -- **No memory leaks detected** in long-running scenarios (tested over 10,000 route changes) -- **Clean cleanup:** All managed elements are removed on unmount - -## Benchmarking Methodology - -### Tools Used -- Chrome DevTools Performance Profiler -- Lighthouse for initial load benchmarks -- Custom timing utilities for reactive updates - -### Measurement Approach -1. Warm-up: 5 operations before measuring -2. 100 iterations for statistical significance -3. Exclude outliers (measurements during garbage collection) -4. Multiple test runs to ensure consistency - -### Statistical Analysis -- Mean: Average across all measurements -- Median: Middle value, robust to outliers -- 95th percentile: 95% of measurements fall below this value - -## Performance Recommendations - -### Do's ✅ - -1. **Use reactive configurations:** Computed properties are efficient -2. **Batch meta changes:** The system already batches updates -3. **Use i18n meta:** Optimized for locale switching -4. **Leverage route meta:** Automatic updates are efficient - -### Don'ts ❌ - -1. **Avoid synchronous heavy computations:** Use computed properties -2. **Don't manually manipulate meta tags:** Let the system manage them -3. **Avoid deep reactivity:** Keep meta configs shallow - -## SSR Performance - -### Server-Side Rendering Benchmarks - -**Test Setup:** -- Generate HTML with meta tags on the server -- Measure rendering time difference - -**Results:** -- Baseline rendering: **~45ms** -- With meta tags: **~47ms** -- **Overhead: ~2ms** - -**Analysis:** SSR meta tag generation adds negligible overhead to server rendering. - -## Conclusion - -The meta tag management system demonstrates excellent performance across all scenarios: - -- **Fast initial load:** <5ms overhead -- **Efficient updates:** ~2-3ms per change -- **SSR-ready:** Minimal server-side impact -- **Memory efficient:** No leaks, small footprint -- **Scalable:** Performance remains consistent with complexity - -The system is production-ready for high-traffic applications with frequent content and route changes. - -## Running Benchmarks Locally - -To run these benchmarks on your machine: - -```bash -# Navigate to the project -cd viossa.net/apps/vdn-static - -# Run performance tests -pnpm test:performance meta-tags - -# Or manually in browser console -import { runBenchmarks } from './tests/performance/meta-benchmarks' -runBenchmarks() -``` - -## Future Optimizations - -Potential future optimizations: - -1. **Batched DOM updates:** Currently uses nextTick, could micro-batch further -2. **Memoization:** Cache resolved meta configs -3. **Virtual meta tags:** Represent meta tags virtually before applying to DOM -4. **Web Worker:** Offload heavy meta processing to worker thread - -These optimizations are currently not needed as performance is already excellent, but may be considered if requirements change. - -## Last Updated - -Benchmarks last updated: May 15, 2026 -Vue version: 3.5.32 -System version: 1.0.0 \ No newline at end of file diff --git a/apps/vdn-static/docs/META_TAG_SYSTEM.md b/apps/vdn-static/docs/META_TAG_SYSTEM.md deleted file mode 100644 index 43e0459..0000000 --- a/apps/vdn-static/docs/META_TAG_SYSTEM.md +++ /dev/null @@ -1,602 +0,0 @@ -# Meta Tag Management System - -A comprehensive meta tag management system for the Viossa Vue.js application that supports SEO optimization, social sharing, and multi-language content. - -## Table of Contents - -- [Overview](#overview) -- [Features](#features) -- [Installation](#installation) -- [Quick Start](#quick-start) -- [API Reference](#api-reference) -- [Examples](#examples) -- [Integration with Drupal](#integration-with-drupal) -- [Best Practices](#best-practices) -- [Performance Considerations](#performance-considerations) - -## Overview - -This system provides a type-safe, reactive approach to managing meta tags in Vue.js applications. It automatically handles: - -- Standard HTML meta tags (title, description, keywords, author, canonical) -- OpenGraph tags for social media sharing -- Twitter Card tags for Twitter sharing -- Custom meta tags for specialized use cases -- i18n integration for multi-language support -- Dynamic updates based on route changes -- SSR-ready architecture for server-side rendering - -## Features - -✅ **Dynamic Meta Tag Generation** - Automatically update meta tags when content or routes change -✅ **OpenGraph Support** - Full support for og:* meta tags for social sharing -✅ **Twitter Card Support** - Complete Twitter Card implementation -✅ **i18n Integration** - Built-in support for multi-language meta tags -✅ **Route Integration** - Automatic meta tag updates based on Vue Router -✅ **Drupal Integration** - Utilities for generating meta tags from Drupal content -✅ **TypeScript Support** - Full type safety with TypeScript interfaces -✅ **SSR Ready** - Designed to work with both client-side and server-side rendering -✅ **Performance Optimized** - Efficient DOM manipulation with reactive updates - -## Installation - -The meta tag management system is already integrated into the Viossa application. No additional installation is required. - -## Quick Start - -### Basic Usage in Components - -```vue - -``` - -### Route-Based Meta Tags - -```typescript -// In your router configuration -const routes = [ - { - path: '/resources/:id', - component: ResourcePage, - meta: { - title: (route) => `Resource: ${route.params.id}`, - description: 'Learning resources and tutorials', - og: { - type: 'article', - image: '/default-og-image.jpg' - } - } - } -] -``` - -### i18n-Aware Meta Tags - -```vue - -``` - -## API Reference - -### `useMeta(config, route?, locale?)` - -Core composable for meta tag management. - -**Parameters:** -- `config` - Meta configuration object (static, reactive, or function) -- `route` - Current route (optional, auto-injected) -- `locale` - Locale reference for i18n (optional) - -**Returns:** Cleanup function to remove managed meta tags - -**Example:** -```typescript -import { useMeta } from '@/composables/useMeta' - -useMeta({ - title: 'Page Title', - description: 'Page description', - og: { - type: 'website' - } -}) -``` - -### `useRouteMeta()` - -Composable for automatic route-based meta tag management. - -**Returns:** Object with `metaConfig` computed property - -**Example:** -```typescript -import { useRouteMeta } from '@/composables/useRouteMeta' - -// In App.vue or main component -useRouteMeta() -``` - -### `useI18nMeta(config)` - -Composable for i18n-aware meta tag management with automatic hreflang generation. - -**Parameters:** -- `config` - i18n meta configuration object - -**Returns:** Object with `resolvedConfig` and `localeId` computed properties - -**Example:** -```typescript -import { useI18nMeta } from '@/composables/useI18nMeta' - -useI18nMeta({ - title: { - 'en-US': 'English Title', - 'es-LA': 'Spanish Title' - } -}) -``` - -### `useDrupalNodeMeta(nodeResult, baseUrl?)` - -Composable for fetching and applying Drupal node meta tags. - -**Parameters:** -- `nodeResult` - Reactive reference to Drupal node -- `baseUrl` - Optional base URL for canonical links - -**Example:** -```typescript -import { useDrupalNodeMeta } from '@/utils/drupal-meta' -import { ref } from 'vue' - -const node = ref(null) -// Load node from API... - -useDrupalNodeMeta(node) -``` - -## Examples - -### Home Page with i18n - -```vue - - - -``` - -### Dynamic Resource Page - -```vue - - - -``` - -### Article Page with Custom Meta - -```vue - -``` - -## Integration with Drupal - -The system provides utilities for generating meta tags from Drupal content nodes via JSON:API: - -### Automatic Node Type Detection - -```typescript -import { useDrupalNodeMeta } from '@/utils/drupal-meta' - -// Automatically detects node type and applies appropriate meta -useDrupalNodeMeta(nodeRef) -``` - -### Manual Meta Generation - -```typescript -import { - generateArticleContentMeta, - generatePageContentMeta, - generateLearningResourceMeta -} from '@/utils/drupal-meta' - -// Generate meta for specific content types -const articleMeta = generateArticleContentMeta(articleNode) -const pageMeta = generatePageContentMeta(pageNode) -const resourceMeta = generateLearningResourceMeta(resourceNode) -``` - -## Best Practices - -### 1. Use i18n-Aware Meta Tags for Multi-Language Content - -```typescript -// ✅ Good - i18n aware -useI18nMeta({ - title: { - 'en-US': 'Welcome', - 'es-LA': 'Bienvenido' - } -}) - -// ❌ Avoid - Hardcoded language -useMeta({ - title: 'Welcome' -}) -``` - -### 2. Use Dynamic Meta Tags for Dynamic Content - -```typescript -// ✅ Good - Dynamic based on route -useMeta({ - title: (route) => `Resource: ${route.params.id}` -}) - -// ❌ Avoid - Static for dynamic content -useMeta({ - title: 'Resource Page' -}) -``` - -### 3. Include Canonical URLs - -```typescript -useMeta({ - canonical: (route) => `https://viossa.net${route.path}` -}) -``` - -### 4. Use Semantic HTML Tags - -```typescript -// ✅ Good - Semantic OpenGraph type -og: { - type: 'article', - title: 'My Article' -} - -// ❌ Avoid - Wrong type for content type -og: { - type: 'website', - title: 'My Article' -} -``` - -### 5. Set Appropriate Robots Meta - -```typescript -// For published content -useMeta({ - custom: [ - { name: 'robots', content: 'index, follow' } - ] -}) - -// For draft/unpublished content -useMeta({ - custom: [ - { name: 'robots', content: 'noindex, nofollow' } - ] -}) -``` - -## Performance Considerations - -### DOM Manipulation - -The system efficiently manages DOM operations: - -- Reuses existing `` tags when possible -- Only updates attributes that have changed -- Removes managed tags on component unmount -- Batches updates using Vue's nextTick - -### Reactive Updates - -Meta tags update reactively when: - -- Route changes (automatic with `useRouteMeta()`) -- Reactive configuration changes -- Locale changes (with `useI18nMeta()`) - -### SSR Preparation - -The system is designed to work with SSR: - -- All meta operations are stateless -- No direct DOM access during initialization -- Compatible with server-side rendering context -- Supports hydration without re-rendering meta tags - -## TypeScript Types - -### MetaConfig - -```typescript -interface MetaConfig { - title?: string | ((route: RouteLocationNormalizedLoaded) => string) - description?: string | ((route: RouteLocationNormalizedLoaded) => string) - keywords?: string | string[] | ((route: RouteLocationNormalizedLoaded) => string | string[]) - author?: string | ((route: RouteLocationNormalizedLoaded) => string) - canonical?: string | ((route: RouteLocationNormalizedLoaded) => string) - og?: OpenGraphConfig | ((route: RouteLocationNormalizedLoaded) => OpenGraphConfig) - twitter?: TwitterCardConfig | ((route: RouteLocationNormalizedLoaded) => TwitterCardConfig) - custom?: CustomMetaTag[] | ((route: RouteLocationNormalizedLoaded) => CustomMetaTag[]) - appendLocale?: boolean - titleTemplate?: (title: string) => string -} -``` - -### OpenGraphConfig - -```typescript -interface OpenGraphConfig { - type?: 'website' | 'article' | 'product' | 'profile' | 'book' | 'video' | 'music' - title?: string - description?: string - image?: string - url?: string - siteName?: string - locale?: string - localeAlternate?: string[] - video?: string - audio?: string -} -``` - -### TwitterCardConfig - -```typescript -interface TwitterCardConfig { - card?: 'summary' | 'summary_large_image' | 'app' | 'player' - site?: string - creator?: string - title?: string - description?: string - image?: string - imageAlt?: string - player?: string - playerWidth?: number - playerHeight?: number -} -``` - -## Troubleshooting - -### Meta tags not updating - -**Issue:** Meta tags are not updating when content changes. - -**Solution:** Ensure you're using reactive references or computed properties: - -```typescript -// ✅ Correct - Reactive -const content = ref({ title: 'Page' }) -useMeta(computed(() => ({ - title: content.value.title -}))) - -// ❌ Incorrect - Non-reactive -useMeta({ - title: content.value.title // Not reactive! -}) -``` - -### i18n meta tags not switching - -**Issue:** Meta tags don't update when locale changes. - -**Solution:** Use `useI18nMeta` instead of `useMeta` for i18n support: - -```typescript -// ✅ Correct -useI18nMeta({ - title: { - 'en-US': 'Welcome', - 'es-LA': 'Bienvenido' - } -}) - -// ❌ Incorrect - Not i18n aware -useMeta({ - title: { - 'en-US': 'Welcome', - 'es-LA': 'Bienvenido' - } -}) -``` - -### Duplicate meta tags - -**Issue:** Multiple sets of meta tags appearing. - -**Solution:** The system automatically cleans up managed tags, but ensure you're not manually managing conflicting tags elsewhere. - -## Migration Guide - -### From Manual Meta Tags - -If you're currently manually managing meta tags: - -```vue - - - - - -``` - -### From vue-meta - -If migrating from vue-meta: - -```typescript -// ❌ Old (vue-meta) -export default { - metaInfo() { - return { - title: 'My Page', - meta: [ - { name: 'description', content: 'My description' } - ] - } - } -} - -// ✅ New (useMeta) - -``` - -## Contributing - -When contributing to this system: - -1. Ensure all new features are TypeScript typed -2. Add corresponding tests -3. Update this documentation -4. Follow the existing code style -5. Consider SSR compatibility - -## License - -This meta tag management system is part of the Viossa project and follows the project's license. \ No newline at end of file diff --git a/apps/vdn-static/src/AUTH_IMPLEMENTATION.md b/apps/vdn-static/src/AUTH_IMPLEMENTATION.md deleted file mode 100644 index 69fb1ff..0000000 --- a/apps/vdn-static/src/AUTH_IMPLEMENTATION.md +++ /dev/null @@ -1,225 +0,0 @@ -# Authentication Implementation - -This directory contains the authentication system for VDN Static, implementing basic authentication for protected content editing. - -## Overview - -The authentication system uses: -- **Pinia** for state management (user session) -- **Vue Router** navigation guards for route protection -- **Drupal REST API** for authentication -- **localStorage** for token persistence - -## Architecture - -### Components - -1. **User Store** (`stores/user.ts`) - - Manages user session state - - Handles token storage - - Provides authentication status - -2. **useAuth Composable** (`composables/useAuth.ts`) - - Login/logout functionality - - Auth header management - - Integration with Drupal REST API - -3. **Login Page** (`pages/login.vue`) - - Username/password form - - Redirects after successful login - - Error handling - -4. **Route Guards** (`router/guards.ts`) - - Protected route enforcement - - Redirect to login when not authenticated - - Role-based access control - -### Flow - -``` -User visits /admin - ↓ -Route guard checks authentication - ↓ -If not authenticated → redirect to /login - ↓ -User enters credentials - ↓ -POST /user/login?_format=json to Drupal - ↓ -Store user info and CSRF token - ↓ -Redirect to /admin or original destination -``` - -## Usage - -### Protecting a Route - -To require authentication for a page, use `definePage` in the component: - -```vue - - - -``` - -### Requiring Admin Role - -For admin-only pages: - -```vue - -``` - -### Using Authentication in Components - -Access authentication state in any component: - -```vue - - - -``` - -### Making Authenticated API Requests - -The useAuth composable provides auth headers: - -```vue - -``` - -## Drupal Configuration - -The authentication system expects: - -1. **Drupal REST API** endpoint at `/user/login?_format=json` -2. **CORS enabled** for the frontend domain -3. **Session authentication** with CSRF token support - -### Expected Drupal Response - -```json -{ - "current_user": { - "uid": "1", - "name": "admin", - "mail": "admin@example.com", - "roles": ["authenticated", "administrator"] - }, - "csrf_token": "abc123...", - "logout_token": "def456..." -} -``` - -## Security Notes - -- **Token Storage**: Tokens are stored in localStorage for simplicity. For production with sensitive data, consider using HttpOnly cookies. -- **CSRF Protection**: Drupal's CSRF token is included in authenticated requests via the `X-CSRF-Token` header. -- **Session Management**: The logout function clears both client state and Drupal session. - -## Testing - -### Manual Testing - -1. Start the development server -2. Navigate to `/login` -3. Enter Drupal credentials -4. Verify redirect to `/admin` -5. Test protected routes by visiting `/admin/*` paths -6. Test logout functionality - -### Integration Test Checklist - -- [ ] Login with valid credentials -- [ ] Login with invalid credentials (shows error) -- [ ] Redirect to protected route after login -- [ ] Redirect to login when accessing protected route while unauthenticated -- [ ] Logout clears session -- [ ] Token persists across page refreshes -- [ ] Admin-only routes reject non-admin users - -## File Structure - -``` -apps/vdn-static/src/ -├── stores/ -│ └── user.ts # Pinia store for user state -├── composables/ -│ └── useAuth.ts # Authentication composable -├── pages/ -│ ├── login.vue # Login page -│ └── admin/ -│ └── index.vue # Protected admin dashboard -├── router/ -│ └── guards.ts # Navigation guards -└── services/ - └── DrupalService.ts # Extended with auth support -``` - -## Troubleshooting - -### "Login failed" error -- Check Drupal credentials -- Verify Drupal REST API is enabled -- Check CORS configuration on Drupal -- Verify proxy is forwarding auth headers - -### Can't access protected routes -- Check localStorage for auth tokens -- Verify Pinia store initialized correctly -- Check browser console for errors - -### Session not persisting -- Check browser localStorage permissions -- Verify no errors clearing/setting tokens -- Check Drupal session configuration - -## Future Enhancements - -- [ ] Password reset flow -- [ ] Remember me functionality -- [ ] Session timeout handling -- [ ] OAuth2/social login support -- [ ] Two-factor authentication \ No newline at end of file diff --git a/apps/vdn-static/src/App.vue b/apps/vdn-static/src/App.vue index 6edd704..517c29a 100644 --- a/apps/vdn-static/src/App.vue +++ b/apps/vdn-static/src/App.vue @@ -7,13 +7,9 @@ import { useRouter } from "vue-router"; import SmartLink from "./components/atoms/SmartLink.vue"; import type { SmartDest } from "./utils/smart-dest"; import { useLocale, type Locale } from "./i18n"; -import { useRouteMeta } from "./composables/useRouteMeta"; const locale = useLocale(); -// Apply route-based meta tags automatically -useRouteMeta(); - const burgerOpen: Ref = ref(false); const toggleBurger = (): void => { diff --git a/apps/vdn-static/src/components/molecules/PreviewModeToggle.vue b/apps/vdn-static/src/components/molecules/PreviewModeToggle.vue deleted file mode 100644 index 67459f2..0000000 --- a/apps/vdn-static/src/components/molecules/PreviewModeToggle.vue +++ /dev/null @@ -1,122 +0,0 @@ - - - - - \ No newline at end of file diff --git a/apps/vdn-static/src/composables/index.ts b/apps/vdn-static/src/composables/index.ts deleted file mode 100644 index e13e97a..0000000 --- a/apps/vdn-static/src/composables/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Meta Tag Management System - Public API - * - * This module exports all public APIs for the meta tag management system. - */ - -// Core composables -export { useMeta } from './useMeta' -export { useRouteMeta, usePageMeta, RouteMetaPlugin } from './useRouteMeta' -export { useI18nMeta, createI18nMeta, mergeI18nMeta } from './useI18nMeta' -export { usePreviewMode } from './usePreviewMode' - -// Type exports -export type { - MetaConfig, - OpenGraphConfig, - TwitterCardConfig, - CustomMetaTag, - LocaleAwareMeta, -} from './useMeta' - -export type { - I18nMetaConfig, -} from './useI18nMeta' - -export type { - PreviewModeConfig, -} from './usePreviewMode' - -// Utility exports -export { createRouteMeta, mergeMeta } from './useMeta' - -// Drupal integration -export { - generateDrupalNodeMeta, - generateArticleContentMeta, - generatePageContentMeta, - generateLearningResourceMeta, - useDrupalNodeMeta, -} from '../utils/drupal-meta' - -// Example configurations -export { - homePageMeta, - resourcesPageMeta, - resourceDetailMeta, - kotobaPageMeta, - discordRulesPageMeta, - generateArticleMeta, - generateResourceMeta, -} from '../config/meta-examples' \ No newline at end of file diff --git a/apps/vdn-static/src/composables/useAuth.ts b/apps/vdn-static/src/composables/useAuth.ts deleted file mode 100644 index a374f0f..0000000 --- a/apps/vdn-static/src/composables/useAuth.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { ref, computed } from 'vue' -import { useUserStore } from '../stores/user' -import { useRouter } from 'vue-router' -import type { Router } from 'vue-router' - -interface LoginCredentials { - username: string - password: string -} - -interface DrupalLoginResponse { - current_user: { - uid: string - name: string - mail: string - roles: string[] - } - csrf_token: string - logout_token: string -} - -export function useAuth() { - const userStore = useUserStore() - const router = useRouter() - - const loading = ref(false) - const error = ref(null) - - // Use Drupal proxy path for auth requests - const apiBaseUrl = import.meta.env.VITE_DRUPAL_API_URL || '/api/drupal-proxy' - - /** - * Login with username and password - * Uses Drupal's /user/login?_format=json endpoint - */ - const login = async (credentials: LoginCredentials): Promise => { - try { - loading.value = true - error.value = null - - // Drupal login endpoint - const loginUrl = `${apiBaseUrl}/../user/login?_format=json` - - const response = await fetch(loginUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: 'include', // Include cookies for session - body: JSON.stringify({ - name: credentials.username, - pass: credentials.password, - }), - }) - - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || 'Login failed') - } - - const data: DrupalLoginResponse = await response.json() - - // Extract CSRF token from response headers or body - const csrfToken = response.headers.get('X-CSRF-Token') || data.csrf_token - - // Store user info - userStore.setUser({ - uid: data.current_user.uid, - name: data.current_user.name, - mail: data.current_user.mail, - roles: data.current_user.roles, - }) - - // Store auth tokens - userStore.setTokens({ - access_token: csrfToken, - refresh_token: data.logout_token, - }) - - return true - } catch (e) { - const errorMessage = e instanceof Error ? e.message : 'An unknown error occurred' - error.value = errorMessage - userStore.error = errorMessage - return false - } finally { - loading.value = false - } - } - - /** - * Logout and clear session - */ - const logout = async (): Promise => { - try { - loading.value = true - - // Call Drupal logout endpoint if we have a token - if (userStore.tokens?.refresh_token) { - const logoutUrl = `${apiBaseUrl}/../user/logout?_format=json&token=${userStore.tokens.refresh_token}` - - await fetch(logoutUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: 'include', - }) - } - } catch (e) { - console.error('Logout error:', e) - } finally { - // Clear local state regardless of server response - userStore.logout() - loading.value = false - - // Redirect to home - router.push('/') - } - } - - /** - * Get auth headers for authenticated requests - */ - const getAuthHeaders = (): Record => { - const headers: Record = {} - - if (userStore.tokens?.access_token) { - headers['X-CSRF-Token'] = userStore.tokens.access_token - } - - return headers - } - - /** - * Check if user is authenticated - */ - const isAuthenticated = computed(() => userStore.isAuthenticated) - - /** - * Check if user has admin role - */ - const isAdmin = computed(() => userStore.isAdmin) - - /** - * Get current user - */ - const currentUser = computed(() => userStore.user) - - return { - login, - logout, - getAuthHeaders, - isAuthenticated, - isAdmin, - currentUser, - loading, - error, - } -} \ No newline at end of file diff --git a/apps/vdn-static/src/composables/useI18nMeta.ts b/apps/vdn-static/src/composables/useI18nMeta.ts deleted file mode 100644 index 1b17e40..0000000 --- a/apps/vdn-static/src/composables/useI18nMeta.ts +++ /dev/null @@ -1,212 +0,0 @@ -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' - -/** - * Composable for i18n-aware meta tag management - * - * This composable extends the basic useMeta composable with automatic - * i18n support. It automatically selects the correct locale version of - * meta tags and adds hreflang tags for international SEO. - * - * @example - * ```typescript - * // In a page component - * useI18nMeta({ - * title: { - * 'en-US': 'Welcome', - * 'es-LA': 'Bienvenido', - * 'vp-VL': 'Vlosso' - * }, - * description: { - * 'en-US': 'Welcome to our website', - * 'es-LA': 'Bienvenido a nuestro sitio web' - * } - * }) - * ``` - * - * @param config - Meta configuration with locale-aware content - * @param route - Current route (optional, auto-injected) - */ -export function useI18nMeta( - config: I18nMetaConfig | Ref | ComputedRef -) { - const route = useRoute() - const localeRef = useLocale() - const localeId = computed(() => { - // Extract the current locale ID from the locale object - // This depends on the i18n implementation - return 'en-US' as LocaleId // This would be dynamic in real implementation - }) - - /** - * Resolves a meta value that can be static, reactive, or a function - */ - const resolveMetaValue = ( - value: RouteMetaValue | undefined, - routeValue: typeof route - ): T | undefined => { - if (value === undefined) return undefined - return typeof value === 'function' ? value(routeValue) : value - } - - /** - * Converts i18n meta config to resolved meta config - */ - const resolvedConfig = computed(() => { - const configValue = unref(config) - const routeValue = route - const currentLocale = localeId.value - - return { - title: resolveI18nContent(resolveMetaValue(configValue.title, routeValue), currentLocale), - description: resolveI18nContent(resolveMetaValue(configValue.description, routeValue), currentLocale), - 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), - custom: resolveI18nCustomTags(resolveMetaValue(configValue.custom, routeValue), currentLocale), - } - }) - - // Use the resolved config with useMeta - useMeta(resolvedConfig, route, localeId) - - // Add hreflang tags for international SEO - const addHreflangTags = () => { - const configValue = unref(config) - if (!configValue.hreflang) return - - const hreflangConfig = configValue.hreflang - const currentPath = window.location.pathname - - // Remove existing hreflang tags - const existingTags = document.querySelectorAll('link[rel="alternate"][hreflang]') - existingTags.forEach(tag => tag.remove()) - - // Add new hreflang tags - Object.entries(hreflangConfig).forEach(([locale, path]) => { - const link = document.createElement('link') - link.setAttribute('rel', 'alternate') - link.setAttribute('hreflang', locale) - link.setAttribute('href', `${window.location.origin}${path}`) - document.head.appendChild(link) - }) - - // Add x-default - const defaultLink = document.createElement('link') - defaultLink.setAttribute('rel', 'alternate') - defaultLink.setAttribute('hreflang', 'x-default') - defaultLink.setAttribute('href', `${window.location.origin}${currentPath}`) - document.head.appendChild(defaultLink) - } - - // Watch for config changes and update hreflang tags - watch(resolvedConfig, addHreflangTags, { immediate: true }) - - return { - resolvedConfig, - localeId, - } -} - -/** - * Resolves i18n-aware content to the appropriate locale string - */ -function resolveI18nContent( - content: string | LocaleAwareMeta | undefined, - currentLocale: LocaleId -): string | undefined { - if (content === undefined) return undefined - - if (typeof content === 'string') { - return content - } - - // It's a locale-aware object - if (currentLocale && content[currentLocale]) { - return content[currentLocale] - } - - // Fallback to default locale or first available - return content['en-US'] || Object.values(content)[0] -} - -/** - * Resolves custom meta tags with i18n support - */ -function resolveI18nCustomTags( - customTags: Array<{ - name?: string - property?: string - content: string | ((route: any) => string) - charset?: string - httpEquiv?: string - }> | undefined, - currentLocale: LocaleId -): Array<{ - name?: string - property?: string - content: string - charset?: string - httpEquiv?: string -}> { - if (!customTags) return [] - - 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 - })) -} - -/** - * i18n-aware meta configuration interface - */ -export interface I18nMetaConfig { - /** Page title (locale-aware) */ - title?: string | LocaleAwareMeta | ((route: any) => string | LocaleAwareMeta) - /** Meta description (locale-aware) */ - description?: string | LocaleAwareMeta | ((route: any) => string | LocaleAwareMeta) - /** Meta keywords */ - keywords?: string | string[] | ((route: any) => string | string[]) - /** Author meta tag */ - author?: string | ((route: any) => string) - /** Canonical URL */ - canonical?: string | ((route: any) => string) - /** OpenGraph configuration */ - og?: MetaConfig['og'] - /** Twitter Card configuration */ - twitter?: MetaConfig['twitter'] - /** Custom meta tags */ - custom?: MetaConfig['custom'] - /** Hreflang configuration for international SEO */ - hreflang?: { - [locale: string]: string - } -} - -/** - * Helper function to create locale-aware meta configuration - */ -export function createI18nMeta( - meta: I18nMetaConfig -): I18nMetaConfig { - return meta -} - -/** - * Helper function to merge i18n meta configurations - */ -export function mergeI18nMeta( - base: I18nMetaConfig, - override: Partial -): I18nMetaConfig { - return { ...base, ...override } -} \ No newline at end of file diff --git a/apps/vdn-static/src/composables/usePreviewMode.ts b/apps/vdn-static/src/composables/usePreviewMode.ts deleted file mode 100644 index 483436f..0000000 --- a/apps/vdn-static/src/composables/usePreviewMode.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { ref, computed, reactive } from 'vue' -import { useUserStore } from '../stores/user' -import { drupalService } from '../services/DrupalService' - -export interface PreviewModeConfig { - enabled: boolean - userId?: string - sessionId?: string - entityType?: string - entityId?: string - token?: string -} - -export function usePreviewMode() { - const userStore = useUserStore() - - // Preview state - const previewEnabled = ref(false) - const previewConfig = reactive({ - enabled: false, - userId: undefined, - sessionId: undefined, - entityType: undefined, - entityId: undefined, - token: undefined - }) - - // Computed properties - const isInPreviewMode = computed(() => previewEnabled.value && userStore.isAuthenticated) - const previewUserInfo = computed(() => ({ - name: previewConfig.userId ? `Preview as ${previewConfig.userId}` : userStore.user?.name || 'Editor', - role: previewConfig.userId ? 'Editor' : userStore.user?.roles?.[0] || 'Authenticated User' - })) - - // Methods - const enablePreviewMode = (config: Partial = {}) => { - if (!userStore.isAuthenticated) { - throw new Error('Authentication required for preview mode') - } - - // Update preview config - Object.assign(previewConfig, config, { enabled: true }) - previewEnabled.value = true - - // Set auth headers for preview requests - drupalService.setAuthHeaders({ - 'X-CSRF-Token': userStore.tokens?.access_token || '' - }) - - console.log('Preview mode enabled', previewConfig) - } - - const disablePreviewMode = () => { - previewConfig.enabled = false - previewEnabled.value = false - console.log('Preview mode disabled') - } - - const togglePreviewMode = (config?: Partial) => { - if (previewEnabled.value) { - disablePreviewMode() - } else { - enablePreviewMode(config || {}) - } - } - - const isPreviewEnabledForEntity = (entityType: string, entityId: string) => { - return previewEnabled.value && - previewConfig.entityType === entityType && - previewConfig.entityId === entityId - } - - const setPreviewContext = (entityType: string, entityId: string, token?: string) => { - if (!previewEnabled.value) { - throw new Error('Preview mode must be enabled first') - } - - previewConfig.entityType = entityType - previewConfig.entityId = entityId - previewConfig.token = token - } - - const clearPreviewContext = () => { - previewConfig.entityType = undefined - previewConfig.entityId = undefined - previewConfig.token = undefined - } - - // Auto-disable preview mode on logout - const watchAuth = () => { - // This would be implemented with a watch on authentication state - // For now, we'll handle this in the logout method of useAuth - } - - return { - // State - previewEnabled, - previewConfig, - - // Computed - isInPreviewMode, - previewUserInfo, - - // Methods - enablePreviewMode, - disablePreviewMode, - togglePreviewMode, - isPreviewEnabledForEntity, - setPreviewContext, - clearPreviewContext, - watchAuth, - } -} \ No newline at end of file diff --git a/apps/vdn-static/src/composables/useRouteMeta.ts b/apps/vdn-static/src/composables/useRouteMeta.ts deleted file mode 100644 index 2b7e3ec..0000000 --- a/apps/vdn-static/src/composables/useRouteMeta.ts +++ /dev/null @@ -1,127 +0,0 @@ -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' - -/** - * Composable for automatic meta tag management based on route configuration - * - * This composable integrates with Vue Router to automatically update meta tags - * when the route changes. It reads the `meta` field from route definitions and - * applies them to the document. - * - * @example - * ```typescript - * // In a route definition - * const routes = [ - * { - * path: '/resources/:id', - * component: ResourcePage, - * meta: { - * title: (route) => `Resource: ${route.params.id}`, - * description: 'Learning resources and tutorials', - * og: { - * type: 'article', - * image: '/default-og-image.jpg' - * } - * } - * } - * ] - * - * // In App.vue or main.ts - * setup() { - * useRouteMeta() - * } - * ``` - */ -export function useRouteMeta() { - const route = useRoute() - - /** - * Resolves a meta value that can be static or a function - */ - const resolveMetaValue = ( - value: RouteMetaValue | undefined, - routeValue: typeof route - ): T | undefined => { - if (value === undefined) return undefined - return typeof value === 'function' ? value(routeValue) : value - } - - /** - * Creates a reactive meta configuration from the current route - */ - const metaConfig = computed(() => { - const meta: RouteMeta = route.meta || {} - - return { - title: resolveMetaValue(meta.title, route), - description: resolveMetaValue(meta.description, route), - keywords: resolveMetaValue(meta.keywords, route), - author: resolveMetaValue(meta.author, route), - canonical: resolveMetaValue(meta.canonical, route), - og: resolveMetaValue(meta.og, route), - twitter: resolveMetaValue(meta.twitter, route), - custom: resolveMetaValue(meta.custom, route), - } - }) - - // Apply meta tags whenever the route changes - useMeta(metaConfig, route) - - return { - metaConfig, - } -} - -/** - * Plugin to install automatic route-based meta tag management - * - * @example - * ```typescript - * // In main.ts - * import { createApp } from 'vue' - * import App from './App.vue' - * import router from './router' - * import { RouteMetaPlugin } from './composables/useRouteMeta' - * - * const app = createApp(App) - * app.use(router) - * app.use(RouteMetaPlugin) - * app.mount('#app') - * ``` - */ -export const RouteMetaPlugin = { - install(app: any) { - // The plugin installation logic will be handled in the root component - // This is just a marker for the plugin system - app.provide('route-meta-installed', true) - } -} - -/** - * Composable to define meta tags for the current route - * This is useful for page components using `definePage` macro - * - * @example - * ```vue - * - * ``` - */ -export function usePageMeta(meta: RouteMeta) { - const route = useRoute() - - // Return a reactive meta config that merges with route meta - return useMeta(meta, route) -} \ No newline at end of file diff --git a/apps/vdn-static/src/config/meta-examples.ts b/apps/vdn-static/src/config/meta-examples.ts deleted file mode 100644 index 713fd4d..0000000 --- a/apps/vdn-static/src/config/meta-examples.ts +++ /dev/null @@ -1,333 +0,0 @@ -/** - * Example Meta Tag Configurations for Different Page Types - * - * This file demonstrates how to configure meta tags for various page types - * in the Viossa application. - */ - -import type { MetaConfig, I18nMetaConfig } from '../composables/useI18nMeta' -import { createRouteMeta } from '../composables/useMeta' - -/** - * Home Page Meta Configuration - * - * Basic configuration for the main landing page with i18n support. - */ -export const homePageMeta: I18nMetaConfig = { - title: { - 'en-US': 'Viossa - Learn a Language Through Immersion', - 'es-LA': 'Viossa - Aprende un Idioma a Través de la Inmersión', - 'vp-VL': 'Viossa - Vlosso pada via imerso', - }, - description: { - 'en-US': 'Viossa is a language learning platform that uses immersive methods to help you learn naturally through interaction with native speakers.', - 'es-LA': 'Viossa es una plataforma de aprendizaje de idiomas que utiliza métodos inmersivos para ayudarte a aprender naturalmente a través de la interacción con hablantes nativos.', - 'vp-VL': 'Viossa es platforma de lernado de linguas ke usa metodos imersiv para ayudar tu aprender naturalmente.', - }, - keywords: ['language learning', 'immersion', 'viossa', 'natural language acquisition', 'polyglot'], - 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.', - }, - 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', - 'x-default': '/', - }, -} - -/** - * Resources Page Meta Configuration - * - * Configuration for the resources listing page. - */ -export const resourcesPageMeta: I18nMetaConfig = { - title: { - 'en-US': 'Learning Resources - Viossa', - 'es-LA': 'Recursos de Aprendizaje - Viossa', - }, - description: { - 'en-US': 'Browse our collection of learning resources, tutorials, and guides to help you on your language learning journey.', - 'es-LA': 'Explora nuestra colección de recursos de aprendizaje, tutoriales y guías para ayudarte en tu viaje de aprendizaje de idiomas.', - }, - 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.', - }, - image: '/images/og-resources.jpg', - }, - twitter: { - card: 'summary', - title: { - 'en-US': 'Learning Resources - Viossa', - }, - description: { - 'en-US': 'Browse tutorials and guides for language learning', - }, - }, -} - -/** - * Resource Detail Page Meta Configuration (Dynamic) - * - * Configuration for individual resource/article pages. - * Uses route parameters to generate dynamic meta tags. - */ -export const resourceDetailMeta = (route: any): I18nMetaConfig => { - // In a real implementation, this would fetch resource data from an API - const resourceId = route.params.id - const resourceTitle = `Resource #${resourceId}` // Would be actual resource title - const resourceDescription = `Detailed information about resource ${resourceId}` // Would be actual description - - return { - title: { - 'en-US': `${resourceTitle} - Viossa Resources`, - 'es-LA': `${resourceTitle} - Recursos de Viossa`, - }, - description: { - 'en-US': resourceDescription, - 'es-LA': resourceDescription, - }, - // Dynamic canonical URL based on resource ID - canonical: `https://viossa.net/resources/${resourceId}`, - og: { - type: 'article', - title: { - 'en-US': resourceTitle, - }, - description: { - 'en-US': 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: [ - { - name: 'resource-id', - content: resourceId, - }, - { - name: 'article:published_time', - content: new Date().toISOString(), // Would be actual publish date - }, - ], - } -} - -/** - * Kotoba Page Meta Configuration - * - * Configuration for the Kotoba language learning tool page. - */ -export const kotobaPageMeta: I18nMetaConfig = { - title: { - 'en-US': 'Kotoba - Interactive Language Learning - Viossa', - 'es-LA': 'Kotoba - Aprendizaje Interactivo de Idiomas - Viossa', - }, - description: { - 'en-US': 'Practice vocabulary and grammar with Kotoba, our interactive language learning tool featuring spaced repetition and contextual learning.', - 'es-LA': 'Practica vocabulario y gramática con Kotoba, nuestra herramienta interactiva de aprendizaje de idiomas con repetición espaciada y aprendizaje contextual.', - }, - 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', - }, - image: '/images/og-kotoba.jpg', - }, - twitter: { - card: 'summary', - title: { - 'en-US': 'Kotoba - Interactive Language Learning', - }, - }, -} - -/** - * Discord Rules Page Meta Configuration - * - * Configuration for the Discord community rules page. - */ -export const discordRulesPageMeta: I18nMetaConfig = { - title: { - 'en-US': 'Discord Community Rules - Viossa', - 'es-LA': 'Reglas de la Comunidad Discord - Viossa', - }, - description: { - 'en-US': 'Learn about our Discord community rules and guidelines for maintaining a respectful and supportive learning environment.', - 'es-LA': 'Conoce las reglas y directrices de nuestra comunidad Discord para mantener un ambiente de aprendizaje respetuoso y de apoyo.', - }, - keywords: ['discord', 'community rules', 'guidelines', 'viossa discord', 'community standards'], - 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', - }, - image: '/images/og-discord-rules.jpg', - }, -} - -/** - * Function to generate article meta configuration - * - * This is an example of how to create meta configurations for - * Drupal content types (articles, blog posts, etc.) - */ -export function generateArticleMeta(article: { - id: string - title: string - summary: string - author: string - publishedDate: string - modifiedDate?: string - image?: string - tags?: string[] -}): MetaConfig { - return { - title: article.title, - description: article.summary, - keywords: article.tags || [], - author: article.author, - canonical: `https://viossa.net/articles/${article.id}`, - og: { - type: 'article', - title: article.title, - description: article.summary, - 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', - content: article.publishedDate, - }, - ...(article.modifiedDate ? [{ - property: 'article:modified_time', - content: article.modifiedDate, - }] : []), - { - property: 'article:author', - content: article.author, - }, - ], - } -} - -/** - * Function to generate resource meta configuration - * - * This is an example of how to create meta configurations for - * Drupal resource content types. - */ -export function generateResourceMeta(resource: { - id: string - title: string - description: string - author: string - type: 'tutorial' | 'guide' | 'video' | 'course' - difficulty: 'beginner' | 'intermediate' | 'advanced' - tags?: string[] -}): MetaConfig { - return { - title: `${resource.title} - Viossa Resources`, - description: resource.description, - keywords: [...(resource.tags || []), resource.type, resource.difficulty], - author: resource.author, - canonical: `https://viossa.net/resources/${resource.id}`, - og: { - type: 'article', - title: resource.title, - description: resource.description, - image: '/images/og-resource-default.jpg', - url: `https://viossa.net/resources/${resource.id}`, - }, - custom: [ - { - name: 'resource-type', - content: resource.type, - }, - { - name: 'difficulty', - content: resource.difficulty, - }, - { - name: 'author', - content: resource.author, - }, - ], - } -} - -/** - * Example: Route configuration with meta tags - * - * This shows how to configure routes with meta tags in the router definition. - */ -export const exampleRouteWithMeta = { - path: '/resources/:id', - name: 'resource-detail', - component: () => import('../pages/resources/[id].vue'), - meta: createRouteMeta({ - title: (route) => `Resource: ${route.params.id}`, - description: 'Learning resources and tutorials', - keywords: ['resources', 'tutorials', 'learning'], - og: { - type: 'article', - image: '/default-og-image.jpg' - }, - twitter: { - card: 'summary_large_image' - } - }) -} \ No newline at end of file diff --git a/apps/vdn-static/src/i18n/index.ts b/apps/vdn-static/src/i18n/index.ts index 4d6ff4b..23e5e4f 100644 --- a/apps/vdn-static/src/i18n/index.ts +++ b/apps/vdn-static/src/i18n/index.ts @@ -12,7 +12,6 @@ import enUsFtlSrc from "@/assets/locale/en_US.ftl"; import esLaFtlSrc from "@/assets/locale/es_LA.ftl"; import vpVlFtlSrc from "@/assets/locale/vp_VL.ftl"; import wpVlFtlSrc from "@/assets/locale/wp_VL.ftl"; - import type { FluentBundle } from "@fluent/bundle"; import { compileLocale } from "@/vi18n-lib/compile"; import type { ComputedRef } from "vue"; diff --git a/apps/vdn-static/src/main.ts b/apps/vdn-static/src/main.ts index 1b8ca7a..5cf249f 100644 --- a/apps/vdn-static/src/main.ts +++ b/apps/vdn-static/src/main.ts @@ -1,14 +1,8 @@ import { createApp } from "vue"; -import { createPinia } from "pinia"; import App from "./App.vue"; import router from "./router"; import { onI18nInit } from "./i18n"; onI18nInit(() => { - const app = createApp(App); - const pinia = createPinia(); - - app.use(pinia); - app.use(router); - app.mount("#app"); + createApp(App).use(router).mount("#app"); }); diff --git a/apps/vdn-static/src/pages/admin/index.vue b/apps/vdn-static/src/pages/admin/index.vue deleted file mode 100644 index 0323773..0000000 --- a/apps/vdn-static/src/pages/admin/index.vue +++ /dev/null @@ -1,257 +0,0 @@ - - - - - \ No newline at end of file diff --git a/apps/vdn-static/src/pages/login.vue b/apps/vdn-static/src/pages/login.vue deleted file mode 100644 index 817807f..0000000 --- a/apps/vdn-static/src/pages/login.vue +++ /dev/null @@ -1,207 +0,0 @@ - - - - - \ No newline at end of file diff --git a/apps/vdn-static/src/pages/preview.vue b/apps/vdn-static/src/pages/preview.vue deleted file mode 100644 index 9a5ac4b..0000000 --- a/apps/vdn-static/src/pages/preview.vue +++ /dev/null @@ -1,419 +0,0 @@ - - - - - \ No newline at end of file diff --git a/apps/vdn-static/src/router.ts b/apps/vdn-static/src/router.ts index fcd46df..841dfa8 100644 --- a/apps/vdn-static/src/router.ts +++ b/apps/vdn-static/src/router.ts @@ -1,12 +1,8 @@ import { createRouter, createWebHistory } from "vue-router"; import { routes, handleHotUpdate } from "vue-router/auto-routes"; -import { setupAuthGuards } from "./router/guards"; const router = createRouter({ history: createWebHistory(), routes }); -// Setup authentication guards -setupAuthGuards(router); - if (import.meta.hot) { handleHotUpdate(router); } diff --git a/apps/vdn-static/src/router/guards.ts b/apps/vdn-static/src/router/guards.ts deleted file mode 100644 index 9eca589..0000000 --- a/apps/vdn-static/src/router/guards.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Route Guards for Authentication - * - * This module provides navigation guards for protecting routes. - * Works with unplugin-vue-router's definePage meta system. - */ - -import type { Router } from 'vue-router' -import { useUserStore } from '../stores/user' - -/** - * Setup navigation guards on router - */ -export function setupAuthGuards(router: Router) { - router.beforeEach((to, from, next) => { - const userStore = useUserStore() - - // Check if route requires authentication - if (to.meta.requiresAuth) { - // User must be authenticated - if (!userStore.isAuthenticated) { - // Redirect to login withreturn URL - next({ - path: '/login', - query: { redirect: to.fullPath }, - }) - return - } - - // Check if route requires admin role - if (to.meta.requiresAdmin && !userStore.isAdmin) { - // User is not an admin - next({ path: '/' }) - return - } - } - - // If trying to access login page while authenticated, redirect to admin - if (to.path === '/login' && userStore.isAuthenticated) { - next({ path: '/admin' }) - return - } - - next() - }) -} - -/** - * Type augmentation for vue-router - * Allows using definePage with meta properties - */ -declare module 'vue-router' { - interface RouteMeta { - /** Route requires authentication */ - requiresAuth?: boolean - /** Route requires admin role */ - requiresAdmin?: boolean - } -} - -/** - * Example usage in a page component: - * - * - * - * - */ \ No newline at end of file diff --git a/apps/vdn-static/src/services/DrupalService.ts b/apps/vdn-static/src/services/DrupalService.ts deleted file mode 100644 index b9e14e9..0000000 --- a/apps/vdn-static/src/services/DrupalService.ts +++ /dev/null @@ -1,291 +0,0 @@ -/// -/** - * Drupal JSON:API Service - * Provides typed access to Drupal content via JSON:API - */ - -import type { - JsonApiParams, - JsonApiDocument, - JsonApiResource, - JsonApiLinks, - JsonApiError, - DrupalNodeAttributes, - DrupalTaxonomyTermAttributes, -} from "../types/drupal"; - -type ContentType = "article" | "page" | "learning_resource"; -type Vocabulary = "tags" | "categories"; - -interface FetchOptions { - baseUrl?: string; - useProxy?: boolean; -} - -interface NodeListParams extends JsonApiParams { - contentType: ContentType; - langcode?: "en" | "vp-VL"; // Support English and Viossa translations - includeUnpublished?: boolean; // New parameter for preview mode -} - -interface NodeParams extends JsonApiParams { - id: string; - contentType: ContentType; - include?: string[]; - langcode?: "en" | "vp-VL"; - includeUnpublished?: boolean; // New parameter for preview mode -} - -interface TaxonomyParams { - vocabulary: Vocabulary; - langcode?: "en" | "vp-VL"; -} - -/** - * DrupalService - Client for Drupal JSON:API - * - * Supports two modes: - * 1. Direct: https://cms.viossa.net/jsonapi (requires CORS) - * 2. Proxy: /api/drupal-proxy/ (via viossa.net nginx) - * - * Default uses proxy to avoid CORS issues. - */ -export class DrupalService { - private baseUrl: string; - private authHeaders: Record = {}; - - constructor(options: FetchOptions = {}) { - // Use environment variable or default to proxy path - const envUrl = import.meta.env.VITE_DRUPAL_API_URL; - - if (options.baseUrl) { - this.baseUrl = options.baseUrl; - } else if (envUrl) { - this.baseUrl = envUrl; - } else if (options.useProxy === false) { - // Direct access to Drupal - this.baseUrl = "https://cms.viossa.net/jsonapi"; - } else { - // Default: use proxy - this.baseUrl = "/api/drupal-proxy"; - } - } - - /** - * Set authentication headers for subsequent requests - */ - setAuthHeaders(headers: Record) { - this.authHeaders = { ...headers }; - } - - /** - * Clear authentication headers - */ - clearAuthHeaders() { - this.authHeaders = {}; - } - - /** - * Build query string from JsonApiParams - */ - private buildQueryString(params: JsonApiParams = {}, langcode?: string, includeUnpublished?: boolean): string { - const queryParts: string[] = []; - - // Language filter for translated content - if (langcode) { - queryParts.push(`filter[langcode]=${langcode}`); - } - - // Include unpublished content in preview mode - if (includeUnpublished) { - queryParts.push(`filter[status][condition][path]=status&filter[status][condition][operator]=IS NULL&filter[status][condition][value]=1`); - } - - // Include relationships - if (params.include?.length) { - queryParts.push(`include=${params.include.join(",")}`); - } - - // Sparse fieldsets - if (params.fields) { - const fields = Object.entries(params.fields) - .map(([type, fields]) => `fields[${type}]=${fields.join(",")}`) - .join("&"); - queryParts.push(fields); - } - - // Sorting - if (params.sort?.length) { - queryParts.push(`sort=${params.sort.join(",")}`); - } - - // Pagination - if (params.page) { - if (params.page.limit) { - queryParts.push(`page[limit]=${params.page.limit}`); - } - if (params.page.offset) { - queryParts.push(`page[offset]=${params.page.offset}`); - } - if (params.page.cursor) { - queryParts.push(`page[cursor]=${params.page.cursor}`); - } - } - - // Filtering - if (params.filter) { - const buildFilter = (obj: Record, prefix = "filter"): string[] => { - const parts: string[] = []; - for (const [key, value] of Object.entries(obj)) { - const paramKey = `${prefix}[${key}]`; - if (typeof value === "object" && value !== null && !Array.isArray(value)) { - parts.push(...buildFilter(value as Record, paramKey)); - } else if (Array.isArray(value)) { - parts.push(`${paramKey}=${value.join(",")}`); - } else { - parts.push(`${paramKey}=${value}`); - } - } - return parts; - }; - queryParts.push(...buildFilter(params.filter)); - } - - return queryParts.length > 0 ? `?${queryParts.join("&")}` : ""; - } - -/** - * Fetch wrapper with error handling - */ - private async fetchJson(url: string): Promise { - const headers: Record = { - "Accept": "application/vnd.api+json", - ...this.authHeaders, - }; - - const response = await fetch(url, { - headers, - credentials: 'include', // Include cookies for session auth - }); - - if (!response.ok) { - const errorBody = await response.text(); - let errors: JsonApiError[] = []; - - try { - const errorJson = JSON.parse(errorBody); - errors = errorJson.errors || []; - } catch { - // Not JSON error - } - - throw new DrupalApiError( - `HTTP ${response.status}: ${response.statusText}`, - response.status, - errors - ); - } - - return response.json(); - } - - /** - * Get nodes by content type - */ - async getNodes( - params: NodeListParams - ): Promise> { - const { contentType, langcode, includeUnpublished, ...queryParams } = params; - const queryString = this.buildQueryString(queryParams, langcode, includeUnpublished); - const url = `${this.baseUrl}/node/${contentType}${queryString}`; - - return this.fetchJson>(url); - } - - /** - * Get a single node by ID - * Note: Drupal JSON:API uses /jsonapi/node/{type}/{uuid} for individual nodes - */ - async getNode( - params: NodeParams - ): Promise> { - const { id, contentType, include, langcode, includeUnpublished } = params; - const queryParams: JsonApiParams = include ? { include } : {}; - const queryString = this.buildQueryString(queryParams, langcode, includeUnpublished); - const url = `${this.baseUrl}/node/${contentType}/${id}${queryString}`; - - return this.fetchJson>(url); - } - - /** - * Get taxonomy terms by vocabulary - */ - async getTaxonomyTerms( - params: TaxonomyParams - ): Promise> { - const { vocabulary, langcode } = params; - const queryString = langcode ? this.buildQueryString({}, langcode) : ""; - const url = `${this.baseUrl}/taxonomy_term/${vocabulary}${queryString}`; - - return this.fetchJson>(url); - } - - /** - * Follow pagination links - */ - async getNextPage(links: JsonApiLinks): Promise | null> { - if (!links.next) return null; - - // Handle both string and object link formats - const nextUrl = typeof links.next === "string" ? links.next : links.next; - if (!nextUrl) return null; - - // If relative link, prepend base URL - const url = nextUrl.startsWith("/") ? `${this.baseUrl}${nextUrl}` : nextUrl; - - return this.fetchJson>(url); - } - - /** - * Helper: Extract data array from document - */ - extractData(document: JsonApiDocument): JsonApiResource[] { - return Array.isArray(document.data) ? document.data : [document.data]; - } - - /** - * Helper: Get attribute from resource - */ - getAttribute(resource: JsonApiResource): T { - return resource.attributes; - } - - /** - * Helper: Check for errors in response - */ - hasErrors(document: JsonApiDocument): boolean { - return document.errors !== undefined && document.errors.length > 0; - } -} - -/** - * Custom error class for Drupal API errors - */ -export class DrupalApiError extends Error { - status: number; - errors: JsonApiError[]; - - constructor( - message: string, - status: number, - errors: JsonApiError[] = [] - ) { - super(message); - this.name = "DrupalApiError"; - this.status = status; - this.errors = errors; - } -} - -// Export singleton instance with default configuration -export const drupalService = new DrupalService(); \ No newline at end of file diff --git a/apps/vdn-static/src/services/PreviewDrupalService.ts b/apps/vdn-static/src/services/PreviewDrupalService.ts deleted file mode 100644 index 0cd93f4..0000000 --- a/apps/vdn-static/src/services/PreviewDrupalService.ts +++ /dev/null @@ -1,367 +0,0 @@ -/// -/** - * Enhanced Drupal Service with Preview Mode Support - * - * Extends the base DrupalService to handle preview mode functionality - * for viewing unpublished content. - */ - -import type { - JsonApiParams, - JsonApiDocument, - JsonApiResource, - JsonApiLinks, - JsonApiError, - DrupalNodeAttributes, - DrupalTaxonomyTermAttributes, -} from '../types/drupal'; - -type ContentType = "article" | "page" | "learning_resource"; -type Vocabulary = "tags" | "categories"; - -interface FetchOptions { - baseUrl?: string; - useProxy?: boolean; -} - -interface NodeListParams extends JsonApiParams { - contentType: ContentType; - langcode?: "en" | "vp-VL"; - includeUnpublished?: boolean; // New parameter for preview mode - previewContext?: { - entityType: string; - entityId: string; - token?: string; - }; -} - -interface NodeParams extends JsonApiParams { - id: string; - contentType: ContentType; - include?: string[]; - langcode?: "en" | "vp-VL"; - includeUnpublished?: boolean; // New parameter for preview mode - previewContext?: { - entityType: string; - entityId: string; - token?: string; - }; -} - -interface TaxonomyParams { - vocabulary: Vocabulary; - langcode?: "en" | "vp-VL"; -} - -/** - * Enhanced DrupalService with Preview Mode Support - */ -export class PreviewDrupalService { - private baseUrl: string; - private authHeaders: Record = {}; - private previewEnabled: boolean = false; - private previewConfig: { - entityType?: string; - entityId?: string; - token?: string; - } = {}; - - constructor(options: FetchOptions = {}) { - // Use environment variable or default to proxy path - const envUrl = import.meta.env.VITE_DRUPAL_API_URL; - - if (options.baseUrl) { - this.baseUrl = options.baseUrl; - } else if (envUrl) { - this.baseUrl = envUrl; - } else if (options.useProxy === false) { - // Direct access to Drupal - this.baseUrl = "https://cms.viossa.net/jsonapi"; - } else { - // Default: use proxy - this.baseUrl = "/api/drupal-proxy"; - } - } - - /** - * Set authentication headers for subsequent requests - */ - setAuthHeaders(headers: Record) { - this.authHeaders = { ...headers }; - } - - /** - * Clear authentication headers - */ - clearAuthHeaders() { - this.authHeaders = {}; - } - - /** - * Enable preview mode - */ - enablePreviewMode(config: { entityType?: string; entityId?: string; token?: string } = {}) { - this.previewEnabled = true; - this.previewConfig = { ...config }; - console.log('Preview mode enabled', config); - } - - /** - * Disable preview mode - */ - disablePreviewMode() { - this.previewEnabled = false; - this.previewConfig = {}; - console.log('Preview mode disabled'); - } - - /** - * Check if preview mode is enabled - */ - isPreviewEnabled(): boolean { - return this.previewEnabled; - } - - /** - * Get preview configuration - */ - getPreviewConfig() { - return { ...this.previewConfig }; - } - - /** - * Add preview headers to request - */ - private getPreviewHeaders(): Record { - const headers: Record = { ...this.authHeaders }; - - if (this.previewEnabled && this.previewConfig.token) { - headers['X-Preview-Token'] = this.previewConfig.token; - } - - if (this.previewEnabled) { - headers['X-Preview-Mode'] = 'true'; - } - - return headers; - } - - /** - * Build query string from JsonApiParams with preview support - */ - private buildQueryString(params: JsonApiParams = {}, langcode?: string, includeUnpublished?: boolean): string { - const queryParts: string[] = []; - - // Language filter for translated content - if (langcode) { - queryParts.push(`filter[langcode]=${langcode}`); - } - - // Include unpublished content in preview mode - if (includeUnpublished) { - queryParts.push(`filter[status][condition][path]=status&filter[status][condition][operator]=IS NULL&filter[status][condition][value]=1`); - } - - // Include relationships - if (params.include?.length) { - queryParts.push(`include=${params.include.join(",")}`); - } - - // Sparse fieldsets - if (params.fields) { - const fields = Object.entries(params.fields) - .map(([type, fields]) => `fields[${type}]=${fields.join(",")}`) - .join("&"); - queryParts.push(fields); - } - - // Sorting - if (params.sort?.length) { - queryParts.push(`sort=${params.sort.join(",")}`); - } - - // Pagination - if (params.page) { - if (params.page.limit) { - queryParts.push(`page[limit]=${params.page.limit}`); - } - if (params.page.offset) { - queryParts.push(`page[offset]=${params.page.offset}`); - } - if (params.page.cursor) { - queryParts.push(`page[cursor]=${params.page.cursor}`); - } - } - - // Filtering - if (params.filter) { - const buildFilter = (obj: Record, prefix = "filter"): string[] => { - const parts: string[] = []; - for (const [key, value] of Object.entries(obj)) { - const paramKey = `${prefix}[${key}]`; - if (typeof value === "object" && value !== null && !Array.isArray(value)) { - parts.push(...buildFilter(value as Record, paramKey)); - } else if (Array.isArray(value)) { - parts.push(`${paramKey}=${value.join(",")}`); - } else { - parts.push(`${paramKey}=${value}`); - } - } - return parts; - }; - queryParts.push(...buildFilter(params.filter)); - } - - return queryParts.length > 0 ? `?${queryParts.join("&")}` : ""; - } - - /** - * Fetch wrapper with error handling and preview support - */ - private async fetchJson(url: string): Promise { - const headers: Record = { - "Accept": "application/vnd.api+json", - ...this.getPreviewHeaders(), - }; - - const response = await fetch(url, { - headers, - credentials: 'include', // Include cookies for session auth - }); - - if (!response.ok) { - const errorBody = await response.text(); - let errors: JsonApiError[] = []; - - try { - const errorJson = JSON.parse(errorBody); - errors = errorJson.errors || []; - } catch { - // Not JSON error - } - - throw new DrupalApiError( - `HTTP ${response.status}: ${response.statusText}`, - response.status, - errors - ); - } - - return response.json(); - } - - /** - * Get nodes by content type with preview support - */ - async getNodes( - params: NodeListParams - ): Promise> { - const { contentType, langcode, includeUnpublished, previewContext, ...queryParams } = params; - const queryString = this.buildQueryString(queryParams, langcode, includeUnpublished); - const url = `${this.baseUrl}/node/${contentType}${queryString}`; - - return this.fetchJson>(url); - } - - /** - * Get a single node by ID with preview support - */ - async getNode( - params: NodeParams - ): Promise> { - const { id, contentType, include, langcode, includeUnpublished, previewContext } = params; - const queryParams: JsonApiParams = include ? { include } : {}; - const queryString = this.buildQueryString(queryParams, langcode, includeUnpublished); - const url = `${this.baseUrl}/node/${contentType}/${id}${queryString}`; - - return this.fetchJson>(url); - } - - /** - * Get taxonomy terms by vocabulary (no preview support for taxonomy) - */ - async getTaxonomyTerms( - params: TaxonomyParams - ): Promise> { - const { vocabulary, langcode } = params; - const queryString = langcode ? this.buildQueryString({}, langcode) : ""; - const url = `${this.baseUrl}/taxonomy_term/${vocabulary}${queryString}`; - - return this.fetchJson>(url); - } - - /** - * Follow pagination links - */ - async getNextPage(links: JsonApiLinks): Promise | null> { - if (!links.next) return null; - - // Handle both string and object link formats - const nextUrl = typeof links.next === "string" ? links.next : links.next; - if (!nextUrl) return null; - - // If relative link, prepend base URL - const url = nextUrl.startsWith("/") ? `${this.baseUrl}${nextUrl}` : nextUrl; - - return this.fetchJson>(url); - } - - /** - * Helper: Extract data array from document - */ - extractData(document: JsonApiDocument): JsonApiResource[] { - return Array.isArray(document.data) ? document.data : [document.data]; - } - - /** - * Helper: Get attribute from resource - */ - getAttribute(resource: JsonApiResource): T { - return resource.attributes; - } - - /** - * Helper: Check for errors in response - */ - hasErrors(document: JsonApiDocument): boolean { - return document.errors !== undefined && document.errors.length > 0; - } - - /** - * Get preview URL for a specific entity - */ - getPreviewUrl(entityType: string, entityId: string): string { - const baseUrl = window.location.origin; - return `${baseUrl}/preview?entity_type=${entityType}&entity_id=${entityId}`; - } - - /** - * Generate Drupal preview link format - */ - generateDrupalPreviewLink(entityType: string, entityId: string): string { - const baseUrl = this.baseUrl.replace('/jsonapi', ''); - return `${baseUrl}/node/${entityType}/${entityId}/preview`; - } -} - -/** - * Custom error class for Drupal API errors - */ -export class DrupalApiError extends Error { - status: number; - errors: JsonApiError[]; - - constructor( - message: string, - status: number, - errors: JsonApiError[] = [] - ) { - super(message); - this.name = "DrupalApiError"; - this.status = status; - this.errors = errors; - } -} - -// Export singleton instance with default configuration -export const previewDrupalService = new PreviewDrupalService(); \ No newline at end of file diff --git a/apps/vdn-static/src/stores/user.ts b/apps/vdn-static/src/stores/user.ts deleted file mode 100644 index fcc872b..0000000 --- a/apps/vdn-static/src/stores/user.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { defineStore } from 'pinia' -import { ref, computed } from 'vue' - -interface User { - uid: string - name: string - mail: string - roles?: string[] -} - -interface AuthTokens { - access_token: string - refresh_token?:string - expires_at?: number -} - -export const useUserStore = defineStore('user', () => { - // State - const user = ref(null) - const tokens = ref(null) - const loading = ref(false) - const error = ref(null) - - // Computed - const isAuthenticated = computed(() => !!tokens.value?.access_token && !!user.value) - - const isAdmin = computed(() => { - if (!user.value?.roles) return false - return user.value.roles.includes('administrator') || user.value.roles.includes('admin') - }) - - // Initialize from localStorage - const initializeFromStorage = () => { - try { - const storedUser = localStorage.getItem('viossa_user') - const storedTokens = localStorage.getItem('viossa_tokens') - - if (storedUser) { - user.value = JSON.parse(storedUser) - } - - if (storedTokens) { - tokens.value = JSON.parse(storedTokens) - } - } catch (e) { - console.error('Failed to load auth state from localStorage:', e) - // Clear corrupted data - localStorage.removeItem('viossa_user') - localStorage.removeItem('viossa_tokens') - } - } - - // Actions - const setUser = (newUser: User | null) => { - user.value = newUser - if (newUser) { - localStorage.setItem('viossa_user', JSON.stringify(newUser)) - } else { - localStorage.removeItem('viossa_user') - } - } - - const setTokens = (newTokens: AuthTokens | null) => { - tokens.value = newTokens - if (newTokens) { - localStorage.setItem('viossa_tokens', JSON.stringify(newTokens)) - } else { - localStorage.removeItem('viossa_tokens') - } - } - - const logout = () => { - setUser(null) - setTokens(null) - error.value = null - } - - const clearError = () => { - error.value = null - } - - // Initialize on store creation - initializeFromStorage() - - return { - // State - user, - tokens, - loading, - error, - - // Computed - isAuthenticated, - isAdmin, - - // Actions - setUser, - setTokens, - logout, - clearError, - initializeFromStorage, - } -}) \ No newline at end of file diff --git a/apps/vdn-static/src/types/drupal.ts b/apps/vdn-static/src/types/drupal.ts deleted file mode 100644 index d66caca..0000000 --- a/apps/vdn-static/src/types/drupal.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * TypeScript interfaces for Drupal JSON:API responses. - * Based on JSON:API specification 1.1 - */ - -// JSON:API Resource Object -export interface JsonApiResource> { - type: string; - id: string; - attributes: T; - relationships?: Record; - links?: JsonApiLinks; -} - -// JSON:API Relationship Object -export interface JsonApiRelationship { - data: JsonApiResourceIdentifier | JsonApiResourceIdentifier[]; - links?: JsonApiLinks; -} - -// JSON:API Resource Identifier -export interface JsonApiResourceIdentifier { - type: string; - id: string; -} - -// JSON:API Links Object -export interface JsonApiLinks { - self?: string; - next?: string; - prev?: string; - first?: string; - last?: string; - [key: string]: string | undefined; -} - -// JSON:API Document (Top-level response) -export interface JsonApiDocument> { - jsonapi?: { version: string; meta?: Record }; - data: JsonApiResource | JsonApiResource[]; - included?: JsonApiResource[]; - links?: JsonApiLinks; - meta?: Record; - errors?: JsonApiError[]; -} - -// JSON:API Error Object -export interface JsonApiError { - id?: string; - status?: string; - code?: string; - title?: string; - detail?: string; - source?: { pointer?: string; parameter?: string }; - links?: JsonApiLinks; -} - -// JSON:API Query Parameters -export interface JsonApiParams { - include?: string[]; - fields?: Record; - sort?: string[]; - page?: { limit?: number; offset?: number; cursor?: string }; - filter?: Record; -} - -// Drupal Node Attributes (Base) -export interface DrupalNodeAttributes { - title: string; - created: string; - changed: string; - status: boolean; - sticky: boolean; - promote: boolean; - path?: { alias: string; pid: number; langcode: string }; - body?: { - value: string; - format: string; - summary: string; - processed: string; - }; -} - -// Drupal Taxonomy Term Attributes -export interface DrupalTaxonomyTermAttributes { - name: string; - description?: { value: string; format: string; processed: string }; - weight: number; - parent?: JsonApiResourceIdentifier[]; -} - -// Content Type Specific Interfaces -export interface ArticleNodeAttributes extends DrupalNodeAttributes { - // Article-specific fields can be added here -} - -export interface PageNodeAttributes extends DrupalNodeAttributes { - // Page-specific fields can be added here -} - -export interface LearningResourceNodeAttributes extends DrupalNodeAttributes { - // Learning resource-specific fields can be added here -} - -// Typed Response Helpers -export type ArticleNode = JsonApiResource; -export type PageNode = JsonApiResource; -export type LearningResourceNode = JsonApiResource; - -export type TaxonomyTerm = JsonApiResource; - -// Collection Response Types -export type NodeList> = JsonApiDocument; -export type SingleNode> = JsonApiDocument; -export type TaxonomyTermList = JsonApiDocument; \ No newline at end of file diff --git a/apps/vdn-static/src/types/route-meta.d.ts b/apps/vdn-static/src/types/route-meta.d.ts deleted file mode 100644 index ab58a94..0000000 --- a/apps/vdn-static/src/types/route-meta.d.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type { RouteLocationNormalizedLoaded } from 'vue-router' -import type { MetaConfig } from '../composables/useMeta' - -/** - * Extended route meta interface for SEO and meta tag configuration - * - * This extends Vue Router's built-in RouteMeta interface to include - * SEO-related fields for meta tag generation. - * - * @example - * ```typescript - * // In a page component with definePage macro - * definePage({ - * meta: { - * title: 'Home Page', - * description: 'Welcome to our website', - * og: { - * type: 'website', - * image: '/og-home.jpg' - * } - * } - * }) - * ``` - */ -declare module 'vue-router' { - interface RouteMeta { - /** - * Page title for meta tag and document.title - * Can be static string or a function that receives the route - */ - title?: string | ((route: RouteLocationNormalizedLoaded) => string) - - /** - * Meta description for SEO - * Can be static string or a function that receives the route - */ - description?: string | ((route: RouteLocationNormalizedLoaded) => string) - - /** - * Meta keywords (comma-separated string or array) - */ - keywords?: string | string[] | ((route: RouteLocationNormalizedLoaded) => string | string[]) - - /** - * Author meta tag - */ - author?: string | ((route: RouteLocationNormalizedLoaded) => string) - - /** - * Canonical URL for the page - */ - canonical?: string | ((route: RouteLocationNormalizedLoaded) => string) - - /** - * OpenGraph configuration for social media sharing - */ - og?: MetaConfig['og'] - - /** - * Twitter Card configuration for Twitter sharing - */ - twitter?: MetaConfig['twitter'] - - /** - * Custom meta tags for specialized use cases - */ - custom?: MetaConfig['custom'] - - /** - * Locale-specific meta configuration - * Allows different meta content based on current locale - */ - localeMeta?: { - [locale: string]: { - title?: string - description?: string - keywords?: string | string[] - author?: string - } - } - - /** - * Whether this route requires SSR - */ - ssr?: boolean - - /** - * Whether to index this page for search engines - */ - index?: boolean - - /** - * Whether to follow links on this page - */ - follow?: boolean - - /** - * Robots meta tag value - */ - robots?: string - - /** - * Custom hreflang tags for international SEO - */ - hreflang?: { - [locale: string]: string - } - } -} - -/** - * Helper type for extracting route meta values - */ -export type RouteMetaValue = T | ((route: RouteLocationNormalizedLoaded) => T) - -/** - * Helper function type for dynamic meta values - */ -export type MetaValueFunction = (route: RouteLocationNormalizedLoaded) => T - -/** - * Type guard for checking if a meta value is a function - */ -export function isMetaFunction( - value: RouteMetaValue -): value is MetaValueFunction { - return typeof value === 'function' -} - -/** - * Type for route with extended meta - */ -export interface RouteWithMeta { - meta: RouteMeta - name?: string | symbol - path: string -} - -/** - * Type for pages that support meta configuration - */ -export interface PageMetaConfig { - meta?: RouteMeta -} \ No newline at end of file diff --git a/apps/vdn-static/src/utils/drupal-meta.ts b/apps/vdn-static/src/utils/drupal-meta.ts deleted file mode 100644 index 9078f38..0000000 --- a/apps/vdn-static/src/utils/drupal-meta.ts +++ /dev/null @@ -1,252 +0,0 @@ -/** - * Drupal Content Type Meta Tag Integration - * - * This module provides utilities for generating meta tags from Drupal content types - * via the JSON:API responses. - */ - -import { computed } from 'vue' -import { useRoute } from 'vue-router' -import { useMeta, type MetaConfig } from '../composables/useMeta' -import type { - ArticleNode, - PageNode, - LearningResourceNode, - DrupalNodeAttributes -} from '../types/drupal' - -/** - * Generates meta configuration from a Drupal node - * - * @param node - Drupal node from JSON:API response - * @param baseUrl - Base URL for canonical links - * @returns Meta configuration object - */ -export function generateDrupalNodeMeta( - node: ArticleNode | PageNode | LearningResourceNode, - baseUrl: string = 'https://viossa.net' -): MetaConfig { - const { attributes } = node - const path = attributes.path?.alias || `node/${node.id}` - - const meta: MetaConfig = { - title: attributes.title, - description: attributes.body?.summary || generateExcerpt(attributes.body?.processed || ''), - canonical: `${baseUrl}${path}`, - author: 'Viossa Team', // Could be extracted from relationships if available - og: { - type: 'article', - title: attributes.title, - description: attributes.body?.summary || generateExcerpt(attributes.body?.processed || ''), - 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', - }) - } - - return meta -} - -/** - * Generates meta configuration specifically for Article content type - * - * @param node - Article node from JSON:API - * @param baseUrl - Base URL for canonical links - * @returns Meta configuration object - */ -export function generateArticleContentMeta( - node: ArticleNode, - baseUrl: string = 'https://viossa.net' -): MetaConfig { - const baseMeta = generateDrupalNodeMeta(node, baseUrl) - - return { - ...baseMeta, - og: { - ...baseMeta.og, - type: 'article', - }, - custom: [ - ...(baseMeta.custom || []), - // Article-specific meta tags - { - property: 'article:section', - content: 'Language Learning', - }, - ], - } -} - -/** - * Generates meta configuration specifically for Page content type - * - * @param node - Page node from JSON:API - * @param baseUrl - Base URL for canonical links - * @returns Meta configuration object - */ -export function generatePageContentMeta( - node: PageNode, - baseUrl: string = 'https://viossa.net' -): MetaConfig { - const baseMeta = generateDrupalNodeMeta(node, baseUrl) - - return { - ...baseMeta, - og: { - ...baseMeta.og, - type: 'website', - }, - } -} - -/** - * Generates meta configuration specifically for Learning Resource content type - * - * @param node - Learning Resource node from JSON:API - * @param baseUrl - Base URL for canonical links - * @returns Meta configuration object - */ -export function generateLearningResourceMeta( - node: LearningResourceNode, - baseUrl: string = 'https://viossa.net' -): MetaConfig { - const baseMeta = generateDrupalNodeMeta(node, baseUrl) - - return { - ...baseMeta, - title: `${baseMeta.title} - Viossa Resources`, - og: { - ...baseMeta.og, - type: 'article', - }, - custom: [ - ...(baseMeta.custom || []), - // Learning resource-specific meta tags - { - name: 'resource-type', - content: 'learning-material', - }, - ], - } -} - -/** - * Helper function to generate an excerpt from HTML content - * - * @param html - HTML content string - * @param maxLength - Maximum length of excerpt (default: 160) - * @returns Plain text excerpt - */ -function generateExcerpt(html: string, maxLength: number = 160): string { - // Strip HTML tags - const text = html.replace(/<[^>]*>/g, '') - - // Decode HTML entities - const decoded = text - .replace(/ /g, ' ') - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, "'") - - // Trim to max length - if (decoded.length <= maxLength) { - return decoded - } - - // Truncate at word boundary - const truncated = decoded.substring(0, maxLength) - const lastSpace = truncated.lastIndexOf(' ') - - return truncated.substring(0, lastSpace > 0 ? lastSpace : maxLength) + '...' -} - -/** - * Composable for fetching and applying Drupal node meta tags - * - * @example - * ```vue - * - * ``` - */ -export function useDrupalNodeMeta( - nodeResult: { value: ArticleNode | PageNode | LearningResourceNode | null }, - baseUrl?: string -) { - const route = useRoute() - - // Determine which meta generator to use based on node type - const getMetaGenerator = (node: ArticleNode | PageNode | LearningResourceNode) => { - switch (node.type) { - case 'node--article': - return generateArticleContentMeta - case 'node--page': - return generatePageContentMeta - case 'node--learning_resource': - return generateLearningResourceMeta - default: - return generateDrupalNodeMeta - } - } - - // Compute meta config based on current node - const metaConfig = computed(() => { - const node = nodeResult.value - if (!node) { - return { - title: 'Loading...', - description: '', - } - } - - const generator = getMetaGenerator(node) - return generator(node, baseUrl) - }) - - // Apply meta tags - useMeta(metaConfig, route) -} \ No newline at end of file diff --git a/apps/vdn-static/src/viossa.net/apps/vdn-static/src/composables/useMeta.ts b/apps/vdn-static/src/viossa.net/apps/vdn-static/src/composables/useMeta.ts deleted file mode 100644 index 4d5766c..0000000 --- a/apps/vdn-static/src/viossa.net/apps/vdn-static/src/composables/useMeta.ts +++ /dev/null @@ -1,438 +0,0 @@ -import { - watch, - onMounted, - onUnmounted, - type Ref, - type ComputedRef, - unref, - nextTick, -} from 'vue' -import type { RouteLocationNormalizedLoaded } from 'vue-router' - -/** - * Meta tag configuration interface - */ -export interface MetaConfig { - /** Page title */ - title?: string | ((route: RouteLocationNormalizedLoaded) => string) - /** Meta description */ - description?: string | ((route: RouteLocationNormalizedLoaded) => string) - /** Meta keywords */ - keywords?: string | string[] | ((route: RouteLocationNormalizedLoaded) => string | string[]) - /** Author meta tag */ - author?: string | ((route: RouteLocationNormalizedLoaded) => string) - /** Canonical URL */ - canonical?: string | ((route: RouteLocationNormalizedLoaded) => string) - /** OpenGraph configuration */ - og?: OpenGraphConfig | ((route: RouteLocationNormalizedLoaded) => OpenGraphConfig) - /** Twitter Card configuration */ - twitter?: TwitterCardConfig | ((route: RouteLocationNormalizedLoaded) => TwitterCardConfig) - /** Custom meta tags */ - custom?: CustomMetaTag[] | ((route: RouteLocationNormalizedLoaded) => CustomMetaTag[]) - /** Whether to auto-append locale to title */ - appendLocale?: boolean - /** Title template for formatting */ - titleTemplate?: (title: string) => string -} - -/** - * OpenGraph meta tag configuration - */ -export interface OpenGraphConfig { - type?: 'website' | 'article' | 'product' | 'profile' | 'book' | 'video' | 'music' - title?: string - description?: string - image?: string - url?: string - siteName?: string - locale?: string - localeAlternate?: string[] - video?: string - audio?: string -} - -/** - * Twitter Card meta tag configuration - */ -export interface TwitterCardConfig { - card?: 'summary' | 'summary_large_image' | 'app' | 'player' - site?: string - creator?: string - title?: string - description?: string - image?: string - imageAlt?: string - player?: string - playerWidth?: number - playerHeight?: number -} - -/** - * Custom meta tag definition - */ -export interface CustomMetaTag { - name?: string - property?: string - content: string | ((route: RouteLocationNormalizedLoaded) => string) - charset?: string - httpEquiv?: string -} - -/** - * Locale-aware meta tag content - */ -export interface LocaleAwareMeta { - [locale: string]: string -} - -/** - * Meta tag element with tracking - */ -interface ManagedTag { - element: HTMLMetaElement | HTMLTitleElement | HTMLLinkElement - type: 'title' | 'meta' | 'link' -} - -/** - * Default meta configuration - */ -const DEFAULT_META: Partial = { - appendLocale: false, - titleTemplate: (title) => title, -} - -/** - * Meta tag management composable for Vue.js applications - * - * This composable manages all aspects of meta tag generation and updates: - * - Standard HTML meta tags (title, description, keywords, author, canonical) - * - OpenGraph tags for social media sharing - * - Twitter Card tags for Twitter sharing - * - Custom meta tags for specialized use cases - * - i18n integration for multi-language support - * - SSR support for server-side rendering - * - Automatic reactive updates - * - * @example - * ```typescript - * // Basic usage - * useMeta({ - * title: 'Home Page', - * description: 'Welcome to our website', - * }) - * - * // With dynamic route-based content - * useMeta({ - * title: (route) => `Resource: ${route.params.id}`, - * description: 'Learning resources and tutorials', - * og: { - * type: 'article', - * image: '/default-og-image.jpg' - * } - * }) - * - * // With i18n integration - * useMeta({ - * title: { 'en-US': 'Welcome', 'es-LA': 'Bienvenido' }, - * description: { 'en-US': 'Welcome to our site', 'es-LA': 'Bienvenido a nuestro sitio' } - * }, locale) - * ``` - * - * @param config - Meta configuration object - * @param route - Current route (optional, auto-injected) - * @param locale - Current locale reference (optional) - * @returns Cleanup function to remove managed meta tags - */ -export function useMeta( - config: MetaConfig | Ref | ComputedRef, - route?: RouteLocationNormalizedLoaded | Ref, - locale?: Ref -): () => void { - // Track managed elements for cleanup - const managedElements: ManagedTag[] = [] - - /** - * Creates or updates a meta element - */ - const setMetaTag = ( - name?: string, - property?: string, - content?: string, - charset?: string, - httpEquiv?: string - ): void => { - if (!content && !charset) return - - // Find existing tag or create new one - let element: HTMLMetaElement | null = null - - if (name) { - element = document.querySelector(`meta[name="${name}"]`) - } else if (property) { - element = document.querySelector(`meta[property="${property}"]`) - } else if (charset) { - element = document.querySelector('meta[charset]') - } else if (httpEquiv) { - element = document.querySelector(`meta[http-equiv="${httpEquiv}"]`) - } - - if (!element) { - element = document.createElement('meta') - if (name) element.setAttribute('name', name) - if (property) element.setAttribute('property', property) - if (charset) element.setAttribute('charset', charset) - if (httpEquiv) element.setAttribute('http-equiv', httpEquiv) - document.head.appendChild(element) - managedElements.push({ element, type: 'meta' }) - } - - if (content) { - element.setAttribute('content', content) - } - } - - /** - * Creates or updates a link element - */ - const setLinkTag = (rel: string, href: string): void => { - if (!href) return - - let element: HTMLLinkElement | null = document.querySelector(`link[rel="${rel}"]`) - - if (!element) { - element = document.createElement('link') - element.setAttribute('rel', rel) - document.head.appendChild(element) - managedElements.push({ element, type: 'link' }) - } - - element.setAttribute('href', href) - } - - /** - * Sets the page title - */ - const setTitle = (title: string): void => { - if (!title) return - - let titleElement: HTMLTitleElement | null = document.querySelector('title') - - if (!titleElement) { - titleElement = document.createElement('title') - document.head.appendChild(titleElement) - managedElements.push({ element: titleElement, type: 'title' }) - } - - const template = unref(config).titleTemplate || DEFAULT_META.titleTemplate! - titleElement.textContent = template(title) - } - - /** - * Resolves a meta value that can be static, reactive, or a function - */ - const resolveValue = ( - value: T | ((route: RouteLocationNormalizedLoaded) => T) | undefined, - routeValue: RouteLocationNormalizedLoaded - ): T | undefined => { - if (value === undefined) return undefined - return typeof value === 'function' ? value(routeValue) : value - } - - /** - * Resolves locale-aware content - */ - const resolveLocaleContent = ( - content: string | LocaleAwareMeta | undefined, - currentLocale?: string - ): string | undefined => { - if (content === undefined) return undefined - - if (typeof content === 'string') { - return content - } - - // It's a locale-aware object - if (currentLocale && content[currentLocale]) { - return content[currentLocale] - } - - // Fallback to first available locale or default - return content['en-US'] || Object.values(content)[0] - } - - /** - * Updates all meta tags - */ - const updateMeta = (): void => { - const configValue = unref(config) - const routeValue = route ? unref(route) : null - - // Resolve standard meta tags - const title = resolveValue(configValue.title, routeValue!) - const description = resolveValue(configValue.description, routeValue!) - const keywords = resolveValue(configValue.keywords, routeValue!) - const author = resolveValue(configValue.author, routeValue!) - const canonical = resolveValue(configValue.canonical, routeValue!) - - // Set standard meta tags - if (title) { - const currentLocale = locale?.value - const localizedTitle = resolveLocaleContent(title, currentLocale) - if (localizedTitle) { - setTitle(localizedTitle) - } - } - - if (description) { - const currentLocale = locale?.value - const localizedDesc = resolveLocaleContent(description, currentLocale) - if (localizedDesc) { - setMetaTag('description', undefined, localizedDesc) - } - } - - if (keywords) { - const keywordsValue = Array.isArray(keywords) ? keywords.join(', ') : keywords - setMetaTag('keywords', undefined, keywordsValue) - } - - if (author) { - setMetaTag('author', undefined, author) - } - - if (canonical) { - setLinkTag('canonical', canonical) - } - - // Set OpenGraph tags - const ogConfig = resolveValue(configValue.og, routeValue!) - if (ogConfig) { - setOpenGraphTags(ogConfig, locale?.value) - } - - // Set Twitter Card tags - const twitterConfig = resolveValue(configValue.twitter, routeValue!) - if (twitterConfig) { - setTwitterCardTags(twitterConfig, locale?.value) - } - - // Set custom meta tags - const customTags = resolveValue(configValue.custom, routeValue!) - if (customTags && Array.isArray(customTags)) { - customTags.forEach((tag) => { - const content = typeof tag.content === 'function' - ? tag.content(routeValue!) - : tag.content - if (content) { - setMetaTag(tag.name, tag.property, content, tag.charset, tag.httpEquiv) - } - }) - } - } - - /** - * Sets OpenGraph meta tags - */ - const setOpenGraphTags = (config: OpenGraphConfig, currentLocale?: string): void => { - if (config.type) setMetaTag(undefined, 'og:type', config.type) - if (config.title) { - const localized = resolveLocaleContent(config.title, currentLocale) - if (localized) setMetaTag(undefined, 'og:title', localized) - } - if (config.description) { - const localized = resolveLocaleContent(config.description, currentLocale) - if (localized) setMetaTag(undefined, 'og:description', localized) - } - if (config.image) setMetaTag(undefined, 'og:image', config.image) - if (config.url) setMetaTag(undefined, 'og:url', config.url) - if (config.siteName) setMetaTag(undefined, 'og:site_name', config.siteName) - if (config.locale) setMetaTag(undefined, 'og:locale', config.locale) - if (config.localeAlternate) { - config.localeAlternate.forEach((loc) => { - setMetaTag(undefined, 'og:locale:alternate', loc) - }) - } - if (config.video) setMetaTag(undefined, 'og:video', config.video) - if (config.audio) setMetaTag(undefined, 'og:audio', config.audio) - } - - /** - * Sets Twitter Card meta tags - */ - const setTwitterCardTags = (config: TwitterCardConfig, currentLocale?: string): void => { - if (config.card) setMetaTag('twitter:card', undefined, config.card) - if (config.site) setMetaTag('twitter:site', undefined, config.site) - if (config.creator) setMetaTag('twitter:creator', undefined, config.creator) - if (config.title) { - const localized = resolveLocaleContent(config.title, currentLocale) - if (localized) setMetaTag('twitter:title', undefined, localized) - } - if (config.description) { - const localized = resolveLocaleContent(config.description, currentLocale) - if (localized) setMetaTag('twitter:description', undefined, localized) - } - if (config.image) setMetaTag('twitter:image', undefined, config.image) - if (config.imageAlt) setMetaTag('twitter:image:alt', undefined, config.imageAlt) - if (config.player) setMetaTag('twitter:player', undefined, config.player) - if (config.playerWidth) setMetaTag('twitter:player:width', undefined, String(config.playerWidth)) - if (config.playerHeight) setMetaTag('twitter:player:height', undefined, String(config.playerHeight)) - } - - /** - * Cleanup function to remove managed meta tags - */ - const cleanup = (): void => { - managedElements.forEach(({ element }) => { - if (element.parentNode) { - element.parentNode.removeChild(element) - } - }) - managedElements.length = 0 - } - - // Setup reactive watcher - let stopWatch: (() => void) | null = null - - onMounted(() => { - // Initial update - nextTick(updateMeta) - - // Watch for changes - stopWatch = watch( - () => [unref(config), route ? unref(route) : null, locale?.value], - () => { - updateMeta() - }, - { deep: true } - ) - }) - - // Cleanup on unmount - onUnmounted(() => { - if (stopWatch) { - stopWatch() - } - cleanup() - }) - - return cleanup -} - -/** - * Utility function to create meta configuration for a route - */ -export function createRouteMeta( - meta: MetaConfig -): MetaConfig { - return { ...DEFAULT_META, ...meta } -} - -/** - * Utility function to merge meta configurations - */ -export function mergeMeta( - base: MetaConfig, - override: Partial -): MetaConfig { - return { ...base, ...override } -} \ No newline at end of file diff --git a/apps/vdn-static/src/vite-env.d.ts b/apps/vdn-static/src/vite-env.d.ts index 48fef1e..dabd0de 100644 --- a/apps/vdn-static/src/vite-env.d.ts +++ b/apps/vdn-static/src/vite-env.d.ts @@ -1,10 +1,2 @@ /// /// - -interface ImportMetaEnv { - readonly VITE_DRUPAL_API_URL?: string; -} - -interface ImportMeta { - readonly env: ImportMetaEnv; -} \ No newline at end of file