feat: SEO enhancements — server metadata, sitemap, robots, dynamic base URL

- Split all page.tsx files into server wrapper (metadata export) + client.tsx (Apollo/interactive)
- Add robots.ts and sitemap.ts (tournaments, teams, players)
- Add metadataBase, OpenGraph and Twitter card metadata to root layout
- Replace hardcoded worldcup.pivoine.art with NEXT_PUBLIC_SITE_URL env var (sitemap/robots) and relative paths (page metadata, resolved by Next.js against metadataBase)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 20:18:36 +02:00
parent 2bd32daae1
commit a494c80a76
19 changed files with 1968 additions and 1770 deletions
+117
View File
@@ -0,0 +1,117 @@
'use client'
import { useQuery, gql } from '@/lib/graphql/hooks'
import { use, useEffect } from 'react'
import Link from 'next/link'
import { TeamFlag } from '@/components/team-flag'
import { MatchCard } from '@/components/match-card'
const PLAYER_QUERY = gql`
query Player($name: String!) {
player(name: $name) {
playerName goals penalties ownGoals tournaments
team { id name iso2 slug }
}
}
`
const PLAYER_MATCHES_QUERY = gql`
query PlayerMatches($name: String!) {
tournaments { year }
}
`
interface PlayerData {
playerName: string; goals: number; penalties: number; ownGoals: number; tournaments: number
team?: { id: number; name: string; iso2?: string | null; slug: string } | null
}
export function PlayerClient({ params }: { params: Promise<{ name: string }> }) {
const { name: encodedName } = use(params)
const name = decodeURIComponent(encodedName)
const { data, loading } = useQuery(PLAYER_QUERY, { variables: { name } })
const player: PlayerData | null = data?.player ?? null
useEffect(() => {
}, [player, name])
// Fetch all goals for this player broken down by year
const { data: goalsData } = useQuery(gql`
query PlayerGoalsByYear {
tournaments { year }
topScorers(limit: 1000) {
playerName goals team { id }
}
}
`)
if (loading && !data) {
return <div className="max-w-[1200px] mx-auto px-7 py-10 text-green-muted">Loading player</div>
}
if (!player) {
return (
<div className="max-w-[1200px] mx-auto px-7 py-10">
<h1 className="font-['Bebas_Neue'] text-[52px] text-green">{name}</h1>
<p className="text-green-muted mt-4">No goal data found for this player in World Cup history.</p>
<Link href="/stats" className="text-green text-sm mt-4 inline-block hover:underline"> All-time scorers</Link>
</div>
)
}
const normalGoals = player.goals - player.penalties - player.ownGoals
return (
<div className="max-w-[900px] mx-auto px-7 py-10 pb-16">
{/* Hero */}
<div className="pitch-grid glass-card-hero rounded-2xl p-8 mb-8">
<div className="flex items-center gap-6 flex-wrap">
{player.team && <TeamFlag name={player.team.name} iso2={player.team.iso2} size="xl" />}
<div>
<h1 className="font-['Bebas_Neue'] text-[clamp(36px,6vw,64px)] text-green leading-none">{player.playerName}</h1>
{player.team && (
<Link href={`/teams/${player.team.slug}`} className="text-green-sec text-sm mt-1 hover:text-text transition-colors inline-block">
{player.team.name}
</Link>
)}
</div>
<div className="ml-auto text-right">
<div className="font-['Bebas_Neue'] text-[80px] text-green leading-none">{player.goals}</div>
<div className="text-[10px] text-green-muted tracking-[0.12em] uppercase">World Cup Goals</div>
</div>
</div>
</div>
{/* Stats breakdown */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8">
{[
{ label: 'Total Goals', value: player.goals },
{ label: 'Open Play', value: normalGoals },
{ label: 'Penalties', value: player.penalties },
{ label: 'Tournaments', value: player.tournaments },
].map(item => (
<div key={item.label} className="glass-card rounded-xl p-4">
<div className="text-[9px] text-green-muted tracking-[0.1em] uppercase mb-1.5">{item.label}</div>
<div className="font-['Bebas_Neue'] text-3xl text-green">{item.value}</div>
</div>
))}
</div>
{player.ownGoals > 0 && (
<div className="mb-6 glass-card rounded-xl p-3 px-4 text-sm text-green-muted">
Includes {player.ownGoals} own goal{player.ownGoals !== 1 ? 's' : ''}
</div>
)}
{/* Back links */}
<div className="flex gap-4 mt-8">
<Link href="/stats" className="text-green text-sm hover:underline"> All-time scorers</Link>
{player.team && (
<Link href={`/teams/${player.team.slug}`} className="text-green text-sm hover:underline">
{player.team.name} team page
</Link>
)}
</div>
</div>
)
}