Text Browsers: Lynx, Links 2, ELinks and ELinks with JavaScript (rebuild guide) 05OCT2026

Posted by on 5th Oct 2026

A small Debian container for trying terminal web browsers side by side over SSH: Lynx, Links 2, the stock ELinks, and elinks-js, a self-built ELinks 0.20 with the QuickJS-ng JavaScript engine. It was built to compare how each one reads with a screen reader. This post is the complete rebuild guide; every code block has a Copy button and long blocks scroll inside their box.

How it works

  1. You SSH into the container (SecureCRT, PuTTY, or any terminal) as a normal user.
  2. You start one of four browsers with a web address:
    • lynx: the most linear output, numbered links, no JavaScript.
    • links2: better table/frame layout, no JavaScript (its graphics mode needs a real display, so it is text-only over SSH).
    • elinks: Debian's ELinks 0.13.2, tabs and menus, no JavaScript.
    • elinks-js: ELinks 0.20.0 built from source with QuickJS-ng, so modern JavaScript (arrow functions, const, spread) runs on the page.
  3. elinks-js lives entirely in /opt/elinks-js, so it never touches the Debian packages. A tiny wrapper in /usr/local/bin/elinks-js starts it with JavaScript switched on.

What to expect from JavaScript: simple scripts work, but ELinks only implements part of the browser DOM, so heavy sites (Gmail, Facebook, React apps) still break. Google search blocks every text browser ("Your browser isn't supported anymore"), with or without JavaScript. DuckDuckGo Lite works very well in all four.

Containers involved

Container Role LAN IP
CT 124 (TextBrowser) Debian 12, the four browsers, SSH login x.x.0.237 (DHCP)

Sizing: 2 cores, 2 GB RAM, 2 GB swap, 8 GB disk, unprivileged, nesting=1. The build uses about 1 GB of disk; 1 core and 512 MB is enough just to browse. No Caddy route, no DNS name, no Uptime Kuma monitor: it is only reached over SSH on the LAN.

Where to run each command

  • Proxmox host shell: in the Proxmox web UI click pve → Shell (or SSH to the host as root).
  • Inside CT 124 (TextBrowser): Proxmox web UI → 124 (TextBrowser) → Console, or pct enter 124 from the host shell.
  • Your PC: your SSH client (SecureCRT etc.).

Rebuild, step by step

1. Get the Debian 12 template

Proxmox host shell:

pveam update
pveam download local debian-12-standard_12.12-1_amd64.tar.zst

(If that exact version is gone, pveam available | grep debian-12 shows the current name; use it in the next step too.)

2. Create and start CT 124 (TextBrowser)

Proxmox host shell. Bulk is the storage name on this host; use yours (local-lvm works too).

pct create 124 local:vztmpl/debian-12-standard_12.12-1_amd64.tar.zst --hostname TextBrowser --cores 2 --memory 2048 --swap 2048 --rootfs Bulk:8 --net0 name=eth0,bridge=vmbr0,firewall=1,ip=dhcp --unprivileged 1 --features nesting=1 --start 1

3. Make a working folder on the host

Proxmox host shell:

mkdir -p /root/textbrowser && cd /root/textbrowser

4. Create both files

Paste each block into the Proxmox host shell. Each one writes one file into /root/textbrowser/.

install_browsers.sh: installs Lynx, Links 2 and ELinks from Debian, plus a UTF-8 locale.

cat > /root/textbrowser/install_browsers.sh <<'EOF'
#!/bin/bash
# Installs the three stock text browsers from Debian, plus a UTF-8 locale.
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update -q
apt-get install -y -q lynx links2 elinks locales
sed -i 's/^# *en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen
locale-gen
update-locale LANG=en_US.UTF-8
echo "--- versions ---"
lynx -version | head -1
links2 -version | head -1
elinks --version | head -1
EOF

build_elinks_js.sh: builds the NetSurf libraries (libdom, libcss and friends), QuickJS-ng and ELinks 0.20.0 into /opt/elinks-js, then installs the elinks-js command.

cat > /root/textbrowser/build_elinks_js.sh <<'EOF'
#!/bin/bash
# Builds ELinks with the QuickJS-ng JavaScript engine into /opt/elinks-js
# and installs an "elinks-js" command that starts it with JavaScript on.
# The stock Debian elinks is left untouched.
set -e
export DEBIAN_FRONTEND=noninteractive
PREFIX=/opt/elinks-js
ELINKS_TAG=v0.20.0
QJS_TAG=v0.17.0
NS=https://download.netsurf-browser.org/libs/releases

echo "=== build dependencies ==="
apt-get update -q
apt-get install -y -q build-essential git meson ninja-build pkg-config cmake wget xxd \
  gperf flex bison perl gettext libssl-dev zlib1g-dev libbz2-dev liblzma-dev \
  libzstd-dev libbrotli-dev libexpat1-dev libidn2-dev libgpm-dev \
  libcurl4-openssl-dev libsqlite3-dev libtre-dev libevent-dev libev-dev \
  libncurses-dev >/dev/null

export PKG_CONFIG_PATH="$PREFIX/lib/pkgconfig"
mkdir -p "$PREFIX/lib/pkgconfig"
echo "$PREFIX/lib" > /etc/ld.so.conf.d/elinks-js.conf

# NetSurf libraries: ELinks needs libdom + libcss for JavaScript and CSS.
echo "=== NetSurf libraries ==="
mkdir -p /usr/local/src/netsurf
cd /usr/local/src/netsurf
[ -f buildsystem-1.10.tar.gz ] || wget -q "$NS/buildsystem-1.10.tar.gz"
rm -rf buildsystem-1.10
tar -xf buildsystem-1.10.tar.gz
make -C buildsystem-1.10 install Q= PREFIX="$PREFIX" >/dev/null
for p in libparserutils-0.2.5 libwapcaplet-0.4.3 libhubbub-0.3.8 libdom-0.4.2 libcss-0.9.2; do
  echo "--- $p"
  [ -f "$p-src.tar.gz" ] || wget -q "$NS/$p-src.tar.gz"
  rm -rf "$p"
  tar -xf "$p-src.tar.gz"
  CFLAGS="-O2 -Wno-error" make -C "$p" install -j2 Q= PREFIX="$PREFIX" LIBDIR=lib \
    COMPONENT_TYPE=lib-shared NSSHARED="$PREFIX/share/netsurf-buildsystem" >/dev/null
done
ldconfig

# QuickJS-ng: modern JavaScript engine (mujs from Debian is ES5 only).
echo "=== QuickJS-ng $QJS_TAG ==="
cd /usr/local/src
[ -d quickjs-ng ] || git clone -q https://github.com/quickjs-ng/quickjs.git quickjs-ng
cd quickjs-ng
git fetch -q --tags
git checkout -q "$QJS_TAG"
rm -rf build
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON \
  -DCMAKE_INSTALL_PREFIX="$PREFIX" -DCMAKE_INSTALL_LIBDIR=lib >/dev/null
cmake --build build -j2 >/dev/null
cmake --install build >/dev/null
ldconfig

# quickjs-ng installs no pkg-config file, and ELinks' meson looks for one.
cat > "$PREFIX/lib/pkgconfig/qjs.pc" <<PC
prefix=$PREFIX
libdir=\${prefix}/lib
includedir=\${prefix}/include

Name: qjs
Description: QuickJS-ng JavaScript engine
Version: ${QJS_TAG#v}
Libs: -L\${libdir} -lqjs -lm -lpthread
Cflags: -I\${includedir}
PC

echo "=== ELinks $ELINKS_TAG ==="
cd /usr/local/src
[ -d elinks ] || git clone -q https://github.com/rkd77/elinks.git
cd elinks
git fetch -q --tags
git checkout -q "$ELINKS_TAG"
rm -rf build-qjs
meson setup build-qjs --prefix="$PREFIX" -Dquickjs=true -Dmujs=false \
  -Dlibsixel=false -Ddoc=false -Dnls=false > /root/elinks_meson.log 2>&1 \
  || { tail -30 /root/elinks_meson.log; exit 1; }
ninja -C build-qjs > /root/elinks_ninja.log 2>&1 \
  || { grep -m20 -i error /root/elinks_ninja.log; exit 1; }
ninja -C build-qjs install >/dev/null
ldconfig

cat > /usr/local/bin/elinks-js <<'WRAP'
#!/bin/sh
# ELinks built with QuickJS-ng; JavaScript on by default
exec /opt/elinks-js/bin/elinks -eval 'set ecmascript.enable = 1' "$@"
WRAP
chmod 755 /usr/local/bin/elinks-js

echo "=== done ==="
grep -E "quickjs|libdom|libcss" /root/elinks_meson.log | grep -i -E "found: YES|: true" || true
/usr/local/bin/elinks-js --version | head -1
EOF

5. Push the scripts into CT 124

Proxmox host shell:

pct push 124 /root/textbrowser/install_browsers.sh /root/install_browsers.sh
pct push 124 /root/textbrowser/build_elinks_js.sh /root/build_elinks_js.sh

6. Install the stock browsers (about 1 minute)

Proxmox host shell. It ends by printing three version lines.

pct exec 124 -- bash /root/install_browsers.sh

7. Build elinks-js (about 5 to 10 minutes on 2 cores)

Proxmox host shell. It prints a line per stage; at the end you should see libcss found: YES, libdom found: YES, quickjs : true and ELinks 0.20.0.

pct exec 124 -- bash /root/build_elinks_js.sh

8. Add a login user

Inside CT 124 (TextBrowser) console. It asks for a password twice; the other questions can be left empty with Enter. Replace yourname with the user you want.

adduser yourname

Debian's SSH server allows password logins for normal users but not for root, so log in as this user.

9. Find the IP and connect

Proxmox host shell:

pct exec 124 -- hostname -I

On your PC, point your SSH client at that address, port 22, with the user from step 8. Optionally give the container a DHCP reservation in your router so the address never changes.

Using the browsers

Search with DuckDuckGo Lite (from the SSH session):

elinks-js https://lite.duckduckgo.com/lite/

Search straight from the command line (change the words after q=, use + for spaces):

lynx "https://lite.duckduckgo.com/lite/?q=proxmox+backup"

Quit keys: Lynx q then y; Links 2 q then y; ELinks / elinks-js q then Enter on Yes. In ELinks, Escape opens the menu bar; Setup → Options manager is where to turn off the tab bar and status bar for less screen-reader clutter (Save before leaving).

Troubleshooting: where the logs are

elinks-js build (meson configure step)

  • Where: /root/elinks_meson.log inside CT 124 (TextBrowser)
  • Open: CT 124 console
grep -i -E "error|not found|found: NO" /root/elinks_meson.log
  • Look for: ERROR: Dependency "xyz" not found means a missing -dev package: install it with apt and rerun step 7. Many found: NO lines (gnutls, lua, cmake for quickjs-ng, execinfo) are normal; only a line starting with ERROR stops the build.

elinks-js build (compile step)

  • Where: /root/elinks_ninja.log inside CT 124 (TextBrowser)
  • Open: CT 124 console
grep -i -m20 "error" /root/elinks_ninja.log
  • Look for: a C compile error names the file and line. Warnings are harmless. A healthy log ends with a Linking target src/elinks line.

elinks-js won't start

  • Where: the shared-library loader
  • Open: CT 124 console
ldd /opt/elinks-js/bin/elinks | grep "not found"
  • Look for: no output is healthy. libparserutils.so.0 => not found (or any other /opt/elinks-js library) means /etc/ld.so.conf.d/elinks-js.conf is missing: recreate it with /opt/elinks-js/lib in it and run ldconfig.

JavaScript errors on a page

  • Where: the ELinks status line (bottom row) while the page is open
  • Open: your SSH session
  • Look for: a J in the status indicators means a script failed; SyntaxError: unexpected token means the engine could not parse the script (this is what mujs printed before the switch to QuickJS-ng). Pages that use DOM features ELinks has not implemented simply show their non-JavaScript content.

SSH login refused

  • Where: sshd journal inside CT 124 (TextBrowser)
  • Open: CT 124 console
journalctl -u ssh -n 30 --no-pager
  • Look for: Accepted password for yourname is a good login. Failed password is a wrong password. ROOT LOGIN REFUSED / Failed password for root means you tried root, which is blocked for passwords by default; use the user from step 8.

Quick health check

Proxmox host shell; should print four version lines:

pct exec 124 -- sh -c "lynx -version | head -1; links2 -version | head -1; elinks --version | head -1; /usr/local/bin/elinks-js --version | head -1"

Notes

  • mujs is the wrong engine. Debian ships libmujs-dev and ELinks builds with it fine, but mujs only speaks ES5: modern pages fail with SyntaxError. QuickJS-ng fixed it.
  • elinks -dump never runs JavaScript, so it can't be used to test the build. Test interactively, or in a detached tmux session with tmux capture-pane -p.
  • quickjs-ng installs no pkg-config file; the build script writes /opt/elinks-js/lib/pkgconfig/qjs.pc by hand, otherwise meson stops with Dependency "qjs" not found.
  • Meson strips the rpath at install, so the binary can't find the NetSurf libraries unless /opt/elinks-js/lib is registered with ldconfig (done by the script).
  • NetSurf's buildsystem tarball is buildsystem-1.10.tar.gz (no -src, unlike the libraries), and its git address on git.netsurf-browser.org over https does not exist.
  • ELinks' meson option for images is libsixel, not sixel. It's off here: SecureCRT's sixel support is uncertain and images don't help a screen reader.
  • ELinks 0.20 also needs xxd, libtre-dev, libevent-dev/libev-dev and libncurses-dev, all in the script's apt line.
  • To update later: change ELINKS_TAG / QJS_TAG at the top of build_elinks_js.sh, push it again (step 5) and rerun step 7.
  • To remove elinks-js completely: rm -rf /opt/elinks-js /usr/local/src/elinks /usr/local/src/quickjs-ng /usr/local/src/netsurf /usr/local/bin/elinks-js /etc/ld.so.conf.d/elinks-js.conf and ldconfig.