Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Redirect Language (RDL)

RDL (Redirect Language) is the configuration format used by liblin. It maps an incoming host and path to the location of a text file. The first line of that text file is the URL the visitor is redirected to.

An RDL file (also called a domain-slurps file) is a single EDN map:

  • each key is the host and port of an incoming request ("example.com:80");
  • each value is a Liquid-style URL template rendered with the request’s domain and path segments.

The file is read on every request, so entries can be changed without restarting the server. For deployment and environment setup, see Hosting.

Quick example

{"example.com:80"
 "https://example.com/links/{{ paths[0] | index-if-empty }}.txt"}

A request for http://example.com:80/hello is resolved as follows:

ValueResult
Matched keyexample.com:80
paths["hello"]
Rendered locationhttps://example.com/links/hello.txt

liblin fetches that location. If the file begins with:

https://example.com/welcome

the response is 302 Found with Location: https://example.com/welcome. Only the first line of the file is used.

Configuration file

The RDL file is read from the path in the LIBLIN_DOMAINS environment variable:

export LIBLIN_DOMAINS=/etc/redirect/domain-slurps.edn

domain-slurps.example.edn in the liblin source repository is a starting-point template. An RDL file must:

  • contain exactly one EDN map;
  • use string keys of the form host:port;
  • use string values containing the URL template;
  • have unique keys.

In EDN, ; starts a comment and commas are optional whitespace. LIBLIN_DOMAINS is required; if it is unset, liblin cannot read any RDL entries.

How a request is matched

For an incoming URL, liblin derives the lookup key as follows:

  1. Remove the scheme (http:// or https://).
  2. Remove the path, query string, and fragment.
  3. Remove a leading lowercase www..
  4. Keep the port.

The resulting key is looked up with an exact, case-sensitive string match.

Incoming URLLookup key
http://example.com:80/aliceexample.com:80
http://www.example.com:80/aliceexample.com:80
https://sub.example.com:443/alicesub.example.com:443
http://example.com:3000/aliceexample.com:3000

Rules to keep in mind:

  • The port is always part of the key. The request handler builds the URL with the server port, so a default HTTP request looks like http://example.com:80/...; use example.com:80, not example.com. HTTPS requests similarly use :443.
  • Do not put www. in keys. It is stripped before lookup, so a key such as www.example.com:80 can never match.
  • Matching is exact. There are no wildcards and no fallback entry. Each subdomain needs its own key.
  • The scheme is not part of the key. HTTP and HTTPS are distinguished by their ports, not by an http/https prefix.
  • Use the port the application sees. Behind a reverse proxy this may be an internal port unless forwarded headers are configured accordingly.

Path segments

The request path is split on /. The leading empty segment and a trailing slash are discarded, and the result is stored in paths:

Request pathpaths
/alice["alice"]
/alice/software/libre["alice" "software" "libre"]
/alice/["alice"]
/[]

Query strings and fragments are removed before splitting: /alice?ref=mail#top gives paths equal to ["alice"]. Repeated slashes produce empty segments in paths; the join filter skips them.

paths is a vector. Segments are zero-based and can be read with bracket indexing or collection lookups:

ExpressionResult for ["alice" "software" "libre"]
{{ paths[0] }}alice
{{ paths[1] }}software
{{ paths.first }}alice
{{ paths.last }}libre
{{ paths.size }}3

A segment that does not exist renders as an empty string. Combine it with index-if-empty when a fallback is needed.

In liblin, / is served by the landing page and /about is a fixed route, so neither reaches RDL. The empty paths case is still useful when calling the redirect functions directly or in tests.

Template variables

VariableDescriptionExample
domainThe matched key: host and portexample.com:80
pathsThe vector of path segments["alice" "software" "libre"]

Filters

Templates use Liquid syntax: {{ ... }} outputs a value and | applies a filter. liblin provides four filters:

FilterBehavior
index-if-emptyReturns "index" when the input is nil, "", or an empty collection; otherwise returns the input unchanged.
firstReturns the first element of a collection.
restReturns all elements after the first. Pipe it into join before outputting it.
joinJoins collection elements with /, skipping empty elements.

With paths equal to ["alice" "software" "libre"]:

  • {{ paths | first }} produces alice
  • {{ paths | rest | join }} produces software/libre
  • {{ paths | join }} produces alice/software/libre
  • {{ paths[3] | index-if-empty }} produces index

index-if-empty is most often used to fall back to index.txt when a segment is missing:

"https://example.com/{{ paths[0] }}/repo/raw/branch/main/{{ paths[1] | index-if-empty }}.txt"

A request for /alice renders .../main/index.txt, while /alice/0 renders .../main/0.txt.

Notes:

  • join is liblin’s own filter, not Liquid’s join. It always uses / and takes no separator argument; {{ paths | join: "," }} is an error.
  • first, rest, and join operate on collections. rest returns a sequence, so do not output it directly: {{ paths | rest }} renders a Clojure-style list.
  • Other Liquid filters from the underlying engine (for example append, upcase, default, and size) remain available. liblin’s filters take precedence when names collide.

Rendering and redirecting

Rendering a template produces the location of a text file, not the final redirect target. The full flow is:

  1. Derive the lookup key from the request.
  2. Find the matching template.
  3. Render it with domain and paths.
  4. Read the rendered location with slurp. It can be an HTTP(S) URL or a local file path.
  5. Take the first line, trim it, and use it as the redirect target.
  6. Send 302 Found with that target in the Location header.

Only the first line is used; later lines are ignored, so a target file can contain notes below the URL.

For example, if the rendered location is https://example.com/alice/repo/raw/branch/main/software/libre.txt and that file contains:

https://example.com/docs/libre

the client receives:

HTTP/1.1 302 Found
Location: https://example.com/docs/libre

Both the RDL file and the rendered target are read on every request; nothing is cached.

Failure behavior:

  • No entry for the key: 404.
  • The rendered location cannot be read (missing file, network error, invalid URL): 404.
  • The target file is empty: the redirect target is empty; keep at least one non-empty line in every target file.
  • Malformed EDN, duplicate keys, or an invalid template: an exception (normally HTTP 500), not a 404.

Complete example

{
 ;; Username first; the remaining segments form the document path.
 "example.com:80"
 "https://example.com/{{ paths[0] }}/repo/raw/branch/main/{{ paths | rest | join | index-if-empty }}.txt"

 ;; Every path segment is part of the document path.
 "sub.example.com:80"
 "https://example.com/repo/raw/branch/main/{{ paths | join | index-if-empty }}.txt"

 ;; One flat document per first segment.
 "api.example.com:80"
 "https://example.com/raw/branch/main/{{ paths[0] | index-if-empty }}.txt"

 ;; Only the first two segments are used; extra segments are ignored.
 "example.com:3000"
 "https://example.com/{{ paths[0] }}/repo/raw/branch/main/{{ paths[1] | index-if-empty }}.txt"

 ;; Same request shape, different branch layout.
 "alt.example.com:3000"
 "https://example.com/{{ paths[0] }}/repo/refs/heads/main/{{ paths[1] | index-if-empty }}.txt"

 ;; One flat document per first segment, on a subdomain.
 "alt.sub.example.com:3000"
 "https://example.com/repo/raw/branch/main/{{ paths[0] | index-if-empty }}.txt"

 ;; `domain` renders as host:port, so it can be reused in the target.
 "meta.example.com:80"
 "http://{{ domain }}/repo/raw/branch/main/{{ paths | join | index-if-empty }}.txt"
}

The entries above produce these results:

RequestpathsRendered location
http://example.com:80/alice["alice"]https://example.com/alice/repo/raw/branch/main/index.txt
http://example.com:80/alice/software/libre["alice" "software" "libre"]https://example.com/alice/repo/raw/branch/main/software/libre.txt
http://sub.example.com:80/software/libre["software" "libre"]https://example.com/repo/raw/branch/main/software/libre.txt
http://api.example.com:80/fs/["fs"]https://example.com/raw/branch/main/fs.txt
http://example.com:3000/alice["alice"]https://example.com/alice/repo/raw/branch/main/index.txt
http://example.com:3000/alice/0["alice" "0"]https://example.com/alice/repo/raw/branch/main/0.txt
http://alt.example.com:3000/alice/0["alice" "0"]https://example.com/alice/repo/refs/heads/main/0.txt
http://alt.sub.example.com:3000/fs["fs"]https://example.com/repo/raw/branch/main/fs.txt
http://meta.example.com:80/a/b["a" "b"]http://meta.example.com:80/repo/raw/branch/main/a/b.txt
http://www.example.com:80/alice["alice"]https://example.com/alice/repo/raw/branch/main/index.txt
http://unknown.example.com:80/alice["alice"]No entry: 404

Troubleshooting

  • A valid-looking host returns 404. Check the port first: :80, :443, and :3000 are different keys. Also check that the key does not start with www. and that the exact subdomain has an entry.
  • The wrong file is selected. paths[0] is the first segment after the leading slash; the slash itself is not part of paths. Trailing slashes and query strings do not create segments.
  • Subfolders are dropped. A template that only uses paths[0] or paths[1] ignores later segments. Use {{ paths | rest | join }} or {{ paths | join }} to include them.
  • A missing segment becomes index when it should be empty. That is what index-if-empty does; remove the filter if an empty value is wanted.
  • Changes do not appear. Confirm LIBLIN_DOMAINS points at the file the server is reading. There is no cache, so the next request should use the new value.
  • The server returns 500. Check for invalid EDN, duplicate keys, a non-string template, or an invalid Liquid expression such as {{ paths | join: "," }}.
  • Only the first line of the target file matters. A blank first line produces an empty redirect target.

RDL is trusted configuration: templates can read local files as well as remote URLs, so only trusted operators should be able to edit the RDL file.

See also