Gesamte Doku

Start

  1. 01Erste Schritte
  2. 02Installation
  3. 03Tracking

Funktionen

  1. 04Das Dashboard
  2. 05Ziele
  3. 06Kurzlinks
  4. 07KI-Quellen und Agenten
  5. 08E-Mail-Berichte
  6. 09Stündlicher Check
  7. 10Frag deine KI

Referenz

  1. 11Konfiguration
  2. 12HTTP-API

Plattformen

  1. 13Eigenständiger Server
  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

Vertrauen

  1. 26Datenschutz

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.