Skip to content

Commit 43dc200

Browse files
committed
chore: extract plain text for meta descriptions and refine blog SEO
- Added `extractPlainTextFromExcerpt` utility to process `excerpt` AST. - Updated SEO metadata in blog pages with improved description handling using plain text extraction. - Enhanced type definitions for `BlogSEO` and updated `BlogArticle` interface. - Refined Open Graph (`og`) and Twitter metadata logic in blog components.
1 parent c7cc6c5 commit 43dc200

File tree

3 files changed

+69
-2
lines changed

3 files changed

+69
-2
lines changed

pages/blog/[...slug].vue

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import {definePageMeta} from "#imports";
33
import SocialMediaShare from "~/components/blog/SocialMediaShare.vue";
44
import type { BlogArticle } from "~/types/blog";
5+
import { extractPlainTextFromExcerpt } from "~/utils/content";
56
67
const { locale, t, d } = useI18n()
78
const config = useRuntimeConfig()
@@ -23,6 +24,12 @@ const previewSocial = computed(() =>
2324
})
2425
)
2526
27+
// Canonical URL for OG tags
28+
const baseUrl = computed(() => config.public.siteUrl || config.public.baseUrl || 'https://blog.onelitefeather.net')
29+
const canonicalUrl = computed(() =>
30+
blog.value ? (blog.value.canonical || `${baseUrl.value}/${locale.value}/blog/${blog.value.slug}`) : baseUrl.value
31+
)
32+
2633
// Use a single useHead call to set links (and merge with any useSeoMeta output)
2734
useHead(() => ({ link: headLinks.value }))
2835
useHead(() => (blog.value as BlogArticle | null)?.head || {})
@@ -39,15 +46,20 @@ const title = computed(() => {
3946
useSeoMeta(() => {
4047
const seo = (blog.value as any)?.seo || {}
4148
const metaTitle = seo.title || title.value
42-
const metaDescription = seo.description || blog.value?.description || ''
49+
const metaDescription =
50+
seo.description || blog.value?.description || extractPlainTextFromExcerpt((blog.value as any)?.excerpt) || ''
4351
return {
4452
title: metaTitle,
4553
ogTitle: seo.ogTitle || metaTitle,
4654
twitterTitle: seo.twitterTitle || metaTitle,
4755
description: metaDescription,
4856
ogDescription: seo.ogDescription || metaDescription,
4957
ogImage: previewSocial.value,
50-
twitterImage: previewSocial.value
58+
twitterImage: previewSocial.value,
59+
ogType: 'article',
60+
ogUrl: canonicalUrl.value,
61+
twitterCard: 'summary_large_image',
62+
ogImageAlt: blog.value?.headerImageAlt || blog.value?.title || ''
5163
}
5264
})
5365
</script>

types/blog.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,22 @@ export interface BlogAlternateLanguageLink {
1010
url: string
1111
}
1212

13+
export interface BlogSEO {
14+
title?: string
15+
description?: string
16+
ogTitle?: string
17+
ogDescription?: string
18+
twitterTitle?: string
19+
twitterDescription?: string
20+
}
21+
1322
// Extend content-generated item type with optional new header fields
1423
export type BlogArticle = (
1524
| BlogDeCollectionItem
1625
| BlogEnCollectionItem
1726
) & {
1827
canonical?: string
1928
alternates?: BlogAlternateHeader[]
29+
seo?: BlogSEO
30+
head?: Record<string, any>
2031
}

utils/content.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Utilities for working with @nuxt/content document structures.
3+
* Focus: extracting plain text from the `excerpt` AST for meta descriptions.
4+
*/
5+
6+
export function extractPlainTextFromExcerpt(excerpt: any, maxLength = 180): string {
7+
if (!excerpt) return ''
8+
9+
// Nuxt Content often provides an object with `type` and `children`
10+
const parts: string[] = []
11+
12+
const walk = (node: any) => {
13+
if (!node) return
14+
// Text node
15+
if (typeof node.value === 'string') {
16+
parts.push(node.value)
17+
return
18+
}
19+
// Element with children (paragraphs, links, strong, etc.)
20+
if (Array.isArray(node.children)) {
21+
for (const child of node.children) walk(child)
22+
// Add space between block-ish nodes
23+
if (node.type === 'paragraph') parts.push(' ')
24+
return
25+
}
26+
// Arrays of nodes
27+
if (Array.isArray(node)) {
28+
for (const child of node) walk(child)
29+
return
30+
}
31+
}
32+
33+
walk(excerpt)
34+
35+
const text = parts.join(' ').replace(/\s+/g, ' ').trim()
36+
if (!text) return ''
37+
38+
if (text.length <= maxLength) return text
39+
// Soft trim at word boundary
40+
const clipped = text.slice(0, maxLength)
41+
const lastSpace = clipped.lastIndexOf(' ')
42+
return (lastSpace > 0 ? clipped.slice(0, lastSpace) : clipped).trim() + '…'
43+
}
44+

0 commit comments

Comments
 (0)