11import type { Metadata } from "next" ;
22import Link from "next/link" ;
3+ import { setRequestLocale } from "next-intl/server" ;
4+ import { hasLocale } from "next-intl" ;
5+ import { notFound } from "next/navigation" ;
36import { Header } from "@/app/components/Header" ;
47import { Footer } from "@/app/components/Footer" ;
58import type { EventView } from "./types" ;
69import { sanitizeMediaUrl } from "@/lib/url-safety" ;
10+ import { routing } from "@/i18n/routing" ;
711
8- /**
9- * /events 列表页。
10- *
11- * SSR 直连后端(BACKEND_URL)拉 published + archived 活动。
12- * 错误策略参考 /u/[username]/page.tsx:只有网络 / 5xx 才抛,空列表不是错误。
13- *
14- * revalidate: 300 把 Neon 打压力压到每 5min 一次 SSR,和 PR #286 的 profile 策略一致。
15- */
16-
12+ // ISR 5min:和 profile/feed 同一节流策略,控后端 QPS。
13+ // setRequestLocale + generateStaticParams 是 next-intl SSG 的必要条件,
14+ // 缺任一项会让 next-intl 退回 cookies() 把这条路由钉成 ƒ Dynamic。
1715export const revalidate = 300 ;
1816
1917interface ApiResponse < T > {
@@ -22,24 +20,50 @@ interface ApiResponse<T> {
2220 message ?: string ;
2321}
2422
23+ // 只在 build 阶段允许 fetch 失败降级(让 SSG 不挂),运行时仍 throw 给 Sentry。
24+ const IS_BUILD = process . env . NEXT_PHASE === "phase-production-build" ;
25+
2526async function fetchEvents ( ) : Promise < EventView [ ] > {
2627 const backendUrl = process . env . BACKEND_URL ;
2728 if ( ! backendUrl ) {
28- // 开发环境或 misconfig 时给一个清晰报错,而不是静默空列表
29+ if ( IS_BUILD ) {
30+ console . warn (
31+ "[events] BACKEND_URL not set at build, rendering empty shell; ISR will fetch real data after deploy" ,
32+ ) ;
33+ return [ ] ;
34+ }
2935 throw new Error ( "BACKEND_URL is not configured" ) ;
3036 }
31- const res = await fetch ( `${ backendUrl } /api/events` , {
32- next : { revalidate : 300 } ,
33- headers : {
34- accept : "application/json" ,
35- "user-agent" : "InvolutionHell-SSR/1.0 (+https://involutionhell.com)" ,
36- } ,
37- } ) ;
38- if ( ! res . ok ) {
39- throw new Error ( `/api/events backend ${ res . status } ${ res . statusText } ` ) ;
37+ try {
38+ const res = await fetch ( `${ backendUrl } /api/events` , {
39+ next : { revalidate : 300 } ,
40+ headers : {
41+ accept : "application/json" ,
42+ "user-agent" : "InvolutionHell-SSR/1.0 (+https://involutionhell.com)" ,
43+ } ,
44+ } ) ;
45+ if ( ! res . ok ) {
46+ if ( IS_BUILD ) {
47+ console . warn (
48+ `[events] backend ${ res . status } at build, rendering empty shell` ,
49+ ) ;
50+ return [ ] ;
51+ }
52+ throw new Error ( `/api/events backend ${ res . status } ${ res . statusText } ` ) ;
53+ }
54+ const json = ( await res . json ( ) ) as ApiResponse < EventView [ ] > ;
55+ return json . success && json . data ? json . data : [ ] ;
56+ } catch ( err ) {
57+ if ( IS_BUILD ) {
58+ console . warn (
59+ "[events] fetch failed at build, rendering empty shell:" ,
60+ err ,
61+ ) ;
62+ return [ ] ;
63+ }
64+ // 运行时失败仍然 throw —— Sentry 抓到,错误页正常显示,不掩盖故障
65+ throw err ;
4066 }
41- const json = ( await res . json ( ) ) as ApiResponse < EventView [ ] > ;
42- return json . success && json . data ? json . data : [ ] ;
4367}
4468
4569export const metadata : Metadata = {
@@ -48,7 +72,15 @@ export const metadata: Metadata = {
4872 "Coffee Chat、Mock Interview、Career Journey、Open.Onion 等社群活动汇总,直播入口和历史回放一站式。" ,
4973} ;
5074
51- export default async function EventsListPage ( ) {
75+ interface Props {
76+ params : Promise < { locale : string } > ;
77+ }
78+
79+ export default async function EventsListPage ( { params } : Props ) {
80+ const { locale } = await params ;
81+ if ( ! hasLocale ( routing . locales , locale ) ) notFound ( ) ;
82+ setRequestLocale ( locale ) ;
83+
5284 const all = await fetchEvents ( ) ;
5385 // 按时间划分:进行中 / 即将开始 / 已结束。ongoing + past 由后端标记,剩下的归"即将开始"
5486 const ongoing = all . filter ( ( e ) => e . ongoing ) ;
@@ -204,3 +236,7 @@ function formatDate(iso: string): string {
204236 return iso ;
205237 }
206238}
239+
240+ export function generateStaticParams ( ) {
241+ return routing . locales . map ( ( locale ) => ( { locale } ) ) ;
242+ }
0 commit comments