[refactor] replace SSR with SSG for all SEO pages - #107
Conversation
[optimize] update Upstream pages
|
Warning Review limit reached
Next review available in: 32 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthrough本次变更将多个 Pages Router 页面从 SSR 迁移到 SSG/ISR,增加阻塞式静态路径工具,并更新 Lark 数据加载流程。同时规范化模型字段和 i18n 默认数据。 Changes页面静态生成迁移
模型字段与国际化默认值
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Next as Next.js
participant Lark as lark.client
participant Model as HackathonModel
participant Page as 页面
Next->>Lark: 获取 access token
Lark->>Model: 注入客户端并加载数据
Model-->>Page: 返回序列化 props
Page-->>Next: 返回 revalidate 或 notFound
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
There was a problem hiding this comment.
Pull request overview
This PR refactors multiple Next.js pages router routes from SSR (getServerSideProps) to SSG/ISR (getStaticProps + revalidate, and getStaticPaths with blocking fallback for dynamic routes) to improve SEO performance and reduce server-rendering dependency. It also bumps the Next.js toolchain patch level and updates related dependencies/config.
Changes:
- Migrated several SEO-facing pages from SSR to SSG/ISR, introducing a shared
skipBuildingAllhelper for dynamic SSG routes. - Updated Next.js from 16.2.11 → 16.2.12 (including patched dependency updates) and refreshed related dependency versions.
- Adjusted runtime/config behavior (redirect rules, translation store defaults, Lark-backed model extraction tweaks).
Reviewed changes
Copilot reviewed 17 out of 19 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-workspace.yaml | Updates patched Next.js dependency reference to 16.2.12 patch file. |
| pnpm-lock.yaml | Locks updated dependency graph after Next.js + ecosystem bumps. |
| patches/next@16.2.12.patch | Updates Next.js SWC options patch (decorators + decoratorVersion). |
| package.json | Bumps Next.js + related dev deps; removes sass dependency. |
| next.config.ts | Modifies redirect rules for wiki/recipe/policy routes. |
| pages/api/SSG.ts | Adds skipBuildingAll helper for “blocking” SSG dynamic routes. |
| pages/index.tsx | Switches homepage to getStaticProps with Lark-backed fetching + ISR. |
| pages/award/index.tsx | Switches award page to getStaticProps with Lark-backed fetching. |
| pages/project.tsx | Converts SSR → SSG/ISR for project listing page. |
| pages/issue.tsx | Converts SSR → SSG/ISR for issues listing page. |
| pages/volunteer.tsx | Converts SSR → SSG/ISR for volunteer listing page. |
| pages/open-library/book/[id].tsx | Converts book detail route SSR → dynamic SSG (blocking) + ISR, using Lark client. |
| pages/NGO/[year]/index.tsx | Converts NGO year route to dynamic SSG (blocking). |
| pages/NGO/[year]/landscape.tsx | Converts NGO landscape route to dynamic SSG (blocking). |
| pages/hackathon/[id].tsx | Converts hackathon detail SSR → dynamic SSG (blocking) + ISR, using Lark client. |
| pages/hackathon/[id]/team/[tid].tsx | Converts hackathon team SSR → dynamic SSG (blocking), using Lark client. |
| models/Translation.ts | Changes i18n store defaults (language + fallback map). |
| models/Hackathon.ts | Adjusts extracted fields (project prize, member skills parsing). |
| .vscode/extensions.json | Replaces Thunder Client recommendation with Postman extension. |
Suppressed comments (3)
next.config.ts:52
- The redirect rules no longer match the base routes (/wiki, /recipe, /policy) without a trailing slash, so those URLs will fall through (likely 404) while only /:path* redirects still work. If the intent is to send both the base route and sub-paths to the external wiki, add back an explicit redirect for the base route.
const redirects: NextConfig['redirects'] = async () =>
['/wiki', '/recipe', '/policy'].flatMap(route => [
{
source: `${route}/:path*`,
destination: `https://wiki.fcc-cd.dev${route}/:path*`,
permanent: true,
},
]);
pages/index.tsx:25
- getStaticProps is doing an external Lark token + API fetch. To keep build-time low/consistent with the new SSG pattern used elsewhere in this PR, wrap this handler with skipBuilding so CI builds don't execute the remote call.
export const getStaticProps: GetStaticProps<HomePageProps> = async () => {
await lark.getAccessToken();
const store = new ActivityModel();
store.client = lark.client;
pages/hackathon/[id]/team/[tid].tsx:63
- Without
revalidate, the generated page will not refresh unless you redeploy. If these team/member/product records can change, add an ISR revalidate interval (matching the other SSG pages in this PR) so updates become visible automatically.
productStore.getAll({ project: project.name as string }),
]);
return { props: { activity, project, members, products } };
};
| import { GetStaticProps } from 'next'; | ||
| import { FC } from 'react'; | ||
|
|
||
| import { Award, AwardModel } from '../../models/Award'; | ||
| import { lark } from '../api/Lark/core'; |
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pages/award/index.tsx (1)
18-20: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win
AwardPage未使用awards,getStaticProps的数据拉取当前无效。组件渲染空片段,
awards被完全丢弃。这会在每次构建和每次 ISR 重新生成时产生无意义的 Lark 请求。请确认该页面是否为未完成的占位实现。如果页面尚未开发,建议暂不引入数据获取。如果需要,我可以生成一个使用 React Bootstrap 组件、并用
t()包裹全部文案的奖项列表渲染实现。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pages/award/index.tsx` around lines 18 - 20, 当前 AwardPage 完全丢弃 awards,导致 getStaticProps 的数据请求无效。若页面仍是占位实现,请移除 AwardPage 的 awards 属性及对应的 getStaticProps 数据获取;若需保留数据获取,则必须在 AwardPage 中渲染 awards,避免继续无意义请求。
🧹 Nitpick comments (2)
pages/NGO/[year]/landscape.tsx (1)
17-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win建议让
getStaticProps的 Props 类型与页面组件类型一致。当前
getStaticProps声明Pick<OrganizationModel, 'typeMap'>,而LandscapePage声明OpenCollaborationLandscapeProps。两者由 TypeScript 分别检查,不会互相约束。若OpenCollaborationLandscapeProps后续增删字段,编译器不会报错。直接复用组件的 props 类型可以建立强类型契约。
♻️ 建议修改
-export const getStaticProps: GetStaticProps< - Pick<OrganizationModel, 'typeMap'>, - { year: string } -> = async ({ params }) => { +export const getStaticProps: GetStaticProps< + OpenCollaborationLandscapeProps, + { year: string } +> = async ({ params }) => {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pages/NGO/`[year]/landscape.tsx around lines 17 - 19, Update getStaticProps to use the same OpenCollaborationLandscapeProps type declared by LandscapePage instead of Pick<OrganizationModel, 'typeMap'>, establishing a shared compile-time contract between the data loader and page component.pages/volunteer.tsx (1)
14-18: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value建议在
getStaticProps中使用新的RepositoryModel实例,而不是模块级单例。
repositoryStore是模块级 MobX 单例,在 Node 服务端进程内被所有请求共享。ISR 并发重新生成时,多个getAllContributors()调用会写入同一份 observable 状态。当前代码只使用返回值,不读取 store 状态,所以没有正确性问题。不过
pages/issue.tsx和pages/project.tsx已改为在getStaticProps内new RepositoryModel('Open-Source-Bazaar')。保持一致可以避免服务端共享可变状态。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pages/volunteer.tsx` around lines 14 - 18, Update getStaticProps in pages/volunteer.tsx to instantiate a new RepositoryModel('Open-Source-Bazaar') locally and call getAllContributors() on that instance instead of using the module-level repositoryStore singleton. Preserve the existing returned props and revalidate behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@models/Hackathon.ts`:
- Around line 185-189: Update the skills transformation in the team/member data
mapping to use optional chaining with an empty-array fallback before calling
map, then split the joined text and filter out empty entries so omitted or empty
skills produce [] without throwing. Remove reliance on the non-null assertion
and preserve normalized skill values for populated input.
- Around line 154-163: Update the ProjectPageProps contract and the team detail
page in pages/hackathon/[id]/team/[tid].tsx to treat prize as an optional string
array, preserving and rendering every prize rather than concatenating a single
value; keep ProjectModel.extractFields consistent with this multi-prize output.
In `@pages/hackathon/`[id].tsx:
- Line 90: Update pages/hackathon/[id].tsx lines 90-90 by wrapping
store.getOne(params!.id) in exception handling and returning { notFound: true,
revalidate: Minute / Second } on failure, matching the existing notFound branch.
In pages/hackathon/[id]/team/[tid].tsx lines 41-50, catch failures from
activityStore.getOne(params!.id) and projectStore.getOne(params!.tid) and return
{ notFound: true }. In pages/open-library/book/[id].tsx lines 26-26, catch
store.getOne failures and return notFound as specified by that file’s existing
pattern.
- Around line 97-104: Update the six model constructions in getStaticProps to
explicitly pass lark.client as each model’s client option: PersonModel,
OrganizationModel, AgendaModel, PrizeModel, TemplateModel, and ProjectModel.
Preserve the existing appId and tableIdMap arguments while ensuring all
associated data requests use the authenticated client.
In `@pages/index.tsx`:
- Around line 21-33: Wrap the getStaticProps implementation in pages/index.tsx
(lines 21-33) with skipBuilding<HomePageProps> and import skipBuilding from
./api/SSG, preserving the existing revalidate value. Apply the same wrapper in
pages/award/index.tsx (lines 7-16) using skipBuilding<{ awards: Award[] }>,
import it from ../api/SSG, and add the default revalidate value expected by
skipBuilding.
In `@pages/NGO/`[year]/index.tsx:
- Around line 25-35: 在 NGO 页面导出函数 getStaticProps 的返回值中加入与其他页面一致的 revalidate
配置,使用项目现有的时间常量,使 blocking fallback 生成的页面能够按周期重新生成;保留现有 props 内容不变。
In `@pages/NGO/`[year]/landscape.tsx:
- Around line 25-28: 为 getStaticProps 在 pages/NGO/[year]/landscape.tsx#L25-L28
的返回值加入 revalidate,并从 web-utility 导入 Minute 与 Second,使用现有周期常量;同时在
pages/hackathon/[id]/team/[tid].tsx#L58-L63 的 getStaticProps 返回值中加入相同的
revalidate 配置,使两类页面按周期重新生成。
---
Outside diff comments:
In `@pages/award/index.tsx`:
- Around line 18-20: 当前 AwardPage 完全丢弃 awards,导致 getStaticProps
的数据请求无效。若页面仍是占位实现,请移除 AwardPage 的 awards 属性及对应的 getStaticProps
数据获取;若需保留数据获取,则必须在 AwardPage 中渲染 awards,避免继续无意义请求。
---
Nitpick comments:
In `@pages/NGO/`[year]/landscape.tsx:
- Around line 17-19: Update getStaticProps to use the same
OpenCollaborationLandscapeProps type declared by LandscapePage instead of
Pick<OrganizationModel, 'typeMap'>, establishing a shared compile-time
contract between the data loader and page component.
In `@pages/volunteer.tsx`:
- Around line 14-18: Update getStaticProps in pages/volunteer.tsx to instantiate
a new RepositoryModel('Open-Source-Bazaar') locally and call
getAllContributors() on that instance instead of using the module-level
repositoryStore singleton. Preserve the existing returned props and revalidate
behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: eb5d78cd-f053-44da-b4b8-5be7c663b43e
⛔ Files ignored due to path filters (6)
.vscode/extensions.jsonis excluded by none and included by nonenext.config.tsis excluded by none and included by nonepackage.jsonis excluded by none and included by nonepatches/next@16.2.12.patchis excluded by none and included by nonepnpm-lock.yamlis excluded by!**/pnpm-lock.yaml,!pnpm-lock.yamland included by nonepnpm-workspace.yamlis excluded by none and included by none
📒 Files selected for processing (13)
models/Hackathon.tsmodels/Translation.tspages/NGO/[year]/index.tsxpages/NGO/[year]/landscape.tsxpages/api/SSG.tspages/award/index.tsxpages/hackathon/[id].tsxpages/hackathon/[id]/team/[tid].tsxpages/index.tsxpages/issue.tsxpages/open-library/book/[id].tsxpages/project.tsxpages/volunteer.tsx
| extractFields({ | ||
| fields: { members, products, prize, ...fields }, | ||
| ...meta | ||
| }: TableRecord<Project>) { | ||
| return { | ||
| ...meta, | ||
| ...fields, | ||
| members: (members as TableCellRelation[])?.map(normalizeText), | ||
| products: (products as TableCellRelation[])?.map(normalizeText), | ||
| prize: (prize as TableCellRelation[])?.map(normalizeText), |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 5 '\b(prize|ProjectModel|Project)\b' --glob '*.{ts,tsx}' .Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io
Length of output: 22452
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== candidate files =="
git ls-files | rg '(^models/Hackathon\.ts$|^pages/hackathon/\[id\]/team/\[tid\]\.tsx$|^models/(BiDataTable|base).*\.ts$|^node_modules/mobx-lark|package\.json)'
echo
echo "== models/Hackathon.ts relevant sections =="
sed -n '1,220p' models/Hackathon.ts
echo
echo "== pages/hackathon/[id]/team/[tid].tsx relevant sections =="
sed -n '1,260p' 'pages/hackathon/[id]/team/[tid].tsx'
echo
echo "== search for extractFields/type overrides =="
rg -n "extractFields|BiDataTable|queryOptions|ProjectModel|export type Project" --glob '*.{ts,tsx}' models pagesRepository: Open-Source-Bazaar/Open-Source-Bazaar.github.io
Length of output: 19747
统一 prize 的模型输出和页面渲染契约。
models/Hackathon.ts 已将 ProjectModel.extracFields 的 prize 输出为 string[],但团队详情页仍按单个奖项拼接和渲染,多奖项会丢失。更新 ProjectPageProps 和 pages/hackathon/[id]/team/[tid].tsx,使奖项列表按 prize?: string[] 处理;若只保留单奖项字段,则移除模型的多奖项转换。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@models/Hackathon.ts` around lines 154 - 163, Update the ProjectPageProps
contract and the team detail page in pages/hackathon/[id]/team/[tid].tsx to
treat prize as an optional string array, preserving and rendering every prize
rather than concatenating a single value; keep ProjectModel.extractFields
consistent with this multi-prize output.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 19 changed files in this pull request and generated no new comments.
Suppressed comments (6)
models/Hackathon.ts:189
- MemberModel.extractFields now non-null asserts skills and calls .map() unconditionally. If the "skills" field is missing/null in a record, this will throw at runtime (previously it safely defaulted to []).
skills:
(skills as TableCellText[])!
.map(normalizeText)
.join()
.split(/\s*,\s*/) || [],
pages/award/index.tsx:15
- After switching to SSG, this page no longer updates unless a full rebuild occurs because
revalidateis omitted. If awards are expected to stay fresh (similar to prior SSR behavior), add ISR revalidation like other pages in this PR.
return { props: { awards } };
models/Translation.ts:34
- The new defaulting logic can put the store into an inconsistent state when a non-default language is provided without serializedData (e.g. currentLanguage becomes "en-US" while currentMap contains zh-CN strings, and the zh-CN map is stored under the wrong language key). This can lead to incorrect translations if createI18nStore is ever called without languageMap for a non-zh-CN language.
language = 'zh-CN' as N,
serializedData?: string,
) => {
const data = serializedData
? (JSON.parse(serializedData, decodeFunctions) as TranslationMap<K>)
pages/hackathon/[id]/team/[tid].tsx:64
- The ISR toggle uses
+new Date(activity.endTime as number)but Activity.endTime is a TableCellValue and may not be numeric. If it's non-numeric, this becomes NaN and the page will keep revalidating forever (and may never disable revalidation after the event ends). Prefer a finite numeric conversion before comparing.
revalidate: +new Date(activity.endTime as number) < Date.now() ? undefined : Minute / Second,
pages/hackathon/[id].tsx:123
- The ISR toggle uses
+new Date(activity.endTime as number)but Activity.endTime is a TableCellValue and may not be a number. If it's non-numeric, this becomes NaN and the page will keep revalidating forever (and may never disable revalidation after the event ends). Use a numeric conversion with a finite check before comparing.
revalidate: +new Date(activity.endTime as number) < Date.now() ? undefined : Minute / Second,
next.config.ts:49
- The redirect for the exact base routes (/wiki, /recipe, /policy) was removed, but there are no local pages for those paths. Visiting
/wiki(without a trailing segment) will now 404 instead of redirecting to the external wiki root.
const redirects: NextConfig['redirects'] = async () =>
['/wiki', '/recipe', '/policy'].flatMap(route => [
{
source: `${route}/:path*`,
destination: `https://wiki.fcc-cd.dev${route}/:path*`,
📄 Knowledge reviewDosu skipped reviewing this PR because your organization has used its |
Summary by CodeRabbit
新功能
性能优化
体验改进