Toute la documentation

Démarrer

  1. 01Pour commencer
  2. 02Installation
  3. 03Suivi

Fonctionnalités

  1. 04Le tableau de bord
  2. 05Objectifs
  3. 06Liens courts
  4. 07Sources et agents IA
  5. 08Rapports par e-mail
  6. 09Vérification planifiée
  7. 10Interrogez votre IA

Référence

  1. 11Configuration
  2. 12API HTTP

Plateformes

  1. 13Serveur autonome
  2. 14PHP
  3. 15WordPress
  4. 16Drupal
  5. 17Craft CMS
  6. 18Python
  7. 19Rails
  8. 20Ruby
  9. 21Go
  10. 22Java
  11. 23.NET
  12. 24Elixir
  13. 25Rust

Confiance

  1. 26Confidentialité

Plateformes

PHP

Runlight tourne sous PHP 8.2 ou plus récent, dans une application Laravel, Symfony ou en PHP pur, ou sur son propre domaine chez un hébergeur PHP ordinaire.

Le paquet PHP, c’est Runlight réécrit en PHP. Il sert le même tableau de bord et la même API, et il donne à chaque requête la même réponse que la bibliothèque TypeScript. Ses chiffres vont dans les mêmes tables, donc chacun peut lire une base de données écrite par l’autre. Il lui faut PHP 8.2 ou plus récent, avec PDO et le pilote de votre base de données.

Installation

Ajoutez le paquet avec Composer.

bash

composer require runlight/runlight

Runlight utilise les extensions curl, intl, sodium et APCu quand elles sont chargées, et se rabat sur du PHP pur pour celles qui manquent.

Créer l’instance

Créez un fichier qui renvoie l’instance, et incluez-le partout où vous avez besoin de Runlight.

runlight.php

<?php

use Runlight\Runlight;
use Runlight\Store\Stores;

return new Runlight([
    'store' => Stores::sqlite(__DIR__ . '/data/runlight.db'),
    'site' => ['name' => 'example.com', 'hostnames' => ['example.com'], 'timezone' => 'Europe/London'],
]);

Les options portent les mêmes noms que dans Configuration, donc sites, trustProxy, rateLimit, mail et managedSites fonctionnent tous comme là-bas. Les tables sont créées à la première requête.

Stockages

Les tables de Runlight commencent par rl_. Les stockages SQLite, MySQL et Postgres acceptent une option prefix à utiliser à la place, comme Stores::sqlite(['path' => $path, 'prefix' => 'wp_rl_']) ou Stores::mysql($url, ['prefix' => 'wp_rl_']), pour que deux applications qui partagent une base gardent chacune leurs propres tables, et Stores::fromDb($db, 'wp_rl_') fait de même avec un Db à vous. Un préfixe compte jusqu’à 32 lettres, chiffres et tirets bas, et ne commence pas par un chiffre. $store->tables() liste les tables du stockage par leur nom, pour une application qui retire Runlight.

SQLite

php

Stores::sqlite(__DIR__ . '/data/runlight.db');

SQLite a besoin de l’extension pdo_sqlite, présente dans la plupart des installations PHP. Gardez le fichier hors de la racine web, dans un dossier où le serveur web peut écrire, puisque SQLite écrit un journal à côté. Le dossier est créé s’il manque.

MySQL et MariaDB

php

Stores::mysql('mysql://runlight:password@127.0.0.1:3306/runlight');

Ce stockage a besoin de pdo_mysql et fonctionne avec MySQL 8.0.20 ou plus récent et MariaDB 11.4 ou plus récent. Une URL mariadb:// fonctionne aussi. Un deuxième argument comme ['statementTimeout' => 30000] arrête toute requête SQL au bout de ce nombre de millisecondes, 120000 par défaut, comme dans Configuration.

Postgres

php

Stores::postgres('postgres://runlight:password@127.0.0.1:5432/runlight');

Ce stockage a besoin de pdo_pgsql. Il accepte aussi statementTimeout dans son deuxième argument, et ['schema' => 'analytics'] garde les tables dans leur propre schéma. Stores::url() choisit le stockage d’après un DATABASE_URL qui commence par postgres://, mysql://, mariadb:// ou sqlite:.

PHP pur

Dans une application avec un seul index.php par lequel passe chaque requête, envoyez à Runlight, tout en haut, les chemins sous /runlight et /go.

public/index.php

<?php

require __DIR__ . '/../vendor/autoload.php';

use Runlight\Http\Request;
use Runlight\Server\FrontController;

$rl = require __DIR__ . '/../runlight.php';

$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if ($path === '/runlight' || str_starts_with($path, '/runlight/') || str_starts_with($path, '/go/')) {
    FrontController::serve($rl, $rl->routes());
    return;
}

// AI agents reading your pages; optional.
$rl->observe(Request::fromGlobals());

// The rest of your app.

FrontController::serve() lit la requête dans les variables globales de PHP et envoie la réponse. observe() enregistre les requêtes des agents IA et rend la main aussitôt pour tous les autres. Ajoutez le script à chaque page, juste avant </head>.

html

<script defer src="/runlight/s.js"></script>

Définissez ensuite RUNLIGHT_TOKEN avec une longue chaîne aléatoire et ouvrez une fois /runlight/?token= suivi de cette chaîne pour vous connecter, comme l’explique Pour commencer. Avec routes(['accounts' => true]) et un RUNLIGHT_SECRET, chacun se connecte avec son propre e-mail et son propre mot de passe, comme dans Comptes.

Un domaine de liens répond aux liens courts à la racine de son propre nom, donc ses requêtes ne correspondent jamais aux chemins ci-dessus. Pour en servir un depuis l’application, interrogez d’abord linkDomainResponse(). Elle ne répond que sur vos domaines de liens et renvoie null pour tout autre hôte, donc le reste de l’application continue comme avant.

php

$request = Request::fromGlobals();
$linked = $rl->linkDomainResponse($request, ['ip' => $request->remoteAddress]);
if ($linked !== null) {
    $linked->emit();
    return;
}

Laravel et Symfony le font avec la classe ShortLinks de Runlight, comme le montrent leurs sections.

Request::fromServer($server) lit une requête depuis un tableau qui a la forme de $_SERVER, comme la copie sans antislashs qu’en garde un CMS. Runlight\Log::to(fn (string $line) => ...) envoie les messages de Runlight vers un journal à vous au lieu du journal d’erreurs de PHP.

Laravel

Les routes de Runlight se placent hors du groupe de middleware web de Laravel. Ce groupe demande un jeton CSRF à chaque POST, que le traceur ne peut pas envoyer, et il chiffre les cookies, que Runlight ne pourrait alors plus relire. Runlight vérifie les requêtes à sa façon.

Enregistrez l’instance dans AppServiceProvider.

app/Providers/AppServiceProvider.php

use Runlight\Runlight;
use Runlight\Store\Stores;

public function register(): void
{
    $this->app->singleton(Runlight::class, fn () => new Runlight([
        'store' => Stores::sqlite(storage_path('app/runlight.db')),
        'site' => ['name' => 'example.com', 'hostnames' => ['example.com'], 'timezone' => 'Europe/London'],
    ]));
}

Ajoutez un contrôleur.

app/Http/Controllers/RunlightController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Runlight\Bridge\HttpFoundation;
use Runlight\Runlight;
use Symfony\Component\HttpFoundation\Response;

class RunlightController
{
    public function __invoke(Request $request, Runlight $rl): Response
    {
        $routes = $rl->routes(['token' => (string) config('services.runlight.token')]);

        return HttpFoundation::handle($rl, $routes, $request);
    }
}

Donnez-lui son propre fichier de routes.

routes/runlight.php

<?php

use App\Http\Controllers\RunlightController;
use Illuminate\Support\Facades\Route;

Route::any('/runlight/{path?}', RunlightController::class)->where('path', '.*');
Route::get('/go/{slug}', RunlightController::class);

Chargez ce fichier dans bootstrap/app.php avec then, qui enregistre les routes hors du groupe web.

bootstrap/app.php

use Illuminate\Support\Facades\Route;

->withRouting(
    web: __DIR__.'/../routes/web.php',
    commands: __DIR__.'/../routes/console.php',
    health: '/up',
    then: fn () => Route::group([], base_path('routes/runlight.php')),
)

Pour les domaines de liens, placez le middleware ShortLinks de Runlight devant toutes les routes, dans le même fichier. Il répond à une requête sur un domaine de liens avant que Laravel ne la route, ainsi qu’à /go/<slug> quand le lien existe, et laisse passer toutes les autres requêtes sans y toucher.

bootstrap/app.php

use Illuminate\Foundation\Configuration\Middleware;
use Runlight\Bridge\ShortLinks;

->withMiddleware(function (Middleware $middleware) {
    $middleware->prepend(ShortLinks::class);
})

Lisez le jeton par la configuration, pour qu’il fonctionne encore après php artisan config:cache, et définissez RUNLIGHT_TOKEN dans .env.

config/services.php

'runlight' => [
    'token' => env('RUNLIGHT_TOKEN'),
],

Le (string) du contrôleur transforme un jeton absent en jeton vide, ce qui garde le tableau de bord fermé. Passer null le laisserait ouvert à tout le monde. Lancez la vérification planifiée depuis le planificateur de Laravel.

routes/console.php

use Illuminate\Support\Facades\Schedule;
use Runlight\Runlight;

Schedule::call(fn () => app(Runlight::class)->check())->hourly();

Symfony

Un seul contrôleur sert tout ce qui se trouve sous /runlight et les liens courts sur /go. Sa méthode shortLinks écoute chaque requête avant que Symfony ne la route, donc un domaine de liens atteint lui aussi Runlight, et toutes les autres requêtes continuent sans qu’on y touche.

src/Controller/RunlightController.php

<?php

namespace App\Controller;

use Runlight\Bridge\HttpFoundation;
use Runlight\Bridge\ShortLinks;
use Runlight\Runlight;
use Runlight\Store\Stores;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Attribute\Route;

final class RunlightController
{
    private ?Runlight $rl = null;

    public function __construct(
        #[Autowire('%kernel.project_dir%/var/runlight.db')] private readonly string $database,
        #[Autowire('%env(RUNLIGHT_TOKEN)%')] private readonly string $token,
    ) {
    }

    #[Route('/runlight/{path}', name: 'runlight', requirements: ['path' => '.*'], defaults: ['path' => ''])]
    #[Route('/go/{slug}', name: 'runlight_link', methods: ['GET'])]
    public function __invoke(Request $request): Response
    {
        $rl = $this->runlight();

        return HttpFoundation::handle($rl, $rl->routes(['token' => $this->token]), $request);
    }

    #[AsEventListener(event: KernelEvents::REQUEST, priority: 64)]
    public function shortLinks(RequestEvent $event): void
    {
        (new ShortLinks($this->runlight()))->onKernelRequest($event);
    }

    private function runlight(): Runlight
    {
        return $this->rl ??= new Runlight([
            'store' => Stores::sqlite($this->database),
            'site' => ['name' => 'example.com', 'hostnames' => ['example.com'], 'timezone' => 'Europe/London'],
        ]);
    }
}

Définissez RUNLIGHT_TOKEN dans .env.local. Symfony s’arrête sur une erreur quand il manque, donc le tableau de bord ne reste jamais ouvert par accident. Pour la vérification planifiée, appelez $rl->check() depuis une commande console lancée par cron, ou définissez CRON_SECRET et appelez la vérification en HTTP.

Runlight\Bridge\HttpFoundation transforme une requête Symfony ou Laravel en requête Runlight, et sa réponse dans l’autre sens, et Runlight\Bridge\ShortLinks répond aux liens courts avant les routes de l’application. Ils ont besoin de symfony/http-foundation, que les deux frameworks ont déjà.

ShortLinks ne répond à /go/<name> que si le lien existe et passe tout autre nom à vos routes. Là, pour un nom sans lien, la route /go/{slug} ci-dessus lève l’erreur « introuvable » du framework, si bien que votre propre page 404 y répond dans tous les cas. FrontController et le drop-in autonome n’ont aucune application derrière eux et envoient eux-mêmes ce 404. Sur un domaine de liens, /go/<name> est partout un 404, comme tout autre chemin qui n’est pas l’un des liens de ce domaine.

Autres applications PHP

N’importe quelle application PHP peut passer une requête à Runlight et renvoyer sa réponse. Construisez un Runlight\Http\Request à partir de l’URL, de la méthode, des en-têtes, du corps et de l’adresse de la connexion, puis passez-le à FrontController::answer().

php

use Runlight\Http\Request;
use Runlight\Server\FrontController;

$request = new Request($url, $method, $headers, $body, $_SERVER['REMOTE_ADDR'] ?? '');
$answer = FrontController::answer($rl, $rl->routes(), $request);
// $answer->status, $answer->headers->all(), and $answer->text() are what to send.

Passez l’adresse d’où vient la connexion. Runlight lit lui-même les en-têtes transmis quand trustProxy le permet.

Un site sous WordPress, Drupal ou Craft n’a besoin de rien de tout ce code. Son extension le connecte à un Runlight que vous faites tourner ailleurs, comme le drop-in ci-dessous, ainsi que le montrent les pages WordPress, Drupal et Craft.

Le drop-in autonome

Le drop-in est l’équivalent PHP du serveur autonome. Il fait tourner Runlight sur son propre domaine, comme stats.example.com, avec le tableau de bord à la racine, des sites ajoutés depuis le tableau de bord, des comptes de connexion et des liens courts sur n’importe quel domaine que vous faites pointer vers lui. Il convient aux hébergements qui font tourner PHP sans Node.

L’installer

Créez un dossier de projet à côté de votre racine web, installez-y le paquet, et copiez le drop-in dans le dossier que sert le domaine.

bash

mkdir stats && cd stats
composer require runlight/runlight
cp -r vendor/runlight/runlight/standalone public

Faites pointer le domaine vers public. Sur Apache et LiteSpeed, le fichier .htaccess fourni envoie chaque requête à index.php. Sur nginx, ajoutez un bloc server comme celui-ci.

nginx.conf

server {
    server_name stats.example.com;
    root /var/www/stats/public;
    index index.php;

    location / {
        try_files $uri /index.php$is_args$args;
    }

    location = /index.php {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root/index.php;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }

    location ~ /\.ht {
        deny all;
    }

    # In case the drop-in folder is also the project folder: never serve the data, the code, or the config.
    location ~ ^/(vendor|runlight-data|config\.php|composer\.(json|lock))(/|$) {
        deny all;
    }
}

Les données vivent dans runlight-data, dans le dossier du projet, hors de la racine web, et sont créées à la première requête. Ce dossier contient le fichier SQLite et le secret qui signe les connexions, avec les données de localisation dans leur propre dossier. L’utilisateur du serveur web doit pouvoir écrire dans le dossier du projet, ou dans un dossier runlight-data que vous créez pour lui.

Créer votre compte

La première requête écrit un lien de configuration avec un code à usage unique dans setup.txt, dans le dossier de données. Ouvrez ce lien et saisissez votre adresse e-mail et un mot de passe d’au moins dix caractères, tapé deux fois. Sans RUNLIGHT_URL, le lien commence par une adresse provisoire, donc mettez la vôtre devant /setup. Le lien ne fonctionne que tant qu’il n’y a aucun compte, et la commande cron supprime le fichier dès qu’il y en a un. Pour afficher le lien à nouveau, lancez ceci dans le dossier du projet.

bash

vendor/bin/runlight setup

Quand RUNLIGHT_TOKEN est défini, la page de configuration demande plutôt le jeton, et aucun fichier n’est écrit. Vous pouvez aussi créer le premier compte en ligne de commande avec vendor/bin/runlight password you@example.com, qui affiche son mot de passe. Les rôles, les invitations, la connexion à deux facteurs et l’ajout de sites fonctionnent comme sur le serveur autonome.

Réglages

Les réglages viennent des variables d’environnement, ou d’un config.php dans le dossier du projet qui renvoie les mêmes noms. Les variables d’environnement l’emportent quand les deux sont définis. Copiez l’exemple pour en commencer un.

bash

cp vendor/runlight/runlight/config.example.php config.php

config.php

<?php

return [
    'RUNLIGHT_URL' => 'https://stats.example.com',
    'DATABASE_URL' => 'mysql://runlight:password@localhost:3306/runlight',
];
Réglage Ce qu’il fait
RUNLIGHT_URL L’adresse publique du tableau de bord, par exemple https://stats.example.com. Elle ne peut jamais devenir un domaine de liens, et les e-mails d’invitation et de rapport pointent vers elle.
DATABASE_URL Une adresse postgres://, mysql:// ou mariadb:// garde les données dans cette base au lieu de SQLite.
DATA_DIR Le dossier de données, runlight-data dans le dossier du projet par défaut. Un chemin relatif part du dossier du projet.
RUNLIGHT_SECRET La clé qui signe les connexions et chiffre les clés enregistrées. Sans elle, le drop-in en crée une et la garde dans le dossier de données.
RUNLIGHT_TOKEN Un jeton bearer pour les scripts, qui crée aussi le premier compte à la place du lien de configuration.
TRUST_PROXY Mettez false quand rien ne se trouve devant le serveur web, ou x-real-ip ou cf-connecting-ip quand c’est cet en-tête qui contient l’adresse du visiteur.
RUNLIGHT_GEO D’où viennent les lieux, comme sur le serveur autonome. city par défaut, et peut valoir country, off ou le chemin d’un fichier MMDB.
CRON_SECRET Permet à un planificateur de lancer la vérification sur POST /api/check quand il ne peut pas lancer la commande cron.
RUNLIGHT_OBSERVE_KEY Une seule clé pour les signalements d’agents IA de tous les sites. La clé des agents propre à chaque site, affichée dans Réglages, Installation, est le meilleur choix.
RUNLIGHT_CONFIG Le chemin d’un fichier de configuration situé ailleurs que dans le dossier du projet.

TRUST_PROXY compte chez un hébergeur PHP, où Apache ou nginx répond souvent aux visiteurs sans proxy devant. Par défaut, Runlight lit l’adresse de chaque visiteur dans la dernière entrée de X-Forwarded-For, et sans rien devant pour ajouter cette entrée, un visiteur peut l’écrire lui-même. Mettez TRUST_PROXY=false sur un tel serveur.

Cron

Ajoutez une ligne à la crontab de l’utilisateur sous lequel tourne le site. Toutes les cinq minutes, elle lance la vérification planifiée, et une fois par mois, elle télécharge les nouvelles données de localisation.

bash

*/5 * * * * cd /var/www/stats && vendor/bin/runlight cron

La commande n’affiche rien quand tout va bien, sauf une ligne quand les données de localisation d’un nouveau mois sont prêtes. Si une exécution est encore en cours quand la suivante démarre, la seconde lui laisse le travail. Quand l’hébergeur n’a pas de cron, définissez CRON_SECRET et faites appeler /api/check avec lui par n’importe quel planificateur, comme le montre Vérification planifiée, mais les lieux demandent alors de définir RUNLIGHT_GEO avec un fichier à vous.

Mots de passe oubliés

Lancez la commande password dans le dossier du projet pour donner à un compte un nouveau mot de passe, qu’elle affiche. Elle désactive aussi la connexion à deux facteurs pour ce compte, et elle crée le compte s’il n’existe pas.

bash

vendor/bin/runlight password someone@example.com

Mises à jour et sauvegardes

Lancez composer update runlight/runlight dans le dossier du projet. Les tables se mettent à jour toutes seules à la requête suivante, ou lancez vendor/bin/runlight migrate pour les mettre à jour tout de suite. Sauvegardez le dossier de données et config.php, puisque les clés enregistrées ne peuvent pas être lues sans le secret.

Agents IA depuis un journal

Les agents IA comme GPTBot et ClaudeBot récupèrent les pages sans exécuter de JavaScript, donc la balise script ne les voit jamais. Une application PHP qui contient Runlight les compte là où la page est servie, avec observe(), comme le montre PHP pur. Pour tout autre site que vous hébergez vous-même, le journal d’accès du serveur web les contient, et la commande agents le lit.

bash

vendor/bin/runlight agents --log /var/log/nginx/access.log --to https://stats.example.com --key rlo_... --site https://example.com --follow

La clé est la clé des agents du site, affichée dans Runlight dans Réglages, Installation, et elle ne peut signaler que les requêtes de ce site. Quand --to ou --key est omis, la commande lit RUNLIGHT_URL ou RUNLIGHT_OBSERVE_KEY dans l’environnement ou dans config.php, comme les autres commandes lisent leurs réglages. --site est l’adresse du site, pour les journaux au format habituel de nginx ou d’Apache, qui omettent l’hôte. Les journaux JSON de Caddy contiennent l’hôte, donc il n’y est pas nécessaire. L’utilisateur qui lance la commande doit pouvoir lire le journal.

Avec --follow, la commande continue de tourner, envoie les requêtes au fur et à mesure et continue quand le journal est renouvelé, en finissant l’ancien fichier avant de passer au nouveau. Un service systemd ou un gestionnaire de processus la maintient en marche. Ajoutez --state agents.json pour qu’un redémarrage reprenne là où elle s’est arrêtée. Sans --follow, elle lit le journal une fois et s’arrête, et avec --state, l’exécution suivante commence là où la précédente a fini, ce qui convient à cron.

bash

*/5 * * * * cd /var/www/stats && vendor/bin/runlight agents --log /var/log/nginx/access.log --to https://stats.example.com --key rlo_... --site https://example.com --state agents.json

Une seule exécution à la fois peut utiliser un fichier d’état. Elle pose un verrou à côté du fichier (agents.json.lock), donc une deuxième exécution, comme une tâche cron qui démarre alors que la précédente envoie encore, s’arrête avec un message et n’envoie rien en double. Un verrou laissé par une exécution qui a planté est repris.

Seules les requêtes de pages réussies des agents IA connus quittent la machine, chacune avec l’adresse de la page, le user agent de l’agent et le moment où elle a été servie. Les adresses des visiteurs et tout le reste du journal restent où ils sont. Runlight ne compte que les requêtes de pages de la dernière semaine, donc la première exécution sur un long journal ignore les anciennes.

Ce que fait le serveur Node et pas le drop-in

PHP ne garde rien en marche entre deux requêtes, donc quelques points fonctionnent autrement qu’avec npx runlight.sh.

  • La vérification planifiée et le téléchargement mensuel de DB-IP passent par cron. Jusqu’au premier runlight cron, les visites ne reçoivent un lieu que par des en-têtes comme ceux de Cloudflare.
  • Le lien de configuration est écrit une fois dans un fichier et gardé jusqu’à ce qu’un compte existe.
  • Il n’y a pas d’image Docker, et rien n’écoute sur un port à lui. C’est le serveur web qui fait tourner le drop-in.

Plusieurs serveurs web peuvent partager une même base de données, comme les copies du serveur Node. Chaque requête relit la liste des sites, donc un site ajouté sur l’un apparaît aussitôt sur les autres.

Différences avec la bibliothèque TypeScript

Le paquet PHP passe les tests de conformité imposés à la bibliothèque TypeScript, sur SQLite, Postgres, MySQL et MariaDB. Les différences viennent de la façon dont PHP s’exécute.

  • Il n’y a pas de pool de connexions. Chaque requête ouvre sa propre connexion à la base et la ferme à la fin, donc un site chargé garde une connexion par worker PHP.
  • Ce que la bibliothèque TypeScript fait en arrière-plan, comme supprimer des visites après le choix d’une durée de conservation plus courte, s’exécute après l’envoi de la réponse. PHP-FPM et LiteSpeed ferment d’abord la connexion, et les autres serveurs la gardent ouverte jusqu’à la fin du travail. La vérification planifiée passe par cron ou par le planificateur d’un framework, puisque rien en PHP ne tourne sur une minuterie.
  • Les mots de passe sont hachés avec scrypt en PHP pur, ce qui prend environ 0,7 seconde par connexion sans le compilateur JIT d’OPcache et environ 0,2 seconde avec. Les hachages sont identiques à celles de la bibliothèque TypeScript, donc les comptes se transfèrent.
  • La bibliothèque TypeScript garde certains compteurs en mémoire, que PHP oublie entre deux requêtes. Les limites de mots de passe erronés sont gardées dans la base, donc tous les workers les partagent. La limite de débit du traceur vit dans APCu quand il est chargé, et sinon dans de petits fichiers du dossier temporaire du système, sauf si vous passez un Runlight\Cache à vous comme cache, qui garde alors aussi l’icône du site.
  • Runlight lit RUNLIGHT_TOKEN et les autres variables avec getenv(), puis dans $_ENV, puis dans $_SERVER, puisque les hébergeurs les définissent à des endroits différents.