feat(i18n,drupal): consolidate locale codes and add Drupal infrastructure
Some checks failed
Deploy to Production / deploy (push) Failing after 33s

i18n changes:
- Remove qpv locale code, standardize on vp-VL for Viossa
- Remove wp-VL duplicate, keep locale structure clean
- Update langcode types in DrupalService from 'qpv' to 'vp-VL'

Drupal integration additions:
- Add PreviewDrupalService for viewing unpublished content
- Add authentication system with Pinia store and composables
- Add meta tag management system (SEO, OpenGraph, Twitter)
- Add route guards for protected admin routes
- Add login/preview/admin pages

Locale file changes:
- Deleted: qpv.ftl (duplicate of vp-VL.ftl)
- Kept: en_US.ftl, es_LA.ftl, vp_VL.ftl, wp_VL.ftl
This commit is contained in:
BarnacleBoy 2026-05-15 07:07:28 +00:00
parent 22636f025f
commit 90c7e0cddb
26 changed files with 4833 additions and 41 deletions

View file

@ -0,0 +1,252 @@
/**
* 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(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#039;/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
* <script setup lang="ts">
* import { useDrupalNodeMeta } from '@/composables/drupal-meta'
* import { ref, watch } from 'vue'
*
* const props = defineProps<{ nodeId: string }>()
* const node = ref(null)
*
* // Fetch node data from API
* // ...
*
* // Apply meta tags when node is loaded
* useDrupalNodeMeta(node)
* </script>
* ```
*/
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)
}