---
title: "unlighthouse by harlan-zw · skilld"
canonical_url: "https://skilld.dev/gh/harlan-zw/unlighthouse"
meta:
  description: "Run Google Lighthouse on every page of a site with the unlighthouse CLI, and fail CI on low scores with unlighthouse-ci. Use when a task mentions unlighthouse,… From harlan-zw/unlighthouse."
  "og:description": "Run Google Lighthouse on every page of a site with the unlighthouse CLI, and fail CI on low scores with unlighthouse-ci. Use when a task mentions unlighthouse,… From harlan-zw/unlighthouse."
  "og:title": "unlighthouse by harlan-zw"
  "twitter:description": "Run Google Lighthouse on every page of a site with the unlighthouse CLI, and fail CI on low scores with unlighthouse-ci. Use when a task mentions unlighthouse,… From harlan-zw/unlighthouse."
  "twitter:title": "unlighthouse by harlan-zw"
---

`

[All skills](https://skilld.dev/skills)

[![harlan-zw avatar](https://skilld.dev/_img/avatar?url=https%3A%2F%2Fgithub.com%2Fharlan-zw.png%3Fsize%3D96)](https://skilld.dev/gh/harlan-zw)

# **/unlighthouse**

by [Harlan Wilton](https://skilld.dev/gh/harlan-zw)· [harlan-zw](https://skilld.dev/gh/harlan-zw)/ [unlighthouse](https://skilld.dev/gh/harlan-zw/unlighthouse)·4.9k stars

 174

Run Google Lighthouse on every page of a site with the unlighthouse CLI, and fail CI on low scores with unlighthouse-ci. Use when a task mentions unlighthouse, unlighthouse-ci, site-wide Lighthouse scans, performance budgets in CI, unlighthouse.config.ts, defineUnlighthouseConfig, scanner.exclude, dynamic sampling, the authenticate hook, or a scan that never exits, skips pages, or refuses to clear its output folder.

[GitHub](https://github.com/harlan-zw/unlighthouse/blob/b23fc3bae1918d6d0cccbd49790cc833a27eaf2f/packages/unlighthouse/skills/unlighthouse/SKILL.md "View SKILL.md on GitHub") Updated yesterday

## Skill content

[Raw](https://skilld.dev/api/skills-raw/harlan-zw/unlighthouse/unlighthouse)

## unlighthouse

Tested on Node 24 against the `unlighthouse` release that ships this Skill (requires Node `>=22.18.0`). The package finds the URLs of a site and runs Lighthouse on each one in parallel Chrome instances. It ships two binaries: `unlighthouse` opens a live dashboard, and `unlighthouse-ci` exits with a status code. Docs: https://unlighthouse.dev

### Setup

```
npx unlighthouse-ci --site https://example.com --budget 80
```

- Install `unlighthouse` for both binaries. `@unlighthouse/cli` ships only `unlighthouse-ci`.
- Do not install `puppeteer`. `puppeteer-cluster` already depends on it.
- Chrome: the package uses the system Chrome. If there is none, it downloads Chrome to `~/.unlighthouse`. As root, it adds `--no-sandbox` itself.
- Add `.unlighthouse` to `.gitignore`.
- `@unlighthouse/nuxt`, `@unlighthouse/vite`, and `@unlighthouse/webpack` are deprecated. Run the CLI against the dev server URL instead.
- The config file is `unlighthouse.config.ts` in the current directory. A CLI flag you pass wins over the config file. A flag you leave out keeps the config value.

### Pick the binary

- **`unlighthouse-ci`** scans, writes reports, and exits. Exit code 1 means a budget failed, or no route was left to scan. Use it in scripts, CI, and Agent runs.
- **`unlighthouse`** starts a dashboard server (port 5678 by default), opens a browser, and keeps running after the scan. It never exits by itself.

### Automatic behaviour

- **URL discovery:** `urls` if set, else robots.txt, then sitemap.xml, then a crawler that follows links from the home page. Robots.txt `Disallow` lines become excludes.
- **`--urls` or `urls`** turns off robots.txt, sitemap, crawler, and sampling. The paths are relative to `site`.
- **A `site` with a path**, such as `https://example.com/docs`, turns off sitemap, robots.txt, and sampling.
- **A sitemap with 50 or more URLs** turns off the crawler, except on `localhost`.
- **Dynamic sampling** scans at most 8 random URLs per route group, such as `/blog/*`. Turn it off with `--disable-dynamic-sampling`.
- **`scanner.maxRoutes`** (200) stops the queue. It logs a warning and exits 0.
- **HTML inspection skips JavaScript.** If the home page has no links, the crawler turns JavaScript on and retries. For an SPA, pass `--enable-javascript`.
- **Device** is mobile. Pass `--desktop` for desktop.
- **Throttling** is simulated for a remote site and off for a local one ( `localhost`, `127.0.0.1`). An explicit `scanner.throttle` or `--throttle` wins.
- **Redirects:** if `site` redirects to another host, such as http to https or apex to www, the scan uses the redirect target.

### Common tasks

Per category budgets, reporters, and scope in one config:

```
import { defineUnlighthouseConfig } from 'unlighthouse/config'

export default defineUnlighthouseConfig({
  site: 'https://staging.example.com',
  scanner: {
    exclude: ['/admin/**', '/api/**'],
    include: ['/', /^\/blog\//, '/docs/**'], // keep '/' so the crawler can start
  },
  ci: {
    budget: { 'performance': 70, 'accessibility': 95, 'best-practices': 90, 'seo': 90 },
    reporter: 'jsonExpanded',
  },
})
```

- Budgets use 0 to 100. Report scores use 0 to 1. A budget of 80 fails a score of 0.79.
- Reporters: `json` (default), `jsonExpanded`, `csv`, `csvExpanded`, `lighthouseServer`. The file is `<outputPath>/ci-result.json` or `ci-result.csv`.
- `lighthouseServer` needs `--lhci-host` and `--lhci-build-token`.

Static HTML report: pass `--build-static`, or set `ci.buildStatic: true`. It writes `index.html` to the root of `outputPath` (`.unlighthouse`). Upload that folder. It deletes the per page `lighthouse.json` files.

Authentication: the `authenticate` hook gets the Puppeteer `Page` as its first argument. It runs once. Its cookies reach every scanned page, including the Lighthouse run, and only the hosts they belong to.

```
import { defineUnlighthouseConfig } from 'unlighthouse/config'

export default defineUnlighthouseConfig({
  auth: { username: 'admin', password: process.env.AUTH_PASS! },
  hooks: {
    async authenticate(page) {
      await page.goto('https://example.com/login')
      await page.type('input[name="email"]', 'test@example.com')
      await Promise.all([page.waitForNavigation(), page.click('button[type="submit"]')])
    },
  },
})
```

Read scores in a hook: `task-complete` fires once per task, so check `taskName`. `categories` is an array of `{ key, id, title, score }`.

```
import { defineUnlighthouseConfig } from 'unlighthouse/config'

export default defineUnlighthouseConfig({
  hooks: {
    'task-complete': (path, report, taskName) => {
      if (taskName !== 'runLighthouseTask')
        return
      const performance = report.report?.categories.find(category => category.key === 'performance')
      console.log(path, report.report?.score, performance?.score)
    },
  },
})
```

Programmatic scan: register `worker-finished` before `start()`. Without a provider, reports go to `.unlighthouse/<host>/<config hash>/`.

```
import { createUnlighthouse } from 'unlighthouse'

const unlighthouse = await createUnlighthouse({ site: 'https://example.com', urls: ['/', '/about'] })
unlighthouse.hooks.hook('worker-finished', async () => {
  console.log(unlighthouse.worker.reports().map(r => [r.route.path, r.report?.score]))
  await unlighthouse.worker.cluster.close()
  process.exit(0)
})
await unlighthouse.start()
```

### Traps

- **`unlighthouse-ci` clears `outputPath` before it scans, and refuses a folder it did not create.** `--output-path .` exits 1 with `Refusing to clear ... it is not an unlighthouse output folder`. Point it at a new or empty folder. Unlighthouse marks its folders with a `.unlighthouse-output` file.
- **String patterns are route patterns, not regex.** `--exclude-urls "/blog/.*"` excludes nothing. `*` matches one segment and `**` any depth, so use `/blog/**`, or a `RegExp` in the config.
- **An `include` list that skips `/` stops a crawler scan.** The crawler starts from `/`, so nothing is queued and both binaries exit 1 with `No routes left to scan`. Add `'/'` to `include`, or use `--urls`.
- **A hook that throws stops `unlighthouse-ci` with exit code 1.** Guard on `taskName` and optional fields.
- **Dynamic sampling is random.** Two runs can scan different pages in one group. For stable CI results, pass `--urls` or turn sampling off.

### Output

- `unlighthouse-ci`: `.unlighthouse/ci-result.json` and `.unlighthouse/reports/<path>/lighthouse.json`.
- `unlighthouse`: `.unlighthouse/<host>/<config hash>/`. A config change starts a new cache folder.

### Config

- `scanner.device` ( `'mobile'`), `scanner.samples` (1, runs per page, averaged), `scanner.throttle` (on for remote sites, off for local ones).
- `scanner.dynamicSampling` (8), `scanner.maxRoutes` (200), `scanner.crawler`, `scanner.sitemap`, `scanner.robotsTxt`.
- `puppeteerClusterOptions.maxConcurrency` (half the CPU cores). Set 1 for stable performance scores.
- `lighthouseOptions` passes through to Lighthouse. `onlyAudits` replaces `onlyCategories`.
- `chrome.useSystem`, `chrome.downloadFallbackCacheDir`.
- All options: https://unlighthouse.dev/api-doc/config

### Debug

- `--debug` logs the resolved config. A route that `include` or `exclude` drops logs `Skipping route based on include / exclude rules`.
- "Failed to queue routes for scanning" means discovery found nothing. Check the `site` status and the robots.txt `Disallow` lines.
- "No routes left to scan" means discovery found routes, and `include`, `exclude`, or robots.txt rules skipped all of them.

Source: [SKILL.md on GitHub](https://github.com/harlan-zw/unlighthouse/blob/b23fc3bae1918d6d0cccbd49790cc833a27eaf2f/packages/unlighthouse/skills/unlighthouse/SKILL.md)

## Related skills

-
-
-
-
-
-