browser Documentation

Lightpanda: the headless browser designed for AI and automation

RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy browser on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name browser -p 9222:9222 --restart always lightpanda-io/browser:latest

1. Project Overview & Quickstart (lightpanda-io/browser)

1. Project Overview & Quickstart (lightpanda-io/browser)

Technical Documentation: lightpanda-io/browser

ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference

Overview (`README.md`)

Lightpanda Browser

Benchmarks

Requesting 933 real web pages over the network on a AWS EC2 m5.large instance.
See benchmark details.

Metric Lightpanda Headless Chrome Difference
Memory (peak, 100 pages) 123MB 2GB ~16x less
Execution time (100 pages) 5s 46s ~9x faster

Quick start

Install

Package Managers

Latest nightly from Homebrew:

console
brew install lightpanda-io/browser/lightpanda

Latest nightly from Arch Linux User Repository:

console
yay -S lightpanda-nightly-bin

Download from the nightly builds

You can download the last binary from the nightly
builds
for
Linux and MacOS for both x86_64 and aarch64.

For Linux

console
curl -L -o lightpanda https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-x86_64-linux && \
chmod a+x ./lightpanda

Verify the binary before running anything:

console
./lightpanda version

Linux aarch64 is also available

Note: The Linux release binaries are linked against glibc. On musl-based Linux distributions (Alpine, etc.) the binary fails with cannot execute: required file not found because the glibc dynamic linker is missing. Use a glibc-based base image (e.g., FROM debian:bookworm-slim or FROM ubuntu:24.04) or build from sources.

Android / Termux: there is no native Android build. The Linux aarch64 binary needs the glibc loader (/lib/ld-linux-aarch64.so.1), which Android's Bionic libc does not provide, so it fails with the same cannot execute: required file not found error.

For MacOS

console
curl -L -o lightpanda https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-aarch64-macos && \
chmod a+x ./lightpanda

MacOS x86_64 is also available

For Windows + WSL2

Lightpanda has no native Windows binary. Install it inside WSL following the Linux steps above.

WSL not installed? Run wsl --install from an administrator shell, restart, then open wsl.
See Microsoft's WSL install guide for details.

Your automation client (Puppeteer, Playwright, etc.) can run either inside WSL or on the Windows host. WSL forwards localhost:9222 automatically.

Install from Docker

Lightpanda provides official Docker
images
for both Linux amd64 and
arm64 architectures.
The following command fetches the Docker image and starts a new container exposing Lightpanda's CDP server on port 9222.

console
docker run -d --name lightpanda -p 127.0.0.1:9222:9222 lightpanda/browser:latest

Dump a URL

console
./lightpanda fetch --obey-robots --dump html --log-format pretty  --log-level info https://demo-browser.lightpanda.io/campfire-commerce/

You can use --dump markdown to convert directly into markdown, or
--dump png > page.png or --dump pdf > page.pdf for a text-only rendering
of the page.
--wait-until, --wait-ms, --wait-selector and --wait-script are
available to adjust waiting time before dump.

Start a CDP server

console
./lightpanda serve --obey-robots --log-format pretty  --log-level info --host 127.0.0.1 --port 9222

Once the CDP server started, you can run a Puppeteer script by configuring the
browserWSEndpoint.

Example Puppeteer script

js
import puppeteer from 'puppeteer-core';

// use browserWSEndpoint to pass the Lightpanda's CDP server address.
const browser = await puppeteer.connect({
  browserWSEndpoint: "ws://127.0.0.1:9222",
});

// The rest of your script remains the same.
const context = await browser.createBrowserContext();
const frame = await context.newPage();

// Dump all the links from the frame.
await frame.goto('https://demo-browser.lightpanda.io/amiibo/', {waitUntil: "networkidle0"});

const links = await frame.evaluate(() => {
  return Array.from(document.querySelectorAll('a')).map(row => {
    return row.getAttribute('href');
  });
});

console.log(links);

await frame.close();
await context.close();
await browser.disconnect();

Start a webdriver Bidi server

Use --protocol webdriver to enable Bidi support.
You can start both, CDP and Bidi, with --protocol webdriver --protocol cdp

console
./lightpanda serve --obey-robots --log-format pretty  --log-level info --host 127.0.0.1 --port 9222 --protocol webdriver

Agent mode

lightpanda agent lets you drive the browser with a native agent. Describe what
you want in plain English or with slash commands, and it controls the browser:
navigating pages, clicking through flows, filling forms, extracting structured
data. Think of it as a robot you're directing to use the web, more than a
chatbot you're having a conversation with.

Because the agent runs inside the same process as the browser, every tool call
is a direct operation and you retain Lightpanda's speed and memory advantage.

The output of an agent session is a
PandaScript: vanilla JavaScript
with a small set of native browser primitives built directly into Lightpanda.
Run /save to export one from your current session, then replay it with
lightpanda run <script>.js. Scripts are deterministic and token-free, so
you can prototype with the LLM and ship the output to production without a
model at runtime.

It supports Anthropic, OpenAI, Gemini, Google Vertex AI, Mistral, Hugging
Face, the multi-lab gateways Vercel AI Gateway
and OpenRouter (one key for hundreds of models), any OpenAI-compatible endpoint via
OPENAI_BASE_URL, and local models via Ollama or llama.cpp. You can also run
without an LLM using --no-llm, which drops you into the REPL. See the
agent documentation for the full
reference.

console
./lightpanda agent                                    # auto-detects API key from env
./lightpanda agent --task "top story on news.ycombinator.com?"
./lightpanda agent --no-llm                           # basic REPL, no LLM
./lightpanda run session.js                           # run a recorded script
cat session.js | ./lightpanda run -                   # ...or pipe one in via stdin
./lightpanda agent --provider gemini --task "..."     # force a specific provider
./lightpanda agent --list-models                      # models available for the detected provider
VERTEX_API_KEY=... ./lightpanda agent --provider vertex             # Vertex AI, express mode
GOOGLE_CLOUD_PROJECT=my-proj ./lightpanda agent --provider vertex   # Vertex AI, token via gcloud auth
AI_GATEWAY_API_KEY=... ./lightpanda agent --provider vercel --model moonshotai/kimi-k2   # any model behind Vercel AI Gateway
OPENROUTER_API_KEY=... ./lightpanda agent --provider openrouter --model anthropic/claude-sonnet-5   # any model behind OpenRouter
OPENAI_BASE_URL=https://my-gateway/v1 OPENAI_API_KEY=... ./lightpanda agent            # any OpenAI-compatible server

Native MCP and skill

The MCP server communicates via MCP JSON-RPC 2.0 over stdio.

Add to your MCP configuration:

json
{
  "mcpServers": {
    "lightpanda": {
      "command": "/path/to/lightpanda",
      "args": ["mcp"]
    }
  }
}

HTTP transport and independent sessions

For serving several agents from one process, start the MCP server over HTTP
instead of stdio by giving it a port (add --host x.x.x.x to specify the
interface to listen on):

bash
lightpanda mcp --port 9223

Clients POST JSON-RPC to http://host:9223/mcp. Each connection is routed to
its own browsing session — its own page, cookies and memory — so agents no
longer clobber each other's page:

  • A client that initializes without an Mcp-Session-Id header is assigned a
    fresh session; the id comes back in the response's Mcp-Session-Id header.
    Send it on subsequent requests to stay on that session (isolation).
  • Two agents that send the same Mcp-Session-Id share one browsing context
    (sharing — e.g. a workflow where several agents work the same page).
  • The session_new, session_list and session_close tools manage sessions
    explicitly. Sending DELETE /mcp with an Mcp-Session-Id closes that session.

Read full documentation

A skill is available in lightpanda-io/agent-skill.

Shell completion

lightpanda completion prints a completion script for commands, flags and their values:

console
lightpanda completion fish > ~/.config/fish/completions/lightpanda.fish
lightpanda completion bash > ~/.local/share/bash-completion/completions/lightpanda
lightpanda completion zsh > ~/.zfunc/_lightpanda  # with fpath=(~/.zfunc $fpath) before compinit

Telemetry

By default, Lightpanda collects and sends usage telemetry. This can be disabled by setting an environment variable LIGHTPANDA_DISABLE_TELEMETRY=true. You can read Lightpanda's privacy policy at: https://lightpanda.io/privacy-policy.

Core dumps

Set LIGHTPANDA_DISABLE_CORE_DUMP (to any value) to suppress crash core dumps by zeroing the soft RLIMIT_CORE at startup.

Status

Here are the key features we have implemented.
For full details, see our Web Platform Tests results.

  • CORS
  • HTTP loader (Libcurl)
  • HTML parser (html5ever)
  • DOM tree
  • Javascript support (v8)
  • DOM APIs
  • Ajax
    • XHR API
    • Fetch API
  • DOM and Markdown dump
  • CDP/websockets server
  • Click
  • Input form
  • Cookies
  • Custom HTTP headers
  • Proxy support
  • Network interception
  • Respect robots.txt with option --obey-robots
  • CDP and Webdriver Bidi
  • Adblocker

Build from sources

Prerequisites

Lightpanda is written with Zig 0.15.2. You have to
install it with the right version in order to build the project.

Lightpanda also depends on
v8,
Libcurl and html5ever.

To be able to build the v8 engine, you have to install some libs:

For Debian/Ubuntu based Linux:

text
sudo apt install xz-utils ca-certificates \
    pkg-config libglib2.0-dev \
    clang make curl git

You also need to install Rust.

For systems with Nix, you can use the devShell:

text
nix develop

For MacOS, you need cmake and Rust.

text
brew install cmake

Build and run

You can build the entire browser with make build or make build-dev for debug
env.

But you can directly use the zig command: zig build run.

Embed v8 snapshot

Lighpanda uses v8 snapshot. By default, it is created on startup but you can
embed it by using the following commands:

Generate the snapshot.

text
zig build snapshot_creator -- src/snapshot.bin

Build using the snapshot binary.

text
zig build -Dsnapshot_path=../../snapshot.bin

See #1279 for more details.

Test

Unit Tests

You can test Lightpanda by running make test.

bash
make test                                       # Run all tests
make test F="server"                            # Filter by substring
TEST_FILTER="WebApi: #selector_all" make test   # Filter main + subtest (separator: #)
TEST_VERBOSE=true make test
TEST_FAIL_FIRST=true make test
METRICS=true make test                          # Capture allocation/duration metrics as JSON
TEST_JOBS=1 make test                           # Run in one process (default: up to 4, or 1 when filtered)

End to end tests

To run end to end tests, you need to clone the demo
repository
into ../demo dir.

You have to install the demo's node
requirements

You also need to install Go > v1.24.

text
make end2end

Web Platform Tests

Lightpanda is tested against the standardized Web Platform
Tests
.

We use a fork including a custom
testharnessreport.js. Results are published daily.

For reference, you can execute a WPT test case with your browser via
wpt.live.

Configure WPT HTTP server

To run the test, you must clone the repository, configure the custom hosts and generate the
MANIFEST.json file.

Clone the repository with the fork branch.

text
git clone -b fork --depth=1 [email protected]:lightpanda-io/wpt.git

Enter into the wpt/ dir.

Install custom domains in your /etc/hosts

text
./wpt make-hosts-file | sudo tee -a /etc/hosts

Generate MANIFEST.json

text
./wpt manifest

Use the WPT's setup
guide
for
details.

Run WPT test suite

An external Go runner is provided by
github.com/lightpanda-io/demo/
repository, located into wptrunner/ dir.
You need to clone the project first.

First start the WPT's HTTP server from your wpt/ clone dir.

text
./wpt serve

Run a Lightpanda browser

text
zig build -Dwpt_extensions run -- serve \
    --ws-max-concurrent 64 \
    --insecure-disable-tls-host-verification \
    --load-resources iframe \
    --load-resources image \
    --load-resources worker \
    --load-resources stylesheet

Then you can start the wptrunner from the demo's clone dir:

text
cd wptrunner && go run .

Or one specific test:

text
cd wptrunner && go run . Node-childNodes.html

wptrunner command accepts --summary and --json options modifying output.
Also --concurrency define the concurrency limit.

To check a single test case without the runner, fetch --dump wpt prints its
results as JSON: the harness status, a pass/fail summary and the cases that
did not pass. It relies on the fork's testharnessreport.js.

text
zig build -Dwpt_extensions run -- fetch \
    --insecure-disable-tls-host-verification \
    --load-resources worker \
    --load-resources iframe \
    --dump wpt \
    --wait-script "window.report && report.complete" \
    "http://web-platform.test:8000/dom/nodes/Node-childNodes.html"

:warning: Running the whole test suite will take a long time. In this case,
it's useful to build in releaseFast mode to make tests faster.

text
zig build -Dwpt_extensions -Doptimize=fast run -- serve ...

Contributing

See CONTRIBUTING.md for guidelines.
You must sign our CLA during the pull request process.

Why Lightpanda?

Javascript execution is mandatory for the modern web

Simple HTTP requests used to be enough for web automation. That's no longer the case. Javascript now drives most of the web:

  • Ajax, Single Page Apps, infinite loading, instant search
  • JS frameworks: React, Vue, Angular, and others

Chrome is not the right tool

Running a full desktop browser on a server works, but it does not scale well. Chrome at hundreds or thousands of instances is expensive:

  • Heavy on RAM and CPU
  • Hard to package, deploy, and maintain at scale
  • Many features are not necessary in headless made

Lightpanda is built for performance

Supporting Javascript with real performance meant building from scratch rather than forking Chromium:

  • Not based on Chromium, Blink, or WebKit
  • Written in Zig, a low-level language with explicit memory control
  • No graphical rendering engine

Overview (`orderfile/README.md`)

Hot-code orderfile

lightpanda.ld is an LLD linker script that gathers the functions and
read-only data touched by a typical session into two contiguous output
sections, .text.hot and .rodata.hot, placed in front of the cold .text
and .rodata. It only affects layout; no code changes.

Why

The kernel maps file-backed pages in 64KB fault-around windows
(/sys/kernel/debug/fault_around_bytes). With the default link order the hot
code is scattered across the whole 40MB of text, so every window that holds a
single executed byte becomes resident: ~17MB of .text and ~4MB of .rodata
for a hot set that is really 7.5MB + 2MB. Packing the hot set contiguously
takes the CDP bench (demo/puppeteer/cdp.js, 100 runs) from VmHWM ~28.2MB to
~25.3MB with the stock V8 archive and ~21.2MB once V8 is addressable too (see
below), with no change in run duration.

How it is applied

  • Opt-in via -Dorderfile=orderfile/lightpanda.ld; CI passes it for the Linux
    release artifact and the e2e bench build. Local release builds don't, so
    they keep the sub-second link. Linux/LLD only (it is an ELF linker script).
  • The exe and every C library it links are built with
    -ffunction-sections -fdata-sections so each function/datum has its own
    input section for the script to address.
  • Patterns that match nothing are ignored by LLD, so a stale script degrades
    gracefully: renamed or removed functions simply fall back into cold .text.
    Zig's __anon_NNN names (~7% of the patterns) renumber on unrelated
    changes, so the profile decays a little with every commit; it is
    regenerated nightly by CI (below).
  • Link-time cost: LLD name-checks every input section against every pattern
    of every input-section description, so the script scopes patterns to their
    object file (*api.o(...)). The shipped script covers Zig/Rust/C only
    (~4k patterns, ~2s of link). Once V8's functions are addressable (below) the
    script grows to ~26k patterns and the release link to ~26s; one description
    per pattern instead of per file would make that 85s (the line breaks inside
    a description are free, they exist for the diff). Debug builds pay nothing.
  • The prebuilt V8 archive is compiled with -fno-unique-section-names
    (Chromium hardcodes it), so all of V8's function sections are named .text
    and the script cannot address them. mark_hot_sections.zig (run
    automatically by build.zig when an orderfile is set) rewrites the archive,
    renaming just the hot sections to .text.hot.<symbol> /
    .rodata.hot.<symbol> so the script gathers all of V8 with one
    .text.hot.* glob (which keeps the link at ~2s instead of ~26s). The
    browser's own hot Zig/Rust/C sections are matched by explicit per-file
    patterns. The rewritten archive is a build cache artifact (~132MB -> ~158MB;
    LLVM objects share .shstrtab with .strtab, so each touched member grows);
    the linked binary is byte-for-byte unaffected.
  • V8's embedded builtins blob (the Builtins_* symbols, one ~2MB .text
    section) is deliberately left cold. Pulling it into .text.hot shifts the
    layout so that V8's runtime code range (a separate mmap) no longer reaches
    the blob with a pc-relative call, and V8 then copies the whole 2MB blob into
    an executable anonymous mapping at startup (+~1.8MB RSS, deterministic).
    mark_hot_sections.zig and gen_order.py both skip Builtins_*.

Regenerating the profile

bash
# root for /sys/kernel/debug/fault_around_bytes; ../demo checked out with
# `npm install` done; node, go, python3 and binutils on the PATH.
orderfile/tools/regen.sh -Doptimize=fast -Dsnapshot_path=../../snapshot.bin -Dcpu=x86_64

The script

  1. builds with an empty linker script (the unordered binary; the
    --verbose-link line of that build is where gen_order.py gets the
    objects and archives it maps symbols to sections with);
  2. runs the CDP bench (demo/puppeteer/cdp.js, 100 runs) against it with
    fault-around set to 4KB, dumps the pages that are resident
    (pagemap.py), turns them into hot symbol lists in address order
    (hotlist.py) and into the new lightpanda.ld / v8.txt
    (gen_order.py);
  3. re-runs that build's ld.lld line with the new script to check that it
    links (the script is a cache input of the whole exe compilation, so
    building with it would be a second full compile; the relink takes
    seconds and is the same link, bar V8's hot-marked archive coming from
    the previous v8.txt, which does not affect whether the script parses).

Whether the new profile is any good is not measured here; the e2e bench's
MAX_VmHWM gate is, on the next push to main.

The profile is a list of resident pages, so every function sharing a 4KB page
with a hot one is counted as hot. Profiling the unordered binary keeps that
over-inclusion random: a profile of the ordered binary would re-include every
function that went cold but still sits in .text.hot next to a hot one, and
the hot set would only ever grow. Over-inclusion is the cheap side of the
trade anyway (a hot function left out costs a whole 64KB cold window; one
included needlessly costs its own size), and .text.hot is packed in
address order of the profile, so functions that run together stay together.

The binary is benched from tmpfs (RAMDIR, default /dev/shm): on ext4 with
a recent kernel, large page-cache folios are mapped whole and hide the
64KB-window behaviour that CI sees. The x86_64 profile is applied to the
aarch64 release too; it carries over as far as the section and symbol
names coincide, nothing is measured on aarch64.

To look at what is still resident outside the hot sections, run the bench by
hand and use pagemap.py + nm on the live process.


--- METRICS ---

2. Official Technical Reference & Guides (lightpanda-io/docs)

File: src/content/guides/configure-a-proxy.mdx


title: Use a proxy

import { Tabs } from 'nextra/components'

Use a proxy

Lightpanda supports using your own proxy, both locally and on Lightpanda Cloud. On Lightpanda Cloud, you can also use a shared proxy we provide instead.

<Tabs items={['Local', 'Cloud']}>
<Tabs.Tab>

Use a proxy locally

To use a proxy, start Lightpanda from the command line with --http-proxy followed by an HTTP or HTTPS proxy address. All outgoing requests go through the proxy, regardless of the protocol your client uses to connect to Lightpanda.

The examples below use serve, but fetch, mcp, and agent accept these options too.

sh copy
lightpanda serve --http-proxy http://127.0.0.1:3000

Authenticate with basic auth

Add basic auth credentials to the proxy address with the username:password@ format.

sh copy
lightpanda serve --http-proxy 'http://me:[email protected]:3000'

Authenticate with a bearer token

--proxy-bearer-token authenticates with bearer auth. It adds a Proxy-Authorization header to all outgoing requests.

sh copy
lightpanda serve --http-proxy 'http://127.0.0.1:3000' --proxy-bearer-token 'MY-TOKEN'

Authenticate from your script

You can also pass the proxy credentials from your Puppeteer or Playwright script instead of the command line.

Selenium can't do this, because Lightpanda does not support proxy authentication over WebDriver BiDi. Put the credentials in the --http-proxy URL instead.

<Tabs items={['Puppeteer', 'Playwright']}>
<Tabs.Tab>
With Puppeteer, you have to configure the proxy address when starting
Lightpanda.

sh copy
lightpanda serve --http-proxy 'http://127.0.0.1:3000'

Then you can call page.authenticate function to inject your authentication
from your script.

javascript copy
const page = await context.newPage();

// Set credentials for HTTP Basic Auth
await page.authenticate({
  username: 'my_username',
  password: 'my_password',
});

You can find the full example in our
demo repository.
</Tabs.Tab>
<Tabs.Tab>
With Playwright, configure the proxy when creating the browser's context.

javascript copy
const context = await browser.newContext({
    baseURL: baseURL,
    proxy: {
      server: 'http://127.0.0.1:3000',
      username: 'my_username',
      password: 'my_password',
    },
});

const page = await context.newPage();

You can find the full example in our
demo repository.
</Tabs.Tab>

</Tabs.Tab>
<Tabs.Tab>

Use a proxy on Lightpanda Cloud

Add the proxy query parameter to the connection URL to route the session through a proxy. You have two options: use a shared proxy, or add your own.

Use a shared proxy

Every account can use two shared proxies.

  • fast_dc: the default. A single shared datacenter IP, optimized for speed. Each region has its own, so the IP depends on the endpoint you connect to.
  • datacenter: a pool of shared datacenter IPs with automatic rotation. Accepts an optional country parameter (two-letter country code) to target a specific country.
text copy
wss://euwest.cloud.lightpanda.io/ws?proxy=datacenter&country=de&token=TOKEN

Omit proxy and the session uses fast_dc. Contact us for additional proxy options.

Use your own proxy

Add your proxy on the proxies page in the console, then pass its identifier as the proxy parameter. The console generates the identifier and shows it on the proxy's card. It is not the name you gave the proxy.

text copy
wss://euwest.cloud.lightpanda.io/ws?proxy=xK4mP2qR&token=TOKEN

The kind you set on the proxy, datacenter or residential, is a label. It does not change how the session connects.
</Tabs.Tab>


File: src/content/guides/interact-with-a-webpage.mdx


title: Interact with a webpage

import { Callout, Tabs } from 'nextra/components'

Interact with a webpage

In this guide, you'll use Lightpanda to run a search on HackerNews, wait for dynamic results to load, and extract structured data from the page.

This example requires Lightpanda because HackerNews uses XHR requests to display search results — raw HTML fetching wouldn't capture them.

Prerequisites

You'll need Node.js installed on your computer.

Create a hn-scraper directory and initialize a new Node.js project. You can accept all the default values in the npm init prompts.

sh copy
mkdir hn-scraper && \
  cd hn-scraper && \
  npm init

Install the lightpanda and the puppeteer-core or playwright-core npm package.

Unlike `puppeteer` and `playwright`, `puppeteer-core` and `playwright-core` don't download a Chromium browser.

<Tabs items={['puppeteer', 'playwright']} className="-mb-4">
<Tabs.Tab id="puppeteer" className="m-0">

sh copy
npm install --save puppeteer-core @lightpanda/browser

</Tabs.Tab>
<Tabs.Tab id="playwright" className="m-0">

sh copy
npm install --save playwright-core @lightpanda/browser

</Tabs.Tab>

This guide uses a local Lightpanda instance — see installation for setup details. The code can be adapted to connect to Lightpanda Cloud.

Create index.js

Create an index.js file. Import the packages, configure the connection, and set up the browser lifecycle:

<Tabs items={['puppeteer', 'playwright']} className="-mb-4">
<Tabs.Tab id="puppeteer" className="m-0">

javascript copy
'use strict'

import { lightpanda } from '@lightpanda/browser';
import puppeteer from 'puppeteer-core';

const lpdopts = {
  host: '127.0.0.1',
  port: 9222,
};

const puppeteeropts = {
  browserWSEndpoint: 'ws://' + lpdopts.host + ':' + lpdopts.port,
};

(async () => {
  // Start Lightpanda browser in a separate process.
  const proc = await lightpanda.serve(lpdopts);

  // Connect Puppeteer to the browser.
  const browser = await puppeteer.connect(puppeteeropts);
  const context = await browser.createBrowserContext();
  const page = await context.newPage();

  // Disconnect Puppeteer.
  await page.close();
  await context.close();
  await browser.disconnect();

  // Stop Lightpanda browser process.
  proc.stdout.destroy();
  proc.stderr.destroy();
  proc.kill();
})();

</Tabs.Tab>
<Tabs.Tab id="playwright" className="m-0">

javascript copy
'use strict'

import { lightpanda } from '@lightpanda/browser';
import { chromium } from 'playwright-core';

const lpdopts = {
  host: '127.0.0.1',
  port: 9222,
};

const playwrightopts = {
  endpointURL: 'ws://' + lpdopts.host + ':' + lpdopts.port,
};

(async () => {
  // Start Lightpanda browser in a separate process.
  const proc = await lightpanda.serve(lpdopts);

  // Connect Playwright's chromium driver to the browser.
  const browser = await chromium.connectOverCDP(playwrightopts);
  const context = await browser.newContext({});
  const page = await context.newPage();

  // Disconnect Playwright.
  await page.close();
  await context.close();
  await browser.close();

  // Stop Lightpanda browser process.
  proc.stdout.destroy();
  proc.stderr.destroy();
  proc.kill();
})();

</Tabs.Tab>

Navigate to HackerNews, type a search term, and press Enter:

<Tabs items={['puppeteer', 'playwright']} className="-mb-4">
<Tabs.Tab id="puppeteer" className="m-0">

javascript copy
await page.goto("https://news.ycombinator.com/");

  await page.type('input[name="q"]', 'lightpanda');
  await page.keyboard.press('Enter');

</Tabs.Tab>
<Tabs.Tab id="playwright" className="m-0">

javascript copy
await page.goto("https://news.ycombinator.com/");

  await page.locator('input[name="q"]').fill('lightpanda');
  await page.keyboard.press('Enter');

</Tabs.Tab>

Wait for results

Wait for the search results to appear, with a 5 second timeout:

<Tabs items={['puppeteer', 'playwright']} className="-mb-4">
<Tabs.Tab id="puppeteer" className="m-0">

javascript copy
await page.waitForFunction(() => {
    return document.querySelector('.Story_container') != null;
  }, { timeout: 5000 });

</Tabs.Tab>
<Tabs.Tab id="playwright" className="m-0">

javascript copy
await page.waitForSelector('.Story_container', { timeout: 5000 });

</Tabs.Tab>

Extract the data

Loop over the results to extract the title, URL, and metadata for each story:

<Tabs items={['puppeteer', 'playwright']} className="-mb-4">
<Tabs.Tab id="puppeteer" className="m-0">

javascript copy
const res = await page.evaluate(() => {
    return Array.from(document.querySelectorAll('.Story_container')).map(row => {
      return {
        title: row.querySelector('.Story_title span').textContent,
        url: row.querySelector('.Story_title a').getAttribute('href'),
        meta: Array.from(row.querySelectorAll('.Story_meta > span:not(.Story_separator, .Story_comment)')).map(row => {
          return row.textContent;
        }),
      }
    });
  });

  console.log(res);

</Tabs.Tab>
<Tabs.Tab id="playwright" className="m-0">

javascript copy
const res = await page.evaluate(() => {
    return Array.from(document.querySelectorAll('.Story_container')).map(row => {
      return {
        title: row.querySelector('.Story_title span').textContent,
        url: row.querySelector('.Story_title a').getAttribute('href'),
        meta: Array.from(row.querySelectorAll('.Story_meta > span:not(.Story_separator, .Story_comment)')).map(row => {
          return row.textContent;
        }),
      }
    });
  });

  console.log(res);

</Tabs.Tab>

Full script

<Tabs items={['puppeteer', 'playwright']} className="-mb-4">
<Tabs.Tab id="puppeteer" className="m-0">

javascript copy
'use strict'

import { lightpanda } from '@lightpanda/browser';
import puppeteer from 'puppeteer-core';

const lpdopts = {
  host: '127.0.0.1',
  port: 9222,
};

const puppeteeropts = {
  browserWSEndpoint: 'ws://' + lpdopts.host + ':' + lpdopts.port,
};

(async () => {
  // Start Lightpanda browser in a separate process.
  const proc = await lightpanda.serve(lpdopts);

  // Connect Puppeteer to the browser.
  const browser = await puppeteer.connect(puppeteeropts);
  const context = await browser.createBrowserContext();
  const page = await context.newPage();

  // Go to hackernews home page.
  await page.goto("https://news.ycombinator.com/");

  // Type the search term and submit.
  await page.type('input[name="q"]', 'lightpanda');
  await page.keyboard.press('Enter');

  // Wait until the search results are loaded.
  await page.waitForFunction(() => {
    return document.querySelector('.Story_container') != null;
  }, { timeout: 5000 });

  // Loop over search results to extract data.
  const res = await page.evaluate(() => {
    return Array.from(document.querySelectorAll('.Story_container')).map(row => {
      return {
        title: row.querySelector('.Story_title span').textContent,
        url: row.querySelector('.Story_title a').getAttribute('href'),
        meta: Array.from(row.querySelectorAll('.Story_meta > span:not(.Story_separator, .Story_comment)')).map(row => {
          return row.textContent;
        }),
      }
    });
  });

  console.log(res);

  // Disconnect Puppeteer.
  await page.close();
  await context.close();
  await browser.disconnect();

  // Stop Lightpanda browser process.
  proc.stdout.destroy();
  proc.stderr.destroy();
  proc.kill();
})();

</Tabs.Tab>
<Tabs.Tab id="playwright" className="m-0">

javascript copy
'use strict'

import { lightpanda } from '@lightpanda/browser';
import { chromium } from 'playwright-core';

const lpdopts = {
  host: '127.0.0.1',
  port: 9222,
};

const playwrightopts = {
  endpointURL: 'ws://' + lpdopts.host + ':' + lpdopts.port,
};

(async () => {
  // Start Lightpanda browser in a separate process.
  const proc = await lightpanda.serve(lpdopts);

  // Connect Playwright's chromium driver to the browser.
  const browser = await chromium.connectOverCDP(playwrightopts);
  const context = await browser.newContext({});
  const page = await context.newPage();

  // Go to hackernews home page.
  await page.goto("https://news.ycombinator.com/");

  // Type the search term and submit.
  await page.locator('input[name="q"]').fill('lightpanda');
  await page.keyboard.press('Enter');

  // Wait until the search results are loaded.
  await page.waitForSelector('.Story_container', { timeout: 5000 });

  // Loop over search results to extract data.
  const res = await page.evaluate(() => {
    return Array.from(document.querySelectorAll('.Story_container')).map(row => {
      return {
        title: row.querySelector('.Story_title span').textContent,
        url: row.querySelector('.Story_title a').getAttribute('href'),
        meta: Array.from(row.querySelectorAll('.Story_meta > span:not(.Story_separator, .Story_comment)')).map(row => {
          return row.textContent;
        }),
      }
    });
  });

  console.log(res);

  // Disconnect Playwright.
  await page.close();
  await context.close();
  await browser.close();

  // Stop Lightpanda browser process.
  proc.stdout.destroy();
  proc.stderr.destroy();
  proc.kill();
})();

</Tabs.Tab>

Run it

sh copy
node index.js
sh
$ node index.js
🐼 Running Lightpanda's CDP server… { pid: 598201 }
[
  {
    title: 'Show HN: Lightpanda, an open-source headless browser in Zig',
    url: 'https://news.ycombinator.com/item?id=42817439',
    meta: [ '319 points', 'fbouvier', '9 months ago', '137 comments' ]
  },
  {
    title: 'Lightpanda: Headless browser designed for AI and automation',
    url: 'https://news.ycombinator.com/item?id=42812859',
    meta: [ '154 points', 'tosh', '9 months ago', '1 comments' ]
  },
  ...
]

File: src/content/guides/lightpanda-agent-tutorial.mdx


title: Use Lightpanda Agent

Use Lightpanda Agent

This tutorial will walk you through how to use Lightpanda Agent to create a
reproducible JavaScript browser script.

Find all flags and REPL commands in the agent command reference.