Developer Docs
Developer Documentation

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

  1. Overview
  2. Tech stack
  3. Repository layout
  4. Data sources
  5. Three-layer Data Dragon resolution
  6. Region routing
  7. HTTP route inventory
  8. Description rendering pipeline
  9. Local development
  10. Deploying
  11. Contributing
  12. License & disclaimer

01Overview

The app is a single Go binary that:

Design priorities, in order:

  1. Always-current data. The app should never need a code change to pick up a new patch.
  2. Read-only and stateless. No database; nothing about a user is ever stored.
  3. Graceful degradation. Every external dependency has a fallback path.
  4. Boring tech. Vanilla Go and vanilla HTML keep the surface small.
Live state of this server Local DD: 16.9.1 CDN DD: 16.9.1 Pinned: 16.9.1 Meraki entries: 320

02Tech stack

LayerChoiceWhy
LanguageGo 1.23Single static binary, fast startup, predictable concurrency for the background data watchers.
HTTP servernet/http stdlibNo framework. Handler functions are good enough for ~10 routes.
Templateshtml/template stdlibServer-side rendering with auto-escaping. Custom FuncMap for icon URL helpers, KDA formatting, etc.
FrontendVanilla HTML/CSS, ~50 LOC of inline JSOne small script per page (region selector, expand-row, item card toggle). No bundler, no build step.
HostingRender (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:

SourceUsed forRefresh
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).
Why the hybrid? Data Dragon is fast and authoritative for static catalog + image URLs but its item descriptions ship with empty placeholders for any value computed by the live game (scaling formulas, conditional cooldowns). Meraki is the cleanest community-maintained source that has those values resolved. The combination gives accurate stats and rich descriptions without Riot-internal data.

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.

LayerWhen it runsWhat it readsOutcome
Build scriptEach Render deployversions.json via curlDownloads dragontail-$LATEST.tgz into docs1/dragontail-$LATEST/ inside the container.
discoverLocalDDragon()App startupFilesystem scan of docs1/dragontail-*/Picks the highest semver present and stores it in ddLocalDir + ddLocalVersion; the Repository pages read from there.
startDDVersionWatcher()Goroutine, every 6hversions.json via HTTPUpdates 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:

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:

HelperPurpose
platformToRegionStatic 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

RouteReturns
GET /Landing page with the search form (renders index.html with no data).
GET /match?id=&region=Full match analysis (Blue / Red team panels, expandable per-player detail rows).
GET /timeline?id=&region=Timeline view with chronological event feed and per-minute frames.
GET /repositoryRepository home (champion preview tiles + item card link, current patch label).
GET /repository/championsSearchable / tag-filterable list of all champions.
GET /repository/champions/{name}Single-champion detail page (stats, abilities, lore, tips, skins).
GET /repository/itemsSearchable / tag-filterable list of all purchasable Summoner's Rift items, with Meraki-enriched descriptions.
GET /api/championsPlain 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 /guideUser-facing guide.
GET /devThis 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

VarPurpose
RIOT_API_KEYRequired. Used as X-Riot-Token on every Riot API request.
PORTRender auto-sets this. Locally, defaults to 8080.
Production key in use The deployed app at newmeta.pro runs on a Riot Production API key (no 24-hour rotation), with rate limits of 20 requests / second and 100 requests / 2 minutes. Personal Development keys obtained from the developer portal are fine for local work but rotate every 24h.

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:

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.