Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions src/pages/city/[code].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -179,10 +181,32 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
}),
);

const parsedEvents = pickEventSchema.parse(regionEvents?.records);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

regionEvents?.records 使用可选链,若 regionEventsnull/undefined 则传入 parse(undefined) 引发 ZodError。

建议显式处理空值,与后续对 regionEvents.total 的访问保持一致,避免在 API 异常时产生难以定位的错误。

🛡️ 建议修复
-  const parsedEvents = pickEventSchema.parse(regionEvents?.records);
+  const parsedEvents = pickEventSchema.parse(regionEvents.records);

若 API 存在返回空值的可能,建议在上层统一处理:

+  if (!region || !regionEvents) {
+    return { notFound: true };
+  }
   const parsedEvents = pickEventSchema.parse(regionEvents.records);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/city/`[code].tsx at line 184, The call
pickEventSchema.parse(regionEvents?.records) can pass undefined when
regionEvents is null/undefined and will throw a ZodError; update the code to
explicitly handle a missing regionEvents before parsing (same pattern you use
for accessing regionEvents.total) — e.g., check regionEvents and if absent
provide a safe default (like an empty array) or short-circuit rendering/return,
then call pickEventSchema.parse with that definite value; locate the usage of
regionEvents, regionEvents.total, and parsedEvents / pickEventSchema.parse to
implement the guard and keep behavior consistent on API errors.


return {
props: {
headMetas: {
title: `${region.name} - ${CityPageMeta[locale as currentSupportLocale].title}`,
des: cityDetailDescriptionGenerator(locale as currentSupportLocale, region.name, regionEvents.total),
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"])),
},
};
Expand Down
93 changes: 69 additions & 24 deletions src/utils/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -227,45 +257,38 @@ function eventDescriptionGenerator(
if (!event) {
return defaultDescriptionGenerator(locale);
}
const venue = `${event?.region?.localName || ""}${event?.address || ""}`.trim();

switch (locale) {
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)}${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",
? `“${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)}${venue ? `在“${venue}”舉辦` : "舉辦"}`
: `“${event?.name}”是由“${event?.organization?.name}”舉辦的獸展,展會的舉辦時間還沒有公佈,${venue ? `預計將在“${venue}”舉辦` : "舉辦地點暫未公佈"}`;
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)}${venue ? ` at "${venue}".` : "."}`
: `"${event?.name}" is a furry convention organized by "${
event?.organization?.name
}". 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."}`;
}
}

Expand Down Expand Up @@ -310,11 +333,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 list 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,
};