Customization
Table of contents
- Overview
- Custom Team Overrides
- Custom League Configuration
- leagues.json
- Docker Mount
- How Merging Works
- File Structure
- Required Fields
- Optional Fields
- Example: Adding a New League
- Example: Overriding an Existing League
- Example: League Hierarchy with Feeder Leagues
- skipLogos Mode
- mode: “name” (Render Team Names Instead of Logos)
- teamColors (Deterministic Team Color Palette)
- Finding ESPN Slugs
- League Fonts
- leagues.json
- HockeyTech Leagues (Auto-Configuration)
- Development Setup
- Validation
- Sharing Your Configurations
- Troubleshooting
Overview
Game Thumbs supports customization through two configuration files:
teams.json- Custom team aliases and data overridesleagues.json- Custom league configurations and new leagues
Both files can be mounted as Docker volumes to customize the API without modifying the source code.
Important: These files are additive - they merge with the built-in data rather than replacing it. You only need to specify the teams or leagues you want to customize. All built-in data remains available.
Teamarr TSDB Leagues
Docker images generate an additional built-in league overlay from Teamarr’s dev database schema during the image build. It includes Teamarr’s TheSportsDB team_vs_team leagues and is loaded after leagues.json, but before a legacy mounted leagues.json and files mounted under json/leagues/.
The generated overlay is stored at /app/generated/leagues_teamarr.json, not /app/json/leagues/, so mounting custom leagues does not hide it. Existing hand-maintained league fields and provider priority take precedence on matching keys. Identical provider entries are deduplicated.
For a local source checkout, generate the same overlay before starting the app:
yarn generate:teamarr-leagues
The command downloads Teamarr’s moving dev schema. It generates only team leagues compatible with the TheSportsDB team provider, currently excluding Teamarr’s boxing card and racing event entries. When Teamarr configures light or dark league artwork, the generated logoUrl or logoUrlDark takes precedence over artwork fetched from TheSportsDB.
Custom Team Overrides
teams.json
Add custom team aliases or override ESPN’s team data. Your files are additive - they merge with built-in teams, so you only need to include the teams you want to customize.
Docker Mount
Mount a directory containing one or more JSON files:
docker run -p 3000:3000 \
-v /path/to/custom-teams:/app/json/teams:ro \
ghcr.io/sethwv/game-thumbs:latest
All .json files in the directory will be loaded and merged in alphabetical order.
How Merging Works
- Built-in teams remain available
- Custom teams are added or merged with existing teams
- Aliases from all sources are combined (duplicates removed)
- Your override values take precedence over built-in values
- Files in
json/teams/directory are processed in alphabetical order
File Structure
{
"leagueKey": {
"team-slug": {
"aliases": ["nickname1", "nickname2"],
"override": {
"property": "value"
}
}
}
}
Finding Team Slugs
Quick method: Use the /raw endpoint to find a team’s slug:
curl http://localhost:3000/laliga/celta/raw
# Look for: "slug": "esp.celta_vigo"
# Use in JSON: "celta-vigo" (remove prefix, convert _ to -)
See Team Matching → Team Slugs for complete details on slug formats by provider.
Example
{
"epl": {
"man-utd": {
"aliases": ["man utd", "man u", "mufc", "manchester united"],
"override": {
"abbreviation": "MUN"
}
}
},
"laliga": {
"celta-vigo": {
"aliases": ["celtadevigo", "celta de vigo", "celtavigo"],
"override": {}
}
},
"mls": {
"lafc": {
"aliases": ["losangelesfc", "los angeles fc"],
"override": {
"color": "#000000",
"alternateColor": "#c7a36f"
}
}
}
}
Common Overrides
| Property | Type | Example |
|---|---|---|
abbreviation | string | "MUN" |
color | string | "#000000" |
alternateColor | string | "#ffffff" |
logo | string | "https://..." |
logoAlt | string | "https://..." |
city | string | "Manchester" |
name | string | "Red Devils" |
fullName | string | "Manchester United" |
See the Team Matching documentation for complete details.
Custom League Configuration
leagues.json
Add new leagues or modify existing league configurations. Like teams, your files are additive - they merge with built-in leagues.
Docker Mount
Mount a directory containing one or more JSON files:
docker run -p 3000:3000 \
-v /path/to/custom-leagues:/app/json/leagues:ro \
ghcr.io/sethwv/game-thumbs:latest
All .json files in the directory will be loaded and merged in alphabetical order.
How Merging Works
- Built-in leagues remain available
- Custom leagues are added or merged with existing leagues
- Aliases from all sources are combined (duplicates removed)
- Your values take precedence over built-in values
- Files in
json/leagues/directory are processed in alphabetical order
File Structure
{
"leagueKey": {
"name": "Full League Name",
"shortName": "CODE",
"aliases": ["alias1", "alias2"],
"providerId": "espn",
"logoUrl": "https://...",
"feederLeagues": ["league1", "league2"],
"fallbackLeague": "otherleague",
"espnConfig": {
"espnSport": "sport",
"espnSlug": "slug"
},
"titleFont": "fontFileName.ttf",
"subtitleFont": "fontFileName.ttf"
}
}
Required Fields
| Field | Type | Description |
|---|---|---|
name | string | Full league name (e.g., “National Basketball Association”) |
shortName | string | Display name/abbreviation (e.g., “NBA”) |
providerId | string | Data provider (currently only “espn” supported) |
espnConfig.espnSport | string | ESPN sport category (e.g., “basketball”, “football”, “soccer”) |
espnConfig.espnSlug | string | ESPN league identifier (e.g., “nba”, “nfl”, “eng.1”) |
Optional Fields
| Field | Type | Description |
|---|---|---|
aliases | array | Alternative names for league matching |
logoUrl | string | Custom league logo URL (overrides ESPN) |
feederLeagues | array | Array of league keys to try when team not found (in order) |
fallbackLeague | string | Legacy fallback league (prefer feederLeagues for new configurations) |
skipLogos | boolean | When true, fallback renders colored rectangles with league logo instead of greyscale team logos (see below) |
mode | string | "default" (or omitted) renders team logos as usual. "name" renders each team’s name as text instead of its logo (see below) |
teamColors | array | Hex color palette used to deterministically color-code teams instead of relying on a provider’s own team colors (see below) |
titleFont | string | Custom font to use for league thumbs/covers when the title query parameter is specified. Only static TrueType fonts (ttf) are supported. |
subtitleFont | string | Custom font to use for league thumbs/covers when the subtitle query parameter is specified. Only static TrueType fonts (ttf) are supported. |
Example: Adding a New League
{
"cfl": {
"name": "Canadian Football League",
"shortName": "CFL",
"aliases": ["canadian football"],
"providerId": "espn",
"espnConfig": {
"espnSport": "football",
"espnSlug": "cfl"
}
}
}
Example: Overriding an Existing League
{
"nba": {
"name": "National Basketball Association",
"shortName": "NBA",
"providerId": "espn",
"logoUrl": "https://example.com/custom-nba-logo.png",
"espnConfig": {
"espnSport": "basketball",
"espnSlug": "nba"
}
}
}
Example: League Hierarchy with Feeder Leagues
{
"epl": {
"name": "English Premier League",
"shortName": "EPL",
"aliases": ["premier league", "premier"],
"providerId": "espn",
"feederLeagues": ["championship", "league-one", "league-two"],
"espnConfig": {
"espnSport": "soccer",
"espnSlug": "eng.1"
}
},
"championship": {
"name": "EFL Championship",
"shortName": "Championship",
"aliases": ["efl championship"],
"providerId": "espn",
"espnConfig": {
"espnSport": "soccer",
"espnSlug": "eng.2"
}
},
"league-one": {
"name": "EFL League One",
"shortName": "League One",
"providerId": "espn",
"espnConfig": {
"espnSport": "soccer",
"espnSlug": "eng.3"
}
},
"league-two": {
"name": "EFL League Two",
"shortName": "League Two",
"providerId": "espn",
"espnConfig": {
"espnSport": "soccer",
"espnSlug": "eng.4"
}
}
}
How Feeder Leagues Work:
When a team is not found in the main league (e.g., EPL), the system automatically searches through the feeder leagues in order:
- First tries
championship(EFL Championship) - If not found, tries
league-one(EFL League One) - If not found, tries
league-two(EFL League Two)
This is useful for:
- Promotion/Relegation Systems (soccer leagues)
- Minor League Systems (baseball farm systems)
- Development Leagues (G League for NBA, AHL for NHL)
Note: Feeder leagues must reference existing league keys defined elsewhere in the configuration.
skipLogos Mode
For leagues without team data providers (e.g., motorsports, Olympics), you can enable skipLogos to change how fallback=true renders matchups. Instead of greyscale league logos used as team placeholders, the output will be colored rectangles with the league logo centered — the mood color is automatically extracted from the league logo’s dominant color.
{
"F1": {
"name": "Formula 1",
"providers": [],
"logoUrl": "./assets/F1.png",
"skipLogos": true
}
}
How it works:
- When team resolution fails and
fallback=trueis set, the system extracts the dominant color from the league logo - That color is heavily darkened to create a moody background tone
- The matchup renders as colored rectangles with the league logo overlaid — no team logos are shown
Built-in leagues with skipLogos enabled: Olympics, F1, NASCAR, IndyCar
mode: “name” (Render Team Names Instead of Logos)
Some leagues don’t have usable team logo assets (e.g. youth/amateur leagues where every team is a differently-named local club). Set "mode": "name" on a league to render each team’s name as text instead of fetching/drawing its logo, on matchup thumbnails/covers and single-team thumbs/covers.
{
"llb": {
"name": "Little League Baseball",
"logoUrl": "./assets/LLB_LIGHTMODE.png",
"mode": "name",
"providers": [
{ "espn": { "espnSlug": "llb", "espnSport": "baseball" } }
]
}
}
How it works:
- Team name text is always drawn in white with a dark drop shadow, shrink-to-fit and word-wrapped into the same space a logo would occupy.
- No network fetch is made for the team’s logo asset when
mode: "name"is active, since it isn’t drawn. - Applies to the
/thumb,/cover, and/logoendpoints, for both matchup and single-team requests. League-only endpoints (no team in the URL) are unaffected.
Per-request override: add ?mode=name (or ?mode=default to force logos) to any request to override the league’s configured mode for that request only, regardless of what’s set in leagues.json.
teamColors (Deterministic Team Color Palette)
For leagues where every team shares the same provider-supplied color (or has no meaningful color data at all — e.g. individual-athlete sports like tennis/MMA where “the team” is really a single competitor), set teamColors to a palette of hex colors. Each team is deterministically assigned a color and alternate color from that palette based on a stable hash of its identifier, so the same team always renders with the same colors across requests — no configuration per team required.
{
"llb": {
"teamColors": ["#ef476f", "#ffd166", "#06d6a0", "#118ab2", "#073b4c"]
}
}
When set, teamColors overrides whatever color the provider itself resolved (API-supplied or logo-extracted) for every team in that league. It does not affect teams defined via teams.json custom-team overrides.
Finding ESPN Slugs
To find the correct ESPN slug for a league:
- Visit ESPN’s website for that sport/league
- Look at the URL structure:
espn.com/[sport]/[league] - For soccer leagues, check the league page URL (e.g.,
/soccer/league/_/name/eng.1→ slug iseng.1)
Common ESPN Slugs:
| League | Sport | Slug |
|---|---|---|
| NBA | basketball | nba |
| NFL | football | nfl |
| MLB | baseball | mlb |
| NHL | hockey | nhl |
| EPL | soccer | eng.1 |
| La Liga | soccer | esp.1 |
| Bundesliga | soccer | ger.1 |
| Serie A | soccer | ita.1 |
| Ligue 1 | soccer | fra.1 |
| MLS | soccer | usa.1 |
| UEFA Champions | soccer | uefa.champions |
| UEFA Europa | soccer | uefa.europa |
League Fonts
Custom fonts are supported for the optional text on league thumbs and covers. Defining a custom font requires the definition of a custom league as well to register the added font with a league.
Feature flag required. League event overlays (the
title,subtitle, andiconurlquery parameters along with all custom font loading) are gated behind theALLOW_EVENT_OVERLAYSenvironment variable. SetALLOW_EVENT_OVERLAYS=trueto enable. When disabled, thetitleFontandsubtitleFontleague fields are ignored at startup.
The
iconurlquery parameter causes the server to fetch a user-supplied URL. By default, private IP ranges, loopback, link-local, andlocalhost/*.local/*.internalhostnames are rejected. To reference internal image servers setALLOW_INSECURE_OVERLAY_URLS=trueto skip all validation, or set it to a comma-separated list of hostnames (e.g.192.168.1.5,printer.local) to allow only those hosts through. DNS rebinding is not fully prevented either way; only enable this feature if you trust the clients calling your endpoints.
Docker Mounts
Mount a directory containing one or more ttf files:
docker run -p 3000:3000 \
-v /path/to/custom-fonts:/app/assets/fonts/custom:ro \
ghcr.io/sethwv/game-thumbs:latest
Example
Since the league files are additive, the simplest example of this might look like this:
{
"F1": {
"titleFont": "fontFile.ttf",
"subtitleFont": "fontFile2.ttf"
}
}
HockeyTech Leagues (Auto-Configuration)
For leagues powered by HockeyTech (PWHL, OHL, WHL, QMJHL, CHL), the API can automatically extract configuration from the league’s official website.
Automatic Extraction
Instead of manually finding API keys, simply provide the website URL in the provider config:
{
"pwhl": {
"name": "Professional Women's Hockey League",
"providers": [
{
"hockeyTech": {
"websiteUrl": "https://www.thepwhl.com/en/"
}
}
]
}
}
The system will:
- At startup: Fetch all configured HockeyTech websites
- Extract the
clientCodeandapiKeyautomatically from each site - Cache the configuration for 7 days in
json/hockeytech-config-cache.json - Use the cached config for all subsequent requests
Manual Configuration (Optional)
You can still provide explicit configuration if you prefer or if auto-extraction fails:
{
"pwhl": {
"name": "Professional Women's Hockey League",
"providers": [
{
"hockeyTech": {
"clientCode": "pwhl",
"apiKey": "446521baf8c38984"
}
}
]
}
}
Cache Management
The extracted configurations are automatically cached in json/hockeytech-config-cache.json:
{
"https://www.thepwhl.com/en/": {
"config": {
"clientCode": "pwhl",
"apiKey": "446521baf8c38984"
},
"timestamp": 1702368000000,
"extractedFrom": "https://www.thepwhl.com/en/"
}
}
The cache:
- Expires after 7 days and is automatically refreshed
- Cleans expired entries on each startup
- Persists between restarts so extraction only happens once per week
HockeyTech League Examples
| League | Website URL |
|---|---|
| PWHL | https://www.thepwhl.com/en/ |
| OHL | https://ontariohockeyleague.com/ |
| WHL | https://whl.ca/ |
| QMJHL | https://theqmjhl.ca/ |
| CHL | https://chl.ca/ |
Note: During startup, you’ll see log messages indicating which HockeyTech configs were successfully preloaded.
Development Setup
For local development, simply edit the files directly in the repository:
# Edit configuration files
nano teams.json
nano leagues.json
# Restart the server to reload
yarn start
Validation
teams.json Validation
- Must be valid JSON
- League keys must be lowercase (e.g.,
epl, notEPL) - Team slugs should match ESPN’s team identifiers (check via
/rawendpoint) - Aliases are case-insensitive and flexible with spacing
leagues.json Validation
- Must be valid JSON
- League keys must be lowercase and URL-safe (alphanumeric, hyphens)
providerIdmust be"espn"(only supported provider currently)- ESPN slugs must match ESPN’s API identifiers
Check for Errors
View container logs to check for configuration errors:
docker logs <container-id>
Look for warnings like:
Failed to load teams.jsonFailed to load leagues.json
Sharing Your Configurations
If you’ve added useful team aliases or new leagues, consider contributing them back to the project!
See the Contributing Guide for how to submit your configurations via pull request.
Troubleshooting
Configuration Not Loading
Check file is mounted correctly:
docker exec <container-id> cat /app/teams.json
docker exec <container-id> cat /app/leagues.json
Verify JSON syntax: Use a JSON validator or:
cat teams.json | python -m json.tool
cat leagues.json | python -m json.tool
Changes Not Applied
The configuration files are loaded on server startup. Restart the container:
docker restart <container-id>
Team Override Not Working
- Confirm league key is lowercase
- Verify team slug is correct (use
/rawendpoint) - Check that the file is valid JSON
- Review container logs for errors
New League Not Available
- Verify ESPN slug is correct
- Check that ESPN has data for that league
- Confirm the league key is lowercase and URL-safe
- Test with a known team from that league