Toda la documentación

Empezar

  1. 01Primeros pasos
  2. 02Instalación
  3. 03Seguimiento

Funciones

  1. 04El panel
  2. 05Objetivos
  3. 06Enlaces cortos
  4. 07Fuentes y agentes de IA
  5. 08Informes por email
  6. 09Comprobación programada
  7. 10Pregunta a tu IA

Referencia

  1. 11Configuración
  2. 12API HTTP

Plataformas

  1. 13Servidor independiente
  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

Confianza

  1. 26Privacidad

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.