Customization
Table of contents
- Overview
- Custom Team Overrides
- Custom League Configuration
- 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.
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) |
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
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