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:
Justin Edmund 2025-09-04 02:41:03 -07:00 committed by GitHub
parent e4b7f0c356
commit 1f8de7ee30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 127 additions and 0 deletions

View file

@ -1,5 +1,8 @@
import { Metadata } from 'next' import { Metadata } from 'next'
import { getTranslations } from 'next-intl/server' 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' import AboutPageClient from './AboutPageClient'
export async function generateMetadata({ export async function generateMetadata({

View file

@ -2,6 +2,9 @@ import { Metadata } from 'next'
import { getRaidGroups } from '~/app/lib/data' import { getRaidGroups } from '~/app/lib/data'
import NewPartyClient from './NewPartyClient' import NewPartyClient from './NewPartyClient'
// Force dynamic rendering because getRaidGroups uses cookies
export const dynamic = 'force-dynamic'
// Metadata // Metadata
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Create a new team / granblue.team', title: 'Create a new team / granblue.team',

View file

@ -2,6 +2,9 @@ import { Metadata } from 'next'
import { Link } from '~/i18n/navigation' import { Link } from '~/i18n/navigation'
import { getTranslations } from 'next-intl/server' 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 = { export const metadata: Metadata = {
title: 'Page not found / granblue.team', title: 'Page not found / granblue.team',
description: 'The page you were looking for could not be found' description: 'The page you were looking for could not be found'

View file

@ -1,5 +1,8 @@
import { redirect } from 'next/navigation' import { redirect } from 'next/navigation'
// Force dynamic rendering because redirect needs dynamic context
export const dynamic = 'force-dynamic'
export default function HomePage() { export default function HomePage() {
// In the App Router, we can use redirect directly in a Server Component // In the App Router, we can use redirect directly in a Server Component
redirect('/new') redirect('/new')

View file

@ -1,5 +1,8 @@
import { Metadata } from 'next' import { Metadata } from 'next'
import { getTranslations } from 'next-intl/server' 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' import RoadmapPageClient from './RoadmapPageClient'
export async function generateMetadata({ export async function generateMetadata({

View file

@ -4,6 +4,9 @@ import { cookies } from 'next/headers'
import { getFavorites, getRaidGroups } from '~/app/lib/data' import { getFavorites, getRaidGroups } from '~/app/lib/data'
import SavedPageClient from './SavedPageClient' import SavedPageClient from './SavedPageClient'
// Force dynamic rendering because we use cookies and searchParams
export const dynamic = 'force-dynamic'
// Metadata // Metadata
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Your saved teams / granblue.team', title: 'Your saved teams / granblue.team',

View file

@ -1,6 +1,9 @@
import { Metadata } from 'next' import { Metadata } from 'next'
import Link from 'next/link' import Link from 'next/link'
// Force dynamic rendering to avoid useContext issues during static generation
export const dynamic = 'force-dynamic'
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Server Error / granblue.team', title: 'Server Error / granblue.team',
description: 'The server encountered an internal error and was unable to complete your request' description: 'The server encountered an internal error and was unable to complete your request'

View file

@ -3,6 +3,9 @@ import React from 'react'
import { getTeams as fetchTeams, getRaidGroups } from '~/app/lib/data' import { getTeams as fetchTeams, getRaidGroups } from '~/app/lib/data'
import TeamsPageClient from './TeamsPageClient' import TeamsPageClient from './TeamsPageClient'
// Force dynamic rendering because we use searchParams
export const dynamic = 'force-dynamic'
// Metadata // Metadata
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Discover teams / granblue.team', title: 'Discover teams / granblue.team',

View file

@ -1,6 +1,9 @@
import { Metadata } from 'next' import { Metadata } from 'next'
import Link from 'next/link' import Link from 'next/link'
// Force dynamic rendering to avoid useContext issues during static generation
export const dynamic = 'force-dynamic'
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Unauthorized / granblue.team', title: 'Unauthorized / granblue.team',
description: "You don't have permission to perform that action" description: "You don't have permission to perform that action"

View file

@ -1,5 +1,8 @@
import { Metadata } from 'next' import { Metadata } from 'next'
import { getTranslations } from 'next-intl/server' 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' import UpdatesPageClient from './UpdatesPageClient'
export async function generateMetadata({ export async function generateMetadata({

View file

@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { fetchFromApi } from '~/app/lib/api-utils' 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 // GET handler for fetching all jobs
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {

View file

@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { fetchFromApi } from '~/app/lib/api-utils' 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 // GET handler for fetching all job skills
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {

View file

@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { postToApi, revalidate } from '~/app/lib/api-utils'; 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 // POST handler for remixing a party
export async function POST( export async function POST(
request: NextRequest, request: NextRequest,

View file

@ -2,6 +2,9 @@ import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod'; import { z } from 'zod';
import { fetchFromApi, putToApi, deleteFromApi, revalidate, PartySchema } from '~/app/lib/api-utils'; 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 // GET handler for fetching a single party by shortcode
export async function GET( export async function GET(
request: NextRequest, request: NextRequest,

View file

@ -2,6 +2,9 @@ import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod'; import { z } from 'zod';
import { fetchFromApi, postToApi, PartySchema } from '~/app/lib/api-utils'; 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 // GET handler for fetching parties with filters
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {

View file

@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { fetchFromApi } from '~/app/lib/api-utils'; 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 // GET handler for fetching raid groups
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {

View file

@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { fetchFromApi } from '~/app/lib/api-utils'; 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 // GET handler for fetching version info
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {

29
app/not-found.tsx Normal file
View 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&apos;re looking for doesn&apos;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
View 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