Fix Railway build errors by marking dynamic routes (#437)
## Summary - Fixes Railway deployment build failures caused by dynamic server usage errors - Marks routes that use runtime features as `force-dynamic` to prevent static generation attempts - Creates proper error pages to handle 404/500 scenarios ## Problem The build was failing with "Dynamic server usage" errors because Next.js was trying to statically generate pages that use runtime features like: - `cookies()` for authentication - `searchParams` for filtering - Dynamic data fetching that requires request-time context ## Solution Added `export const dynamic = 'force-dynamic'` to: ### API Routes - `/api/jobs/route.ts` - uses searchParams - `/api/jobs/skills/route.ts` - uses cookies via fetchFromApi - `/api/version/route.ts` - uses cookies via fetchFromApi - `/api/raids/groups/route.ts` - uses cookies via fetchFromApi - `/api/parties/route.ts` - uses searchParams and cookies - `/api/parties/[shortcode]/route.ts` - uses cookies - `/api/parties/[shortcode]/remix/route.ts` - uses cookies ### Page Components - `/app/[locale]/teams/page.tsx` - uses searchParams - `/app/[locale]/new/page.tsx` - fetches dynamic data - `/app/[locale]/saved/page.tsx` - uses cookies and searchParams - Additional pages to avoid useContext errors during static generation ### Error Handling - Created `/pages/_error.tsx` - Simple error page without i18n complexity - Created `/app/not-found.tsx` - App Router 404 page ## Test plan - [x] Build completes successfully locally with `npm run build` - [ ] Deploy to Railway staging environment - [ ] Verify all dynamic routes work correctly - [ ] Check error pages display properly 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
e4b7f0c356
commit
1f8de7ee30
19 changed files with 127 additions and 0 deletions
|
|
@ -1,5 +1,8 @@
|
|||
import { Metadata } from 'next'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
// Force dynamic rendering to avoid useContext issues during static generation
|
||||
export const dynamic = 'force-dynamic'
|
||||
import AboutPageClient from './AboutPageClient'
|
||||
|
||||
export async function generateMetadata({
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ import { Metadata } from 'next'
|
|||
import { getRaidGroups } from '~/app/lib/data'
|
||||
import NewPartyClient from './NewPartyClient'
|
||||
|
||||
// Force dynamic rendering because getRaidGroups uses cookies
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: 'Create a new team / granblue.team',
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ import { Metadata } from 'next'
|
|||
import { Link } from '~/i18n/navigation'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
// Force dynamic rendering to avoid useContext issues during static generation
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Page not found / granblue.team',
|
||||
description: 'The page you were looking for could not be found'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { redirect } from 'next/navigation'
|
||||
|
||||
// Force dynamic rendering because redirect needs dynamic context
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default function HomePage() {
|
||||
// In the App Router, we can use redirect directly in a Server Component
|
||||
redirect('/new')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { Metadata } from 'next'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
// Force dynamic rendering to avoid useContext issues during static generation
|
||||
export const dynamic = 'force-dynamic'
|
||||
import RoadmapPageClient from './RoadmapPageClient'
|
||||
|
||||
export async function generateMetadata({
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import { cookies } from 'next/headers'
|
|||
import { getFavorites, getRaidGroups } from '~/app/lib/data'
|
||||
import SavedPageClient from './SavedPageClient'
|
||||
|
||||
// Force dynamic rendering because we use cookies and searchParams
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: 'Your saved teams / granblue.team',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { Metadata } from 'next'
|
||||
import Link from 'next/link'
|
||||
|
||||
// Force dynamic rendering to avoid useContext issues during static generation
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Server Error / granblue.team',
|
||||
description: 'The server encountered an internal error and was unable to complete your request'
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import React from 'react'
|
|||
import { getTeams as fetchTeams, getRaidGroups } from '~/app/lib/data'
|
||||
import TeamsPageClient from './TeamsPageClient'
|
||||
|
||||
// Force dynamic rendering because we use searchParams
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: 'Discover teams / granblue.team',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { Metadata } from 'next'
|
||||
import Link from 'next/link'
|
||||
|
||||
// Force dynamic rendering to avoid useContext issues during static generation
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Unauthorized / granblue.team',
|
||||
description: "You don't have permission to perform that action"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { Metadata } from 'next'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
// Force dynamic rendering to avoid useContext issues during static generation
|
||||
export const dynamic = 'force-dynamic'
|
||||
import UpdatesPageClient from './UpdatesPageClient'
|
||||
|
||||
export async function generateMetadata({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { fetchFromApi } from '~/app/lib/api-utils'
|
||||
|
||||
// Force dynamic rendering because we use searchParams
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// GET handler for fetching all jobs
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { fetchFromApi } from '~/app/lib/api-utils'
|
||||
|
||||
// Force dynamic rendering because fetchFromApi uses cookies
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// GET handler for fetching all job skills
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { postToApi, revalidate } from '~/app/lib/api-utils';
|
||||
|
||||
// Force dynamic rendering because postToApi uses cookies
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
// POST handler for remixing a party
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||
import { z } from 'zod';
|
||||
import { fetchFromApi, putToApi, deleteFromApi, revalidate, PartySchema } from '~/app/lib/api-utils';
|
||||
|
||||
// Force dynamic rendering because fetchFromApi uses cookies
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
// GET handler for fetching a single party by shortcode
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||
import { z } from 'zod';
|
||||
import { fetchFromApi, postToApi, PartySchema } from '~/app/lib/api-utils';
|
||||
|
||||
// Force dynamic rendering because we use searchParams and cookies
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
// GET handler for fetching parties with filters
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { fetchFromApi } from '~/app/lib/api-utils';
|
||||
|
||||
// Force dynamic rendering because fetchFromApi uses cookies
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
// GET handler for fetching raid groups
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { fetchFromApi } from '~/app/lib/api-utils';
|
||||
|
||||
// Force dynamic rendering because fetchFromApi uses cookies
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
// GET handler for fetching version info
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
|
|
|
|||
29
app/not-found.tsx
Normal file
29
app/not-found.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { Metadata } from 'next'
|
||||
|
||||
// Force dynamic rendering to avoid issues
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Page not found / granblue.team',
|
||||
description: 'The page you were looking for could not be found'
|
||||
}
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="error-container">
|
||||
<div className="error-content">
|
||||
<h1>404</h1>
|
||||
<h2>Page Not Found</h2>
|
||||
<p>The page you're looking for doesn't exist.</p>
|
||||
<div className="error-actions">
|
||||
<a href="/new" className="button primary">
|
||||
Create a new party
|
||||
</a>
|
||||
<a href="/teams" className="button secondary">
|
||||
Browse teams
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
47
pages/_error.tsx
Normal file
47
pages/_error.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { NextPageContext } from 'next'
|
||||
import Head from 'next/head'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface ErrorProps {
|
||||
statusCode: number
|
||||
}
|
||||
|
||||
function Error({ statusCode }: ErrorProps) {
|
||||
return (
|
||||
<div className="error-page" style={{ padding: '2rem', textAlign: 'center' }}>
|
||||
<Head>
|
||||
<title>
|
||||
{statusCode
|
||||
? `${statusCode} - Server Error / granblue.team`
|
||||
: 'Client Error / granblue.team'}
|
||||
</title>
|
||||
</Head>
|
||||
|
||||
<div style={{ maxWidth: '600px', margin: '0 auto' }}>
|
||||
<h1 style={{ fontSize: '4rem', marginBottom: '1rem' }}>{statusCode || 'Error'}</h1>
|
||||
<p style={{ marginBottom: '2rem' }}>
|
||||
{statusCode
|
||||
? `A ${statusCode} error occurred on the server.`
|
||||
: 'An error occurred on the client.'}
|
||||
</p>
|
||||
<Link href="/" style={{
|
||||
display: 'inline-block',
|
||||
padding: '0.75rem 1.5rem',
|
||||
backgroundColor: '#007bff',
|
||||
color: 'white',
|
||||
borderRadius: '0.25rem',
|
||||
textDecoration: 'none'
|
||||
}}>
|
||||
Go Home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Error.getInitialProps = ({ res, err }: NextPageContext) => {
|
||||
const statusCode = res ? res.statusCode : err ? err.statusCode : 404
|
||||
return { statusCode }
|
||||
}
|
||||
|
||||
export default Error
|
||||
Loading…
Reference in a new issue