feat(seo): improve SEO configuration and enable SSR

- Enable `ssr: true` in `nuxt.config.ts` for server-side rendering of meta tags.
- Implement `useSeoMeta` in `events/.vue` with fallback logic for Open Graph and Twitter cards.
- Update `content.config.ts` to use `asSeoCollection` for the news collection.
- Migrate event markdown frontmatter to use standardized SEO fields.
This commit is contained in:
xiaomai
2025-11-27 23:07:24 +08:00
parent 6288a1b01b
commit 9bca019b50
4 changed files with 102 additions and 67 deletions

View File

@@ -7,9 +7,13 @@
<!-- 卡片装饰边框浅色渐变 --> <!-- 卡片装饰边框浅色渐变 -->
<!-- <div class="absolute inset-0 bg-linear-to-r from-blue-100 to-purple-100 rounded-2xl blur-sm"></div> --> <!-- <div class="absolute inset-0 bg-linear-to-r from-blue-100 to-purple-100 rounded-2xl blur-sm"></div> -->
<div class="relative rounded-xl border border-gray-200 shadow-xl overflow-hidden"> <div
class="relative rounded-xl border border-gray-200 shadow-xl overflow-hidden"
>
<!-- 顶部装饰条明亮渐变 --> <!-- 顶部装饰条明亮渐变 -->
<div class="h-1 bg-linear-to-r from-blue-400 via-purple-400 to-cyan-400"></div> <div
class="h-1 bg-linear-to-r from-blue-400 via-purple-400 to-cyan-400"
></div>
<div class="p-8 sm:p-10 lg:p-12"> <div class="p-8 sm:p-10 lg:p-12">
<!-- 内容渲染器 --> <!-- 内容渲染器 -->
@@ -17,16 +21,32 @@
<ContentRenderer :value="event ?? {}"> <ContentRenderer :value="event ?? {}">
<template #empty> <template #empty>
<div class="text-center py-16"> <div class="text-center py-16">
<div class="inline-flex items-center justify-center w-16 h-16 bg-blue-100 rounded-full mb-4"> <div
<svg class="w-8 h-8 text-blue-500 animate-spin" fill="none" viewBox="0 0 24 24"> class="inline-flex items-center justify-center w-16 h-16 bg-blue-100 rounded-full mb-4"
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"> >
</circle> <svg
<path class="opacity-75" fill="currentColor" class="w-8 h-8 text-blue-500 animate-spin"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"> fill="none"
</path> viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg> </svg>
</div> </div>
<p class="text-gray-700 text-lg font-medium">内容加载中...</p> <p class="text-gray-700 text-lg font-medium">
内容加载中...
</p>
<p class="text-gray-400 text-sm mt-2">请稍等片刻</p> <p class="text-gray-400 text-sm mt-2">请稍等片刻</p>
</div> </div>
</template> </template>
@@ -41,16 +61,43 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
const route = useRoute() const route = useRoute();
const { data: event } = await useAsyncData('event-detail', () => const { data: event } = await useAsyncData("event-detail", () =>
queryCollection('events') queryCollection("events").path(`/events/${route.params.slug}`).first()
.path(`/events/${route.params.slug}`) );
.first()
)
useHead({ if (event.value) {
title: event.value?.title // 1. 确定图片:优先用 ogImage没有就用 cover
}) const shareImage = event.value.ogImage || event.value.cover;
// 2. 确定标题和描述:优先用 seoTitle没有就用 title
const shareTitle = event.value.seoTitle || event.value.title;
const shareDesc = event.value.seoDescription || event.value.description;
// 3. 注入 SEO
useSeoMeta({
// 基础
title: shareTitle,
description: shareDesc,
// Open Graph (Facebook / WhatsApp)
ogTitle: shareTitle,
ogDescription: shareDesc,
ogImage: shareImage,
ogType: "article",
// Twitter Card
twitterCard: "summary_large_image",
twitterTitle: shareTitle,
twitterDescription: shareDesc,
twitterImage: shareImage,
});
// 如果你用了 nuxt-og-image 模块生成动态图
if (shareImage) {
defineOgImage({ url: shareImage });
}
}
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -1,4 +1,5 @@
import { defineContentConfig, defineCollection, z } from "@nuxt/content"; import { defineContentConfig, defineCollection, z } from "@nuxt/content";
import { asSeoCollection } from "@nuxtjs/seo/content";
export default defineContentConfig({ export default defineContentConfig({
collections: { collections: {
@@ -12,11 +13,12 @@ export default defineContentConfig({
date: z.coerce.date(), date: z.coerce.date(),
location: z.string(), location: z.string(),
cover: z.string().url(), cover: z.string().url(),
draft: z.boolean().optional().default(false) draft: z.boolean().optional().default(false),
}), }),
}), }),
// 新闻集合 // 新闻集合
news: defineCollection({ news: defineCollection(
asSeoCollection({
type: "page", type: "page",
source: "news/*.md", source: "news/*.md",
schema: z.object({ schema: z.object({
@@ -32,9 +34,10 @@ export default defineContentConfig({
seoTitle: z.string().optional(), seoTitle: z.string().optional(),
seoDescription: z.string().optional(), seoDescription: z.string().optional(),
ogImage: z.string().optional(), ogImage: z.string().optional(),
draft: z.boolean().optional().default(false) draft: z.boolean().optional().default(false),
}),
}), }),
})
),
// 名人堂 // 名人堂
hallOfFames: defineCollection({ hallOfFames: defineCollection({
type: "page", type: "page",

View File

@@ -17,35 +17,19 @@ keywords:
- 刘连升老师 - 刘连升老师
- 永中校友会 - 永中校友会
# Open Graph / Facebook # --- SEO 专用字段 (对应你的 Zod Schema) ---
og:
title: "永平中学第 60 届毕业典礼|初中第 67 届毕业典礼"
description: "2025 年永平中学毕业典礼隆重举行,包含师长致辞、奖学金颁发、荣休老师欢送,以及学生精彩演出等精彩环节。"
image: "https://img.yphsalumni.org/i/2025/11/27/st6hzt.jpg"
type: "article"
# Twitter 卡片 # 对应 schema: seoTitle
twitter: # 如果不填,代码里会默认使用 title
card: "summary_large_image" seoTitle: "永平中学第 60 届毕业典礼|初中第 67 届毕业典礼"
title: "永平中学第 60 届毕业典礼"
description: "永平中学 2025 毕业典礼精彩回顾:致辞、演出、奖学金颁发与荣休教师表扬。"
image: "https://img.yphsalumni.org/i/2025/11/27/st6hzt.jpg"
# 文章结构化数据可选Nuxt SEO module 会自动识别) # 对应 schema: seoDescription
structuredData: # 如果不填,代码里会默认使用 description
"@type": "NewsArticle" seoDescription: "2025 年永平中学毕业典礼隆重举行,包含师长致辞、奖学金颁发、荣休老师欢送,以及学生精彩演出等精彩环节。"
headline: "永平中学第 60 届毕业典礼圆满举行"
image: "https://img.yphsalumni.org/i/2025/11/27/st6hzt.jpg" # 对应 schema: ogImage
datePublished: "2025-11-15" # 只有当你想要分享的图片和封面图不一样时才填,否则代码里会默认用 cover
author: ogImage: "https://img.yphsalumni.org/i/2025/11/27/st6hzt.jpg"
"@type": "Organization"
name: "永平中学校友会"
publisher:
"@type": "Organization"
name: "永平中学"
logo:
"@type": "ImageObject"
url: "/logo.png"
--- ---
# 永平中学高中第 60 届、初中第 67 届毕业典礼圆满举行 # 永平中学高中第 60 届、初中第 67 届毕业典礼圆满举行

View File

@@ -5,14 +5,15 @@ export default defineNuxtConfig({
compatibilityDate: "2025-07-15", compatibilityDate: "2025-07-15",
devtools: { enabled: true }, devtools: { enabled: true },
modules: [ modules: [
"@nuxtjs/seo",
"@nuxt/ui", "@nuxt/ui",
"@nuxtjs/seo",
"@nuxt/content", "@nuxt/content",
"@nuxt/image", "@nuxt/image",
"reka-ui/nuxt", "reka-ui/nuxt",
"@nuxtjs/robots", "@nuxtjs/robots",
"@nuxtjs/sitemap", "@nuxtjs/sitemap",
], ],
ssr: true,
css: ["~/assets/css/main.css"], css: ["~/assets/css/main.css"],
vite: { vite: {
plugins: [tailwindcss()], plugins: [tailwindcss()],