Architecture & integration guide
NEW META PRO is a small, opinionated Go monolith that renders League of Legends match data on the server. No database, no JS framework, no client-side state. This document explains how the pieces fit together so you can read the source confidently or fork it.
Contents
01Overview
The app is a single Go binary that:
- Serves a handful of HTML pages built with
html/template(no client-side framework). - Calls Riot's Match-V5 API on demand for match and timeline data.
- Reads pre-extracted Riot Data Dragon assets from disk for the Repository pages and static images.
- Pulls Meraki Analytics' parsed item dataset in the background to enrich item descriptions that Data Dragon ships incomplete.
- Discovers and tracks the latest Data Dragon version automatically at three layers — build, runtime startup, and live CDN.
Design priorities, in order:
- Always-current data. The app should never need a code change to pick up a new patch.
- Read-only and stateless. No database; nothing about a user is ever stored.
- Graceful degradation. Every external dependency has a fallback path.
- Boring tech. Vanilla Go and vanilla HTML keep the surface small.
16.9.1
CDN DD: 16.9.1
Pinned: 16.9.1
Meraki entries: 320
02Tech stack
| Layer | Choice | Why |
|---|---|---|
| Language | Go 1.23 | Single static binary, fast startup, predictable concurrency for the background data watchers. |
| HTTP server | net/http stdlib | No framework. Handler functions are good enough for ~10 routes. |
| Templates | html/template stdlib | Server-side rendering with auto-escaping. Custom FuncMap for icon URL helpers, KDA formatting, etc. |
| Frontend | Vanilla HTML/CSS, ~50 LOC of inline JS | One small script per page (region selector, expand-row, item card toggle). No bundler, no build step. |
| Hosting | Render (Docker) | Free tier, auto-deploy on push, declared in render.yaml and app/Dockerfile. |
03Repository layout
NEW-META-PRO-PROJECT/ ├─ app/ │ ├─ main.go ← all handlers, fetchers, helpers (~2k LOC) │ ├─ go.mod ← Go 1.23, no external dependencies │ ├─ Dockerfile ← runtime image (alpine + Data Dragon fetch) │ └─ templates/ │ ├─ index.html ← match analyzer + landing page │ ├─ timeline.html ← timeline view │ ├─ playerrows.html ← shared per-player row + expanded detail │ ├─ repository.html ← /repository home │ ├─ champions.html ← all champions list │ ├─ champion_detail.html │ ├─ items.html ← all items list (Meraki-enriched) │ ├─ guide.html ← this user guide │ └─ dev.html ← this page ├─ docs1/ ← Data Dragon assets, populated at deploy time │ └─ dragontail-X.Y.Z/ ← (gitignored) ├─ render.yaml ← Render Blueprint (Docker runtime) ├─ RENDER_SETTINGS.md ← native-runtime build command alternative ├─ README.md ← local-dev quickstart └─ PRODUCT_DESCRIPTION.md ← high-level product description
Everything that matters at runtime lives in app/. The repo root holds deploy / docs metadata only.
04Data sources
The app talks to three different upstreams. Each has a different freshness profile, so they're combined deliberately:
| Source | Used for | Refresh |
|---|---|---|
| Riot Match-V5 API *.api.riotgames.com | Live match metadata, participant stats, timeline events. Anything keyed by a Match ID. | On demand per request. |
| Riot Data Dragon ddragon.leagueoflegends.com | Champion / item / spell static data and icons. Versioned by patch (e.g. 16.9.1). Source of truth for the item catalog and stat blocks. | Pulled at build time; live CDN URLs refreshed every 6h. |
| Meraki Analytics cdn.merakianalytics.com | Parsed-from-wiki passive / active item descriptions with all the scaling values DDragon ships empty (e.g. (+ 10% AP)). Falls back to DDragon if Meraki has no entry. | Refreshed every 6h in-process. Upstream is updated on a community schedule (lag of 1–3 weeks behind live patches is normal). |
05Three-layer Data Dragon resolution
The app never has the patch version hardcoded in its product code. Instead, three independent layers each resolve "what's the current Data Dragon version?" — and they all agree by design.
| Layer | When it runs | What it reads | Outcome |
|---|---|---|---|
| Build script | Each Render deploy | versions.json via curl | Downloads dragontail-$LATEST.tgz into docs1/dragontail-$LATEST/ inside the container. |
discoverLocalDDragon() | App startup | Filesystem scan of docs1/dragontail-*/ | Picks the highest semver present and stores it in ddLocalDir + ddLocalVersion; the Repository pages read from there. |
startDDVersionWatcher() | Goroutine, every 6h | versions.json via HTTP | Updates the in-process variable used for live CDN image URLs (champion / item / spell icons in match analyzer views). |
If any layer's network call fails, the next call simply doesn't update — every layer falls back to ddVersionDefault (currently 16.9.1), which is the only literal patch number in the codebase. That literal exists purely as a "last known good" so the app starts cleanly even on a complete CDN outage.
Why three layers and not one?
They serve different concerns:
- Build layer ships the local files the Repository reads. These don't change after the build — they're baked into the deployed image.
- Local discovery doesn't trust a hardcoded folder name; it reads what was actually unpacked.
- CDN watcher handles the case where Riot ships a new patch between deploys — match views still get the freshest icons without redeploying.
06Region routing
Riot's Match-V5 endpoint is region-clustered: a Match ID like EUN1_3921786674 must be queried at europe.api.riotgames.com, not eun1.api.riotgames.com. The app handles that transparently through two helpers:
| Helper | Purpose |
|---|---|
platformToRegion | Static map: EUN1 → europe, NA1 → americas, KR → asia, SG2 → sea, etc. Updated whenever Riot adds a platform (e.g. ME1). |
normalizeMatchID(rawID, region) | Combines a numeric input (3921786674) with the dropdown-selected platform (EUN1) to produce a full ID. If the raw input already contains an underscore (full ID with prefix), it's returned untouched — paste-from-anywhere just works. |
On the frontend, a tiny inline script handles three UX wins: (1) auto-splitting a pasted full ID into its region + numeric parts, (2) saving the chosen region to localStorage so it sticks across sessions, (3) re-syncing the dropdown from the displayed match's platform when a result is rendered.
07HTTP route inventory
| Route | Returns |
|---|---|
| GET / | Landing page with the search form (renders index.html with no data). |
| GET /match?id=®ion= | Full match analysis (Blue / Red team panels, expandable per-player detail rows). |
| GET /timeline?id=®ion= | Timeline view with chronological event feed and per-minute frames. |
| GET /repository | Repository home (champion preview tiles + item card link, current patch label). |
| GET /repository/champions | Searchable / tag-filterable list of all champions. |
| GET /repository/champions/{name} | Single-champion detail page (stats, abilities, lore, tips, skins). |
| GET /repository/items | Searchable / tag-filterable list of all purchasable Summoner's Rift items, with Meraki-enriched descriptions. |
| GET /api/champions | Plain JSON dump of the champion index. Useful as a programmatic view of the local Data Dragon data. |
| GET /api/champions/{name} | Plain JSON for a single champion. |
| GET /data/{version}/img/... | Static file server in front of the locally extracted Data Dragon image tree. |
| GET /static/... | App-owned static assets. |
| GET /guide | User-facing guide. |
| GET /dev | This page. |
08Description rendering pipeline
Champion ability text and item descriptions arrive in two different markup dialects: Riot's HTML-flavored tooltip syntax and Meraki's wiki-flavored markup. Both go through a deterministic clean-up before display.
Riot HTML (cleanRiotMarkup)
Translates a small set of true block-level tags into newlines (<p>, <stats>, <passive>, <active>, <mainText>) and <li> into a bullet, then strips every other tag inline (colour / scale / damage type / status / keyword markers like <attention>, <magicDamage>, <scaleAP>, <healing>). Per-line trim and blank-run collapse keep the output tidy.
Meraki wiki (cleanWikiMarkup)
Handles the LoL Fandom Wiki templating that Meraki ships verbatim:
{{as|75% base AD}} -> 75% base AD
{{tt|healing|heal}} -> healing
{{tt|45% of value|1*0.45|hp}} -> 45% of value
{{fd|1.5}} -> 1.5
{{Item_Mythic_Passive}} -> "" (unresolved id, dropped)
[[Champion ability|ability]] -> ability
[[base AD]] -> base AD
'''bold''' -> bold
''italic'' -> italic
Templates can nest, so the cleaner runs the inner replacements iteratively until the string converges (capped at five rounds).
Item description composition (enhanceItemDescription)
For each DDragon item, the cleaned final description is built like:
[ DDragon <stats> block, run through cleanRiotMarkup ] [ Meraki passive name + (Unique) / (Mythic) tag ]: [ Meraki passive effects, run through cleanWikiMarkup ] [ ...more passives... ] Active: [ Meraki active name ] [ Meraki active effects ]
If Meraki has no entry for the item, the function falls back to the cleaned full DDragon description — boots, components, and other simple items still render correctly without any Meraki coverage.
09Local development
You need Go 1.23+ and a Riot API key from developer.riotgames.com. A personal Development key is fine for local work — it just rotates every 24 hours.
# Clone (private repo — ask in our Discord at discord.gg/um8NubeacW for access)
git clone <repository-url>
cd NEW-META-PRO-PROJECT
# Optional: download Data Dragon if you want the Repository pages to work
mkdir -p docs1
LATEST=$(curl -fsL https://ddragon.leagueoflegends.com/api/versions.json \
| grep -oE '"[0-9]+\.[0-9]+\.[0-9]+[a-z]?"' | head -n1 | tr -d '"')
LATEST=${LATEST:-16.9.1}
curl -L -o docs1/dragontail.tgz "https://ddragon.leagueoflegends.com/cdn/dragontail-$LATEST.tgz"
tar -xzf docs1/dragontail.tgz -C docs1 \
&& mv "docs1/$LATEST" "docs1/dragontail-$LATEST" \
&& rm docs1/dragontail.tgz
# Set the Riot dev key and run
echo "RIOT_API_KEY=RGAPI-xxxx" > app/.env
cd app && go run .
The match analyzer and timeline work without the local Data Dragon download (icons come from Riot's CDN). Only the Repository pages need the local files.
10Deploying
The repo ships two deploy paths; pick whichever your Render service is configured for.
Docker runtime (recommended)
render.yaml declares runtime: docker and points at app/Dockerfile. The Dockerfile fetches versions.json, downloads the matching Data Dragon tarball, and bakes it into the runtime image.
Native runtime (Build Command in dashboard)
If your service is on the Native runtime, paste the build command from RENDER_SETTINGS.md into Render → Settings → Build & Deploy → Build Command. It does the same dynamic version fetch as the Dockerfile, then runs go build -o server.
Environment variables
| Var | Purpose |
|---|---|
| RIOT_API_KEY | Required. Used as X-Riot-Token on every Riot API request. |
| PORT | Render auto-sets this. Locally, defaults to 8080. |
11Contributing
The codebase is intentionally small (~2k Go lines + a handful of templates) so contributions are easy to read and review. Some good starter ideas:
- Per-item override file for description text (manual override of stale Meraki entries).
- Champion-detail page enrichment from Meraki (champion data has its own description gaps).
- An optional rune-detail page.
- JSON API endpoints for items and timeline.
- Localization (Riot's Data Dragon ships
en_GB,de_DE,ko_KR, etc.).
The source repository is currently private; access is granted on request — pop into our Discord and say hi. Keep contributions focused, comments minimal, and prefer extending existing helpers over adding dependencies.
12License & disclaimer
NEW META PRO is not endorsed by Riot Games and does not reflect the views or opinions of Riot Games or anyone officially involved in producing or managing Riot Games properties. Riot Games and all associated properties are trademarks or registered trademarks of Riot Games, Inc.
Static game data is provided by Riot Games via Data Dragon and by the Meraki Analytics open dataset; both are used as-published and credited above.