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:
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 届毕业典礼圆满举行
|
||||||
|
|||||||
@@ -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()],
|
||||||
|
|||||||
Reference in New Issue
Block a user