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 { 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({
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
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