Empezar
Instalación
Monta Runlight en Next.js, Nuxt, SvelteKit, Astro, Remix, Express, NestJS, Fastify, Koa, Hono, Bun, Deno o Cloudflare Workers, o encuentra la guía para tu lenguaje.
Toda instalación necesita una instancia (consulta Primeros pasos), sus rutas montadas bajo /runlight y la etiqueta de script en tus páginas. Entre frameworks solo cambian las rutas.
rl.routes() devuelve un handler Fetch, handler(request), y además la misma función con el nombre de cada método, así que cualquier framework basado en Request y Response web puede usarlo directamente. Cambia la ruta con rl.routes({ basePath: "/stats" }), y la etiqueta de script pasa a ser /stats/s.js.
Next.js
En el App Router, añade una ruta comodín.
app/runlight/[[...path]]/route.ts
import { rl } from "@/lib/runlight";
export const { GET, POST, PUT, PATCH, DELETE, OPTIONS } = rl.routes();Añade el script a tu layout raíz.
app/layout.tsx
<head>
<script defer src="/runlight/s.js"></script>
</head>Runlight necesita el runtime de Node (usa SQLite, Postgres o MySQL), que es el predeterminado para los route handlers.
Express y Node sin framework
@runlight/sdk/node adapta el handler al módulo http de Node, y por tanto a Express, Connect y Koa (a través de ctx.req y 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));Puede ir antes o después de cualquier body parser, porque el adaptador también lee un cuerpo que Express ya ha procesado, y deja sin leer los cuerpos de las peticiones propias de tu aplicación. shortLinks va antes de tus propias rutas, para que un dominio de enlaces llegue primero a Runlight.
NestJS
Nest funciona sobre Express por defecto, así que el mismo middleware va en main.ts, antes de 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);Añade app.use(shortLinks(rl)) antes de él para los enlaces cortos.
Fastify
Fastify lee los cuerpos de las peticiones por sí mismo, así que dale a Runlight su propio ámbito de plugin sin body parsers y pásale la petición sin procesar.
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); });
});Los enlaces cortos van en un hook onRequest, como muestra Enlaces cortos.
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);
});Para los enlaces cortos, añade el middleware de Enlaces cortos antes de este.
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 y Nitro
Pon la instancia en server/utils/runlight.ts (Nitro importa por ti todo lo que hay allí) y añade esta ruta.
server/routes/runlight/[...path].ts
export default defineEventHandler((event) => rl.routes().handler(toWebRequest(event)));Añade el mismo archivo como server/routes/runlight.ts para que /runlight también responda. La etiqueta de script va en app.head en 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 necesita un adaptador para la salida de servidor.
src/pages/runlight/[...path].ts
import { rl } from "../../lib/runlight";
export const prerender = false;
export const ALL = ({ request }) => rl.routes().handler(request);Remix y 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 no puede cargar better-sqlite3, así que usa su SQLite integrado a través de @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 y otros frameworks de Bun pasan request de la misma forma.
Deno
Deno sirve Requests web directamente. Usa libSQL (un archivo local o Turso) o Postgres como almacén.
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
Los Workers no tienen disco, así que guarda las cifras en D1. La ubicación sale de las cabeceras propias de Cloudflare sin configurar nada.
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 guarda sus secretos en env, que Runlight no puede leer por sí solo, así que pasa el token a routes() como arriba. Defínelo con wrangler secret put RUNLIGHT_TOKEN.
Enruta solo /runlight/* a este Worker (o comprueba primero la ruta, como en el ejemplo de Bun), y programa la comprobación horaria con un Cron Trigger que llame a rl.check().
Cualquier otro
Si tu servidor te da un Request web, pásalo a rl.routes().handler y devuelve la Response. Si te da el req y el res de Node, usa toNodeHandler de @runlight/sdk/node.
Otros lenguajes
Runlight está escrito de nuevo para cada uno de estos lenguajes, con el mismo panel y las mismas tablas. Cada página tiene la instalación, las rutas para sus frameworks, la comprobación programada y lo que cambia respecto a la biblioteca de TypeScript.
| Lenguaje | Instalación | Funciona en |
|---|---|---|
| PHP | composer require runlight/runlight |
Laravel, Symfony, PHP sin framework y una versión lista para usar en un dominio propio |
| Python | pip install runlight |
Django, Flask, FastAPI y cualquier aplicación WSGI o ASGI |
| Rails | bundle add runlight |
Rails 7.2, 8.0 y 8.1, mediante un engine y un generador |
| Ruby | bundle add runlight sqlite3 |
Sinatra, Hanami, Roda y cualquier otra aplicación Rack |
| Go | go get runlight.sh/go |
net/http, chi, Echo y cualquier router que acepte un http.Handler |
| Java | sh.runlight:runlight desde Maven Central |
El servidor propio del JDK, contenedores de servlets y Spring Boot |
| .NET | dotnet add package Runlight.AspNetCore |
ASP.NET Core y cualquier otra aplicación .NET |
| Elixir | {:runlight, "~> 0.1"} en mix.exs |
Phoenix y cualquier aplicación Plug |
| Rust | cargo add runlight runlight-sqlx |
axum, hyper y cualquier servidor que acepte un servicio tower |
Todos los lenguajes salvo Elixir y Rust tienen también un servidor independiente propio, y la página del servidor independiente trata el de Node.
Detrás de un proxy
Runlight lee la dirección del visitante de la última entrada de X-Forwarded-For, la que añade tu proxy, y si no, de X-Real-IP o CF-Connecting-IP. Eso es lo correcto detrás de Vercel, Netlify, Cloudflare y la mayoría de los balanceadores de carga. Cuando una petición pasa por dos proxies, como Cloudflare delante de nginx, indica la cabecera que contiene la dirección del propio visitante, por ejemplo trustProxy: "cf-connecting-ip". La dirección se usa para el hash diario de visitantes y para averiguar la ubicación, y el límite de peticiones la cuenta en forma de hash. Nunca se guarda. Si tu aplicación está expuesta directamente sin proxy, define trustProxy: false.