feat(i18n,drupal): consolidate locale codes and add Drupal infrastructure
Some checks failed
Deploy to Production / deploy (push) Failing after 33s
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:
parent
22636f025f
commit
90c7e0cddb
26 changed files with 4833 additions and 41 deletions
252
apps/vdn-static/src/utils/drupal-meta.ts
Normal file
252
apps/vdn-static/src/utils/drupal-meta.ts
Normal 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(/ /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
|
||||
* <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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue