mirror of
https://github.com/mmahdium/portfolio.git
synced 2026-08-16 21:14:31 +03:30
feat(blog): update SEO metadata and improve RSS feed handling for blog posts
This commit is contained in:
@@ -1,195 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const route = useRoute()
|
|
||||||
const { data: page, error } = await useAsyncData(route.path, () => queryContent(route.path).findOne())
|
|
||||||
|
|
||||||
if (error.value) {
|
|
||||||
throw createError({
|
|
||||||
statusCode: 404,
|
|
||||||
message: 'Page not found',
|
|
||||||
fatal: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
useSeoMeta({
|
|
||||||
title: page.value?.title,
|
|
||||||
description: page.value?.description,
|
|
||||||
ogTitle: page.value?.title,
|
|
||||||
ogDescription: page.value?.description,
|
|
||||||
})
|
|
||||||
|
|
||||||
const { data: relatedArticles } = await useAsyncData(`content:related-articles:${page.value?.title}`, () => queryContent('/articles').where({ categories: { $in: page.value?.categories }, _extension: 'md' }).only(['_path', 'title', 'categories', 'description', 'publishedAt', 'image', 'authors']).sort({ publishedAt: -1 }).limit(3).find())
|
|
||||||
|
|
||||||
const appConfig = useAppConfig()
|
|
||||||
const runtimeConfig = useRuntimeConfig()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<UContainer
|
|
||||||
v-if="page"
|
|
||||||
>
|
|
||||||
<UPage>
|
|
||||||
<UPageHeader
|
|
||||||
class="max-w-5xl mx-auto"
|
|
||||||
:title="page.title"
|
|
||||||
:description="page.description"
|
|
||||||
>
|
|
||||||
<template #headline>
|
|
||||||
<!-- Waiting for https://github.com/nuxt/ui-pro/issues/114 -->
|
|
||||||
<!-- <dl>
|
|
||||||
<dt class="sr-only">
|
|
||||||
Categories
|
|
||||||
</dt>
|
|
||||||
<dd> -->
|
|
||||||
<template
|
|
||||||
v-for="(category, index) in page.categories"
|
|
||||||
:key="category"
|
|
||||||
>
|
|
||||||
<ULink
|
|
||||||
:to="`/categories/${category}`"
|
|
||||||
>
|
|
||||||
{{ formatCategory(category) }}
|
|
||||||
</ULink>
|
|
||||||
|
|
||||||
<span v-if="index < page.categories.length - 1">
|
|
||||||
-
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<!-- </dd>
|
|
||||||
</dl> -->
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<NuxtImg
|
|
||||||
v-if="page.image"
|
|
||||||
:src="page.image.src"
|
|
||||||
:alt="page.image.alt"
|
|
||||||
class="mt-8 w-full object-cover rounded-lg aspect-[16/9]"
|
|
||||||
>
|
|
||||||
<dl class="mt-8 flex justify-between text-stone-700 text-sm">
|
|
||||||
<dt class="sr-only">
|
|
||||||
Author
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
<ol class="space-x-4">
|
|
||||||
<li
|
|
||||||
v-for="author in page.authors"
|
|
||||||
:key="author.name"
|
|
||||||
>
|
|
||||||
<ULink
|
|
||||||
:to="author.social"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
class="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<UAvatar
|
|
||||||
:src="author.avatar"
|
|
||||||
:alt="author.name"
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
{{ author.name }}
|
|
||||||
</span>
|
|
||||||
</ULink>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</dd>
|
|
||||||
<dt class="sr-only">
|
|
||||||
Published at
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
<time :datetime="page.publishedAt">
|
|
||||||
{{ formatDate(page.publishedAt) }}
|
|
||||||
</time>
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
</nuxtimg>
|
|
||||||
</UPageHeader>
|
|
||||||
|
|
||||||
<div class="mt-8 max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-[96px_768px_1fr]">
|
|
||||||
<div class="lg:px-8 flex lg:flex-col lg:items-end gap-2">
|
|
||||||
<UTooltip text="Share on X">
|
|
||||||
<UButton
|
|
||||||
:to="`https://twitter.com/share?text=${page.title}&url=${runtimeConfig.app.name}${page._path}`"
|
|
||||||
target="_blank"
|
|
||||||
icon="i-simple-icons-x"
|
|
||||||
size="sm"
|
|
||||||
color="primary"
|
|
||||||
square
|
|
||||||
variant="ghost"
|
|
||||||
/>
|
|
||||||
</UTooltip>
|
|
||||||
<UTooltip text="Share on Facebook">
|
|
||||||
<UButton
|
|
||||||
:to="`https://www.facebook.com/sharer/sharer.php?u=${runtimeConfig.app.name}${page._path}&t=${page.title}`"
|
|
||||||
target="_blank"
|
|
||||||
icon="i-simple-icons-facebook"
|
|
||||||
size="sm"
|
|
||||||
color="primary"
|
|
||||||
square
|
|
||||||
variant="ghost"
|
|
||||||
/>
|
|
||||||
</UTooltip>
|
|
||||||
<UTooltip text="Share on LinkedIn">
|
|
||||||
<UButton
|
|
||||||
:to="`https://www.linkedin.com/shareArticle?url=${runtimeConfig.app.name}${page._path}&title=${page.title}`"
|
|
||||||
target="_blank"
|
|
||||||
icon="i-simple-icons-linkedin"
|
|
||||||
size="sm"
|
|
||||||
color="primary"
|
|
||||||
square
|
|
||||||
variant="ghost"
|
|
||||||
/>
|
|
||||||
</UTooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-8 lg:mt-0 w-full">
|
|
||||||
<UPageBody
|
|
||||||
prose
|
|
||||||
:ui="{ wrapper: 'mt-0' }"
|
|
||||||
>
|
|
||||||
<ContentRenderer :value="page" />
|
|
||||||
</UPageBody>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row-start-2 lg:row-start-1 lg:col-start-3 lg:px-8 space-y-8">
|
|
||||||
<UButton
|
|
||||||
v-bind="appConfig.page.article.cta"
|
|
||||||
color="primary"
|
|
||||||
size="lg"
|
|
||||||
:ui="{ base: 'w-full justify-center' }"
|
|
||||||
class="hidden lg:inline-flex"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UDivider />
|
|
||||||
|
|
||||||
<UContentToc
|
|
||||||
:links="page.body?.toc?.links"
|
|
||||||
:ui="{ wrapper: 'top-4', container: { base: 'py-0 pb-3 lg:py-0 lg:pb-8' } }"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UDivider class="lg:hidden" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section v-if="relatedArticles">
|
|
||||||
<h2 class="text-2xl text-stone-900 font-bold">
|
|
||||||
Related Articles
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="mt-3 border-b border-stone-200" />
|
|
||||||
|
|
||||||
<UPageGrid class="mt-8">
|
|
||||||
<ArticleCard
|
|
||||||
v-for="article in relatedArticles"
|
|
||||||
:key="article._path"
|
|
||||||
:to="article._path!"
|
|
||||||
:title="article.title!"
|
|
||||||
:description="article.description"
|
|
||||||
:date="article.publishedAt"
|
|
||||||
:image="article.image"
|
|
||||||
:authors="article.authors"
|
|
||||||
/>
|
|
||||||
</UPageGrid>
|
|
||||||
</section>
|
|
||||||
</UPage>
|
|
||||||
</UContainer>
|
|
||||||
</template>
|
|
||||||
@@ -56,6 +56,8 @@ const siteUrl = 'https://aliarghyani.vercel.app' // TODO: Move to runtime config
|
|||||||
// Custom meta tags
|
// Custom meta tags
|
||||||
if (post.value) {
|
if (post.value) {
|
||||||
const postData = post.value as any
|
const postData = post.value as any
|
||||||
|
const canonicalPath = postData.path ? postData.path.replace(/^\/(en|fa)/, '') : ''
|
||||||
|
const canonicalUrl = canonicalPath ? `${siteUrl}${canonicalPath}` : siteUrl
|
||||||
|
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title: `${postData.title} | ${t('blog.title')}`,
|
title: `${postData.title} | ${t('blog.title')}`,
|
||||||
@@ -64,7 +66,7 @@ if (post.value) {
|
|||||||
ogDescription: postData.description,
|
ogDescription: postData.description,
|
||||||
ogImage: postData.image || '/img/blog/default-cover.jpg',
|
ogImage: postData.image || '/img/blog/default-cover.jpg',
|
||||||
ogType: 'article',
|
ogType: 'article',
|
||||||
ogUrl: `${siteUrl}${postData.path}`,
|
ogUrl: canonicalUrl,
|
||||||
twitterCard: 'summary_large_image',
|
twitterCard: 'summary_large_image',
|
||||||
twitterTitle: postData.title,
|
twitterTitle: postData.title,
|
||||||
twitterDescription: postData.description,
|
twitterDescription: postData.description,
|
||||||
@@ -77,6 +79,14 @@ if (post.value) {
|
|||||||
|
|
||||||
// JSON-LD structured data
|
// JSON-LD structured data
|
||||||
useHead({
|
useHead({
|
||||||
|
link: canonicalPath
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
rel: 'canonical',
|
||||||
|
href: canonicalUrl
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [],
|
||||||
script: [
|
script: [
|
||||||
{
|
{
|
||||||
type: 'application/ld+json',
|
type: 'application/ld+json',
|
||||||
@@ -140,7 +150,7 @@ if (post.value) {
|
|||||||
</article>
|
</article>
|
||||||
|
|
||||||
<!-- Share Buttons -->
|
<!-- Share Buttons -->
|
||||||
<BlogShare :title="(post as any).title" :url="`${siteUrl}${(post as any).path}`" />
|
<BlogShare :title="(post as any).title" :url="`${siteUrl}${(post as any).path.replace(/^\/(en|fa)/, '')}`" />
|
||||||
|
|
||||||
<!-- Blog Navigation (Prev/Next) -->
|
<!-- Blog Navigation (Prev/Next) -->
|
||||||
<BlogNavigation :prev="prevPost" :next="nextPost" />
|
<BlogNavigation :prev="prevPost" :next="nextPost" />
|
||||||
|
|||||||
+3
-2
@@ -175,9 +175,10 @@ export default defineNuxtConfig({
|
|||||||
nitro: {
|
nitro: {
|
||||||
prerender: {
|
prerender: {
|
||||||
crawlLinks: true,
|
crawlLinks: true,
|
||||||
routes: ['/', '/blog', '/fa/blog'],
|
routes: ['/', '/blog', '/fa/blog', '/blog/rss.xml', '/fa/blog/rss.xml'],
|
||||||
failOnError: false,
|
failOnError: false,
|
||||||
ignore: ['/_vercel/image']
|
// Avoid crawling and prerendering internal/asset endpoints (speeds up builds significantly)
|
||||||
|
ignore: ['/_vercel/image', '/_ipx', '/_nuxt', '/_i18n', '/__nuxt_content']
|
||||||
},
|
},
|
||||||
devProxy: {
|
devProxy: {
|
||||||
host: '0.0.0.0'
|
host: '0.0.0.0'
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ export default defineEventHandler(async (event) => {
|
|||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const siteUrl = config.public.siteUrl || 'https://example.com'
|
const siteUrl = config.public.siteUrl || 'https://example.com'
|
||||||
|
|
||||||
// Detect locale from path
|
|
||||||
const locale = 'en'
|
const locale = 'en'
|
||||||
|
|
||||||
// Fetch published blog posts
|
// In Nitro server routes, `queryCollection` expects the H3 event as the first argument.
|
||||||
const posts = await serverQueryContent(event, `${locale}/blog`)
|
const allPosts = await queryCollection(event, 'blog').all()
|
||||||
.where({ draft: { $ne: true } })
|
|
||||||
.sort({ date: -1 })
|
const posts = allPosts
|
||||||
.find()
|
.filter((post: any) => post.path?.startsWith('/en/blog/') && post.draft !== true)
|
||||||
|
.sort((a: any, b: any) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||||
|
|
||||||
const escapeXml = (str: string) => {
|
const escapeXml = (str: string) => {
|
||||||
return str
|
return str
|
||||||
@@ -22,7 +22,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
const rssItems = posts
|
const rssItems = posts
|
||||||
.map((post) => {
|
.map((post) => {
|
||||||
const link = `${siteUrl}${post._path}`
|
const routePath = post.path?.replace(/^\/en/, '') || ''
|
||||||
|
const link = `${siteUrl}${routePath}`
|
||||||
const pubDate = new Date(post.date).toUTCString()
|
const pubDate = new Date(post.date).toUTCString()
|
||||||
|
|
||||||
return `
|
return `
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ export default defineEventHandler(async (event) => {
|
|||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const siteUrl = config.public.siteUrl || 'https://example.com'
|
const siteUrl = config.public.siteUrl || 'https://example.com'
|
||||||
|
|
||||||
// Persian locale
|
|
||||||
const locale = 'fa'
|
const locale = 'fa'
|
||||||
|
|
||||||
// Fetch published blog posts
|
// In Nitro server routes, `queryCollection` expects the H3 event as the first argument.
|
||||||
const posts = await serverQueryContent(event, `${locale}/blog`)
|
const allPosts = await queryCollection(event, 'blog').all()
|
||||||
.where({ draft: { $ne: true } })
|
|
||||||
.sort({ date: -1 })
|
const posts = allPosts
|
||||||
.find()
|
.filter((post: any) => post.path?.startsWith('/fa/blog/') && post.draft !== true)
|
||||||
|
.sort((a: any, b: any) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||||
|
|
||||||
const escapeXml = (str: string) => {
|
const escapeXml = (str: string) => {
|
||||||
return str
|
return str
|
||||||
@@ -22,7 +22,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
const rssItems = posts
|
const rssItems = posts
|
||||||
.map((post) => {
|
.map((post) => {
|
||||||
const link = `${siteUrl}${post._path}`
|
const link = `${siteUrl}${post.path}`
|
||||||
const pubDate = new Date(post.date).toUTCString()
|
const pubDate = new Date(post.date).toUTCString()
|
||||||
|
|
||||||
return `
|
return `
|
||||||
|
|||||||
Reference in New Issue
Block a user