Compare commits

..

2 Commits

Author SHA1 Message Date
xiaomai
9bca019b50 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.
2025-11-27 23:07:24 +08:00
xiaomai
6288a1b01b feat(content): add draft support for content collections
This commit introduces a draft system for the 'events' and 'news' collections. A `draft` boolean field has been added to the content schema, and frontend queries are now updated to only fetch and
display content where `draft` is `false`. This allows content to be created and saved without being publicly visible, improving the publishing workflow.
2025-11-27 17:53:42 +08:00
4 changed files with 120 additions and 69 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,29 +13,31 @@ 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(
type: "page", asSeoCollection({
source: "news/*.md", type: "page",
schema: z.object({ source: "news/*.md",
title: z.string(), schema: z.object({
date: z.coerce.date(), title: z.string(),
updated: z.coerce.date().optional(), date: z.coerce.date(),
author: z.string(), updated: z.coerce.date().optional(),
description: z.string(), author: z.string(),
cover: z.string().optional(), description: z.string(),
tags: z.array(z.string()).optional(), cover: z.string().optional(),
category: z.enum(["活动", "通知", "招聘", "博客"]).optional(), tags: z.array(z.string()).optional(),
highlight: z.boolean().optional(), category: z.enum(["活动", "通知", "招聘", "博客"]).optional(),
seoTitle: z.string().optional(), highlight: z.boolean().optional(),
seoDescription: z.string().optional(), seoTitle: z.string().optional(),
ogImage: z.string().optional(), seoDescription: z.string().optional(),
draft: z.boolean().optional().default(false) ogImage: z.string().optional(),
}), draft: z.boolean().optional().default(false),
}), }),
})
),
// 名人堂 // 名人堂
hallOfFames: defineCollection({ hallOfFames: defineCollection({
type: "page", type: "page",

View File

@@ -3,7 +3,7 @@ title: "永平中学第 60 届毕业典礼"
subtitle: "暨初中第 67 届毕业典礼" subtitle: "暨初中第 67 届毕业典礼"
date: "2025-11-15" date: "2025-11-15"
location: "永平中学邱廉书礼堂" location: "永平中学邱廉书礼堂"
# cover: "/events/20250927-return-to-school/event-photo-1.jpg" cover: "https://img.yphsalumni.org/i/2025/11/27/st6hzt.jpg"
draft: false draft: false
# SEO 主要字段 # SEO 主要字段
@@ -17,49 +17,39 @@ keywords:
- 刘连升老师 - 刘连升老师
- 永中校友会 - 永中校友会
# Open Graph / Facebook # --- SEO 专用字段 (对应你的 Zod Schema) ---
og:
title: "永平中学第 60 届毕业典礼|初中第 67 届毕业典礼"
description: "2025 年永平中学毕业典礼隆重举行,包含师长致辞、奖学金颁发、荣休老师欢送,以及学生精彩演出等精彩环节。"
# image: "/events/20250927-return-to-school/event-photo-1.jpg"
type: "article"
# Twitter 卡片 # 对应 schema: seoTitle
twitter: # 如果不填,代码里会默认使用 title
card: "summary_large_image" seoTitle: "永平中学第 60 届毕业典礼|初中第 67 届毕业典礼"
title: "永平中学第 60 届毕业典礼"
description: "永平中学 2025 毕业典礼精彩回顾:致辞、演出、奖学金颁发与荣休教师表扬。"
# image: "/events/20250927-return-to-school/event-photo-1.jpg"
# 文章结构化数据可选Nuxt SEO module 会自动识别) # 对应 schema: seoDescription
structuredData: # 如果不填,代码里会默认使用 description
"@type": "NewsArticle" seoDescription: "2025 年永平中学毕业典礼隆重举行,包含师长致辞、奖学金颁发、荣休老师欢送,以及学生精彩演出等精彩环节。"
headline: "永平中学第 60 届毕业典礼圆满举行"
# image: "/events/20250927-return-to-school/event-photo-1.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 届毕业典礼圆满举行
![高三毕业生与嘉宾合照](https://img.yphsalumni.org/i/2025/11/27/st6hzt.jpg)
2025 年 11 月 15 日,永平中学邱廉书礼堂见证了一个充满祝福与感动的重要时刻:高中第 60 届与初中第 67 届毕业典礼隆重举行。礼堂内座无虚席,毕业生、家长、师长与嘉宾齐聚一堂,共同庆祝学子们学习旅程的重要里程碑。 2025 年 11 月 15 日,永平中学邱廉书礼堂见证了一个充满祝福与感动的重要时刻:高中第 60 届与初中第 67 届毕业典礼隆重举行。礼堂内座无虚席,毕业生、家长、师长与嘉宾齐聚一堂,共同庆祝学子们学习旅程的重要里程碑。
经过六年的努力与成长,高三学生正式迎来人生的新阶段;初三毕业生亦迈向更高层次的挑战。典礼在庄严的国州歌声中启幕,掀开充满意义的一天。 经过六年的努力与成长,高三学生正式迎来人生的新阶段;初三毕业生亦迈向更高层次的挑战。典礼在庄严的国州歌声中启幕,掀开充满意义的一天。
## 董事长马彣清博士致词(由代表宣读) ## 董事长马彣清博士致词(由代表宣读)
![董事会秘书长黄仰力董事代表致辞](https://img.yphsalumni.org/i/2025/11/27/sndspp.jpg)
因公务繁忙未克出席,马彣清董事长由董事会秘书长黄仰力董事代表致辞。他转达董事长的寄语:“成功从来不是侥幸,而是毅力不断累积的成果。” 同时肯定毕业生在求学路上的坚持,并勉励他们继续以积极态度面对未来。 因公务繁忙未克出席,马彣清董事长由董事会秘书长黄仰力董事代表致辞。他转达董事长的寄语:“成功从来不是侥幸,而是毅力不断累积的成果。” 同时肯定毕业生在求学路上的坚持,并勉励他们继续以积极态度面对未来。
## 张嘉群校长三项叮咛:以品格与眼界开创未来 ## 张嘉群校长三项叮咛:以品格与眼界开创未来
![张嘉群校长致辞](https://img.yphsalumni.org/i/2025/11/27/smvqcm.jpg)
张嘉群校长在致辞中寄语毕业生,并提出三项叮咛,期许他们在往后的道路上继续发光发热: 张嘉群校长在致辞中寄语毕业生,并提出三项叮咛,期许他们在往后的道路上继续发光发热:
### 叮咛一:永怀初心 ### 叮咛一:永怀初心
@@ -79,10 +69,14 @@ structuredData:
## 柔佛州董联会主席兼校务顾问陈大锦先生致辞 ## 柔佛州董联会主席兼校务顾问陈大锦先生致辞
![董联会主席兼校务顾问陈大锦先生致辞](https://img.yphsalumni.org/i/2025/11/27/smxf8t.jpg)
陈大锦先生分享了他访问上海中医药大学的见闻,特别是该校完善的奖助学金制度。他希望本校辅导处与升学资讯处能协助学生把握机会,踊跃申请明年 9 月开课的相关奖学金,为升学之路开拓更多可能。 陈大锦先生分享了他访问上海中医药大学的见闻,特别是该校完善的奖助学金制度。他希望本校辅导处与升学资讯处能协助学生把握机会,踊跃申请明年 9 月开课的相关奖学金,为升学之路开拓更多可能。
## 欢送刘连升老师光荣荣休 ## 欢送刘连升老师光荣荣休
![刘连升老师光荣荣休](https://img.yphsalumni.org/i/2025/11/27/spazik.jpg)
在典礼上,校方特别举行了“欢送刘连升老师荣休”环节,为这位服务永平中学 35 年、深受师生爱戴的资深教师献上诚挚祝福。 在典礼上,校方特别举行了“欢送刘连升老师荣休”环节,为这位服务永平中学 35 年、深受师生爱戴的资深教师献上诚挚祝福。
刘连升老师在任职期间,以其和蔼可亲的性格与渊博的历史知识,陪伴无数学生走过求学阶段。他不仅课堂上“聊古论今”,从古代帝王先烈谈到世界变迁,更在课余时与学生畅谈时事,从传统文化一路聊到新兴科技与 AI 技术,是学生眼中既严谨又风趣的良师益友。 刘连升老师在任职期间,以其和蔼可亲的性格与渊博的历史知识,陪伴无数学生走过求学阶段。他不仅课堂上“聊古论今”,从古代帝王先烈谈到世界变迁,更在课余时与学生畅谈时事,从传统文化一路聊到新兴科技与 AI 技术,是学生眼中既严谨又风趣的良师益友。
@@ -94,10 +88,12 @@ structuredData:
## 颁发董事长设立的奖学金 ## 颁发董事长设立的奖学金
典礼上同步进行董事长设立的各项奖学金颁发仪式,以表扬在学业、操行与综合表现上有卓越成绩的学生。 典礼上同步进行董事长设立的各项奖学金颁发仪式,以表扬在学业、操行与综合表现上有卓越成绩的学生。
(详见图集 (详见[图集](https://drive.google.com/drive/folders/1n3aRxxBwn-k6jzAOogV2ChBHKDA0c0qG)
## 特别演出:永中之星冠军林妤桐献唱 ## 特别演出:永中之星冠军林妤桐献唱
![林妤桐学妹献唱](https://img.yphsalumni.org/i/2025/11/27/sr370l.jpg)
在典礼进入高潮之前,校方安排了一场温馨惊喜表演 —— 第一届“永中之星”冠军、来自初三诚的林妤桐学妹献唱两首歌曲《Forever Young》以及《凤凰花开的路口》。她以清亮而富感染力的歌声向高三学长姐献上最真挚的祝福也为典礼增添一份青春与深情的色彩。 在典礼进入高潮之前,校方安排了一场温馨惊喜表演 —— 第一届“永中之星”冠军、来自初三诚的林妤桐学妹献唱两首歌曲《Forever Young》以及《凤凰花开的路口》。她以清亮而富感染力的歌声向高三学长姐献上最真挚的祝福也为典礼增添一份青春与深情的色彩。
## 高三教师团队呈献毕业回忆视频 ## 高三教师团队呈献毕业回忆视频
@@ -108,6 +104,10 @@ structuredData:
## 歌声中道别,在掌声中启航 ## 歌声中道别,在掌声中启航
![高三毕业生合唱](https://img.yphsalumni.org/i/2025/11/27/ss2wuz.jpg)
典礼尾声,高三毕业生深情演唱《再见》,而初三与高三学生共同合唱《毕业歌》,歌声回荡礼堂,为今年的毕业典礼写下温馨句点。现场气氛在歌声中变得格外动人,许多毕业生眼眶泛红,纷纷表示将铭记这一刻。 典礼尾声,高三毕业生深情演唱《再见》,而初三与高三学生共同合唱《毕业歌》,歌声回荡礼堂,为今年的毕业典礼写下温馨句点。现场气氛在歌声中变得格外动人,许多毕业生眼眶泛红,纷纷表示将铭记这一刻。
上午 11 时正,典礼圆满结束。为感谢师生与来宾的参与,马彣清董事长特别准备了供 600 人享用的自助餐,设宴于礼堂楼下。现场气氛融洽温馨。同时,本校友会也成功招收 14 名新会员,为未来的发展注入新的力量。 上午 11 时正,典礼圆满结束。为感谢师生与来宾的参与,马彣清董事长特别准备了供 600 人享用的自助餐,设宴于礼堂楼下。现场气氛融洽温馨。同时,本校友会也成功招收 14 名新会员,为未来的发展注入新的力量。
[2025 年毕业典礼图集](https://drive.google.com/drive/folders/1n3aRxxBwn-k6jzAOogV2ChBHKDA0c0qG)

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()],