Customization

Table of contents

  1. Overview
  2. Custom Team Overrides
    1. teams.json
      1. Docker Mount
      2. How Merging Works
      3. File Structure
      4. Finding Team Slugs
      5. Example
      6. Common Overrides
  3. Custom League Configuration
    1. leagues.json
      1. Docker Mount
      2. How Merging Works
      3. File Structure
      4. Required Fields
      5. Optional Fields
      6. Example: Adding a New League
      7. Example: Overriding an Existing League
      8. Example: League Hierarchy with Feeder Leagues
      9. skipLogos Mode
      10. Finding ESPN Slugs
    2. League Fonts
      1. Docker Mounts
      2. Example
  4. HockeyTech Leagues (Auto-Configuration)
    1. Automatic Extraction
    2. Manual Configuration (Optional)
    3. Cache Management
    4. HockeyTech League Examples
  5. Development Setup
  6. Validation
    1. teams.json Validation
    2. leagues.json Validation
    3. Check for Errors
  7. Sharing Your Configurations
  8. Troubleshooting
    1. Configuration Not Loading
    2. Changes Not Applied
    3. Team Override Not Working
    4. New League Not Available

Overview

Game Thumbs supports customization through two configuration files:

  • teams.json - Custom team aliases and data overrides
  • leagues.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:

  1. First tries championship (EFL Championship)
  2. If not found, tries league-one (EFL League One)
  3. 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:

  1. When team resolution fails and fallback=true is set, the system extracts the dominant color from the league logo
  2. That color is heavily darkened to create a moody background tone
  3. 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:

  1. Visit ESPN’s website for that sport/league
  2. Look at the URL structure: espn.com/[sport]/[league]
  3. For soccer leagues, check the league page URL (e.g., /soccer/league/_/name/eng.1 → slug is eng.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, and iconurl query parameters along with all custom font loading) are gated behind the ALLOW_EVENT_OVERLAYS environment variable. Set ALLOW_EVENT_OVERLAYS=true to enable. When disabled, the titleFont and subtitleFont league fields are ignored at startup.

The iconurl query parameter causes the server to fetch a user-supplied URL. By default, private IP ranges, loopback, link-local, and localhost/*.local/*.internal hostnames are rejected. To reference internal image servers set ALLOW_INSECURE_OVERLAY_URLS=true to 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:

  1. At startup: Fetch all configured HockeyTech websites
  2. Extract the clientCode and apiKey automatically from each site
  3. Cache the configuration for 7 days in json/hockeytech-config-cache.json
  4. 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, not EPL)
  • Team slugs should match ESPN’s team identifiers (check via /raw endpoint)
  • 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)
  • providerId must 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.json
  • Failed 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 /raw endpoint)
  • 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