@@ -3,9 +3,6 @@ import { ReactElement } from "react";
33import type { GetStaticPaths , GetStaticProps } from "next" ;
44import { useRouter } from "next/router" ;
55
6- import { APP_METADATA } from "@/common/constants" ;
7- import { stripHtml } from "@/common/lib/datetime" ;
8- import { fetchWithTimeout } from "@/common/lib/fetch-with-timeout" ;
96import { CampaignBanner , CampaignDonorsTable } from "@/entities/campaign" ;
107import { CampaignLayout } from "@/layout/campaign/components/layout" ;
118import { RootLayout } from "@/layout/components/root-layout" ;
@@ -42,69 +39,74 @@ export const getStaticPaths: GetStaticPaths = async () => {
4239 } ;
4340} ;
4441
45- // Pre-build each campaign page with its data
42+ // Default SEO values (inline to avoid import issues in serverless)
43+ const DEFAULT_SEO = {
44+ title : "Potlock | Fund Public Goods" ,
45+ description :
46+ "Discover and fund public goods projects on NEAR Protocol. Support open source, community initiatives, and impactful projects." ,
47+ image : "https://app.potlock.org/assets/images/meta-image.png" ,
48+ } ;
49+
50+ // Simple HTML strip function (inline to avoid import issues)
51+ const stripHtmlTags = ( html : string | undefined | null ) : string => {
52+ if ( ! html ) return "" ;
53+ return html . replace ( / < [ ^ > ] * > / g, "" ) . trim ( ) ;
54+ } ;
55+
56+ // Generate campaign page data on-demand
4657export const getStaticProps : GetStaticProps < SeoProps > = async ( { params } ) => {
47- try {
48- const campaignId = params ?. campaignId as string ;
58+ const campaignId = params ?. campaignId as string ;
59+
60+ // Fallback props for any error case
61+ const fallbackProps = {
62+ props : {
63+ seoTitle : campaignId ? `Campaign ${ campaignId } ` : DEFAULT_SEO . title ,
64+ seoDescription : DEFAULT_SEO . description ,
65+ seoImage : DEFAULT_SEO . image ,
66+ } ,
67+ revalidate : 60 , // Retry sooner on error
68+ } ;
4969
50- if ( ! campaignId ) {
51- return {
52- notFound : true ,
53- } ;
54- }
70+ if ( ! campaignId ) {
71+ return { notFound : true } ;
72+ }
5573
56- // Fetch with timeout to prevent server timeouts
57- const res = await fetchWithTimeout (
74+ try {
75+ // Use native fetch with AbortController for timeout
76+ const controller = new AbortController ( ) ;
77+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , 8000 ) ;
78+
79+ const res = await fetch (
5880 `https://dev.potlock.io/api/v1/campaigns/${ encodeURIComponent ( campaignId ) } ` ,
59- { } ,
60- 8000 , // 8 second timeout
81+ { signal : controller . signal } ,
6182 ) ;
6283
63- if ( ! res . ok ) {
64- // If campaign not found, return 404 instead of erroring
65- if ( res . status === 404 ) {
66- return {
67- notFound : true ,
68- } ;
69- }
70-
71- throw new Error ( `Failed to fetch campaign: ${ res . status } ` ) ;
72- }
73-
74- let campaign ;
84+ clearTimeout ( timeoutId ) ;
7585
76- try {
77- campaign = await res . json ( ) ;
78- } catch ( jsonError ) {
79- console . error ( "Error parsing campaign JSON:" , jsonError ) ;
80- throw new Error ( "Invalid campaign data format" ) ;
86+ // If campaign not found, return 404
87+ if ( res . status === 404 ) {
88+ return { notFound : true } ;
8189 }
8290
83- const seoTitle = campaign ?. name ?? `Campaign ${ campaignId } ` ;
84-
85- const seoDescription = stripHtml ( campaign ?. description ) ?? "Support this campaign on Potlock." ;
86-
87- // Use cover_image_url field which is the correct field for campaign images
88- const seoImage = campaign ?. cover_image_url ?? APP_METADATA . openGraph . images . url ;
91+ // For other errors, return fallback props (don't throw)
92+ if ( ! res . ok ) {
93+ console . error ( `Campaign API returned ${ res . status } for campaign ${ campaignId } ` ) ;
94+ return fallbackProps ;
95+ }
8996
90- return {
91- props : { seoTitle, seoDescription, seoImage } ,
92- // Revalidate every 2 minutes (120 seconds) to keep data fresh
93- revalidate : 120 ,
94- } ;
95- } catch ( error ) {
96- console . error ( "Error generating static props:" , error ) ;
97+ const campaign = await res . json ( ) ;
9798
98- // Return fallback props instead of throwing error to prevent 500
99- // This allows the page to render with default SEO data
10099 return {
101100 props : {
102- seoTitle : `Campaign ${ params ?. campaignId || "" } `,
103- seoDescription : APP_METADATA . description ,
104- seoImage : APP_METADATA . openGraph . images . url ,
101+ seoTitle : campaign ?. name || `Campaign `,
102+ seoDescription : stripHtmlTags ( campaign ?. description ) || DEFAULT_SEO . description ,
103+ seoImage : campaign ?. cover_image_url || DEFAULT_SEO . image ,
105104 } ,
106- // Shorter revalidate for error cases to retry sooner
107- revalidate : 60 ,
105+ revalidate : 120 , // 2 minutes
108106 } ;
107+ } catch ( error ) {
108+ // Log but don't throw - return fallback props
109+ console . error ( `Error fetching campaign ${ campaignId } :` , error ) ;
110+ return fallbackProps ;
109111 }
110112} ;
0 commit comments