Skip to content

feat: enhance city detail page with SEO improvements and structured data#301

Merged
PaiJi merged 4 commits intomainfrom
feat/update-meta
Feb 22, 2026
Merged

feat: enhance city detail page with SEO improvements and structured data#301
PaiJi merged 4 commits intomainfrom
feat/update-meta

Conversation

@PaiJi
Copy link
Member

@PaiJi PaiJi commented Feb 22, 2026

Summary by CodeRabbit

发布说明

  • 新功能
    • 城市页面增强多语言 SEO:自动生成本地化标题、描述与关键词
    • 为城市详情输出结构化数据,提升搜索引擎对地域层级的理解
    • 城市详情描述与关键词根据语言和活动数量自动调整
    • 活动列表解析与传递优化,提升页面中活动信息的一致性与准确性

@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

Warning

Rate limit exceeded

@PaiJi has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 44 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

引入基于区域与本地化的城市详情 SEO 数据生成:解析区域事件为独立变量,扩展 getServerSideProps 返回的 props,新增 headMetas(title、des、keywords、link)与 structuredData,并在工具中添加城市关键词与描述生成器。

Changes

Cohort / File(s) Summary
城市页面元数据集成
src/pages/city/[code].tsx
新增导入 currentSupportLocalebreadcrumbGeneratorcityDetailDescriptionGeneratorCityPageMeta;解析 regionEvents?.recordsparsedEvents;在 getServerSideProps 返回值中新增 headMetasstructuredData,并将组件接收的 events 改为 parsedEvents
元数据生成工具函数
src/utils/meta.ts
新增并导出 generateCityDetailKeywordscityDetailKeywordGeneratorcityDetailDescriptionGenerator;实现对 zh-Hanszh-Hanten 的城市关键词与描述生成;同时对事件/组织描述生成的日期与状态格式化做了补充。

Sequence Diagram(s)

(无序列图)

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 分钟

Poem

🐰 我在代码田野里翻土,
为城市摘下关键词与诗句,
面包屑串成小径,指向页面心里,
多语的风吹过每一行代码,
访客与搜索在远处轻轻敲门 🌿

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 提交标题准确概括了主要改动:为城市详情页面添加SEO优化和结构化数据,与代码修改内容(新增元数据生成函数和服务端属性)完全一致。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/update-meta

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

📦 Next.js Bundle Analysis for furrycons-site

This analysis was generated by the Next.js Bundle Analysis action. 🤖

⚠️ Global Bundle Size Increased

Page Size (compressed)
global 333.75 KB (🟡 +2 B)
Details

The global bundle is the javascript bundle that loads alongside every page. It is in its own category because its impact is much higher - an increase to its size means that every page on your website loads slower, and a decrease means every page loads faster.

Any third party scripts you have added directly to your app using the <script> tag are not accounted for in this analysis

If you want further insight into what is behind the changes, give @next/bundle-analyzer a try!

One Page Changed Size

The following page changed size from the code in this PR compared to its base branch:

Page Size (compressed) First Load % of Budget (350 KB)
/city/[code] 1.69 KB 335.43 KB 95.84% (+/- <0.01%)
Details

Only the gzipped size is provided here based on an expert tip.

First Load is the size of the global bundle plus the bundle for the individual page. If a user were to show up to your website and land on a given page, the first load size represents the amount of javascript that user would need to download. If next/link is used, subsequent page loads would only need to download that page's bundle (the number in the "Size" column), since the global bundle has already been downloaded.

Any third party scripts you have added directly to your app using the <script> tag are not accounted for in this analysis

The "Budget %" column shows what percentage of your performance budget the First Load total takes up. For example, if your budget was 100kb, and a given page's first load size was 10kb, it would be 10% of your budget. You can also see how much this has increased or decreased compared to the base branch of your PR. If this percentage has increased by 20% or more, there will be a red status indicator applied, indicating that special attention should be given to this. If you see "+/- <0.01%" it means that there was a change in bundle size, but it is a trivial enough amount that it can be ignored.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pages/city/[code].tsx (1)

169-182: ⚠️ Potential issue | 🔴 Critical

startAtendAt 未允许 null/undefined,会在含未公布日期活动的城市页面抛出 ZodError(500 错误)。

组件代码在多处明确处理了 startAt/endAt 为空的情况(第 25、37、43–46、138 行),eventDescriptionGenerator 的类型签名也是 startAt?: string | null。只要某城市包含一条未公布日期的活动,pickEventSchema.parse() 就会抛出 ZodError,导致整个页面服务端渲染失败。

🐛 建议修复
-    startAt: z.string(),
-    endAt: z.string(),
+    startAt: z.string().nullable(),
+    endAt: z.string().nullable(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/city/`[code].tsx around lines 169 - 182, pickEventSchema currently
requires startAt and endAt as non-null strings which makes
pickEventSchema.parse() throw when events have unpublished dates; update the
schema to allow null/undefined for those fields (e.g. make startAt and endAt
z.string().optional().nullable()) so it matches the component and
eventDescriptionGenerator signatures (startAt?: string | null) and prevents
ZodError during SSR.
🧹 Nitpick comments (1)
src/utils/meta.ts (1)

351-353: cityDetailKeywordGeneratorgenerateCityDetailKeywords 参数顺序不一致(仅风格问题,逻辑正确)。

cityDetailKeywordGenerator(locale, city) 与内部调用 generateCityDetailKeywords(city, locale) 参数顺序相反。其他 generate*Keywords 辅助函数均以实体作为第一个参数(如 generateOrganizationKeywords(organization, locale)),建议将包装函数统一为相同顺序。

♻️ 建议调整参数顺序
-function cityDetailKeywordGenerator(locale: currentSupportLocale, city: { name: string }) {
-  return generateCityDetailKeywords(city, locale).join(",");
+function cityDetailKeywordGenerator(city: { name: string }, locale: currentSupportLocale) {
+  return generateCityDetailKeywords(city, locale).join(",");
 }

对应调用方(src/pages/city/[code].tsx 第 191 行)也需同步更新:

-keywords: cityDetailKeywordGenerator(locale as currentSupportLocale, { name: region.name }),
+keywords: cityDetailKeywordGenerator({ name: region.name }, locale as currentSupportLocale),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/meta.ts` around lines 351 - 353, The wrapper
cityDetailKeywordGenerator currently accepts (locale, city) but calls
generateCityDetailKeywords(city, locale), which is inconsistent with other
helpers; change cityDetailKeywordGenerator signature to (city: { name: string },
locale: currentSupportLocale) and have it return
generateCityDetailKeywords(city, locale). Then update all call sites that invoke
cityDetailKeywordGenerator to pass the city object as the first argument and
locale second (e.g., the page/component that currently calls
cityDetailKeywordGenerator(locale, city)).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/city/`[code].tsx:
- Around line 188-193: The SEO description currently uses parsedEvents.length
(bounded by the hardcoded pageSize "50") in headMetas.title/des via
cityDetailDescriptionGenerator, causing cities with >50 events to show wrong
counts; update the call to cityDetailDescriptionGenerator to pass
regionEvents.total (the actual total count object from the region query) instead
of parsedEvents.length so the description reflects the real stored count (mirror
the approach used with regions.total in the other page), and ensure any
reference to pageSize/"50" remains for pagination only while the descriptive
count uses regionEvents.total.
- 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.

In `@src/utils/meta.ts`:
- Around line 264-290: The templates interpolate event?.region?.localName which
can be null and stringifies as "null"; update all locale branches (zh, zh-Hant,
en) for both the date-present and date-absent paths to use a safe fallback
(e.g., event?.region?.localName ?? "" or a trimmed conditional) and similarly
ensure event?.address has a fallback (address || "") so the generated strings
never contain the literal "null" — change the interpolations inside the template
strings where event?.region?.localName and event?.address are used.

---

Outside diff comments:
In `@src/pages/city/`[code].tsx:
- Around line 169-182: pickEventSchema currently requires startAt and endAt as
non-null strings which makes pickEventSchema.parse() throw when events have
unpublished dates; update the schema to allow null/undefined for those fields
(e.g. make startAt and endAt z.string().optional().nullable()) so it matches the
component and eventDescriptionGenerator signatures (startAt?: string | null) and
prevents ZodError during SSR.

---

Nitpick comments:
In `@src/utils/meta.ts`:
- Around line 351-353: The wrapper cityDetailKeywordGenerator currently accepts
(locale, city) but calls generateCityDetailKeywords(city, locale), which is
inconsistent with other helpers; change cityDetailKeywordGenerator signature to
(city: { name: string }, locale: currentSupportLocale) and have it return
generateCityDetailKeywords(city, locale). Then update all call sites that invoke
cityDetailKeywordGenerator to pass the city object as the first argument and
locale second (e.g., the page/component that currently calls
cityDetailKeywordGenerator(locale, city)).

}),
);

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/utils/meta.ts (1)

352-354: 可考虑将城市关键词生成统一到 keywordGenerator 中。

目前 keywordGenerator 已支持 "home" | "event" | "organization",新增 "city" case 可收拢对外 API 面,减少导出符号数量,同时保持与现有模式一致。

♻️ 可选重构建议
 function keywordGenerator({
   page,
   locale,
   event,
   organization,
+  city,
 }: {
-  page: "home" | "event" | "organization";
+  page: "home" | "event" | "organization" | "city";
   locale: currentSupportLocale;
   event?: { name?: string; city?: string; startDate?: string | Date | null };
   organization?: { name: string };
+  city?: { name: string };
 }) {
   // ...
+  case "city":
+    return city
+      ? generateCityDetailKeywords(city, locale).join(",")
+      : universalKeywords(locale).join(",");
   // ...
 }

同时可移除独立导出的 cityDetailKeywordGenerator,改为通过 keywordGenerator 统一调用。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/meta.ts` around lines 352 - 354, The PR adds a standalone
cityDetailKeywordGenerator but the existing keywordGenerator already handles
"home" | "event" | "organization"; unify city logic by adding a "city" branch to
keywordGenerator and have it call generateCityDetailKeywords(locale, city) (or
generateCityDetailKeywords(city, locale) matching signature) and return the
joined string, then remove the exported cityDetailKeywordGenerator to collapse
public API; update any callers to use keywordGenerator("city", localeOrPayload)
and ensure the generateCityDetailKeywords usage and parameter order match the
original function signature.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/utils/meta.ts`:
- Around line 336-350: The zh-Hans branch in cityDetailDescriptionGenerator
currently says "活动列表" which is semantically different from the "时间轴"/"timeline"
wording used for zh-Hant and en; update the zh-Hans return template to use the
same "时间轴" wording (or a Chinese equivalent matching the timeline concept) so
all locale cases are consistent, keeping the same placeholders cityName and
eventCount in the function.

---

Nitpick comments:
In `@src/utils/meta.ts`:
- Around line 352-354: The PR adds a standalone cityDetailKeywordGenerator but
the existing keywordGenerator already handles "home" | "event" | "organization";
unify city logic by adding a "city" branch to keywordGenerator and have it call
generateCityDetailKeywords(locale, city) (or generateCityDetailKeywords(city,
locale) matching signature) and return the joined string, then remove the
exported cityDetailKeywordGenerator to collapse public API; update any callers
to use keywordGenerator("city", localeOrPayload) and ensure the
generateCityDetailKeywords usage and parameter order match the original function
signature.

@github-actions
Copy link

📦 Next.js Bundle Analysis for furrycons-site

This analysis was generated by the Next.js Bundle Analysis action. 🤖

⚠️ Global Bundle Size Increased

Page Size (compressed)
global 333.75 KB (🟡 +3 B)
Details

The global bundle is the javascript bundle that loads alongside every page. It is in its own category because its impact is much higher - an increase to its size means that every page on your website loads slower, and a decrease means every page loads faster.

Any third party scripts you have added directly to your app using the <script> tag are not accounted for in this analysis

If you want further insight into what is behind the changes, give @next/bundle-analyzer a try!

@github-actions
Copy link

📦 Next.js Bundle Analysis for furrycons-site

This analysis was generated by the Next.js Bundle Analysis action. 🤖

🎉 Global Bundle Size Decreased

Page Size (compressed)
global 333.74 KB (-1 B)
Details

The global bundle is the javascript bundle that loads alongside every page. It is in its own category because its impact is much higher - an increase to its size means that every page on your website loads slower, and a decrease means every page loads faster.

Any third party scripts you have added directly to your app using the <script> tag are not accounted for in this analysis

If you want further insight into what is behind the changes, give @next/bundle-analyzer a try!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/utils/meta.ts`:
- Line 260: The current const venue = `${event?.region?.localName ||
""}${event?.address || ""}` can produce an empty string which then renders as ""
in templates; change the venue construction in src/utils/meta.ts to build a
trimmed combined value from event?.region?.localName and event?.address and if
the result is empty set venue to null/undefined (or a fallback generic string),
and update any template usage that interpolates venue to only include the
quoted/address fragment when venue is non-empty (i.e., check venue truthiness
before adding quotes or the phrase "at/在 ...").

---

Duplicate comments:
In `@src/utils/meta.ts`:
- Around line 336-350: The translations in cityDetailDescriptionGenerator are
now consistent across the three branches (zh-Hans, zh-Hant, en) so no code
change is required; approve this change, keep the current default fallback
behavior intact, and when adding new locales in the future add explicit case
entries in cityDetailDescriptionGenerator to avoid falling back to the default.

@github-actions
Copy link

📦 Next.js Bundle Analysis for furrycons-site

This analysis was generated by the Next.js Bundle Analysis action. 🤖

This PR introduced no changes to the JavaScript bundle! 🙌

@PaiJi PaiJi merged commit d93f8c7 into main Feb 22, 2026
9 checks passed
@PaiJi PaiJi deleted the feat/update-meta branch February 22, 2026 09:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant