Démarrer
Installation
Montez Runlight dans Next.js, Nuxt, SvelteKit, Astro, Remix, Express, NestJS, Fastify, Koa, Hono, Bun, Deno ou Cloudflare Workers, ou trouvez le guide de votre langage.
Chaque installation a besoin d’une instance (voyez Pour commencer), de ses routes montées sous /runlight et de la balise script sur vos pages. Seules les routes changent d’un framework à l’autre.
rl.routes() renvoie un handler Fetch, handler(request), ainsi que la même fonction sous le nom de chaque méthode. Tout framework construit sur les objets web Request et Response peut donc l’utiliser directement. Changez le chemin avec rl.routes({ basePath: "/stats" }), et la balise script devient /stats/s.js.
Next.js
Dans l’App Router, ajoutez une route attrape-tout.
app/runlight/[[...path]]/route.ts
import { rl } from "@/lib/runlight";
export const { GET, POST, PUT, PATCH, DELETE, OPTIONS } = rl.routes();Ajoutez le script à votre layout racine.
app/layout.tsx
<head>
<script defer src="/runlight/s.js"></script>
</head>Runlight a besoin du runtime Node (il utilise SQLite, Postgres ou MySQL), qui est celui par défaut pour les route handlers.
Express et Node seul
@runlight/sdk/node adapte le handler au module http de Node, et donc à Express, Connect et Koa (via ctx.req et ctx.res).
server.ts
import express from "express";
import { toNodeHandler, observer, shortLinks } from "@runlight/sdk/node";
import { rl } from "./runlight.js";
const app = express();
app.use(shortLinks(rl)); // short links at /go and on custom domains
app.use(observer(rl)); // AI agents reading your pages; optional
app.use(toNodeHandler(rl.routes().handler));Il peut se placer avant ou après n’importe quel parseur de corps, car l’adaptateur lit aussi un corps qu’Express a déjà analysé, et il ne touche pas aux corps des requêtes propres à votre application. shortLinks se place avant vos propres routes, pour qu’un domaine de liens atteigne Runlight en premier.
NestJS
Nest tourne sur Express par défaut, donc le même middleware va dans main.ts, avant listen.
src/main.ts
import { NestFactory } from "@nestjs/core";
import { toNodeHandler } from "@runlight/sdk/node";
import { AppModule } from "./app.module";
import { rl } from "./runlight";
const app = await NestFactory.create(AppModule);
app.use(toNodeHandler(rl.routes().handler));
await app.listen(3000);Ajoutez app.use(shortLinks(rl)) avant pour les liens courts.
Fastify
Fastify lit lui-même le corps des requêtes. Donnez donc à Runlight son propre scope de plugin, sans parseur de corps, et passez-lui la requête brute.
ts
import Fastify from "fastify";
import { toNodeHandler } from "@runlight/sdk/node";
import { rl } from "./runlight.js";
const app = Fastify();
const runlight = toNodeHandler(rl.routes().handler);
app.register(async (scope) => {
scope.removeAllContentTypeParsers();
scope.addContentTypeParser("*", (_req, _payload, done) => done(null));
scope.all("/runlight", (req, reply) => { reply.hijack(); return runlight(req.raw, reply.raw); });
scope.all("/runlight/*", (req, reply) => { reply.hijack(); return runlight(req.raw, reply.raw); });
});Les liens courts passent par un hook onRequest, comme le montre Liens courts.
Koa
ts
import Koa from "koa";
import { toNodeHandler } from "@runlight/sdk/node";
import { rl } from "./runlight.js";
const app = new Koa();
const runlight = toNodeHandler(rl.routes().handler);
app.use(async (ctx, next) => {
if (ctx.path !== "/runlight" && !ctx.path.startsWith("/runlight/")) return next();
ctx.respond = false;
await runlight(ctx.req, ctx.res);
});Pour les liens courts, ajoutez le middleware décrit dans Liens courts avant celui-ci.
Hono
ts
import { Hono } from "hono";
import { rl } from "./runlight";
const app = new Hono();
const runlight = rl.routes().handler;
app.all("/runlight/*", (c) => runlight(c.req.raw));
app.all("/runlight", (c) => runlight(c.req.raw));Nuxt et Nitro
Placez l’instance dans server/utils/runlight.ts (Nitro importe pour vous tout ce qui s’y trouve), puis ajoutez cette route.
server/routes/runlight/[...path].ts
export default defineEventHandler((event) => rl.routes().handler(toWebRequest(event)));Ajoutez le même fichier sous server/routes/runlight.ts pour que /runlight lui-même réponde aussi. La balise script va dans app.head, dans nuxt.config.ts.
nuxt.config.ts
export default defineNuxtConfig({
app: { head: { script: [{ src: "/runlight/s.js", defer: true }] } },
});SvelteKit
src/routes/runlight/[...path]/+server.ts
import { rl } from "$lib/runlight";
const { handler } = rl.routes();
export const GET = ({ request }) => handler(request);
export const POST = GET;
export const PUT = GET;
export const PATCH = GET;
export const DELETE = GET;
export const OPTIONS = GET;Astro
Astro a besoin d’un adaptateur pour le rendu côté serveur.
src/pages/runlight/[...path].ts
import { rl } from "../../lib/runlight";
export const prerender = false;
export const ALL = ({ request }) => rl.routes().handler(request);Remix et React Router
app/routes/runlight.$.ts
import { rl } from "~/runlight.server";
const { handler } = rl.routes();
export const loader = ({ request }) => handler(request);
export const action = ({ request }) => handler(request);Bun
Bun ne peut pas charger better-sqlite3, alors utilisez son SQLite intégré via @runlight/sdk/bun.
server.ts
import { runlight } from "@runlight/sdk";
import { bunSqlite } from "@runlight/sdk/bun";
const rl = runlight({ store: bunSqlite({ path: "./data/runlight.db" }), site: { hostnames: ["example.com"] } });
const runlightRoutes = rl.routes().handler;
Bun.serve({
port: 3000,
fetch(request) {
const { pathname } = new URL(request.url);
if (pathname === "/runlight" || pathname.startsWith("/runlight/")) return runlightRoutes(request);
return new Response("Your app");
},
});Elysia et les autres frameworks Bun transmettent request de la même façon.
Deno
Deno sert directement les Requests web. Utilisez libSQL (un fichier local ou Turso) ou Postgres pour le stockage.
main.ts
import { runlight } from "npm:@runlight/sdk";
import { libsql } from "npm:@runlight/sdk/libsql";
import { createClient } from "npm:@libsql/client";
const rl = runlight({ store: libsql({ client: createClient({ url: "file:runlight.db" }) }), site: { hostnames: ["example.com"] } });
const runlightRoutes = rl.routes().handler;
Deno.serve((request) => {
const { pathname } = new URL(request.url);
if (pathname === "/runlight" || pathname.startsWith("/runlight/")) return runlightRoutes(request);
return new Response("Your app");
});Cloudflare Workers
Les Workers n’ont pas de disque, alors gardez les chiffres dans D1. La localisation vient des en-têtes de Cloudflare, sans aucune configuration.
src/index.ts
import { runlight } from "@runlight/sdk";
import { d1 } from "@runlight/sdk/d1";
let routes: ReturnType<ReturnType<typeof runlight>["routes"]> | undefined;
export default {
fetch(request: Request, env: { DB: D1Database; RUNLIGHT_TOKEN: string }) {
// Made once per Worker instance, so the tables are checked when the instance starts.
routes ??= runlight({ store: d1({ database: env.DB }), site: { hostnames: ["example.com"] } }).routes({ token: env.RUNLIGHT_TOKEN });
return routes.handler(request);
},
};Un Worker garde ses secrets dans env, que Runlight ne peut pas lire tout seul, alors passez le jeton à routes() comme ci-dessus. Définissez-le avec wrangler secret put RUNLIGHT_TOKEN.
Ne routez que /runlight/* vers ce Worker (ou vérifiez d’abord le chemin, comme dans l’exemple Bun), et planifiez la vérification horaire avec un Cron Trigger qui appelle rl.check().
Tout le reste
Si votre serveur vous donne une Request web, passez-la à rl.routes().handler et renvoyez la Response. S’il vous donne les objets req et res de Node, utilisez toNodeHandler de @runlight/sdk/node.
Autres langages
Runlight est réécrit pour chacun de ces langages, avec le même tableau de bord et les mêmes tables. Chaque page couvre l’installation, les routes pour ses frameworks, la vérification planifiée et ce qui diffère de la bibliothèque TypeScript.
| Langage | Installation | Tourne dans |
|---|---|---|
| PHP | composer require runlight/runlight |
Laravel, Symfony, PHP pur, et un drop-in autonome sur son propre domaine |
| Python | pip install runlight |
Django, Flask, FastAPI et toute application WSGI ou ASGI |
| Rails | bundle add runlight |
Rails 7.2, 8.0 et 8.1, via un engine et un générateur |
| Ruby | bundle add runlight sqlite3 |
Sinatra, Hanami, Roda et toute autre application Rack |
| Go | go get runlight.sh/go |
net/http, chi, Echo et tout routeur qui accepte un http.Handler |
| Java | sh.runlight:runlight depuis Maven Central |
Le serveur intégré au JDK, les conteneurs de servlets et Spring Boot |
| .NET | dotnet add package Runlight.AspNetCore |
ASP.NET Core et toute autre application .NET |
| Elixir | {:runlight, "~> 0.1"} dans mix.exs |
Phoenix et toute application Plug |
| Rust | cargo add runlight runlight-sqlx |
axum, hyper et tout serveur qui accepte un service tower |
Tous les langages sauf Elixir et Rust ont aussi leur propre serveur autonome, et la page Serveur autonome couvre celui de Node.
Derrière un proxy
Runlight lit l’adresse du visiteur dans la dernière entrée de X-Forwarded-For, celle qu’ajoute votre proxy, puis dans X-Real-IP ou CF-Connecting-IP. C’est le bon choix derrière Vercel, Netlify, Cloudflare et la plupart des répartiteurs de charge. Quand une requête traverse deux proxys, comme Cloudflare devant nginx, indiquez l’en-tête qui contient l’adresse du visiteur lui-même, par exemple trustProxy: "cf-connecting-ip". L’adresse sert au hachage quotidien des visiteurs et à la localisation, et la limite de débit la compte sous forme hachée. Elle n’est jamais stockée. Si votre application est exposée directement, sans proxy, définissez trustProxy: false.