From 72b52f3d60d2e9e8a0859fea94ef97d04fe0b87e Mon Sep 17 00:00:00 2001 From: JiPai Date: Sun, 22 Feb 2026 16:20:48 +0800 Subject: [PATCH 1/4] feat: enhance city detail page with SEO improvements and structured data --- src/pages/city/[code].tsx | 28 +++++++++++- src/utils/meta.ts | 92 +++++++++++++++++++++++++++++---------- 2 files changed, 94 insertions(+), 26 deletions(-) diff --git a/src/pages/city/[code].tsx b/src/pages/city/[code].tsx index cbe8c29..5e7d8ca 100644 --- a/src/pages/city/[code].tsx +++ b/src/pages/city/[code].tsx @@ -12,9 +12,11 @@ import "dayjs/locale/zh-tw"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { groupBy } from "es-toolkit"; import { useTranslation } from "next-i18next"; -import { getDayjsLocale, monthNumberFormatter } from "@/utils/locale"; +import { currentSupportLocale, getDayjsLocale, monthNumberFormatter } from "@/utils/locale"; import { EventsAPI } from "@/api/events"; import dayjs from "dayjs"; +import { breadcrumbGenerator } from "@/utils/structuredData"; +import { cityDetailDescriptionGenerator, cityDetailKeywordGenerator, CityPageMeta } from "@/utils/meta"; export default function CityDetail({ region, events }: { region: Region; events: EventItem[] }) { const { t, i18n } = useTranslation(); @@ -179,10 +181,32 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }), ); + const parsedEvents = pickEventSchema.parse(regionEvents?.records); + return { props: { + headMetas: { + title: `${region.name} - ${CityPageMeta[locale as currentSupportLocale].title}`, + des: cityDetailDescriptionGenerator(locale as currentSupportLocale, region.name, parsedEvents.length), + keywords: cityDetailKeywordGenerator(locale as currentSupportLocale, { name: region.name }), + link: `/city/${regionCode}`, + }, + structuredData: { + ...breadcrumbGenerator({ + items: [ + { + name: CityPageMeta[locale as currentSupportLocale].title, + item: "/city", + }, + { + name: region.name, + item: `/city/${regionCode}`, + }, + ], + }), + }, region: region, - events: pickEventSchema.parse(regionEvents?.records), + events: parsedEvents, ...(await serverSideTranslations(locale, ["common"])), }, }; diff --git a/src/utils/meta.ts b/src/utils/meta.ts index 54bed19..18b821d 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -148,6 +148,36 @@ function generateOrganizationKeywords(organization: { name: string }, locale: cu } } +function generateCityDetailKeywords(city: { name: string }, locale: currentSupportLocale) { + switch (locale) { + case "zh-Hans": + default: + return [ + `${city.name}`, + `${city.name}兽展`, + `${city.name}兽聚`, + `${city.name}兽展时间`, + `${city.name}兽展日历`, + ]; + case "zh-Hant": + return [ + `${city.name}`, + `${city.name}獸展`, + `${city.name}獸聚`, + `${city.name}獸展時間`, + `${city.name}獸展日曆`, + ]; + case "en": + return [ + `${city.name}`, + `${city.name} Furry Convention`, + `${city.name} Furry Gathering`, + `${city.name} Furry Convention Schedule`, + `${city.name} Furry Event Calendar`, + ]; + } +} + function keywordGenerator({ page, locale, @@ -232,40 +262,32 @@ function eventDescriptionGenerator( case "zh-Hans": default: return event.startAt && event.endAt - ? `欢迎来到兽展日历!兽展日历提供关于“${event?.name}”的详细信息:这是由“${ - event?.organization?.name - }”举办的兽展,将于${formatDate( - event?.startAt!, + ? `“${event?.name}”是由“${event?.organization?.name}”举办的兽展,展会定于${formatDate( + event?.startAt, "YYYY年MM月DD日", locale, - )}至${formatDate(event?.endAt!, "YYYY年MM月DD日", locale)}在“${ - event?.region?.localName - }${event?.address}”举办,喜欢的朋友记得关注开始售票时间~` - : `欢迎来到兽展日历!兽展日历提供关于“${event?.name}”的详细信息:这是由“${event?.organization?.name}”举办的兽展,将在“${event?.region?.localName}${event?.address}”举办,喜欢的朋友记得关注开始售票时间~`; + )}至${formatDate(event?.endAt, "YYYY年MM月DD日", locale)}在“${event?.region?.localName}${event?.address}”举办` + : `“${event?.name}”是由“${event?.organization?.name}”举办的兽展,展会的举办时间还没有公布,预计将在“${event?.region?.localName}${event?.address || ""}”举办`; case "zh-Hant": return event.startAt && event.endAt - ? `歡迎來到獸展日曆!獸展日曆提供關於“${event?.name}”的詳細信息:這是由“${ - event?.organization?.name - }”舉辦的獸展,將於${formatDate(event?.startAt!, "YYYY/MM/DD", locale)}至${formatDate( - event?.endAt!, - "YYYY/MM/DD", + ? `“${event?.name}”是由“${event?.organization?.name}”舉辦的獸展,展會定於${formatDate( + event?.startAt, + "YYYY年MM月DD日", locale, - )}在“${ - event?.region?.localName - }${event?.address}”舉辦,喜歡的朋友記得關注開票時間~` - : `歡迎來到獸展日曆!獸展日曆提供關於“${event?.name}”的詳細信息:這是由“${event?.organization?.name}”舉辦的獸展,將在“${event?.region?.localName}${event?.address}”舉辦,喜歡的朋友記得關注開票時間~`; + )}至${formatDate(event?.endAt, "YYYY年MM月DD日", locale)}在“${event?.region?.localName}${event?.address}”舉辦` + : `“${event?.name}”是由“${event?.organization?.name}”舉辦的獸展,展會的舉辦時間還沒有公佈,預計將在“${event?.region?.localName}${event?.address || ""}”舉辦`; case "en": return event.startAt && event.endAt - ? `Details about "${event?.name}": This furry convention is organized by "${ + ? `"${event?.name}" is a furry convention organized by "${ event?.organization?.name - }" and will be held from ${formatDate(event?.startAt!, "MMMM DD, YYYY", locale)} to ${formatDate( - event?.endAt!, + }". The event is scheduled to be held from ${formatDate( + event?.startAt, "MMMM DD, YYYY", locale, - )} at "${ - event?.region?.localName - }${event?.address}". Stay tuned for ticket sales!` - : `Welcome to FurConsCalendar! FCC provides detailed information about "${event?.name}": This furry convention is organized by "${event?.organization?.name}" and will be held at "${event?.region?.localName}${event?.address}". Stay tuned for ticket sales!`; + )} to ${formatDate(event?.endAt, "MMMM DD, YYYY", locale)} at "${event?.region?.localName}${event?.address}".` + : `"${event?.name}" is a furry convention organized by "${ + event?.organization?.name + }". The event date has not been announced yet and is expected to be held at "${event?.region?.localName}${event?.address || ""}".`; } } @@ -310,11 +332,33 @@ function organizationDetailDescriptionGenerator( } } +function cityDetailDescriptionGenerator( + locale: currentSupportLocale, + cityName: string, + eventCount: number, +) { + switch (locale) { + case "zh-Hans": + default: + return `这里是 ${cityName} 的兽展活动列表,累计收录了 ${eventCount} 场兽展(兽聚)活动。`; + case "zh-Hant": + return `這裡是 ${cityName} 的獸展時間軸,累計收錄了 ${eventCount} 場獸展(獸聚)活動。`; + case "en": + return `This is the furry event timeline for ${cityName}, with a total of ${eventCount} recorded furry conventions and gatherings.`; + } +} + +function cityDetailKeywordGenerator(locale: currentSupportLocale, city: { name: string }) { + return generateCityDetailKeywords(city, locale).join(","); +} + export { universalKeywords, keywordGenerator, + cityDetailKeywordGenerator, titleGenerator, defaultDescriptionGenerator as descriptionGenerator, eventDescriptionGenerator, + cityDetailDescriptionGenerator, organizationDetailDescriptionGenerator, }; From a608faf55873b402486b0e5ffac304458cdb7619 Mon Sep 17 00:00:00 2001 From: JiPai Date: Sun, 22 Feb 2026 17:35:11 +0800 Subject: [PATCH 2/4] fix: region name may be null --- src/pages/city/[code].tsx | 2 +- src/utils/meta.ts | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/pages/city/[code].tsx b/src/pages/city/[code].tsx index 5e7d8ca..5e61c6e 100644 --- a/src/pages/city/[code].tsx +++ b/src/pages/city/[code].tsx @@ -187,7 +187,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { props: { headMetas: { title: `${region.name} - ${CityPageMeta[locale as currentSupportLocale].title}`, - des: cityDetailDescriptionGenerator(locale as currentSupportLocale, region.name, parsedEvents.length), + des: cityDetailDescriptionGenerator(locale as currentSupportLocale, region.name, regionEvents.total), keywords: cityDetailKeywordGenerator(locale as currentSupportLocale, { name: region.name }), link: `/city/${regionCode}`, }, diff --git a/src/utils/meta.ts b/src/utils/meta.ts index 18b821d..a33edc7 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -257,6 +257,7 @@ function eventDescriptionGenerator( if (!event) { return defaultDescriptionGenerator(locale); } + const venue = `${event?.region?.localName || ""}${event?.address || ""}`; switch (locale) { case "zh-Hans": @@ -266,16 +267,16 @@ function eventDescriptionGenerator( event?.startAt, "YYYY年MM月DD日", locale, - )}至${formatDate(event?.endAt, "YYYY年MM月DD日", locale)}在“${event?.region?.localName}${event?.address}”举办` - : `“${event?.name}”是由“${event?.organization?.name}”举办的兽展,展会的举办时间还没有公布,预计将在“${event?.region?.localName}${event?.address || ""}”举办`; + )}至${formatDate(event?.endAt, "YYYY年MM月DD日", locale)}在“${venue}”举办` + : `“${event?.name}”是由“${event?.organization?.name}”举办的兽展,展会的举办时间还没有公布,预计将在“${venue}”举办`; case "zh-Hant": return event.startAt && event.endAt ? `“${event?.name}”是由“${event?.organization?.name}”舉辦的獸展,展會定於${formatDate( event?.startAt, "YYYY年MM月DD日", locale, - )}至${formatDate(event?.endAt, "YYYY年MM月DD日", locale)}在“${event?.region?.localName}${event?.address}”舉辦` - : `“${event?.name}”是由“${event?.organization?.name}”舉辦的獸展,展會的舉辦時間還沒有公佈,預計將在“${event?.region?.localName}${event?.address || ""}”舉辦`; + )}至${formatDate(event?.endAt, "YYYY年MM月DD日", locale)}在“${venue}”舉辦` + : `“${event?.name}”是由“${event?.organization?.name}”舉辦的獸展,展會的舉辦時間還沒有公佈,預計將在“${venue}”舉辦`; case "en": return event.startAt && event.endAt ? `"${event?.name}" is a furry convention organized by "${ @@ -284,10 +285,10 @@ function eventDescriptionGenerator( event?.startAt, "MMMM DD, YYYY", locale, - )} to ${formatDate(event?.endAt, "MMMM DD, YYYY", locale)} at "${event?.region?.localName}${event?.address}".` + )} to ${formatDate(event?.endAt, "MMMM DD, YYYY", locale)} at "${venue}".` : `"${event?.name}" is a furry convention organized by "${ event?.organization?.name - }". The event date has not been announced yet and is expected to be held at "${event?.region?.localName}${event?.address || ""}".`; + }". The event date has not been announced yet and is expected to be held at "${venue}".`; } } From 1939b171ca87e14e5af59414fcb1407368280174 Mon Sep 17 00:00:00 2001 From: JiPai Date: Sun, 22 Feb 2026 17:40:58 +0800 Subject: [PATCH 3/4] fix: update city detail descriptions --- src/utils/meta.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/meta.ts b/src/utils/meta.ts index a33edc7..c5129b3 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -343,9 +343,9 @@ function cityDetailDescriptionGenerator( default: return `这里是 ${cityName} 的兽展活动列表,累计收录了 ${eventCount} 场兽展(兽聚)活动。`; case "zh-Hant": - return `這裡是 ${cityName} 的獸展時間軸,累計收錄了 ${eventCount} 場獸展(獸聚)活動。`; + return `這裡是 ${cityName} 的獸展活動列表,累計收錄了 ${eventCount} 場獸展(獸聚)活動。`; case "en": - return `This is the furry event timeline for ${cityName}, with a total of ${eventCount} recorded furry conventions and gatherings.`; + return `This is the furry event list for ${cityName}, with a total of ${eventCount} recorded furry conventions and gatherings.`; } } From 2a192810469dbb81a29e30c8def26d1c6faa6639 Mon Sep 17 00:00:00 2001 From: JiPai Date: Sun, 22 Feb 2026 17:50:23 +0800 Subject: [PATCH 4/4] fix: trim venue string in event description --- src/utils/meta.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/utils/meta.ts b/src/utils/meta.ts index c5129b3..d783663 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -257,7 +257,7 @@ function eventDescriptionGenerator( if (!event) { return defaultDescriptionGenerator(locale); } - const venue = `${event?.region?.localName || ""}${event?.address || ""}`; + const venue = `${event?.region?.localName || ""}${event?.address || ""}`.trim(); switch (locale) { case "zh-Hans": @@ -267,16 +267,16 @@ function eventDescriptionGenerator( event?.startAt, "YYYY年MM月DD日", locale, - )}至${formatDate(event?.endAt, "YYYY年MM月DD日", locale)}在“${venue}”举办` - : `“${event?.name}”是由“${event?.organization?.name}”举办的兽展,展会的举办时间还没有公布,预计将在“${venue}”举办`; + )}至${formatDate(event?.endAt, "YYYY年MM月DD日", locale)}${venue ? `在“${venue}”举办` : "举办"}` + : `“${event?.name}”是由“${event?.organization?.name}”举办的兽展,展会的举办时间还没有公布,${venue ? `预计将在“${venue}”举办` : "举办地点暂未公布"}`; case "zh-Hant": return event.startAt && event.endAt ? `“${event?.name}”是由“${event?.organization?.name}”舉辦的獸展,展會定於${formatDate( event?.startAt, "YYYY年MM月DD日", locale, - )}至${formatDate(event?.endAt, "YYYY年MM月DD日", locale)}在“${venue}”舉辦` - : `“${event?.name}”是由“${event?.organization?.name}”舉辦的獸展,展會的舉辦時間還沒有公佈,預計將在“${venue}”舉辦`; + )}至${formatDate(event?.endAt, "YYYY年MM月DD日", locale)}${venue ? `在“${venue}”舉辦` : "舉辦"}` + : `“${event?.name}”是由“${event?.organization?.name}”舉辦的獸展,展會的舉辦時間還沒有公佈,${venue ? `預計將在“${venue}”舉辦` : "舉辦地點暫未公佈"}`; case "en": return event.startAt && event.endAt ? `"${event?.name}" is a furry convention organized by "${ @@ -285,10 +285,10 @@ function eventDescriptionGenerator( event?.startAt, "MMMM DD, YYYY", locale, - )} to ${formatDate(event?.endAt, "MMMM DD, YYYY", locale)} at "${venue}".` + )} to ${formatDate(event?.endAt, "MMMM DD, YYYY", locale)}${venue ? ` at "${venue}".` : "."}` : `"${event?.name}" is a furry convention organized by "${ event?.organization?.name - }". The event date has not been announced yet and is expected to be held at "${venue}".`; + }". The event date has not been announced yet${venue ? ` and is expected to be held at "${venue}".` : ", and the venue has not been announced yet."}`; } }