feat: enhance city detail page with SEO improvements and structured data#301
feat: enhance city detail page with SEO improvements and structured data#301
Conversation
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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. 📝 WalkthroughWalkthrough引入基于区域与本地化的城市详情 SEO 数据生成:解析区域事件为独立变量,扩展 getServerSideProps 返回的 props,新增 headMetas(title、des、keywords、link)与 structuredData,并在工具中添加城市关键词与描述生成器。 Changes
Sequence Diagram(s)(无序列图) Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 分钟 Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
📦 Next.js Bundle Analysis for furrycons-siteThis analysis was generated by the Next.js Bundle Analysis action. 🤖
|
| 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.
There was a problem hiding this comment.
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
startAt和endAt未允许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:cityDetailKeywordGenerator与generateCityDetailKeywords参数顺序不一致(仅风格问题,逻辑正确)。
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); |
There was a problem hiding this comment.
regionEvents?.records 使用可选链,若 regionEvents 为 null/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.
There was a problem hiding this comment.
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.
📦 Next.js Bundle Analysis for furrycons-siteThis analysis was generated by the Next.js Bundle Analysis action. 🤖
|
| 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!
📦 Next.js Bundle Analysis for furrycons-siteThis analysis was generated by the Next.js Bundle Analysis action. 🤖 🎉 Global Bundle Size Decreased
DetailsThe 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 If you want further insight into what is behind the changes, give @next/bundle-analyzer a try! |
There was a problem hiding this comment.
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.
📦 Next.js Bundle Analysis for furrycons-siteThis analysis was generated by the Next.js Bundle Analysis action. 🤖 This PR introduced no changes to the JavaScript bundle! 🙌 |
Summary by CodeRabbit
发布说明