Esports News

Media Outlets as Team Sponsors: Decoding the CS3NEWS, Spirit, and K27 Deals

Aug 25, 2026, 4:00 PM · 1 min read

Media Outlets as Team Sponsors: Decoding the CS3NEWS, Spirit, and K27 Deals

Oracles.gg — Architecture de départ (sans code)

Site francophone d’analyse à la demande : l’utilisateur colle un lien Warcraft Logs, on extrait le report code, on interroge l’API GraphQL v2 (OAuth client credentials), on compare au référentiel éditorial, on affiche des conseils en français. Pas de comptes, pas de crawl de masse, pas de profil personnage.

Hypothèse V1 : Retail uniquement (Normal / Heroic). Classic / Era / SoD hors scope.

Choix figés : Next.js App Router + TypeScript, Drizzle ORM, next-intl, PostgreSQL, déploiement VPS Hetzner via Docker Compose (output: 'standalone').

Flux principal (contrainte WCL n°1)

flowchart LR user[Utilisateur] -->|"colle URL WCL"| home[Page accueil] home -->|reportCode + fight optionnel| api[Route serveur analyse] api --> cache{wcl_cache} cache -->|hit et non expire| analyze[Moteur conseil] cache -->|miss| oauth[Token OAuth process] oauth --> wcl[GraphQL v2 client] wcl --> cache cache --> analyze analyze --> editorial[(guides + rules + refs)] analyze --> page[Page resultat + credit WCL]

Le client navigateur ne voit jamais WCL_CLIENT_SECRET. Tous les appels WCL passent par le serveur Next.js.

Garde-fou anti-proxy de masse (pour ne pas contourner l’accord via notre propre site) : rate-limit IP sur la route d’analyse + refus des URLs non-Retail + pas d’endpoint « analyser une zone / un ranking ».

  1. Arborescence

Une seule locale routable : fr. Les dossiers messages/en.json ou locales: ['fr','en'] n’existent pas tant qu’une traduction humaine n’est pas validée.

oracles/ ├── app/ │ ├── [locale]/ │ │ ├── layout.tsx │ │ ├── page.tsx # collage URL │ │ ├── analyse/[reportCode]/page.tsx # resultat │ │ ├── guides/page.tsx │ │ ├── guides/[classSlug]/[specSlug]/page.tsx │ │ └── a-propos/page.tsx │ ├── api/analyse/route.ts # POST serveur, hors locale │ ├── sitemap.ts # FR uniquement │ └── robots.ts ├── components/ │ ├── wcl/WclAttribution.tsx # credit obligatoire par bloc de donnees │ ├── analysis/ │ └── guides/ ├── messages/ │ └── fr.json # seule locale active ├── i18n/ │ ├── routing.ts # locales: ['fr'], defaultLocale: 'fr' │ ├── request.ts │ └── planned-locales.ts # liste documentaire, PAS branchee au router ├── lib/ │ ├── wcl/ │ │ ├── auth.ts # client credentials + cache token │ │ ├── client.ts # POST GraphQL │ │ ├── parse-url.ts │ │ ├── cache-policy.ts # TTL par famille WCL │ │ └── queries/ │ ├── analysis/ # comparaison + conseils (plus tard) │ ├── db/client.ts │ └── seo.ts ├── db/ │ ├── schema/ │ │ ├── editorial.ts │ │ ├── cache.ts │ │ └── index.ts │ └── migrations/ ├── deploy/ │ ├── Caddyfile # oracles.gg + TLS │ └── docker-compose.yml ├── Dockerfile ├── drizzle.config.ts ├── next.config.ts # output: 'standalone' ├── .env.example └── package.json

i18n — ne pas reproduire l’erreur des 11 langues :

routing.locales = ['fr'] uniquement. Ajouter en au tableau = rendre /en indexable.

localePrefix: 'as-needed' : le français est sur /, pas /fr.

localeDetection: false : pas de redirection selon Accept-Language.

planned-locales.ts sert de mémo (['en']), jamais branché au middleware ni au sitemap.

Contenu long (guides, conseils) en français dans la DB ; pas de colonnes _en vides. Quand une locale sera validée : nouveau messages/{locale}.json et migration content_translations à ce moment-là.

Attribution WCL (contrainte n°3) : WclAttribution n’est pas un footer unique. Chaque carte / tableau / timeline issu de l’API affiche un crédit visible (« Données combat : Warcraft Logs ») + lien vers le rapport source. Le footer en plus, pas à la place.

Déploiement Hetzner (Helsinki) : Compose = app (Next standalone) + db (Postgres 16). Caddy termine TLS pour oracles.gg. Pas de Vercel / pas de serverless. Un job léger (cron container ou node-cron) purge wcl_cache où expires_at < now().

  1. Variables d’environnement

WCL v2 n’utilise pas une clé API unique : c’est un couple OAuth.

Serveur seulement (jamais NEXT_PUBLIC_)

WCL_CLIENT_ID — client OAuth (portail WCL)

WCL_CLIENT_SECRET

WCL_TOKEN_URL — défaut https://www.warcraftlogs.com/oauth/token

WCL_API_URL — défaut https://www.warcraftlogs.com/api/v2/client (Retail)

WCL_AUTHORIZE_URL — https://www.warcraftlogs.com/oauth/authorize (inutilisé en V1, réservé si comptes / logs privés plus tard)

DATABASE_URL — postgresql://user:pass@db:5432/oracles

Cache / politique (surcharges optionnelles, valeurs par défaut dans cache-policy.ts)

WCL_CACHE_GAME_DATA_TTL_SECONDS — défaut long (ex. 30 jours) ; 0 = indéfini jusqu’à invalidation patch

WCL_CACHE_REPORT_META_RECENT_TTL_SECONDS — court si combat récent / inProgress

WCL_CACHE_REPORT_META_SETTLED_TTL_SECONDS — long si rapport / combat terminé

Events / tables / graphs : pas d’env — expires_at = NULL dès que inProgress === false ; TTL court sinon

App / site

NEXT_PUBLIC_SITE_URL — https://oracles.gg

NEXT_PUBLIC_DEFAULT_LOCALE — fr

NODE_ENV

PORT — 3000 derrière Caddy

Compose (DB uniquement)

POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB

Garde-fou

ANALYSIS_RATE_LIMIT_PER_IP_HOUR — ex. 30

Explicitement absentes au lancement (placeholders commentés dans .env.example seulement) : AUTH_, STRIPE_, clés pubs. L’architecture (routes /api, secrets serveur, pas de couplage UI ↔ billing) reste extensible sans refactor.

Token OAuth : en mémoire processus + filet dans wcl_cache famille oauth_token (survit à un restart Docker, un seul row). Pas de refresh token sur ce flow ; on redemande un token à l’expiry.

  1. Schéma PostgreSQL minimal

Deux familles de tables, aucune identité joueur.

Interdit (ne pas créer)

users, accounts, sessions, profils

characters, player_profiles, historique d’analyses par perso

files d’import / crawl de rankings / logs publics en masse

index de recherche sur les report codes mis en cache

Le JSON WCL mis en cache contient des noms de persos : c’est le payload du rapport, pas un modèle métier. On ne les extrait pas dans une table, on ne les rend pas cherchables, on les expire.

Éditorial (tu rédiges / tu maintiens)

wow_classes / wow_specs — catalogue stable (id slug, ids WCL, nom FR, rôle). Référentiel, pas un cache de joueurs.

seasons — patch / saison (wow_patch, is_current). Sert à invalider guides + game_data au changement de patch.

guides — une page par spé × saison : talents recommandés, timeline de CDs, stuff, body_md. Contenu écrit par toi, jamais généré depuis les logs.

advice_rules — conditions machine (condition jsonb) + message_fr relus par un humain. Le différenciateur « conseils clairs » est un moteur de règles, pas un LLM à la volée (même leçon que l’i18n).

reference_logs — pointeurs curés à la main : wcl_report_code, wcl_fight_id optionnel, spec_id, season_id, encounter / difficulté, note (pourquoi ce log). Pas de colonne « nom de personnage » comme identité. Fetch à la demande quand une analyse de cette spé/boss le requiert, puis cache comme n’importe quel rapport. Aucun job qui parcourt les rankings WCL pour remplir cette table.

Cache technique (perf uniquement)

wcl_cache

cache_key (PK) — hash family + reportCode + fightId + query

family — oauth_token | game_data | report_meta | report_events | report_table | report_graph

report_code / fight_id — fragments de clé, pas un annuaire

in_progress — dernier état connu du combat

payload jsonb

fetched_at, expires_at (NULL = indéfini, interdit si in_progress = true)

cache_invalidations — watermark wow_patch pour vider game_data (et éventuellement guides obsolètes) sans cron magique.

Politique de cache (contrainte n°5)

Famille

TTL

Invalidation

game_data (classes, sorts)

long / indéfini

changement seasons.wow_patch

report_meta

court si récent ou inProgress ; long sinon

expiry

report_events / table / graph

court si inProgress ; indéfini une fois le combat terminé

jamais (combat clos) ; purge seulement si besoin disque

Un combat inProgress ne doit jamais être stocké avec expires_at IS NULL.

Pas de Redis en V1 : un seul VPS, moins de pièces. Redis possible plus tard sans changer le contrat cache-policy.ts.

Logs de référence — maintien

Tu sélectionnes 1–N rapports publics par spé (et éventuellement par boss Normal/Heroic). Critères éditoriaux (ex. parse propre, talents actuels, pas un one-shot mythic), stockés dans reference_logs. Mise à jour manuelle à chaque patch via seasons.

V1 peut même comparer d’abord log utilisateur ↔ guide (talents / rotation rédigés) et n’utiliser un reference log que lorsqu’il existe pour cette spé. Ça évite de dépendre d’un fetch WCL « top 100 » interdit.

Hors V1 (architecture laissée ouverte, pas construite)

Comptes, OAuth user (authorization code WCL), logs privés

Pub / abonnement

Locales autres que fr

Classic, M+, scoring global, classements

Prochaine étape (après validation de ce plan)

Scaffold uniquement : repo Next.js, Drizzle, fr only, .env.example, Docker Compose, schéma ci-dessus, client WCL + parse d’URL + composant d’attribution. Pas encore le moteur d’analyse ni le contenu des guides.

You may also like
Best Org 2026: Vitality leads the ReadyCheck fan rankingBest Org 2026: Vitality leads the ReadyCheck fan rankingBLAST Rivals Fall 2026: Vitality vs Falcons open the LANBLAST Rivals Fall 2026: Vitality vs Falcons open the LANIEM Beijing 2026: Groups, playoff format, and scheduleIEM Beijing 2026: Groups, playoff format, and scheduleFISSURE Playground 3 2026: format, schedule, and the Suzhou LANFISSURE Playground 3 2026: format, schedule, and the Suzhou LANEsports World Cup 2026: AG.AL Dethrones Team Falcons to Win the Club ChampionshipEsports World Cup 2026: AG.AL Dethrones Team Falcons to Win the Club ChampionshipBLAST Open Porto Fall 2026: Format, groups, and playoff scheduleBLAST Open Porto Fall 2026: Format, groups, and playoff scheduleEsports World Cup 2026 CS2: Playoffs start WednesdayEsports World Cup 2026 CS2: Playoffs start WednesdayEsports Sunday: EWC Rocket League final, CS2 and The InternationalEsports Sunday: EWC Rocket League final, CS2 and The InternationalEsports mercato: Weibo, JDG, NRG and this week’s big movesEsports mercato: Weibo, JDG, NRG and this week’s big movesDplus KIA vs Bilibili Gaming at Worlds, 3-0 Swiss vs 3-2Dplus KIA vs Bilibili Gaming at Worlds, 3-0 Swiss vs 3-2Gen.G beat LYON 3-0 at Worlds in Allen on 4 November.Gen.G beat LYON 3-0 at Worlds in Allen on 4 November.Worlds 2026: Quarterfinals, bracket, and Brooklyn grand finalWorlds 2026: Quarterfinals, bracket, and Brooklyn grand finalWorlds 2026: Swiss in Allen, quarterfinals, and Brooklyn grand finalWorlds 2026: Swiss in Allen, quarterfinals, and Brooklyn grand finalBLAST R6 Stage 2 2026: SAL, EML, NAL schedule, and Osaka qualificationBLAST R6 Stage 2 2026: SAL, EML, NAL schedule, and Osaka qualificationCBLOL Split 2 2026: Playoff format, schedule, and Worlds qualificationCBLOL Split 2 2026: Playoff format, schedule, and Worlds qualificationLEC Summer 2026: Playoff format, schedule, and Worlds qualificationLEC Summer 2026: Playoff format, schedule, and Worlds qualificationLFL Summer 2026: Galions in the grand final and the road to the titleLFL Summer 2026: Galions in the grand final and the road to the titleTeam Vitality Eyeing Rainbow Six Return via Brazil's South America LeagueTeam Vitality Eyeing Rainbow Six Return via Brazil's South America LeagueThe International 2026: Team Spirit win a third AegisThe International 2026: Team Spirit win a third AegisVCT China Stage 2 2026: TYLOO win the title and a Champions ticketVCT China Stage 2 2026: TYLOO win the title and a Champions ticketVCT Americas Stage 2 2026: Playoff Format, Qualified Teams & Full ScheduleVCT Americas Stage 2 2026: Playoff Format, Qualified Teams & Full ScheduleRoV Pro League Winter 2026: Format, group schedule, and playoffsRoV Pro League Winter 2026: Format, group schedule, and playoffsLCK 2026 Play-In: Format, Schedule & Race for the Final Playoff SpotsLCK 2026 Play-In: Format, Schedule & Race for the Final Playoff SpotsVCT Pacific Stage 2 2026: Format, playoff schedule, and Champions qualificationVCT Pacific Stage 2 2026: Format, playoff schedule, and Champions qualification