Start
Installation
Binde Runlight in Next.js, Nuxt, SvelteKit, Astro, Remix, Express, NestJS, Fastify, Koa, Hono, Bun, Deno oder Cloudflare Workers ein, oder finde die Anleitung für deine Sprache.
Jede Installation braucht eine Instanz (siehe Erste Schritte), ihre unter /runlight eingebundenen Routen und das Script-Tag auf deinen Seiten. Von Framework zu Framework ändern sich nur die Routen.
rl.routes() gibt einen Fetch-Handler zurück, handler(request), und dazu dieselbe Funktion unter jedem Methodennamen. Jedes Framework, das auf den Web-Klassen Request und Response aufbaut, kann ihn also direkt nutzen. Den Pfad änderst du mit rl.routes({ basePath: "/stats" }), und das Script-Tag wird dann zu /stats/s.js.
Next.js
Füge im App Router eine Catch-all-Route hinzu.
app/runlight/[[...path]]/route.ts
import { rl } from "@/lib/runlight";
export const { GET, POST, PUT, PATCH, DELETE, OPTIONS } = rl.routes();Füge das Skript zu deinem Root-Layout hinzu.
app/layout.tsx
<head>
<script defer src="/runlight/s.js"></script>
</head>Runlight braucht die Node-Runtime (es nutzt SQLite, Postgres oder MySQL), und die ist für Route Handler der Standard.
Express und reines Node
@runlight/sdk/node passt den Handler an das http-Modul von Node an und damit an Express, Connect und Koa (über ctx.req und 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));Er kann vor oder nach jedem Body-Parser stehen, weil der Adapter auch einen Body liest, den Express schon geparst hat, und die Bodies der eigenen Anfragen deiner App ungelesen lässt. shortLinks kommt vor deine eigenen Routen, damit eine Link-Domain zuerst Runlight erreicht.
NestJS
Nest läuft standardmäßig auf Express, deshalb kommt dieselbe Middleware in main.ts, vor 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);Füge app.use(shortLinks(rl)) davor ein, um Kurzlinks zu nutzen.
Fastify
Fastify liest Request-Bodies selbst. Gib Runlight daher einen eigenen Plugin-Scope ohne Body-Parser und übergib ihm die rohe Anfrage.
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); });
});Kurzlinks kommen in einen onRequest-Hook, wie Kurzlinks zeigt.
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);
});Für Kurzlinks fügst du die Middleware unter Kurzlinks vor dieser ein.
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 und Nitro
Leg die Instanz in server/utils/runlight.ts ab (Nitro importiert dort alles für dich) und füge dann diese Route hinzu.
server/routes/runlight/[...path].ts
export default defineEventHandler((event) => rl.routes().handler(toWebRequest(event)));Füge dieselbe Datei auch als server/routes/runlight.ts hinzu, damit auch /runlight selbst antwortet. Das Script-Tag kommt in app.head in 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 braucht für Server-Output einen Adapter.
src/pages/runlight/[...path].ts
import { rl } from "../../lib/runlight";
export const prerender = false;
export const ALL = ({ request }) => rl.routes().handler(request);Remix und 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 kann better-sqlite3 nicht laden, nutze daher sein eingebautes SQLite über @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 und andere Bun-Frameworks übergeben request auf dieselbe Weise.
Deno
Deno verarbeitet Web-Requests direkt. Nutze libSQL (eine lokale Datei oder Turso) oder Postgres als Store.
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
Workers haben keine Festplatte, bewahre die Zahlen daher in D1 auf. Der Standort kommt ohne Einrichtung aus den eigenen Headern von Cloudflare.
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);
},
};Ein Worker bewahrt seine Secrets in env auf, das Runlight nicht selbst lesen kann. Übergib das Token deshalb wie oben an routes(). Setze es mit wrangler secret put RUNLIGHT_TOKEN.
Leite nur /runlight/* an diesen Worker (oder prüfe zuerst den Pfad, wie im Bun-Beispiel), und plane den stündlichen Check mit einem Cron Trigger, der rl.check() aufruft.
Alles andere
Wenn dein Server dir einen Web-Request gibt, übergib ihn an rl.routes().handler und gib die Response zurück. Wenn er dir req und res von Node gibt, nutze toNodeHandler aus @runlight/sdk/node.
Andere Sprachen
Runlight ist für jede dieser Sprachen neu geschrieben, mit demselben Dashboard und denselben Tabellen. Jede Seite enthält die Installation, die Routen für ihre Frameworks, den stündlichen Check und das, was sich von der TypeScript-Bibliothek unterscheidet.
| Sprache | Installation | Läuft in |
|---|---|---|
| PHP | composer require runlight/runlight |
Laravel, Symfony, reinem PHP und als Drop-in für eine eigene Domain |
| Python | pip install runlight |
Django, Flask, FastAPI und jeder WSGI- oder ASGI-App |
| Rails | bundle add runlight |
Rails 7.2, 8.0 und 8.1, über eine Engine und einen Generator |
| Ruby | bundle add runlight sqlite3 |
Sinatra, Hanami, Roda und jeder anderen Rack-App |
| Go | go get runlight.sh/go |
net/http, chi, Echo und jedem Router, der einen http.Handler annimmt |
| Java | sh.runlight:runlight aus Maven Central |
Dem eigenen Server des JDK, Servlet-Containern und Spring Boot |
| .NET | dotnet add package Runlight.AspNetCore |
ASP.NET Core und jeder anderen .NET-App |
| Elixir | {:runlight, "~> 0.1"} in mix.exs |
Phoenix und jeder Plug-App |
| Rust | cargo add runlight runlight-sqlx |
axum, hyper und jedem Server, der einen Tower-Service annimmt |
Jede Sprache außer Elixir und Rust hat auch einen eigenständigen Server, und die Seite Eigenständiger Server behandelt den für Node.
Hinter einem Proxy
Runlight liest die Adresse des Besuchers aus dem letzten Eintrag in X-Forwarded-For, dem, den dein Proxy hinzufügt, und danach aus X-Real-IP oder CF-Connecting-IP. Das ist hinter Vercel, Netlify, Cloudflare und den meisten Load Balancern richtig. Wenn eine Anfrage durch zwei Proxys läuft, etwa Cloudflare vor nginx, nenne den Header, der die eigene Adresse des Besuchers enthält, etwa trustProxy: "cf-connecting-ip". Die Adresse wird für den täglichen Besucher-Hash und die Standortsuche genutzt, und das Rate-Limit zählt sie gehasht. Sie wird nie gespeichert. Wenn deine App ohne Proxy direkt erreichbar ist, setze trustProxy: false.