Media Outlets as Team Sponsors: Decoding the CS3NEWS, Spirit, and K27 Deals
Aug 25, 2026, 4:00 PM · 1 min read

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 ».
- 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().
- 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.
- 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.























