; rel="alternate"; hreflang="es"
```
**XML sitemap (for large sites):**
```xml
https://example.com/
```
Pick one method and stick with it. Lighthouse only checks HTML head and HTTP headers - not sitemaps.
## Framework-Specific Solutions
**Next.js**: Use `**next-intl**` or similar i18n library that handles hreflang automatically. Or add links in your `**_document.js**` or via the `**Head**` component with computed absolute URLs from your domain config.
**Nuxt**: Use `**@nuxtjs/i18n**` module which generates correct hreflang tags automatically. Set `**baseUrl**` in your i18n config so the module generates absolute URLs.
## Verify the Fix
1. Run Lighthouse SEO audit again
2. Confirm "Document has a valid hreflang" shows as passing
3. Use Google Search Console → URL Inspection to verify Google sees your hreflang
4. Check the International Targeting report in Search Console for hreflang errors
## Common Mistakes
- **Forgetting the self-referencing link**: Each page must include an hreflang link to itself. The English page needs `**hreflang="en"**` pointing to itself, not just to other languages.
- **Mixing implementation methods**: Don't use HTML head on some pages and HTTP headers on others. Pick one method for consistency.
- **Not updating hreflang when URLs change**: When you change a URL structure, update all hreflang links across all language versions. Broken links break the entire hreflang cluster.
- **Using hreflang in body**: Hreflang links must be in `****` or HTTP headers. Links in `****` are ignored by Lighthouse and may be ignored by search engines.
- **X-default on a page that redirects**: If your x-default URL redirects, it can't link back to the hreflang cluster, creating validation errors.
- **Missing self-referencing tags**: Each page must include an hreflang pointing to itself. This is one of the most [**~~common errors in international SEO implementations~~**](https://developers.google.com/search/docs/specialty/international/localized-versions).
## Related Issues
Hreflang issues often appear alongside:
- [**~~Canonical~~**](https://unlighthouse.dev/learn-lighthouse/seo/canonical) - Canonical URLs must match hreflang URLs
- [**~~HTML Lang~~**](https://unlighthouse.dev/learn-lighthouse/accessibility/html-has-lang) - The HTML lang attribute must match hreflang language
- [**~~Meta Description~~**](https://unlighthouse.dev/learn-lighthouse/seo/meta-description) - Each language version needs unique descriptions
## Test Your Entire Site
Hreflang issues compound across a multilingual site. If your English-to-Spanish links are broken, every page in both languages is affected. Unlighthouse scans your entire site and identifies every page with hreflang errors, so you can fix the pattern once rather than hunting page by page. e by page.
### **Related **
[**SEO Audits Overview**](https://unlighthouse.dev/learn-lighthouse/seo) [**All SEO Issues**](https://unlighthouse.dev/learn-lighthouse/seo#all-seo-audits)
[**Crawlable Anchors** Learn how to fix anchor elements that search engines can't follow due to missing or invalid href attributes.](https://unlighthouse.dev/learn-lighthouse/seo/crawlable-anchors) [**HTTP Status Code** Fix pages returning 4xx or 5xx HTTP status codes that prevent proper indexing. Learn status code meanings, common causes, and how to resolve server errors.](https://unlighthouse.dev/learn-lighthouse/seo/http-status-code)
**On this page **
- [What's the Problem?](#whats-the-problem)
- [How to Identify This Issue](#how-to-identify-this-issue)
- [The Fix](#the-fix)
- [Framework-Specific Solutions](#framework-specific-solutions)
- [Verify the Fix](#verify-the-fix)
- [Common Mistakes](#common-mistakes)
- [Related Issues](#related-issues)
- [Test Your Entire Site](#test-your-entire-site)
---
- **Page:** Fix Unsuccessful HTTP Status Code for Better SEO · Unlighthouse
- **Source:** https://unlighthouse.dev/learn-lighthouse/seo/http-status-code
- **Description:** Fix pages returning 4xx or 5xx HTTP status codes that prevent proper indexing. Learn status code meanings, common causes, and how to resolve server errors.
Star Unlighthouse on GitHubUnlighthouse on GitHub
# **Fix Unsuccessful HTTP Status Code for Better SEO**
Fix pages returning 4xx or 5xx HTTP status codes that prevent proper indexing. Learn status code meanings, common causes, and how to resolve server errors.
[Harlan Wilton](https://x.com/harlan-zw)4 min read Published **Jan 18, 2025**
Pages returning 4xx or 5xx status codes are invisible to search engines. Google's documentation is explicit: pages with unsuccessful HTTP status codes "may not be indexed properly" - and in practice, they almost never are.
## What's the Problem?
Lighthouse flags "Page has unsuccessful HTTP status code" when your page returns any status code between 400-599. These codes signal to search engines that the page is either unavailable (4xx client errors) or broken (5xx server errors). Googlebot treats both the same way: it won't index the content.
The 4xx range indicates the request was invalid, the page doesn't exist (404), requires authentication (401/403), or the request was malformed. The 5xx range indicates server failures, your code crashed (500), a downstream service failed (502), or the server is overloaded (503). From an SEO perspective, both are equally damaging.
The impact extends beyond the single page. When Googlebot encounters many error responses on your site, it reduces crawl rate, assuming your server can't handle the load. This means even your healthy pages get crawled less frequently, delaying index updates and hurting fresh content discovery.
**Redirect limits:** [**~~Google follows maximum 10 redirect hops~~**](https://developers.google.com/search/docs/crawling-indexing/site-move-with-url-changes) before giving up. Recommend keeping chains to 3-5 hops max. Good news: [**~~301 redirects are recommended for permanent moves~~**](https://developers.google.com/search/docs/crawling-indexing/301-redirects) and consolidate link equity, but chains still waste crawl budget.
## How to Identify This Issue
### Chrome DevTools
1. Open DevTools (F12) and go to the Network tab
2. Reload the page and click on the main document request
3. Check the Status column - anything 400-599 is a problem
4. For 5xx errors, check the Response tab for error messages
5. For 4xx errors, verify the URL is correct and the resource exists
### Lighthouse
Run a Lighthouse SEO audit. The "Page has unsuccessful HTTP status code" audit fails and displays the specific status code (e.g., "404", "500", "503"). This tells you exactly what category of problem you're dealing with.
## The Fix
### 1. Fix 404 Not Found Errors
404s mean the requested URL doesn't map to any content. Common causes:
**Deleted or moved content without redirects:**
```nginx
location /old-page {
return 301 /new-page;
}
location /blog/ {
rewrite ^/blog/(.*)$ /articles/$1 permanent;
}
```
```js
// Next.js - next.config.js
export default {
async redirects() {
return [
{
source: '/old-page',
destination: '/new-page',
permanent: true,
},
]
},
}
```
**Dynamic routes not handling all cases:**
```js
// Check that your dynamic route handler validates the ID exists
export async function GET(request, { params }) {
const post = await getPost(params.id)
if (!post) {
// Return 404 with proper status - don't return 200 with error message
return new Response('Not Found', { status: 404 })
}
return Response.json(post)
}
```
### 2. Fix 500 Internal Server Errors
500 errors indicate unhandled exceptions in your code. Find and fix the root cause:
**Check server logs for stack traces:**
```bash
tail -100 /var/log/nginx/error.log
tail -100 /var/log/your-app/error.log
grep '" 500 ' /var/log/nginx/access.log
```
**Add error boundaries and fallbacks:**
```js
// API route with proper error handling
export async function GET(request) {
const data = await fetchExternalAPI()
.catch((err) => {
console.error('External API failed:', err)
return null
})
if (!data) {
// Return graceful degradation, not a 500
return Response.json({ items: [], cached: true })
}
return Response.json(data)
}
```
### 3. Fix 502/503 Gateway Errors
These indicate infrastructure problems - your app server crashed, is overloaded, or a reverse proxy can't reach the backend.
**Check upstream service health:**
```nginx
upstream backend {
server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
keepalive 32;
}
server {
location / {
proxy_pass http://backend;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
proxy_next_upstream error timeout http_502 http_503;
}
}
```
**Implement retry logic for transient failures:**
```js
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
const response = await fetch(url)
.catch((error) => {
console.warn('Request attempt failed; retrying.', error)
return null
})
if (response?.ok)
return response
// Exponential backoff
await new Promise(r => setTimeout(r, 2 ** i * 100))
}
return null
}
```
### 4. Handle 403 Forbidden Appropriately
If pages require authentication, don't serve them at public URLs:
```js
// Middleware to check auth before serving protected routes
export function middleware(request) {
const isProtectedRoute = request.nextUrl.pathname.startsWith('/dashboard')
const hasSession = request.cookies.get('session')
if (isProtectedRoute && !hasSession) {
// Redirect to login instead of returning 403
return Response.redirect(new URL('/login', request.url))
}
}
```
## Framework-Specific Solutions
**Next.js** - Use `**notFound()**` from `**next/navigation**` to properly return 404s in App Router. For API routes, always return explicit status codes. Configure `**redirects**` in `**next.config.js**` for moved content. Use error boundaries (`**error.tsx**`) to catch and handle errors gracefully.
**Nuxt** - Use `**createError({ statusCode: 404 })**` or `**showError()**` for proper error responses. Configure redirects in `**nuxt.config.ts**` under `**routeRules**`. Create `**error.vue**` at the root for custom error pages. Use `**$fetch**` with error handling instead of raw fetch.
## Verify the Fix
1. Clear any CDN or server caches that might serve stale error responses
2. Request the page directly and check the HTTP status code in DevTools
3. Run Lighthouse SEO audit and confirm the status code audit passes
4. Use `**curl -I https://your-site.com/page**` to verify status code from command line
5. Check Google Search Console's Page Indexing report for coverage issues
6. Request re-indexing in Search Console after fixing persistent errors
## Common Mistakes
- **Custom error pages returning 200**: Your pretty 404 page must still return a 404 status code, not 200. Check your framework's error page configuration.
- **Ignoring intermittent 5xx errors**: Because a page "usually" works doesn't mean Googlebot will catch it on a good day. Monitor error rates and fix flaky endpoints.
- **Blocking Googlebot then wondering why pages aren't indexed**: 403 errors from WAF rules, rate limiting, or geo-blocking prevent indexing. Whitelist known crawler IPs or user agents.
- **Soft 404s**: Returning 200 OK with "Page not found" content. [**~~Google deindexes pages that return 200 OK but have no content (soft 404s)~~**](https://developers.google.com/search/docs/crawling-indexing/http-network-errors#soft-404-errors). Return actual 404 status codes.
- **Using 302/307 for permanent moves**: These provide weak canonicalization signals. Use 301 for permanent redirects to properly consolidate SEO equity.
- **404 for temporarily removed content**: 404 is a strong "don't recrawl" signal. Use 503 with Retry-After header for temporary removals.
## Related Issues
HTTP status code issues often appear alongside:
- [**~~Is Crawlable~~**](https://unlighthouse.dev/learn-lighthouse/seo/is-crawlable) - Both block indexing, but for different reasons
- [**~~Robots.txt~~**](https://unlighthouse.dev/learn-lighthouse/seo/robots-txt) - Check robots isn't blocking before checking status codes
- [**~~Redirects~~**](https://unlighthouse.dev/learn-lighthouse/lcp/redirects) - Redirect chains can end in error codes
## Test Your Entire Site
One broken page is annoying. Fifty broken pages across your site is an SEO disaster. Scan your entire site to find all pages returning error status codes before search engines discover them.
[Scan Your Site with Unlighthouse](https://unlighthouse.dev/)
}
### **Related **
[**SEO Audits Overview**](https://unlighthouse.dev/learn-lighthouse/seo) [**All SEO Issues**](https://unlighthouse.dev/learn-lighthouse/seo#all-seo-audits)
[**hreflang** Learn how to fix hreflang validation errors including unexpected language codes and relative URLs in your multi-language site.](https://unlighthouse.dev/learn-lighthouse/seo/hreflang) [**Blocked from Indexing** Remove crawl blocks preventing search engines from indexing your pages. Fix robots.txt, meta robots, and X-Robots-Tag issues.](https://unlighthouse.dev/learn-lighthouse/seo/is-crawlable)
**On this page **
- [What's the Problem?](#whats-the-problem)
- [How to Identify This Issue](#how-to-identify-this-issue)
- [The Fix](#the-fix)
- [Framework-Specific Solutions](#framework-specific-solutions)
- [Verify the Fix](#verify-the-fix)
- [Common Mistakes](#common-mistakes)
- [Related Issues](#related-issues)
- [Test Your Entire Site](#test-your-entire-site)
---
- **Page:** Fix Page Blocked from Indexing for Better SEO · Unlighthouse
- **Source:** https://unlighthouse.dev/learn-lighthouse/seo/is-crawlable
- **Description:** Remove crawl blocks preventing search engines from indexing your pages. Fix robots.txt, meta robots, and X-Robots-Tag issues.
Star Unlighthouse on GitHubUnlighthouse on GitHub
# **Fix Page Blocked from Indexing for Better SEO**
Remove crawl blocks preventing search engines from indexing your pages. Fix robots.txt, meta robots, and X-Robots-Tag issues.
[Harlan Wilton](https://x.com/harlan-zw)5 min read Published **Jan 18, 2025**
Your page might have perfect content, optimized keywords, and fast loading times - but if search engines can't crawl it, none of that matters. Pages blocked from indexing are invisible to organic search.
**Crawl budget note:** [**~~Noindex wastes crawl budget~~**](https://developers.google.com/search/blog/2024/12/crawling-december-resources) - Google still crawls the page before seeing the tag and dropping it. If you have pages that should never be indexed, consider using robots.txt to block crawling instead.
**JavaScript rendering caveat:** For low-authority sites, [**~~JavaScript rendering happens hours to days after initial crawl~~**](https://www.botify.com/blog/from-crawl-budget-to-render-budget) in a separate queue. If your noindex directive is JavaScript-rendered, Google may have already decided to index before seeing it.
## What's the Problem?
Search engines respect your instructions about what to crawl and index. When you (intentionally or accidentally) tell them "don't index this page," they comply. The page disappears from search results.
Three mechanisms can block indexing:
**1. Meta robots tag**
```html
```
This tells all search engines to not index the page.
**2. X-Robots-Tag HTTP header**
```http
X-Robots-Tag: noindex
```
Same effect, but set at the server level rather than in HTML.
**3. robots.txt disallow**
```text
User-agent: *
Disallow: /private/
```
Prevents crawlers from accessing the URL at all.
The most common cause is accidental. A developer adds `**noindex**` during staging and forgets to remove it before launch. A robots.txt rule intended to block one directory matches more than expected. A CMS setting gets toggled without understanding the consequences.
The result is the same: search engines skip your page entirely, and you get zero organic traffic regardless of how good your content is.
## How to Identify This Issue
### Chrome DevTools
Check for meta robots tags:
1. Open Elements panel
2. Press `**Ctrl/Cmd + F**` and search for `**robots**`
3. Look for `****` with `**noindex**` or `**none**` in the content
```js
// Console check for meta robots
const robotsMeta = document.querySelector('meta[name="robots"]')
if (robotsMeta) {
const content = robotsMeta.content.toLowerCase()
if (content.includes('noindex') || content.includes('none')) {
console.warn('Page is blocked from indexing:', robotsMeta.outerHTML)
}
}
```
Check HTTP headers in Network panel:
1. Reload with Network panel open
2. Click the main document request
3. Look for `**X-Robots-Tag**` in Response Headers
### Lighthouse
Run a Lighthouse SEO audit. Look for "Page is blocked from indexing" in the results.
The audit checks:
- `****` for `**noindex**` or `**none**` directives
- `**X-Robots-Tag**` HTTP header for blocking directives
- `**robots.txt**` rules that disallow the page URL
- `**unavailable_after**` directives with past dates
Lighthouse tests against major bot user agents: Googlebot, Bingbot, DuckDuckBot, and others. If all bots are blocked, you fail. If at least one is allowed, you pass with a warning.
## The Fix
### 1. Remove Meta Robots Noindex
Find and remove or modify the blocking meta tag:
```html
```
If you need `**nofollow**` (don't follow links) but want the page indexed:
```html
```
For bot-specific tags, check for both generic and specific:
```html
```
### 2. Remove X-Robots-Tag Header
The fix depends on your server:
**Apache (.htaccess):**
```apache
Header set X-Robots-Tag "noindex"
Header set X-Robots-Tag "noindex"
```
**Nginx:**
```nginx
add_header X-Robots-Tag "noindex";
location /admin/ {
add_header X-Robots-Tag "noindex";
}
```
**Node.js/Express:**
```js
// Remove this middleware
app.use((req, res, next) => {
res.setHeader('X-Robots-Tag', 'noindex')
next()
})
// Or apply selectively
app.use('/admin', (req, res, next) => {
res.setHeader('X-Robots-Tag', 'noindex')
next()
})
```
### 3. Fix robots.txt Rules
Check your robots.txt for overly broad rules:
```txt
User-agent: *
Disallow: /
User-agent: *
Disallow: /admin/
Disallow: /api/
Disallow: /tmp/
```
Common mistakes:
```txt
Disallow: /*?*
Disallow: /*.pdf$
Disallow: /staging # Also matches /staging-guide, /staging-area
```
Test your robots.txt rules:
```bash
curl https://yourdomain.com/robots.txt
```
Use Google Search Console's robots.txt Tester to verify specific URLs are allowed.
### 4. Check for Conditional Noindex
Some setups add noindex based on conditions:
```js
// Development environment check gone wrong
if (process.env.NODE_ENV !== 'production') {
// This is correct
}
// But this is dangerous if NODE_ENV isn't set correctly in production
if (!process.env.PRODUCTION) {
meta.push({ name: 'robots', content: 'noindex' })
}
```
Verify your production environment doesn't have staging configurations.
## Framework-Specific Solutions
**Next.js** - Check your metadata configuration:
```tsx
// Remove robots noindex from your metadata
export const metadata = {
robots: {
index: true, // Explicitly allow indexing
follow: true
}
}
// For pages that should not be indexed (intentional)
export const metadata = {
robots: {
index: false,
follow: false
}
}
```
Also check `**next.config.js**` for any custom headers adding X-Robots-Tag.
**Nuxt** - Check `**nuxt.config.ts**` and page-level settings:
```ts
// nuxt.config.ts - remove if blocking unintentionally
export default defineNuxtConfig({
app: {
head: {
meta: [
// Remove this if present
// { name: 'robots', content: 'noindex' }
]
}
}
})
```
```html
```
## Verify the Fix
After removing indexing blocks:
**1. Re-run Lighthouse**
The "Page is blocked from indexing" audit should pass.
**2. Check multiple sources**
Verify all three mechanisms are clear:
```bash
curl -s https://example.com/page | grep -i "robots"
curl -I https://example.com/page | grep -i "x-robots"
curl -s https://example.com/robots.txt
```
**3. Google Search Console**
Use URL Inspection to submit the page for indexing. The tool shows exactly what Google sees and whether indexing is allowed.
**4. Wait and verify indexing**
Search `**site:example.com/page-url**` after a few days. If the page appears, it's being indexed correctly.
## Common Mistakes
- **Removing noindex from pages that should be blocked**: Admin pages, user dashboards, checkout flows, and internal tools should remain noindexed. Don't remove blocks without understanding why they exist.
- **Leaving robots.txt Disallow in place**: Removing meta noindex but keeping robots.txt Disallow means crawlers still can't access the page. Both need to be addressed.
- **Environment-specific configurations leaking**: Staging settings deployed to production is extremely common. Always verify production environment after deployments.
- **CMS settings overriding code**: WordPress, Shopify, and other CMS platforms have their own SEO settings that might add noindex. Check the CMS configuration, not just the code.
- **Expired unavailable\_after dates**: The directive `**unavailable_after: 01-Jan-2024**` blocks indexing after that date. If you used this for temporary content, remove it when you want the content back.
- **CDN or proxy adding headers**: [**~~Cloudflare~~**](https://cloudflare.com), Fastly, or your CDN might add X-Robots-Tag. Check your edge configuration, not just origin server.
## Related Issues
Crawlability issues often appear alongside:
- [**~~Robots.txt~~**](https://unlighthouse.dev/learn-lighthouse/seo/robots-txt) - Check robots.txt for Disallow rules blocking the page
- [**~~HTTP Status Code~~**](https://unlighthouse.dev/learn-lighthouse/seo/http-status-code) - Both prevent indexing, diagnose which applies
- [**~~Canonical~~**](https://unlighthouse.dev/learn-lighthouse/seo/canonical) - Noindexed pages shouldn't be canonical targets
## Test Your Entire Site
One misconfigured template can block thousands of pages. A robots.txt typo can exclude your entire site. CMS updates can reset SEO settings to defaults. Unlighthouse scans every URL on your site and identifies every page blocked from indexing, so you can catch issues that would otherwise remain invisible until you notice organic traffic dropping.
### **Related **
[**SEO Audits Overview**](https://unlighthouse.dev/learn-lighthouse/seo) [**All SEO Issues**](https://unlighthouse.dev/learn-lighthouse/seo#all-seo-audits)
[**HTTP Status Code** Fix pages returning 4xx or 5xx HTTP status codes that prevent proper indexing. Learn status code meanings, common causes, and how to resolve server errors.](https://unlighthouse.dev/learn-lighthouse/seo/http-status-code) [**Link Text** Learn how to replace generic link text like "click here" with descriptive text that helps search engines understand your content.](https://unlighthouse.dev/learn-lighthouse/seo/link-text)
**On this page **
- [What's the Problem?](#whats-the-problem)
- [How to Identify This Issue](#how-to-identify-this-issue)
- [The Fix](#the-fix)
- [Framework-Specific Solutions](#framework-specific-solutions)
- [Verify the Fix](#verify-the-fix)
- [Common Mistakes](#common-mistakes)
- [Related Issues](#related-issues)
- [Test Your Entire Site](#test-your-entire-site)
---
- **Page:** Fix Non-Descriptive Link Text for Better SEO · Unlighthouse
- **Source:** https://unlighthouse.dev/learn-lighthouse/seo/link-text
- **Description:** Learn how to replace generic link text like "click here" with descriptive text that helps search engines understand your content.
Star Unlighthouse on GitHubUnlighthouse on GitHub
# **Fix Non-Descriptive Link Text for Better SEO**
Learn how to replace generic link text like "click here" with descriptive text that helps search engines understand your content.
[Harlan Wilton](https://x.com/harlan-zw)4 min read Published **Jan 18, 2025**
Links with generic text like "click here" or "read more" tell search engines nothing about the destination page. Google uses anchor text to understand what the linked page is about - and you're wasting that signal.
**Accessibility overlap:** "Click here" links fail for both SEO and accessibility. Screen reader users navigating by links hear "click here, click here, click here" with no context. [**~~For linked images, alt text functions as anchor text~~**](https://developers.google.com/search/docs/crawling-indexing/links-crawlable) - so empty alt on image links means zero anchor text signal.
## What's the Problem?
When you write `**click here**` instead of `**view our pricing plans**`, you're missing two opportunities:
**1. Search Engine Context**
Anchor text is one of the signals Google uses to understand what a page is about. When multiple sites link to your page with descriptive text like "Vue.js documentation," Google associates those terms with your page. Internal links work the same way - they help Google understand the relationship between your pages.
**2. Accessibility**
Screen reader users often navigate by jumping between links. Hearing "click here, click here, click here" tells them nothing. They need context without reading surrounding text.
Lighthouse flags these generic phrases as non-descriptive:
- "click here" / "click this"
- "here" / "this"
- "read more" / "learn more" / "more"
- "go" / "start"
- "more info" / "more information"
- "see more" / "information"
The audit also detects equivalents in Japanese, Spanish, Portuguese, Korean, Swedish, German, Tamil, and Persian.
## How to Identify This Issue
### Chrome DevTools
1. Open DevTools (F12) → Elements tab
2. Press Ctrl+F (or Cmd+F on Mac) to search
3. Search for common offenders: `**>click here<**`, `**>read more<**`, `**>here<**`
4. Review each match and its destination
### Lighthouse
Run a Lighthouse SEO audit. Look for "Links do not have descriptive text" in the results. The audit lists each failing link with its destination URL and current text.
## The Fix
### 1. Describe the Destination
Replace generic text with words that describe where the link goes.
```html
To see our products, click here.
Browse our complete product catalog.
```
```html
Learn more
Get started with our API
```
The destination should be obvious from the link text alone.
### 2. Include Keywords Naturally
Use anchor text that includes relevant keywords for the destination page - but keep it natural.
```html
For help with deployment, click here.
Our deployment documentation covers all hosting options.
```
Don't stuff keywords. "Click here for cheap flights booking discount airline tickets" is worse than "click here."
### 3. Avoid Redundant Phrases
Don't repeat "link" or "click" in anchor text - users know it's clickable.
```html
Click this link to learn about us
About our company
```
### 4. Keep Context in Mind
Surrounding text provides context. That's fine for humans, but search engines evaluate links independently.
```html
Check out our new features. Read more
Check out our new feature announcements.
```
### 5. Handle Repeated Links
When linking to the same destination multiple times, vary the text while keeping it descriptive.
```html
pricing plans
compare our plans
```
## Framework-Specific Solutions
**Next.js**: Use the `**Link**` component with descriptive child text. If you're using a design system with generic button text, override it at the component level or create a wrapper that enforces descriptive text.
**Nuxt**: Same principle with `**NuxtLink**`. If you have a component library with "Read more" buttons, consider a linting rule or component prop that requires descriptive text.
## Verify the Fix
1. Run Lighthouse SEO audit again
2. Confirm "Links have descriptive text" shows as passing
3. Manually spot-check key pages for remaining generic links
4. Test with a screen reader (VoiceOver on Mac, NVDA on Windows) to verify links make sense in isolation
## Common Mistakes
- **Over-optimizing anchor text**: "Best cheap affordable budget software solution" reads like spam. Keep it natural: "our pricing" or "project management software."
- **Using image-only links without alt text**: An image link with no alt text has no anchor text at all. Add descriptive alt text: `**
**`
- **Hiding descriptive text**: Don't hide descriptive text with CSS to "trick" the audit. Search engines can detect this. Make it visible for everyone.
- **Ignoring nofollow links**: Lighthouse skips `**rel="nofollow"**` links since they don't pass SEO value anyway. But they still matter for accessibility - fix them too.
## Related Issues
Link text issues often appear alongside:
- [**~~Crawlable Anchors~~**](https://unlighthouse.dev/learn-lighthouse/seo/crawlable-anchors) - Links need both valid hrefs and descriptive text
- [**~~Link Name~~**](https://unlighthouse.dev/learn-lighthouse/accessibility/link-name) - Both SEO and accessibility require descriptive links
- [**~~Image Alt~~**](https://unlighthouse.dev/learn-lighthouse/accessibility/image-alt) - Image links get their text from alt attributes
## Test Your Entire Site
Generic link text is often a pattern - if one "click here" slipped through, there are probably more. Unlighthouse scans your entire site and identifies every page with non-descriptive link text, so you can fix them systematically.
### **Related **
[**SEO Audits Overview**](https://unlighthouse.dev/learn-lighthouse/seo) [**All SEO Issues**](https://unlighthouse.dev/learn-lighthouse/seo#all-seo-audits)
[**Blocked from Indexing** Remove crawl blocks preventing search engines from indexing your pages. Fix robots.txt, meta robots, and X-Robots-Tag issues.](https://unlighthouse.dev/learn-lighthouse/seo/is-crawlable) [**Meta Description** Add compelling meta descriptions to improve click-through rates from search results. Learn how to write effective descriptions that drive traffic.](https://unlighthouse.dev/learn-lighthouse/seo/meta-description)
**On this page **
- [What's the Problem?](#whats-the-problem)
- [How to Identify This Issue](#how-to-identify-this-issue)
- [The Fix](#the-fix)
- [Framework-Specific Solutions](#framework-specific-solutions)
- [Verify the Fix](#verify-the-fix)
- [Common Mistakes](#common-mistakes)
- [Related Issues](#related-issues)
- [Test Your Entire Site](#test-your-entire-site)
---
- **Page:** Fix Missing Meta Description for Better SEO · Unlighthouse
- **Source:** https://unlighthouse.dev/learn-lighthouse/seo/meta-description
- **Description:** Add compelling meta descriptions to improve click-through rates from search results. Learn how to write effective descriptions that drive traffic.
Star Unlighthouse on GitHubUnlighthouse on GitHub
# **Fix Missing Meta Description for Better SEO**
Add compelling meta descriptions to improve click-through rates from search results. Learn how to write effective descriptions that drive traffic.
[Harlan Wilton](https://x.com/harlan-zw)5 min read Published **Jan 18, 2025**
Pages with custom meta descriptions get 5.8% higher click-through rates than those using auto-generated snippets. Without one, Google decides what to show - and it's rarely optimal.
**Reality check:** [**~~Google often rewrites meta descriptions~~**](https://developers.google.com/search/docs/appearance/snippet#meta-descriptions) anyway, choosing to display content it thinks better matches the query. And [**~~many top-ranking pages lack explicit meta descriptions~~**](https://developers.google.com/search/docs/appearance/snippet). So missing descriptions won't prevent ranking - but you lose control of what appears in search results.
## What's the Problem?
When your page lacks a meta description, search engines generate one automatically by extracting text from your page content. This sounds helpful until you realize the algorithm doesn't understand context, value propositions, or what makes users click.
Google might pull a random sentence from your footer. Bing might grab your cookie policy notice. The result is a search snippet that fails to communicate why someone should visit your page over the other nine results.
**The technical issue is simple:** Lighthouse checks for a `****` tag in your document head. If it's missing or empty, you fail the audit. But the real problem is strategic - you're giving up control of the single piece of text that represents your page in search results.
Consider what happens when someone searches for "best project management software":
- **With meta description:** "Compare 15 project management tools with real user reviews. Find the right fit for teams of 1-1000. Updated January 2025."
- **Without meta description:** "...cookies to improve your experience. By continuing to use this site..."
One gets clicked. One gets scrolled past.
## How to Identify This Issue
### Chrome DevTools
Open the Elements panel and search for `**meta name="description"**`:
1. Press `**Ctrl/Cmd + F**` in Elements panel
2. Search for `**meta name="description"**`
3. Check if it exists and has content
Quick console check:
```js
const meta = document.querySelector('meta[name="description"]')
console.log(meta ? meta.content : 'No meta description found')
```
If the content is empty or missing entirely, you have the issue.
### Lighthouse
Run a Lighthouse SEO audit. Look for "Document does not have a meta description" in the results. The audit is binary - you either have a valid meta description or you don't.
Lighthouse specifically checks that:
- A `****` element exists in the document
- The `**content**` attribute contains non-empty text (not just whitespace)
## The Fix
### 1. Add the Meta Description Tag
Place this in your document's `****`:
```html
```
Write descriptions that:
- Summarize what the page offers in 150-160 characters
- Include relevant keywords naturally (not stuffed)
- Contain a value proposition or call to action
- Are unique to each page
**Examples by page type:**
```html
```
### 2. Template-Based Descriptions
For sites with many similar pages, create templates:
```html
```
Templated descriptions are better than none, but custom descriptions outperform templates when you have the resources.
### 3. Dynamic Meta Descriptions
For SPAs and dynamic content, set descriptions programmatically:
```js
// Vanilla JS
// React Helmet
import { Helmet } from 'react-helmet'
function setMetaDescription(content) {
let meta = document.querySelector('meta[name="description"]')
if (!meta) {
meta = document.createElement('meta')
meta.name = 'description'
document.head.appendChild(meta)
}
meta.content = content
}
function ProductPage({ product }) {
return (
<>
{/* page content */}
>
)
}
```
## Framework-Specific Solutions
**Next.js** - Use the Metadata API in App Router:
```tsx
export const metadata = {
description: 'Your page description here'
}
// Or generate dynamically
export async function generateMetadata({ params }) {
const product = await getProduct(params.id)
return { description: product.summary }
}
```
**Nuxt** - Use `**useSeoMeta**` composable:
```html
```
## Verify the Fix
After adding meta descriptions:
**1. View page source**
Press `**Ctrl/Cmd + U**` and search for `**meta name="description"**`. Confirm it appears in the `****` with your content.
**2. Re-run Lighthouse**
The "Document does not have a meta description" audit should pass. If it still fails, check that:
- The tag is in `****`, not `****`
- The `**content**` attribute isn't empty
- The HTML has no syntax errors
**3. Test in Google Search Console**
Use the URL Inspection tool to see how Google renders your page. Check that Google detects the meta description.
**4. Preview your snippet**
Search for `**site:yourdomain.com/page-url**` to see how the description appears in actual search results. Note that Google may choose to show different text if it believes it better matches the query.
## Common Mistakes
- **Duplicate descriptions across pages**: Every page needs a unique description. Using the same text everywhere dilutes your SEO and confuses users about page differences.
- **Descriptions over 160 characters**: Google truncates long descriptions with "...". Keep the most important information in the first 120 characters.
- **Keyword stuffing**: "Best project management software, top project management, free project management tool" reads like spam and may hurt rankings.
- **Descriptions that don't match content**: If your description promises something the page doesn't deliver, users bounce immediately. Google notices.
- **Forgetting dynamic pages**: SPAs often fail to update meta tags on navigation. Test each route, not just the initial load.
- **Empty content attribute**: `****` technically exists but fails the audit. The content must have actual text.
## Related Issues
Meta description issues often appear alongside:
- [**~~Document Title~~**](https://unlighthouse.dev/learn-lighthouse/accessibility/document-title) - Both are critical document metadata
- [**~~Canonical~~**](https://unlighthouse.dev/learn-lighthouse/seo/canonical) - Pages with duplicate descriptions may need canonicalization
- [**~~Hreflang~~**](https://unlighthouse.dev/learn-lighthouse/seo/hreflang) - Each language version needs unique descriptions
## Test Your Entire Site
One template with a missing meta description could affect thousands of pages. A CMS update might have broken your SEO plugin. Unlighthouse scans every URL on your site and flags pages missing meta descriptions, so you can catch issues across your entire domain, not just the pages you remember to check.
### **Related **
[**SEO Audits Overview**](https://unlighthouse.dev/learn-lighthouse/seo) [**All SEO Issues**](https://unlighthouse.dev/learn-lighthouse/seo#all-seo-audits)
[**Link Text** Learn how to replace generic link text like "click here" with descriptive text that helps search engines understand your content.](https://unlighthouse.dev/learn-lighthouse/seo/link-text) [**Invalid robots.txt** Fix robots.txt validation errors that prevent search engines from properly crawling your site. Learn syntax rules, common errors, and proper configuration.](https://unlighthouse.dev/learn-lighthouse/seo/robots-txt)
**On this page **
- [What's the Problem?](#whats-the-problem)
- [How to Identify This Issue](#how-to-identify-this-issue)
- [The Fix](#the-fix)
- [Framework-Specific Solutions](#framework-specific-solutions)
- [Verify the Fix](#verify-the-fix)
- [Common Mistakes](#common-mistakes)
- [Related Issues](#related-issues)
- [Test Your Entire Site](#test-your-entire-site)
---
- **Page:** Fix Invalid robots.txt for Better SEO · Unlighthouse
- **Source:** https://unlighthouse.dev/learn-lighthouse/seo/robots-txt
- **Description:** Fix robots.txt validation errors that prevent search engines from properly crawling your site. Learn syntax rules, common errors, and proper configuration.
Star Unlighthouse on GitHubUnlighthouse on GitHub
# **Fix Invalid robots.txt for Better SEO**
Fix robots.txt validation errors that prevent search engines from properly crawling your site. Learn syntax rules, common errors, and proper configuration.
[Harlan Wilton](https://x.com/harlan-zw)5 min read Published **Jan 18, 2025**
A malformed robots.txt file can silently prevent Google from crawling your entire site. According to Google Search Console data, 23% of websites have robots.txt configuration errors that affect their search visibility.
**Key limits to know:**
- [**~~Max file size: 500 KiB~~**](https://developers.google.com/search/docs/crawling-indexing/robots/intro) - Google stops processing midway if larger
- [**~~Sitemap limits: 50,000 URLs OR 50MB uncompressed~~**](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap) (whichever first)
- [**~~Google ignores ~~**`**crawl-delay**`](https://developers.google.com/search/blog/2019/07/a-note-on-unsupported-rules-in-robotstxt) - retired all code handling it on Sept 1, 2019
## What's the Problem?
Lighthouse flags "robots.txt is not valid" when your robots.txt file contains syntax errors, malformed directives, or structural problems that crawlers cannot parse correctly. When search engine bots encounter an invalid robots.txt, they may interpret your crawling instructions incorrectly or ignore them entirely.
The robots.txt file follows a strict specification. Each directive must be on its own line, use a colon separator, and follow specific formatting rules. Common errors include missing colons, invalid URL patterns, directives without a preceding User-agent declaration, and unrecognized directive names. These seem like minor issues, but they can cascade into major crawling problems.
The stakes are high: if Googlebot misinterprets your robots.txt, it might crawl pages you wanted blocked (wasting crawl budget and potentially indexing private content) or skip pages you wanted indexed (killing your search rankings). A single syntax error can flip the meaning of your entire file.
## How to Identify This Issue
### Chrome DevTools
1. Navigate to `**https://your-site.com/robots.txt**` directly
2. Look for obvious syntax errors: missing colons, typos in directive names
3. Check that every Allow/Disallow directive has a User-agent above it
4. Verify sitemap URLs are fully qualified (include https://)
### Lighthouse
Run a Lighthouse SEO audit. The "robots.txt is not valid" audit will fail and display:
- The specific line number where errors occur
- The problematic content on that line
- A description of what's wrong (e.g., "Unknown directive", "No user-agent specified")
Lighthouse also fails this audit when the robots.txt request returns a 5xx server error, indicating your server cannot reliably serve the file.
## The Fix
### 1. Correct Basic Syntax Errors
Every directive needs the format `**Directive: value**` with a colon separator:
```txt
User-agent *
Disallow /admin
User-agent: *
Disallow: /admin
```
Group member directives (Allow, Disallow) must always follow a User-agent declaration:
```txt
Disallow: /private/
User-agent: *
Disallow: /private/
```
### 2. Fix URL Pattern Errors
Allow and Disallow patterns must start with `**/**`, `*****`, or be empty:
```txt
Disallow: admin/
Disallow: private
Disallow: /admin/
Disallow: /private
Disallow: *private*
Disallow: # Empty disallow (allows everything)
```
The `**$**` wildcard is only valid at the end of a pattern:
```txt
Disallow: /page$.html
Disallow: /page.html$
```
### 3. Validate Sitemap URLs
Sitemap directives require fully qualified URLs with valid protocols:
```txt
Sitemap: /sitemap.xml
Sitemap: ftp://example.com/sitemap.xml
Sitemap: https://example.com/sitemap.xml
```
### 4. Use Only Recognized Directives
Stick to universally supported directives. Unknown directives cause validation failures:
```txt
User-agent: *
Allow: /
Disallow: /admin/
Sitemap: https://example.com/sitemap.xml
Crawl-delay: 10
```
### Complete Valid Example
```txt
User-agent: *
Allow: /
Disallow: /admin/
Disallow: /api/
Disallow: /private/
Disallow: /*.json$
User-agent: GPTBot
Disallow: /
User-agent: CCBot
Disallow: /
Sitemap: https://example.com/sitemap.xml
Sitemap: https://example.com/sitemap-blog.xml
```
## Framework-Specific Solutions
**Next.js** - Create `**public/robots.txt**` for static content, or use `**app/robots.ts**` for dynamic generation. [**~~Next.js~~**](https://nextjs.org) serves files from `**public/**` at the root path automatically. For dynamic robots.txt based on environment, export a `**robots()**` function from `**app/robots.ts**`.
**Nuxt** - Place `**robots.txt**` in the `**public/**` directory, or use the `**@nuxtjs/robots**` module for dynamic generation. The module supports environment-based configuration and automatic sitemap URL injection via `**nuxt.config.ts**`.
## Verify the Fix
1. Navigate to `**https://your-site.com/robots.txt**` and visually inspect for errors
2. Use Google Search Console's robots.txt Tester (Settings > robots.txt)
3. Run Lighthouse SEO audit and confirm the robots.txt audit passes
4. Test specific URLs with Google's URL Inspection tool to verify intended behavior
5. Check server logs to check that robots.txt returns 200 status consistently
## Common Mistakes
- **Blocking CSS and JavaScript**: Don't block `**/css/**` or `**/js/**` directories. Googlebot needs these to render your pages correctly. Blocking render resources hurts your rankings.
- **Using robots.txt for sensitive content**: robots.txt is public and doesn't prevent indexing if pages are linked elsewhere. Use `**noindex**` meta tags or authentication for truly private content.
- **Forgetting trailing slashes**: `**/admin**` blocks only the `**/admin**` file, while `**/admin/**` blocks the directory. Be explicit about what you're blocking.
- **Testing only in production**: Many sites serve different robots.txt in staging vs production. Validate your production file, not your local one.
- **Unicode BOM at start of file**: A byte-order mark makes Google ignore invalid lines including the BOM character.
- **robots.txt in subdirectory**: Invalid. Must be in domain root ( `**/robots.txt**`). Bots won't find it anywhere else.
- **Path values not starting with `**/**` or `*****`** - Directive values like `**Disallow: admin/**` (missing leading slash) are invalid and ignored.
## Related Issues
Robots.txt issues often appear alongside:
- [**~~Is Crawlable~~**](https://unlighthouse.dev/learn-lighthouse/seo/is-crawlable) - Robots.txt can block pages from indexing
- [**~~HTTP Status Code~~**](https://unlighthouse.dev/learn-lighthouse/seo/http-status-code) - A 404 robots.txt causes different behavior than missing
- [**~~Canonical~~**](https://unlighthouse.dev/learn-lighthouse/seo/canonical) - Don't block canonical URLs in robots.txt
## Test Your Entire Site
A valid robots.txt is the first step. Search engines still need to successfully crawl and index your pages. Run a complete scan to verify your entire site is accessible and returns proper status codes.
[Scan Your Site with Unlighthouse](https://unlighthouse.dev/)
### **Related **
[**SEO Audits Overview**](https://unlighthouse.dev/learn-lighthouse/seo) [**All SEO Issues**](https://unlighthouse.dev/learn-lighthouse/seo#all-seo-audits)
[**Meta Description** Add compelling meta descriptions to improve click-through rates from search results. Learn how to write effective descriptions that drive traffic.](https://unlighthouse.dev/learn-lighthouse/seo/meta-description)
**On this page **
- [What's the Problem?](#whats-the-problem)
- [How to Identify This Issue](#how-to-identify-this-issue)
- [The Fix](#the-fix)
- [Framework-Specific Solutions](#framework-specific-solutions)
- [Verify the Fix](#verify-the-fix)
- [Common Mistakes](#common-mistakes)
- [Related Issues](#related-issues)
- [Test Your Entire Site](#test-your-entire-site)
---
- **Page:** Troubleshooting Playwright Lighthouse Integration · Unlighthouse
- **Source:** https://unlighthouse.dev/learn-lighthouse/playwright/troubleshooting
- **Description:** Fix common issues when running Lighthouse with Playwright: port conflicts, authentication problems, flaky scores, and Chrome version mismatches.
Star Unlighthouse on GitHubUnlighthouse on GitHub
# **Troubleshooting Playwright Lighthouse Integration**
Fix common issues when running Lighthouse with Playwright: port conflicts, authentication problems, flaky scores, and Chrome version mismatches.
[Harlan Wilton](https://x.com/harlan-zw) Published **Jan 27, 2026** Updated **Aug 9, 2026**
Common issues and solutions when integrating Lighthouse with [**~~Playwright~~**](https://playwright.dev).
## Port Already in Use
**Error**: `**Error: listen EADDRINUSE: address already in use :::9222**`
The debugging port is already occupied, usually from a previous run that didn't close properly.
**Solution**: Identify the process before stopping it, or assign a different port:
```bash
# Show the process listening on port 9222
lsof -nP -iTCP:9222 -sTCP:LISTEN
```
```js
const PORT = 9223
```
For CI environments, use proper cleanup:
```js
async function audit() {
let browser
try {
browser = await chromium.launch({
args: [`--remote-debugging-port=${PORT}`],
})
// ... audit logic
}
finally {
if (browser)
await browser.close()
}
}
```
## Authentication State Lost
**Symptom**: Lighthouse audits a login page instead of the authenticated page.
**Cause**: Lighthouse opens a new page context, losing cookies/session.
**Solution**: Use `**disableStorageReset: true**`:
```js
const result = await lighthouse(url, {
port: PORT,
disableStorageReset: true, // Prevents Lighthouse from clearing storage
})
```
See [**~~Authentication Guide~~**](https://unlighthouse.dev/learn-lighthouse/playwright/authentication) for complete examples.
## Flaky or Inconsistent Scores
**Symptom**: Performance scores vary significantly between runs (±10-20 points).
**Causes**:
- Network variability
- CPU load on the machine
- Shared CI runners
- Third-party script timing
**Solutions**:
1. **Run multiple audits and use median**:
```js
const runs = 3
const scores = []
for (let i = 0; i < runs; i++) {
const result = await lighthouse(url, { port: PORT })
scores.push(result.lhr.categories.performance.score)
}
scores.sort((a, b) => a - b)
const median = scores[Math.floor(scores.length / 2)]
```
1. **Use the provided network without Lighthouse simulation for local diagnostics**:
```js
const result = await lighthouse(url, {
port: PORT,
throttlingMethod: 'provided',
})
```
Use simulated throttling again for performance budgets. Unthrottled scores describe your test machine and should not be compared with PageSpeed Insights.
1. **Use dedicated CI runners** instead of shared ones.
## Chrome/Chromium Version Mismatch
**Error**: `**Protocol error**` or `**Target closed**` or `**Cannot find context with specified id**`
**Cause**: The installed Playwright package, browser binary, and Lighthouse version came from different lockfile states or caches.
**Solution**: Check the installed versions, reinstall Playwright's matching Chromium, and let Playwright use its bundled executable:
```bash
npm ls @playwright/test lighthouse
npx playwright install chromium
```
Playwright warns that custom executables may not work with its APIs. If you deliberately test branded Chrome, select a supported channel and keep that choice explicit:
```js
const browser = await chromium.launch({
channel: 'chrome',
args: [`--remote-debugging-port=${PORT}`],
})
```
## Timeout Errors
**Error**: `**Lighthouse timeout**` or `**Navigation timeout exceeded**`
**Causes**:
- Page takes too long to load
- `**waitUntil: 'networkidle'**` waiting for never-ending requests
- Large pages with many resources
**Solutions**:
1. **Increase Lighthouse timeout**:
```js
const result = await lighthouse(url, {
port: PORT,
maxWaitForLoad: 60000, // 60 seconds (default is 45s)
})
```
1. **Use different wait strategy**:
```js
// Instead of networkidle, wait for specific element
await page.goto(url)
await page.waitForSelector('#main-content')
```
1. **Check for infinite polling requests** (analytics, websockets).
## Sandbox Errors
**Error**: `**No usable sandbox!**` or `**Running as root without --no-sandbox is not supported**`
**Cause**: Chrome sandboxing issues, common in Docker/CI.
**Solution**: Disable sandbox (only in trusted CI environments):
```js
const browser = await chromium.launch({
args: [
`--remote-debugging-port=${PORT}`,
'--no-sandbox',
'--disable-setuid-sandbox',
],
})
```
Only disable sandbox in controlled CI environments, never in production or when processing untrusted URLs.
## Multiple Concurrent Audits Fail
**Error**: Audits interfere with each other or return incorrect results.
**Cause**: Multiple Lighthouse instances trying to use the same debugging port.
**Solution**: Use different ports for parallel audits:
```js
async function auditWithDynamicPort(url) {
const port = 9222 + Math.floor(Math.random() * 1000)
const browser = await chromium.launch({
args: [`--remote-debugging-port=${port}`],
})
// ... use `port` variable
}
```
Or run audits sequentially with a single worker:
```bash
# Playwright Test
npx playwright test --workers=1
```
## Lighthouse Reports Show Wrong Page
**Symptom**: Report shows homepage or login page instead of target URL.
**Causes**:
- Redirects before audit runs
- Authentication issues
- URL mismatch between Playwright navigation and Lighthouse call
**Solution**: Verify URL after navigation:
```js
await page.goto(targetUrl, { waitUntil: 'networkidle' })
// Check actual URL
const actualUrl = page.url()
if (actualUrl !== targetUrl) {
console.warn(`Redirected from ${targetUrl} to ${actualUrl}`)
}
// Use actual URL for Lighthouse
const result = await lighthouse(actualUrl, { port: PORT })
```
## Empty or Incomplete Reports
**Symptom**: Lighthouse report missing metrics or categories.
**Causes**:
- Page not fully loaded
- JavaScript errors on page
- Missing viewport settings
**Solution**: Check page readiness:
```js
await page.goto(url, { waitUntil: 'networkidle' })
// Wait for critical content
await page.waitForSelector('[data-testid="main-content"]')
// Check for console errors
page.on('console', (msg) => {
if (msg.type() === 'error')
console.warn('Page error:', msg.text())
})
```
## CI-Specific Issues
### GitHub Actions: Chrome Not Found
```yaml
- name: Install Chrome
run: npx playwright install chromium --with-deps
```
The `**--with-deps**` flag installs system dependencies required by Chromium.
### Docker: Missing Dependencies
Add to Dockerfile:
```dockerfile
RUN apt-get update && apt-get install -y \
libnss3 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libcups2 \
libdrm2 \
libxkbcommon0 \
libxcomposite1 \
libxdamage1 \
libxrandr2 \
libgbm1 \
libasound2
```
Or use Playwright's [**~~Docker image~~**](https://playwright.dev/docs/docker). Pin the image tag to the same Playwright version as `**@playwright/test**`; mismatched versions can prevent the client from locating browser executables.
**Crash / Target Closed in Docker**: If Chrome crashes with "Target closed" or "Page crashed", it's often due to small shared memory `**/dev/shm**`. Add this flag:
```js
const browser = await chromium.launch({
args: [
`--remote-debugging-port=${PORT}`,
'--disable-dev-shm-usage', // Critical for Docker
],
})
```
## Visual Debugging
Use Playwright's [**~~Inspector~~**](https://playwright.dev/docs/debug#playwright-inspector) to pause execution right before the Lighthouse audit to verify the page state.
```bash
# Run with Inspector
PWDEBUG=1 npx playwright test
```
When paused:
1. Check that the page has logged you in (if expected).
2. Verify the browser has fully loaded the DOM.
3. Check for any overlay/modals that might block Lighthouse.
## Still Stuck?
- Check [**~~Lighthouse GitHub Issues~~**](https://github.com/GoogleChrome/lighthouse/issues)
- Check [**~~Playwright GitHub Issues~~**](https://github.com/microsoft/playwright/issues)
- For site-wide auditing without these integration headaches, try [**~~Unlighthouse~~**](https://unlighthouse.dev/guide/getting-started/installation)
### **Related **
[**Playwright Setup Guide**](https://unlighthouse.dev/learn-lighthouse/playwright) [**Authentication**](https://unlighthouse.dev/learn-lighthouse/playwright/authentication) [**CI/CD Integration**](https://unlighthouse.dev/learn-lighthouse/playwright/ci-cd)
[**CI/CD** Automate Lighthouse audits with Playwright in GitHub Actions. Run performance tests on every PR with thresholds and artifact reports.](https://unlighthouse.dev/learn-lighthouse/playwright/ci-cd) [**SEO** Pass every Lighthouse SEO audit. Crawlability, canonical, meta description, robots.txt, hreflang, status codes — each check explained with fixes and examples.](https://unlighthouse.dev/learn-lighthouse/seo)
**On this page **
- [Port Already in Use](#port-already-in-use)
- [Authentication State Lost](#authentication-state-lost)
- [Flaky or Inconsistent Scores](#flaky-or-inconsistent-scores)
- [Chrome/Chromium Version Mismatch](#chromechromium-version-mismatch)
- [Timeout Errors](#timeout-errors)
- [Sandbox Errors](#sandbox-errors)
- [Multiple Concurrent Audits Fail](#multiple-concurrent-audits-fail)
- [Lighthouse Reports Show Wrong Page](#lighthouse-reports-show-wrong-page)
- [Empty or Incomplete Reports](#empty-or-incomplete-reports)
- [CI-Specific Issues](#ci-specific-issues)
- [Visual Debugging](#visual-debugging)
- [Still Stuck?](#still-stuck)
---
- **Page:** Bulk Lighthouse Testing for Large Sites · Unlighthouse
- **Source:** https://unlighthouse.dev/guide/recipes/large-sites
- **Description:** Scan large websites with thousands of pages efficiently. Configure sampling, URL filtering, and optimization strategies for bulk Lighthouse testing.
Star Unlighthouse on GitHubUnlighthouse on GitHub
[**User Guide **](https://unlighthouse.dev/guide/getting-started/installation)
[**Integrations **](https://unlighthouse.dev/integrations/cli)
[**API **](https://unlighthouse.dev/api-doc/config)
**Recipes**
# **Bulk Lighthouse Testing for Large Sites**
[Copy for LLMs](https://unlighthouse.dev/guide/recipes/large-sites.md)
Scan websites with thousands of pages efficiently. Unlike single-page tools like PageSpeed Insights, Unlighthouse handles large sites with smart sampling, parallel scanning, and configurable limits.
- **Automatic discovery** - Finds all pages via sitemap and crawling
- **Smart sampling** - Tests representative pages from each template
- **Parallel scanning** - Multiple Chrome instances for speed
- **Aggregated results** - Site-wide scores and insights
Unlighthouse includes smart defaults for large sites. Understanding these helps balance completeness with performance.
## Default Large Site Configuration
These defaults optimize scanning for sites with thousands of pages:
- [**~~ignoreI18nPages~~**](https://unlighthouse.dev/api-doc/config#scanner-ignorei18npages) enabled
- [**~~maxRoutes~~**](https://unlighthouse.dev/api-doc/config#scanner-maxroutes) set to 200
- [**~~skipJavascript~~**](https://unlighthouse.dev/api-doc/config#scanner-skipjavascript) enabled
- [**~~samples~~**](https://unlighthouse.dev/api-doc/config#scanner-samples) set to 1
- [**~~throttling~~**](https://unlighthouse.dev/api-doc/config#scanner-throttle) disabled
- [**~~crawler~~**](https://unlighthouse.dev/api-doc/config#scanner-crawler) enabled
- [**~~dynamicSampling~~**](https://unlighthouse.dev/api-doc/config#scanner-dynamicsampling) set to 5
For example, when scanning a blog with thousands of posts, it may be redundant to scan every single blog post, as the DOM is very similar. Using the configuration we can select exactly how many posts should be scanned.
## Manually select URLs
You can configure Unlighthouse to use an explicit list of relative paths. This can be useful if you have a fairly complex and large site.
See [**~~Manually providing URLs~~**](https://unlighthouse.dev/guide/guides/url-discovery#manually-providing-urls) for more information.
## Provide Route Definitions (optional)
To make the most intelligent sampling decisions, Unlighthouse needs to know which page files are available. When running using the integration API, Unlighthouse will automatically provide this information.
Using the CLI you should follow the [**~~providing route definitions~~**](https://unlighthouse.dev/guide/guides/route-definitions) guide.
Note: When no route definitions are provided it will match based on URL fragments, i.e `**/blog/post-slug-3**` will be mapped to `**blog-slug**`.
## Exclude URL Patterns
Paths to ignore from scanning.
For example, if your site has a documentation section, that doesn't need to be scanned.
```ts
import { defineUnlighthouseConfig } from 'unlighthouse/config'
export default defineUnlighthouseConfig({
scanner: {
exclude: [
'/docs/*',
],
},
})
```
## Include URL Patterns
Explicitly include paths; this will exclude any paths not listed here.
For example, if you run a blog and want to only scan your article and author pages.
```ts
import { defineUnlighthouseConfig } from 'unlighthouse/config'
export default defineUnlighthouseConfig({
scanner: {
include: [
'/articles/*',
'/authors/*',
],
},
})
```
## Change Dynamic Sampling Limit
By default, a URLs will be matched to a specific route definition 5 times.
You can change the sample limit with:
```ts
import { defineUnlighthouseConfig } from 'unlighthouse/config'
export default defineUnlighthouseConfig({
scanner: {
dynamicSampling: 20, // 20 samples per page template
},
})
```
## Disabling Sampling
In cases where the route definitions aren't provided, a less-smart sampling will occur where URLs under the same parent will be sampled.
For these instances you may want to disable the sample as follows:
```ts
import { defineUnlighthouseConfig } from 'unlighthouse/config'
export default defineUnlighthouseConfig({
scanner: {
dynamicSampling: false, // Disable sampling completely
},
})
```
[Edit this page](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/recipes/large-sites.md)
[Markdown For LLMs](https://unlighthouse.dev/guide/recipes/large-sites.md)
**Did this page help you? **
Anything that could be done better? :)
Help us improve this page. You can [~~edit this page ~~](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/recipes/large-sites.md) on GitHub or provide anonymous feedback below.
### **Related **
[**Dynamic Sampling**](https://unlighthouse.dev/guide/guides/dynamic-sampling) [**URL Discovery**](https://unlighthouse.dev/guide/guides/url-discovery) [**Route Definitions**](https://unlighthouse.dev/guide/guides/route-definitions)
[**Improving Accuracy** Optimize Lighthouse scan accuracy with multiple samples and reduced concurrency for more reliable, consistent Core Web Vitals results.](https://unlighthouse.dev/guide/recipes/improving-accuracy) [**SPAs** Configure Unlighthouse to scan single-page applications (SPAs) with client-side routing like React, Vue, and Angular apps.](https://unlighthouse.dev/guide/recipes/spa)
**On this page **
- [Default Large Site Configuration](#default-large-site-configuration)
- [Manually select URLs](#manually-select-urls)
- [Provide Route Definitions (optional)](#provide-route-definitions-optional)
- [Exclude URL Patterns](#exclude-url-patterns)
- [Include URL Patterns](#include-url-patterns)
- [Change Dynamic Sampling Limit](#change-dynamic-sampling-limit)
- [Disabling Sampling](#disabling-sampling)
---
- **Page:** Authentication for Lighthouse Scans · Unlighthouse
- **Source:** https://unlighthouse.dev/guide/guides/authentication
- **Description:** Scan password-protected websites with Unlighthouse. Configure basic auth, cookies, headers, localStorage, and programmatic login flows.
Star Unlighthouse on GitHubUnlighthouse on GitHub
[**User Guide **](https://unlighthouse.dev/guide/getting-started/installation)
[**Integrations **](https://unlighthouse.dev/integrations/cli)
[**API **](https://unlighthouse.dev/api-doc/config)
**Guides**
# **Authentication for Lighthouse Scans**
[Copy for LLMs](https://unlighthouse.dev/guide/guides/authentication.md)
Need to scan pages behind a login? Unlighthouse supports every common auth pattern. Find yours below.
## Quick Reference
| **Auth Type** | **Best For** | **Config Key** |
| --- | --- | --- |
| Basic Auth | Staging environments with HTTP basic auth | `**auth**` |
| Cookies | Session tokens, JWTs in cookies | `**cookies**` |
| Headers | Bearer tokens, API keys | `**extraHeaders**` |
| localStorage | SPAs storing tokens in localStorage | `**localStorage**` |
| Programmatic | Complex login flows, 2FA | `**hooks.authenticate**` |
## Basic Auth
For sites using HTTP Basic Authentication (the browser popup):
```ts
export default defineUnlighthouseConfig({
auth: {
username: process.env.AUTH_USER,
password: process.env.AUTH_PASS,
},
})
```
Or via CLI:
```bash
unlighthouse --site staging.example.com --auth admin:secretpass
```
## Cookies
Most common for session-based auth. Grab your session cookie from browser DevTools (Application → Cookies):
```ts
export default defineUnlighthouseConfig({
cookies: [
{
name: 'session_id',
value: 'abc123...',
domain: 'example.com', // Must match your site
path: '/',
},
],
})
```
**Getting the cookie value:**
1. Log into your site in Chrome
2. Open DevTools → Application → Cookies
3. Copy the session cookie value
4. Paste into config (or use environment variable)
CLI shorthand:
```bash
unlighthouse --site example.com --cookies "session_id=abc123"
# Multiple cookies
unlighthouse --site example.com --cookies "session_id=abc123;csrf_token=xyz789"
```
## Headers (Bearer Tokens, API Keys)
For APIs or sites expecting `**Authorization**` headers:
```ts
export default defineUnlighthouseConfig({
extraHeaders: {
Authorization: `Bearer ${process.env.API_TOKEN}`,
},
})
```
CLI:
```bash
unlighthouse --site api.example.com --extra-headers "Authorization:Bearer abc123"
```
## Query Params
Some staging environments use URL tokens:
```ts
export default defineUnlighthouseConfig({
defaultQueryParams: {
access_token: process.env.STAGING_TOKEN,
},
})
```
Every scanned URL will include `**?access_token=...**`
## localStorage (SPAs)
For React/Vue/Angular apps storing auth tokens in localStorage:
```ts
export default defineUnlighthouseConfig({
localStorage: {
auth_token: process.env.AUTH_TOKEN,
user_id: '12345',
},
})
```
Unlighthouse sets these before each page loads.
## Programmatic Login (Complex Flows)
For login forms, OAuth flows, or anything the simpler methods can't handle:
```ts
export default defineUnlighthouseConfig({
hooks: {
async authenticate({ page }) {
// Navigate to login
await page.goto('https://example.com/login')
// Fill form
await page.type('input[name="email"]', 'test@example.com')
await page.type('input[name="password"]', process.env.PASSWORD)
// Submit and wait for redirect
await Promise.all([
page.click('button[type="submit"]'),
page.waitForNavigation(),
])
},
},
})
```
This runs once before scanning starts. The session persists for all pages.
## Auth Not Sticking?
If authentication isn't persisting between page scans:
```ts
export default defineUnlighthouseConfig({
puppeteerOptions: {
userDataDir: './.unlighthouse-session', // Persist browser data
},
lighthouseOptions: {
disableStorageReset: true, // Don't clear storage between pages
skipAboutBlank: true,
},
})
```
## Debugging Auth Issues
Can't tell if auth is working? Watch it happen:
```ts
export default defineUnlighthouseConfig({
debug: true,
puppeteerOptions: {
headless: false, // See the browser
slowMo: 100, // Slow it down
},
puppeteerClusterOptions: {
maxConcurrency: 1, // One at a time
},
})
```
Now you can watch the browser and see exactly where auth fails.
See the [**~~Debugging Guide~~**](https://unlighthouse.dev/guide/guides/debugging) for more techniques.
[Edit this page](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/authentication.md)
[Markdown For LLMs](https://unlighthouse.dev/guide/guides/authentication.md)
**Did this page help you? **
Anything that could be done better? :)
Help us improve this page. You can [~~edit this page ~~](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/authentication.md) on GitHub or provide anonymous feedback below.
### **Related **
[**Debugging**](https://unlighthouse.dev/guide/guides/debugging) [**Puppeteer Configuration**](https://unlighthouse.dev/guide/guides/puppeteer) [**Configuration**](https://unlighthouse.dev/guide/guides/config)
[**Debugging** Debug and troubleshoot Unlighthouse scans using logging, browser inspection, and diagnostic tools.](https://unlighthouse.dev/guide/guides/debugging) [**Chrome Dependency** Configure Chrome browser settings for Unlighthouse scanning, including system Chrome usage and custom installations.](https://unlighthouse.dev/guide/guides/chrome-dependency)
**On this page **
- [Quick Reference](#quick-reference)
- [Basic Auth](#basic-auth)
- [Cookies](#cookies)
- [Headers (Bearer Tokens, API Keys)](#headers-bearer-tokens-api-keys)
- [Query Params](#query-params)
- [localStorage (SPAs)](#localstorage-spas)
- [Programmatic Login (Complex Flows)](#programmatic-login-complex-flows)
- [Auth Not Sticking?](#auth-not-sticking)
- [Debugging Auth Issues](#debugging-auth-issues)
---
- **Page:** Chrome Dependency · Unlighthouse
- **Source:** https://unlighthouse.dev/guide/guides/chrome-dependency
- **Description:** Configure Chrome browser settings for Unlighthouse scanning, including system Chrome usage and custom installations.
Star Unlighthouse on GitHubUnlighthouse on GitHub
[**User Guide **](https://unlighthouse.dev/guide/getting-started/installation)
[**Integrations **](https://unlighthouse.dev/integrations/cli)
[**API **](https://unlighthouse.dev/api-doc/config)
**Guides**
# **Chrome Dependency**
[Copy for LLMs](https://unlighthouse.dev/guide/guides/chrome-dependency.md)
Unlighthouse uses your system Chrome installation to keep package size minimal. When Chrome isn't available, it automatically downloads a compatible Chromium binary.
## Disabling system Chrome
You can disable the system chrome usage by modifying the `**chrome.useSystem**` flag.
This will make Unlighthouse download and use the latest Chrome binary instead.
```ts
import { defineUnlighthouseConfig } from 'unlighthouse/config'
export default defineUnlighthouseConfig({
chrome: {
useSystem: false
},
})
```
## Customizing the fallback installer
When Chrome can't be found on your system or if the `**chrome.useSystem: false**` flag is passed, then a fallback will be attempted.
This fallback will download a chrome binary for your system and use that path.
There are a number of options you can customize on this.
- `**chrome.useDownloadFallback**` - Disables the fallback installer
- `**chrome.downloadFallbackVersion**` - Which version of chromium to use (default `**1095492**`)
- `**chrome.downloadFallbackCacheDir**` - Where the binary should be saved (default `**$home/.unlighthouse**`)
```ts
export default defineUnlighthouseConfig({
chrome: {
useDownloadFallback: true,
downloadFallbackVersion: '1095492',
downloadFallbackCacheDir: '/tmp/unlighthouse',
},
})
```
## Using your own chrome path
You can provide your own chrome path by setting `**puppeteerOptions.executablePath**`.
```ts
export default defineUnlighthouseConfig({
puppeteerOptions: {
executablePath: '/usr/bin/chrome',
},
})
```
[Edit this page](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/chrome-dependency.md)
[Markdown For LLMs](https://unlighthouse.dev/guide/guides/chrome-dependency.md)
**Did this page help you? **
Anything that could be done better? :)
Help us improve this page. You can [~~edit this page ~~](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/chrome-dependency.md) on GitHub or provide anonymous feedback below.
### **Related **
[**Puppeteer Configuration**](https://unlighthouse.dev/guide/guides/puppeteer) [**Common Errors**](https://unlighthouse.dev/guide/guides/common-errors) [**Docker**](https://unlighthouse.dev/guide/guides/docker)
[**Authentication** Scan password-protected websites with Unlighthouse. Configure basic auth, cookies, headers, localStorage, and programmatic login flows.](https://unlighthouse.dev/guide/guides/authentication) [**Common Errors** Troubleshoot common issues encountered when running Unlighthouse scans, including browser connection and environment problems.](https://unlighthouse.dev/guide/guides/common-errors)
**On this page **
- [Disabling system Chrome](#disabling-system-chrome)
- [Customizing the fallback installer](#customizing-the-fallback-installer)
- [Using your own chrome path](#using-your-own-chrome-path)
---
- **Page:** Debugging Lighthouse Scans · Unlighthouse
- **Source:** https://unlighthouse.dev/guide/guides/debugging
- **Description:** Debug and troubleshoot Unlighthouse scans using logging, browser inspection, and diagnostic tools.
Star Unlighthouse on GitHubUnlighthouse on GitHub
[**User Guide **](https://unlighthouse.dev/guide/getting-started/installation)
[**Integrations **](https://unlighthouse.dev/integrations/cli)
[**API **](https://unlighthouse.dev/api-doc/config)
**Guides**
# **Debugging Lighthouse Scans**
[Copy for LLMs](https://unlighthouse.dev/guide/guides/debugging.md)
Scan not working? Here's how to figure out why.
## Start Here: Debug Logs
90% of issues become obvious with debug logging:
```bash
unlighthouse --site example.com --debug
```
This shows you:
- What URLs are being discovered
- Which pages are being scanned
- Any errors or timeouts
- Chrome connection status
## Watch the Browser
Can't tell what's happening? Watch it:
```ts
export default defineUnlighthouseConfig({
puppeteerOptions: {
headless: false, // Show the browser window
slowMo: 250, // Slow down so you can see what's happening
devtools: true, // Open Chrome DevTools
},
puppeteerClusterOptions: {
maxConcurrency: 1, // One page at a time
},
})
```
Now you'll see exactly what Unlighthouse sees—useful for:
- Auth issues (is login working?)
- JavaScript errors (check the console)
- Missing content (is the page rendering?)
- Blocked requests (check Network tab)
Visual mode is slow. Only use it to diagnose specific problems.
## Common Issues
### Pages Timing Out
Your site might be slow or have heavy JavaScript:
```ts
export default defineUnlighthouseConfig({
lighthouseOptions: {
maxWaitForLoad: 60000, // Wait up to 60s (default: 45s)
},
})
```
### SPA Not Rendering
JavaScript apps need time to hydrate:
```ts
export default defineUnlighthouseConfig({
scanner: {
skipJavascript: false, // Wait for JS to execute
},
lighthouseOptions: {
maxWaitForLoad: 45000,
},
})
```
### SSL/Certificate Errors
On localhost or staging with self-signed certs:
```ts
export default defineUnlighthouseConfig({
puppeteerOptions: {
ignoreHTTPSErrors: true,
args: ['--ignore-certificate-errors'],
},
})
```
### Chrome Won't Start
Try using bundled Chromium instead of system Chrome:
```ts
export default defineUnlighthouseConfig({
chrome: {
useSystem: false, // Download and use bundled Chromium
},
})
```
Or point to a specific Chrome:
```ts
export default defineUnlighthouseConfig({
puppeteerOptions: {
executablePath: '/usr/bin/google-chrome-stable',
},
})
```
### Failed Network Requests
Log all failed requests to find what's breaking:
```ts
export default defineUnlighthouseConfig({
hooks: {
'puppeteer:before-goto': async (page) => {
page.on('requestfailed', (req) => {
console.log('❌ Failed:', req.url(), req.failure()?.errorText)
})
},
},
})
```
## Still Stuck?
1. Check [**~~Common Errors~~**](https://unlighthouse.dev/guide/guides/common-errors) for known issues
2. Search [**~~GitHub Issues~~**](https://github.com/harlan-zw/unlighthouse/issues)
3. Ask on [**~~Discord~~**](https://discord.gg/275MBUBvgP) with your debug logs
[Edit this page](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/1.debugging.md)
[Markdown For LLMs](https://unlighthouse.dev/guide/guides/debugging.md)
**Did this page help you? **
Anything that could be done better? :)
Help us improve this page. You can [~~edit this page ~~](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/1.debugging.md) on GitHub or provide anonymous feedback below.
### **Related **
[**Common Errors**](https://unlighthouse.dev/guide/guides/common-errors) [**Configuration**](https://unlighthouse.dev/guide/guides/config) [**Puppeteer Configuration**](https://unlighthouse.dev/guide/guides/puppeteer)
[**Configuration** Configure Unlighthouse for your specific needs using configuration files and inline options.](https://unlighthouse.dev/guide/guides/config) [**Authentication** Scan password-protected websites with Unlighthouse. Configure basic auth, cookies, headers, localStorage, and programmatic login flows.](https://unlighthouse.dev/guide/guides/authentication)
**On this page **
- [Start Here: Debug Logs](#start-here-debug-logs)
- [Watch the Browser](#watch-the-browser)
- [Common Issues](#common-issues)
- [Still Stuck?](#still-stuck)
---
- **Page:** Unlighthouse --desktop Flag & Device Configuration · Unlighthouse
- **Source:** https://unlighthouse.dev/guide/guides/device
- **Description:** Run Unlighthouse in desktop mode with the --desktop flag: npx unlighthouse --site --desktop. Configure mobile, desktop, or custom viewports with throttling.
Star Unlighthouse on GitHubUnlighthouse on GitHub
[**User Guide **](https://unlighthouse.dev/guide/getting-started/installation)
[**Integrations **](https://unlighthouse.dev/integrations/cli)
[**API **](https://unlighthouse.dev/api-doc/config)
**Guides**
# **Unlighthouse --desktop Flag & Device Configuration**
[Copy for LLMs](https://unlighthouse.dev/guide/guides/device.md)
Run Unlighthouse in desktop mode with the `**--desktop**` flag:
```bash
npx unlighthouse --site https://example.com --desktop
```
This overrides the default mobile emulation and scans every page using a desktop viewport. Prefer a config file? Set `**scanner.device: 'desktop'**` instead.
## When to use `**--desktop**`
Mobile is the default because Google uses mobile-first indexing. But desktop scans still matter for:
- B2B SaaS dashboards (95%+ desktop traffic)
- Admin panels and internal tools
- Documentation sites
- Benchmarking against PageSpeed Insights desktop scores
The `**--desktop**` flag is equivalent to the `**--device desktop**` long form and takes precedence over any config file setting.
## Device Types
### Desktop Scanning
```ts
import { defineUnlighthouseConfig } from 'unlighthouse/config'
export default defineUnlighthouseConfig({
scanner: {
device: 'desktop',
},
})
```
### Mobile Scanning (Default)
```ts
export default defineUnlighthouseConfig({
scanner: {
device: 'mobile',
},
})
```
## Custom Dimensions
Test specific viewport sizes for responsive breakpoints:
```ts
export default defineUnlighthouseConfig({
lighthouseOptions: {
screenEmulation: {
width: 1800,
height: 1000,
},
},
})
```
## Network Throttling
Throttling simulates slower network and CPU conditions for more realistic performance testing:
```ts
export default defineUnlighthouseConfig({
scanner: {
throttle: true,
},
})
```
Throttling is automatically enabled for production sites and disabled for localhost by default.
[Edit this page](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/device.md)
[Markdown For LLMs](https://unlighthouse.dev/guide/guides/device.md)
**Did this page help you? **
Anything that could be done better? :)
Help us improve this page. You can [~~edit this page ~~](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/device.md) on GitHub or provide anonymous feedback below.
### **Related **
[**Lighthouse Configuration**](https://unlighthouse.dev/guide/guides/lighthouse) [**Config Reference**](https://unlighthouse.dev/api-doc/config) [**Core Web Vitals Glossary**](https://unlighthouse.dev/glossary)
[**Common Errors** Troubleshoot common issues encountered when running Unlighthouse scans, including browser connection and environment problems.](https://unlighthouse.dev/guide/guides/common-errors) [**Docker** Run Unlighthouse site-wide Lighthouse scans in Docker containers. Dockerfile examples and CI/CD configuration.](https://unlighthouse.dev/guide/guides/docker)
**On this page **
- [When to use --desktop](#when-to-use-desktop)
- [Device Types](#device-types)
- [Custom Dimensions](#custom-dimensions)
- [Network Throttling](#network-throttling)
---
- **Page:** Dynamic Sampling · Unlighthouse
- **Source:** https://unlighthouse.dev/guide/guides/dynamic-sampling
- **Description:** Automatically sample similar pages to reduce scan time for sites with many similar URLs like blogs or e-commerce.
Star Unlighthouse on GitHubUnlighthouse on GitHub
[**User Guide **](https://unlighthouse.dev/guide/getting-started/installation)
[**Integrations **](https://unlighthouse.dev/integrations/cli)
[**API **](https://unlighthouse.dev/api-doc/config)
**Guides**
# **Dynamic Sampling**
[Copy for LLMs](https://unlighthouse.dev/guide/guides/dynamic-sampling.md)
Automatically group similar pages and scan only representative samples. This significantly reduces scan time for sites with many similar URLs like blogs, e-commerce product pages, or documentation sites.
## How it works
When dynamic sampling is enabled, it will group paths into chunks based on their path tree.
For example, let's imagine we have a blog on our site and there are hundreds of blog posts. Scanning every blog post will take a long time and may even break Unlighthouse.
The path structure is `**/blog/{post}**`.
Unlighthouse will turn this path structure into groups based on the `**/blog**` prefix. By default, it will sample 5 paths starting with this prefix.
A sample being a random selection of paths within this group.
For example if we have the posts:
- `**/blog/post-a**`
- `**/blog/post-b**`
- `**/blog/post-c**`
- `**/blog/post-d**`
- `**/blog/post-e**`
- `**/blog/post-f**`
- `**/blog/post-g**`
- `**/blog/post-h**`
- `**/blog/post-i**`
After sampling, we may end up with the random selection:
- `**/blog/post-c**`
- `**/blog/post-d**`
- `**/blog/post-e**`
- `**/blog/post-h**`
- `**/blog/post-i**`
## Usage
It is configured using the `**scanner.dynamicSampling**` option.
```ts
import { defineUnlighthouseConfig } from 'unlighthouse/config'
export default defineUnlighthouseConfig({
scanner: {
dynamicSampling: 10, // Number of samples per group (default: 5)
},
})
```
### Disable Dynamic Sampling
```ts
export default defineUnlighthouseConfig({
scanner: {
dynamicSampling: false,
},
})
```
Alternatively, you can disable it using the CLI `**--disable-dynamic-sampling**`.
[Edit this page](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/dynamic-sampling.md)
[Markdown For LLMs](https://unlighthouse.dev/guide/guides/dynamic-sampling.md)
**Did this page help you? **
Anything that could be done better? :)
Help us improve this page. You can [~~edit this page ~~](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/dynamic-sampling.md) on GitHub or provide anonymous feedback below.
### **Related **
[**Route Definitions**](https://unlighthouse.dev/guide/guides/route-definitions) [**Large Sites**](https://unlighthouse.dev/guide/recipes/large-sites) [**URL Discovery**](https://unlighthouse.dev/guide/guides/url-discovery)
[**Docker** Run Unlighthouse site-wide Lighthouse scans in Docker containers. Dockerfile examples and CI/CD configuration.](https://unlighthouse.dev/guide/guides/docker) [**Static Reports** Generate static Lighthouse reports for your entire site. Export as HTML, CSV, or JSON. Deploy to Netlify, CloudFlare, or any static host.](https://unlighthouse.dev/guide/guides/generating-static-reports)
**On this page **
- [How it works](#how-it-works)
- [Usage](#usage)
---
- **Page:** Run Lighthouse in Docker · Unlighthouse
- **Source:** https://unlighthouse.dev/guide/guides/docker
- **Description:** Run Unlighthouse site-wide Lighthouse scans in Docker containers. Dockerfile examples and CI/CD configuration.
Star Unlighthouse on GitHubUnlighthouse on GitHub
[**User Guide **](https://unlighthouse.dev/guide/getting-started/installation)
[**Integrations **](https://unlighthouse.dev/integrations/cli)
[**API **](https://unlighthouse.dev/api-doc/config)
**Guides**
# **Run Lighthouse in Docker**
[Copy for LLMs](https://unlighthouse.dev/guide/guides/docker.md)
Run Unlighthouse in Docker containers for consistent CI/CD environments. Docker requires special Puppeteer configuration due to sandboxing restrictions.
Docker support is community-maintained and experimental. Use the CI integration for best results.
## Unlighthouse Config
It's recommended you only use the `**@unlighthouse/ci**` with Docker. Hosting the client does not have known support.
You will need to remove the Chrome sandbox in a Docker environment, this will require using an `**unlighthouse.config.ts**` file.
```ts
import { defineUnlighthouseConfig } from 'unlighthouse/config'
export default defineUnlighthouseConfig({
puppeteerOptions: {
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-gpu',
'--ignore-certificate-errors',
],
},
})
```
If you're using the `**unlighthouse**` binary instead of the CI integration, then you will need to tell Unlighthouse not to use the server and close when the reports are finished.
```ts
export default defineUnlighthouseConfig({
server: {
open: false,
},
hooks: {
'worker-finished': async () => {
process.exit(0)
},
},
})
```
## Docker File
Please see the following community repos:
- [**~~indykoning—Unlighthouse Docker~~**](https://github.com/indykoning/unlighthouse-docker)
[Edit this page](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/docker.md)
[Markdown For LLMs](https://unlighthouse.dev/guide/guides/docker.md)
**Did this page help you? **
Anything that could be done better? :)
Help us improve this page. You can [~~edit this page ~~](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/docker.md) on GitHub or provide anonymous feedback below.
### **Related **
[**CI Integration**](https://unlighthouse.dev/integrations/ci) [**Puppeteer Configuration**](https://unlighthouse.dev/guide/guides/puppeteer) [**Chrome Dependency**](https://unlighthouse.dev/guide/guides/chrome-dependency)
[**Device Configuration** Run Unlighthouse in desktop mode with the --desktop flag: npx unlighthouse --site \ --desktop. Configure mobile, desktop, or custom viewports with throttling.](https://unlighthouse.dev/guide/guides/device) [**Dynamic Sampling** Automatically sample similar pages to reduce scan time for sites with many similar URLs like blogs or e-commerce.](https://unlighthouse.dev/guide/guides/dynamic-sampling)
**On this page **
- [Unlighthouse Config](#unlighthouse-config)
- [Docker File](#docker-file)
---
- **Page:** Integrations · Unlighthouse
- **Source:** https://unlighthouse.dev/guide/getting-started/integrations
- **Description:** Integrate Unlighthouse into your existing build tools, frameworks, and CI/CD pipelines for automated Lighthouse testing.
Star Unlighthouse on GitHubUnlighthouse on GitHub
[**User Guide **](https://unlighthouse.dev/guide/getting-started/installation)
[**Integrations **](https://unlighthouse.dev/integrations/cli)
[**API **](https://unlighthouse.dev/api-doc/config)
**Getting Started**
# **Integrations**
[Copy for LLMs](https://unlighthouse.dev/guide/getting-started/integrations.md)
Choose how to integrate Unlighthouse into your workflow: manual CLI scans for development, automated CI/CD checks for deployment pipelines, or build tool integration for framework-specific setups.
## Command Line
| **Provider** | **Use Case** |
| --- | --- |
| [**~~CLI~~**](https://unlighthouse.dev/integrations/cli) | Scan a production site such as [**~~unlighthouse.dev~~**](https://unlighthouse.dev/).
You can manually provide a project mapping for [**~~routes definitions~~**](https://unlighthouse.dev/guide/guides/route-definitions). |
| [**~~CI~~**](https://unlighthouse.dev/integrations/ci) | Run scans on sites based on automation events, i.e releasing and make [**~~assertions on scores~~**](https://unlighthouse.dev/integrations/ci#assertions).
Can also be used to generate report sites such as [**~~inspect.unlighthouse.dev~~**](https://inspect.unlighthouse.dev/). |
## Build tools / Frameworks
**Deprecation Notice**: Build tool integrations are deprecated and will be removed in v1.0. We recommend using the CLI or CI integrations instead. [**~~Learn more about deprecations~~**](https://unlighthouse.dev/integration-deprecations)
| **Provider** | **Features** |
| --- | --- |
| [**~~Nuxt.js~~**](https://unlighthouse.dev/integrations/nuxt) | Hot Module Reloading, Automatic Route Discovery |
| [**~~Vite~~**](https://unlighthouse.dev/integrations/vite) | Hot Module Reloading, Automatic Route Discovery |
| [**~~webpack~~**](https://unlighthouse.dev/integrations/webpack) | Hot Module Reloading |
[Edit this page](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/1.getting-started/1.integrations.md)
[Markdown For LLMs](https://unlighthouse.dev/guide/getting-started/integrations.md)
**Did this page help you? **
Anything that could be done better? :)
Help us improve this page. You can [~~edit this page ~~](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/1.getting-started/1.integrations.md) on GitHub or provide anonymous feedback below.
### **Related **
[**CLI Integration**](https://unlighthouse.dev/integrations/cli) [**CI Integration**](https://unlighthouse.dev/integrations/ci) [**Configuration**](https://unlighthouse.dev/guide/guides/config)
[**CLI** Install and run Unlighthouse CLI to scan your entire website with Lighthouse. npm, pnpm, and yarn installation options.](https://unlighthouse.dev/guide/getting-started/unlighthouse-cli) [**How It Works** Learn how Unlighthouse automatically discovers pages, runs Lighthouse audits in parallel, and generates site-wide performance reports.](https://unlighthouse.dev/guide/getting-started/how-it-works)
**On this page **
- [Command Line](#command-line)
- [Build tools / Frameworks](#build-tools-frameworks)
---
- **Page:** Lighthouse Configuration · Unlighthouse
- **Source:** https://unlighthouse.dev/guide/guides/lighthouse
- **Description:** Customize Google Lighthouse audit settings, categories, and performance thresholds within Unlighthouse scans.
Star Unlighthouse on GitHubUnlighthouse on GitHub
[**User Guide **](https://unlighthouse.dev/guide/getting-started/installation)
[**Integrations **](https://unlighthouse.dev/integrations/cli)
[**API **](https://unlighthouse.dev/api-doc/config)
**Guides**
# **Lighthouse Configuration**
[Copy for LLMs](https://unlighthouse.dev/guide/guides/lighthouse.md)
Customize audit categories, performance thresholds, and behavior through the `**lighthouseOptions**` configuration key. Unlighthouse passes these options directly to Google Lighthouse.
```ts
import { defineUnlighthouseConfig } from 'unlighthouse/config'
export default defineUnlighthouseConfig({
lighthouseOptions: {
throttlingMethod: 'devtools',
},
})
```
For complete options, see the [**~~Lighthouse Configuration docs~~**](https://github.com/GoogleChrome/lighthouse/blob/master/docs/configuration.md).
## Aliases
Unlighthouse aims to minimise and simplify configuration, where possible.
For this reason, a number of configurations aliases are provided for your convenience.
- [**~~Switching device: mobile and desktop~~**](https://unlighthouse.dev/guide/guides/device)
- [**~~Toggle Throttling~~**](https://unlighthouse.dev/guide/guides/device#network-throttling)
You can always configure lighthouse directly if you are comfortable with the configuration.
## Selecting Categories
By default, Unlighthouse will scan the categories: `**'performance', 'accessibility', 'best-practices', 'seo'**`.
The performance category measures [**~~Core Web Vitals~~**](https://unlighthouse.dev/glossary) including [**~~LCP~~**](https://unlighthouse.dev/glossary/lcp), [**~~CLS~~**](https://unlighthouse.dev/glossary/cls), and [**~~INP~~**](https://unlighthouse.dev/glossary/inp).
It can be useful to remove certain categories from being scanned to improve scan times. The Unlighthouse UI will adapt to any categories you select.
**Only Performance**
```ts
export default defineUnlighthouseConfig({
lighthouseOptions: {
onlyCategories: ['performance'],
},
})
```
[Edit this page](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/lighthouse.md)
[Markdown For LLMs](https://unlighthouse.dev/guide/guides/lighthouse.md)
**Did this page help you? **
Anything that could be done better? :)
Help us improve this page. You can [~~edit this page ~~](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/lighthouse.md) on GitHub or provide anonymous feedback below.
### **Related **
[**Device Configuration**](https://unlighthouse.dev/guide/guides/device) [**Config Reference**](https://unlighthouse.dev/api-doc/config) [**Core Web Vitals Glossary**](https://unlighthouse.dev/glossary)
[**Static Reports** Generate static Lighthouse reports for your entire site. Export as HTML, CSV, or JSON. Deploy to Netlify, CloudFlare, or any static host.](https://unlighthouse.dev/guide/guides/generating-static-reports) [**Puppeteer** Configure Puppeteer launch options in Unlighthouse: headless mode, Chrome args, viewport settings, executable path, and navigation hooks.](https://unlighthouse.dev/guide/guides/puppeteer)
**On this page **
- [Aliases](#aliases)
- [Selecting Categories](#selecting-categories)
---
- **Page:** Route Definitions · Unlighthouse
- **Source:** https://unlighthouse.dev/guide/guides/route-definitions
- **Description:** Configure route discovery and custom sampling patterns for better page organization and intelligent scanning.
Star Unlighthouse on GitHubUnlighthouse on GitHub
[**User Guide **](https://unlighthouse.dev/guide/getting-started/installation)
[**Integrations **](https://unlighthouse.dev/integrations/cli)
[**API **](https://unlighthouse.dev/api-doc/config)
**Guides**
# **Route Definitions**
[Copy for LLMs](https://unlighthouse.dev/guide/guides/route-definitions.md)
Map URLs to source files for intelligent [**~~dynamic sampling~~**](https://unlighthouse.dev/guide/guides/dynamic-sampling). Framework integrations discover routes automatically; CLI users may need manual configuration for optimal sampling.
## Pages directory
By default, the `**pages/**` dir is scanned for files with extensions `**.vue**` and `**.md**`, from the `**root**` directory.
If your project has a different setup you can modify the configuration.
```ts
import { defineUnlighthouseConfig } from 'unlighthouse/config'
export default defineUnlighthouseConfig({
root: './app',
discovery: {
pagesDir: 'routes',
fileExtensions: ['jsx', 'md'],
},
})
```
## Custom sampling
When you have URL patterns which don't use URL segments or the mapping is failing, it can be useful to map the sampling yourself.
By using the `**customSampling**` option you map regex to a route definition.
In the below example we will map any URL such as `**/q-search-query**`, `**/q-where-is-the-thing**` to a single route definition, which allows the sampling to work.
```ts
export default defineUnlighthouseConfig({
scanner: {
customSampling: {
'/q-(.*?)': {
name: 'search-query',
},
},
},
})
```
[Edit this page](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/route-definitions.md)
[Markdown For LLMs](https://unlighthouse.dev/guide/guides/route-definitions.md)
**Did this page help you? **
Anything that could be done better? :)
Help us improve this page. You can [~~edit this page ~~](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/route-definitions.md) on GitHub or provide anonymous feedback below.
### **Related **
[**Dynamic Sampling**](https://unlighthouse.dev/guide/guides/dynamic-sampling) [**URL Discovery**](https://unlighthouse.dev/guide/guides/url-discovery) [**Configuration**](https://unlighthouse.dev/guide/guides/config)
[**Puppeteer** Configure Puppeteer launch options in Unlighthouse: headless mode, Chrome args, viewport settings, executable path, and navigation hooks.](https://unlighthouse.dev/guide/guides/puppeteer) [**URL Discovery** How Unlighthouse discovers pages using sitemaps, robots.txt, and internal link crawling. Configure URL sources and filters.](https://unlighthouse.dev/guide/guides/url-discovery)
**On this page **
- [Pages directory](#pages-directory)
- [Custom sampling](#custom-sampling)
---
- **Page:** Puppeteer Launch Options · Unlighthouse
- **Source:** https://unlighthouse.dev/guide/guides/puppeteer
- **Description:** Configure Puppeteer launch options in Unlighthouse: headless mode, Chrome args, viewport settings, executable path, and navigation hooks.
Star Unlighthouse on GitHubUnlighthouse on GitHub
[**User Guide **](https://unlighthouse.dev/guide/getting-started/installation)
[**Integrations **](https://unlighthouse.dev/integrations/cli)
[**API **](https://unlighthouse.dev/api-doc/config)
**Guides**
# **Puppeteer Launch Options**
[Copy for LLMs](https://unlighthouse.dev/guide/guides/puppeteer.md)
Unlighthouse uses [**~~Puppeteer~~**](https://pptr.dev/) to control Chrome for Lighthouse audits. Configure browser behavior, navigation hooks, and Chrome flags via `**puppeteerOptions**`.
## All Available Options
```ts
export default defineUnlighthouseConfig({
puppeteerOptions: {
// See: https://pptr.dev/api/puppeteer.launchoptions
},
})
```
Full reference: [**~~Puppeteer LaunchOptions API~~**](https://pptr.dev/api/puppeteer.launchoptions)
## Common Configurations
### Headless Mode
```ts
// Run with visible browser (debugging)
export default defineUnlighthouseConfig({
puppeteerOptions: {
headless: false,
},
})
```
### Custom Chrome Executable
```ts
export default defineUnlighthouseConfig({
puppeteerOptions: {
executablePath: '/usr/bin/google-chrome',
},
})
```
### Chrome Arguments
Common args for CI/Docker environments:
```ts
export default defineUnlighthouseConfig({
puppeteerOptions: {
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
],
},
})
```
### Viewport Settings
```ts
export default defineUnlighthouseConfig({
puppeteerOptions: {
defaultViewport: {
width: 1920,
height: 1080,
},
},
})
```
### Timeout Configuration
```ts
export default defineUnlighthouseConfig({
puppeteerOptions: {
timeout: 60000, // 60 seconds
},
})
```
### User Data Directory
Persist browser data between runs:
```ts
export default defineUnlighthouseConfig({
puppeteerOptions: {
userDataDir: './.puppeteer-data',
},
})
```
## Navigation Hooks
Hook into Puppeteer's page navigation for custom logic.
### Before Page Load
```ts
export default defineUnlighthouseConfig({
hooks: {
'puppeteer:before-goto': async (page) => {
// Set localStorage before navigation
await page.evaluateOnNewDocument((token) => {
localStorage.setItem('auth', token)
}, process.env.AUTH_TOKEN)
},
},
})
```
### Modify Page Content
```ts
export default defineUnlighthouseConfig({
hooks: {
'puppeteer:before-goto': async (page) => {
page.waitForNavigation().then(async () => {
// Remove elements that cause CLS
await page.evaluate(() => {
document.querySelector('.cookie-banner')?.remove()
})
})
},
},
})
```
## Troubleshooting
### Chrome not found
See [**~~Chrome Dependency Guide~~**](https://unlighthouse.dev/guide/guides/chrome-dependency).
### Connection refused in Docker
Add `**--no-sandbox**` and `**--disable-dev-shm-usage**` args.
### Memory issues
Reduce concurrent workers or add `**--disable-dev-shm-usage**`.
[Edit this page](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/puppeteer.md)
[Markdown For LLMs](https://unlighthouse.dev/guide/guides/puppeteer.md)
**Did this page help you? **
Anything that could be done better? :)
Help us improve this page. You can [~~edit this page ~~](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/puppeteer.md) on GitHub or provide anonymous feedback below.
### **Related **
[**Chrome Dependency**](https://unlighthouse.dev/guide/guides/chrome-dependency) [**Authentication**](https://unlighthouse.dev/guide/guides/authentication) [**Docker**](https://unlighthouse.dev/guide/guides/docker)
[**Lighthouse Config** Customize Google Lighthouse audit settings, categories, and performance thresholds within Unlighthouse scans.](https://unlighthouse.dev/guide/guides/lighthouse) [**Route Definitions** Configure route discovery and custom sampling patterns for better page organization and intelligent scanning.](https://unlighthouse.dev/guide/guides/route-definitions)
**On this page **
- [All Available Options](#all-available-options)
- [Common Configurations](#common-configurations)
- [Navigation Hooks](#navigation-hooks)
- [Troubleshooting](#troubleshooting)
---
- **Page:** Generate Lighthouse Reports · Unlighthouse
- **Source:** https://unlighthouse.dev/guide/guides/generating-static-reports
- **Description:** Generate static Lighthouse reports for your entire site. Export as HTML, CSV, or JSON. Deploy to Netlify, CloudFlare, or any static host.
Star Unlighthouse on GitHubUnlighthouse on GitHub
[**User Guide **](https://unlighthouse.dev/guide/getting-started/installation)
[**Integrations **](https://unlighthouse.dev/integrations/cli)
[**API **](https://unlighthouse.dev/api-doc/config)
**Guides**
# **Generate Lighthouse Reports**
[Copy for LLMs](https://unlighthouse.dev/guide/guides/generating-static-reports.md)
Create static, shareable Lighthouse reports for your entire site. Export as interactive HTML dashboards, CSV for spreadsheet analysis, or JSON for CI/CD integration.
## Report Types
| **Format** | **Use Case** | **Command** |
| --- | --- | --- |
| HTML | Shareable dashboards | `**--build-static**` |
| JSON | CI/CD integration | `**--reporter json**` |
| CSV | Spreadsheet analysis | `**--reporter csv**` |
| LHCI | Server upload | `**--reporter lighthouseServer**` |
Unlike single-page Lighthouse reports, Unlighthouse aggregates results across your entire site.
Static reports are perfect for stakeholder reviews, automated deployments, and long-term performance tracking.
## Installation
Install the CLI globally to use `**unlighthouse-ci**`:
```bash
npm install -g @unlighthouse/cli
```
For complete CI features, see the [**~~CI Integration Guide~~**](https://unlighthouse.dev/integrations/ci).
## HTML Reports
You can create static, self-hosted reports for your sites using the CI. This allows you to generate an always up-to-date version of how your site is performing overall.
You can see an example of this here: [**~~https://inspect.unlighthouse.dev/~~**](https://inspect.unlighthouse.dev/).
You can generate a report like this by providing the `**--build-static**` flag.
```bash
unlighthouse-ci --site --build-static
```
This will generate files in your `**outputPath**` (`**.unlighthouse**` by default). You can upload the `**client**` directory to a static host from there.
If you want to preview the static report you can run `**npx sirv-cli .unlighthouse/client**`
Note: You will need to host your site using a web server.
### CloudFlare Pages Example
You should create a CloudFlare Pages site using [**~~Direct Upload~~**](https://developers.cloudflare.com/pages/platform/direct-upload/).
You will use the [**~~wrangler~~**](https://developers.cloudflare.com/pages/platform/using-wrangler) CLI to upload the static report.
You will need to init wrangler and configure it for your requirements.
```bash
wrangler init
```
You can then run the following command to generate the static report and upload it to CloudFlare Pages.
```bash
unlighthouse-ci --site www.example.com --build-static && wrangler pages publish .unlighthouse
```
### GitHub Actions & Netlify Example
This example is for GitHub Actions and deploys a static client build to Netlify.
```yml
name: Assertions and static report
on:
workflow_dispatch:
jobs:
demo:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Dependencies
run: npm add -g @unlighthouse/cli puppeteer
- name: Unlighthouse assertions and client
run: unlighthouse-ci --site --build-static
- name: Deploy report to Netlify
uses: nwtgck/actions-netlify@v3.0
with:
publish-dir: ./.unlighthouse
production-branch: main
production-deploy: true
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: New Release Deploy from GitHub Actions
enable-pull-request-comment: false
enable-commit-comment: true
overwrites-pull-request-comment: true
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }}
timeout-minutes: 1
```
## CSV Reports
You can generate a CSV report by providing the `**--reporter csv**` or `**--reporter csvExpanded**` flag.
```bash
unlighthouse-ci --site --reporter csv
```
Note: This report format is experimental and may change in the future.
This will generate a report like the following (`**csvExpanded**` sample):
```csv
URL,Score,Performance,Accessibility,Best Practices,SEO,Largest Contentful Paint,Cumulative Layout Shift,FID,Blocking,Color Contrast,Headings,Image Alts,Link Names,Errors,Inspector Issues,Images Responsive,Image Aspect Ratio,Indexable,Tap Targets
"/",93,72,100,100,100,3211.44,0,290.36,562.29,1,1,1,1,1,1,1,1,1,1
"/blog",94,77,100,100,100,3911.7,0,205.57,260.03,1,1,1,1,1,1,1,1,1,1
"/blog/2023-april",88,51,100,100,100,4925.84,0,311.24,797.95,1,1,1,1,1,1,1,1,1,1
"/blog/2023-february",96,82,100,100,100,3207.98,0,209.96,302.01,1,1,1,1,1,1,1,1,1,1
"/blog/nuxt-3-migration-cheatsheet",85,43,97,100,100,4373.88,0,1581.52,2820.75,0,1,1,1,1,1,1,1,1,1
"/blog/vue-automatic-component-imports",93,74,97,100,100,1696.51,0,793.36,1314.27,0,1,1,1,1,1,1,1,1,1
"/blog/vue-use-head-v1",82,30,97,100,100,8053.8,0,379.59,1127.99,0,1,1,1,1,1,1,1,1,1
"/projects",92,66,100,100,100,3666.86,0,322.48,625.51,1,1,1,1,1,1,1,1,1,1
"/sponsors",92,69,100,100,100,4438.15,0,362.62,408.63,1,1,1,1,1,1,1,1,1,1
"/talks",98,90,100,100,100,864.86,0,390.93,427.94,1,1,1,1,1,1,1,1,1,1
```
## JSON Reports
You can generate a JSON report by providing the `**--reporter json**` or `**--reporter jsonExpanded**` flag.
```bash
unlighthouse-ci --site --reporter json
```
Note: This report format is experimental and may change in the future.
This will generate a report like the following (`**json**` sample):
```json
[
{
"path": "/",
"score": 0.97,
"performance": 0.87,
"accessibility": 1,
"best-practices": 1,
"seo": 1
},
{
"path": "/blog",
"score": 0.98,
"performance": 0.91,
"accessibility": 1,
"best-practices": 1,
"seo": 1
},
{
"path": "/blog/2023-february",
"score": 0.91,
"performance": 0.65,
"accessibility": 1,
"best-practices": 1,
"seo": 1
},
{
"path": "/blog/modern-package-development",
"score": 0.9,
"performance": 0.61,
"accessibility": 0.97,
"best-practices": 1,
"seo": 1
},
{
"path": "/blog/scale-your-vue-components",
"score": 0.87,
"performance": 0.51,
"accessibility": 0.97,
"best-practices": 1,
"seo": 1
},
{
"path": "/blog/vue-automatic-component-imports",
"score": 0.88,
"performance": 0.53,
"accessibility": 0.97,
"best-practices": 1,
"seo": 1
},
{
"path": "/blog/vue-use-head-v1",
"score": 0.97,
"performance": 0.9,
"accessibility": 0.97,
"best-practices": 1,
"seo": 1
},
{
"path": "/projects",
"score": 0.94,
"performance": 0.77,
"accessibility": 1,
"best-practices": 1,
"seo": 1
},
{
"path": "/sponsors",
"score": 0.97,
"performance": 0.88,
"accessibility": 1,
"best-practices": 1,
"seo": 1
},
{
"path": "/talks",
"score": 0.94,
"performance": 0.74,
"accessibility": 1,
"best-practices": 1,
"seo": 1
}
]
```
## LHCI Reports
You can upload your reports to a Lighthouse CI server using the `**lighthouseServer**` reporter.
You will need to provide the `**--lhci-host**` and `**--lhci-build-token**` flags.
```bash
unlighthouse-ci --site --reporter lighthouseServer --lhci-host --lhci-build-token
```
This will upload your reports to the Lighthouse CI server.
### Basic Auth
You can provide basic auth credentials using the `**--lhci-auth**` flag.
```bash
unlighthouse-ci --site --reporter lighthouseServer --lhci-host --lhci-build-token --lhci-auth :
```
[Edit this page](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/generating-static-reports.md)
[Markdown For LLMs](https://unlighthouse.dev/guide/guides/generating-static-reports.md)
**Did this page help you? **
Anything that could be done better? :)
Help us improve this page. You can [~~edit this page ~~](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/generating-static-reports.md) on GitHub or provide anonymous feedback below.
### **Related **
[**CI Integration**](https://unlighthouse.dev/integrations/ci) [**Configuration**](https://unlighthouse.dev/guide/guides/config) [**UI Customization**](https://unlighthouse.dev/guide/recipes/client)
[**Dynamic Sampling** Automatically sample similar pages to reduce scan time for sites with many similar URLs like blogs or e-commerce.](https://unlighthouse.dev/guide/guides/dynamic-sampling) [**Lighthouse Config** Customize Google Lighthouse audit settings, categories, and performance thresholds within Unlighthouse scans.](https://unlighthouse.dev/guide/guides/lighthouse)
**On this page **
- [Report Types](#report-types)
- [Installation](#installation)
- [HTML Reports](#html-reports)
- [CSV Reports](#csv-reports)
- [JSON Reports](#json-reports)
- [LHCI Reports](#lhci-reports)
---
- **Page:** Customizing the UI · Unlighthouse
- **Source:** https://unlighthouse.dev/guide/recipes/client
- **Description:** Modify Unlighthouse client interface columns and display to show custom metrics and data.
Star Unlighthouse on GitHubUnlighthouse on GitHub
[**User Guide **](https://unlighthouse.dev/guide/getting-started/installation)
[**Integrations **](https://unlighthouse.dev/integrations/cli)
[**API **](https://unlighthouse.dev/api-doc/config)
**Recipes**
# **Customizing the UI**
[Copy for LLMs](https://unlighthouse.dev/guide/recipes/client.md)
Customize the Unlighthouse dashboard to display metrics relevant to your workflow. Replace default columns, add custom audit displays, and configure how results appear.
## Customizing Columns
Replace or add columns to display specific Lighthouse metrics:
### Example: Replace FCP with Server Response Time
```ts
import { defineUnlighthouseConfig } from 'unlighthouse/config'
export default defineUnlighthouseConfig({
hooks: {
'resolved-config': function (config) {
config.client.columns.performance[2] = {
cols: 1,
label: 'Response Time',
tooltip: 'Time for the server to respond',
sortKey: 'numericValue',
key: 'report.audits.server-response-time',
}
},
},
})
```
See the [**~~Column API Reference~~**](https://unlighthouse.dev/api-doc/glossary#columns) for all available column options.
[Edit this page](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/recipes/client.md)
[Markdown For LLMs](https://unlighthouse.dev/guide/recipes/client.md)
**Did this page help you? **
Anything that could be done better? :)
Help us improve this page. You can [~~edit this page ~~](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/recipes/client.md) on GitHub or provide anonymous feedback below.
### **Related **
[**Glossary**](https://unlighthouse.dev/api-doc/glossary) [**Configuration**](https://unlighthouse.dev/guide/guides/config) [**Static Reports**](https://unlighthouse.dev/guide/guides/generating-static-reports)
[**URL Discovery** How Unlighthouse discovers pages using sitemaps, robots.txt, and internal link crawling. Configure URL sources and filters.](https://unlighthouse.dev/guide/guides/url-discovery) [**Improving Accuracy** Optimize Lighthouse scan accuracy with multiple samples and reduced concurrency for more reliable, consistent Core Web Vitals results.](https://unlighthouse.dev/guide/recipes/improving-accuracy)
---
- **Page:** Common Errors · Unlighthouse
- **Source:** https://unlighthouse.dev/guide/guides/common-errors
- **Description:** Troubleshoot common issues encountered when running Unlighthouse scans, including browser connection and environment problems.
Star Unlighthouse on GitHubUnlighthouse on GitHub
[**User Guide **](https://unlighthouse.dev/guide/getting-started/installation)
[**Integrations **](https://unlighthouse.dev/integrations/cli)
[**API **](https://unlighthouse.dev/api-doc/config)
**Guides**
# **Common Errors**
[Copy for LLMs](https://unlighthouse.dev/guide/guides/common-errors.md)
Solutions for frequently encountered issues when running Unlighthouse scans. Ensure you're using the latest version before troubleshooting.
For general debugging techniques, see the [**~~Debugging Guide~~**](https://unlighthouse.dev/guide/guides/debugging).
## `**connect ECONNREFUSED 127.0.0.1:**`
**Example**
> *Error: Unable to launch browser for worker, error message: connect ECONNREFUSED 127.0.0.1:51667*
This error is thrown when Chromium is unable to launch. This happens when puppeteer is unable to connect to the browser. This can be from a number of reasons:
- The environment is not configured correctly, likely when using Windows and WSL.
- You have a firewall or antivirus blocking Chrome or Chromium from launching or connecting to the required port.
- You are using an unsupported version of Chrome or Chromium.
**Windows and WSL Solution**
- Install Puppeteer on WSL following the [**~~documentation~~**](https://pptr.dev/troubleshooting#running-puppeteer-on-wsl-windows-subsystem-for-linux).
- Install Chrome in WSL following the [**~~documentation~~**](https://learn.microsoft.com/en-us/windows/wsl/tutorials/gui-apps#install-google-chrome-for-linux).
**Other Environments**
- You can try disabling the system Chrome, instead using the fallback.
```ts
import { defineUnlighthouseConfig } from 'unlighthouse/config'
export default defineUnlighthouseConfig({
chrome: {
useSystem: false,
},
})
```
## Can't stop the scan with `**Ctrl+C**`
Unlighthouse keeps running after a scan so you can browse the dashboard. Press `**Ctrl+C**` in the terminal to stop it; this shuts down the dev server and closes Chrome.
If `**Ctrl+C**` does nothing, you are almost certainly running inside **Git Bash (MinTTY)** on Windows. MinTTY does not forward `**Ctrl+C**` to native console programs such as Node, so the process keeps running and you have to kill it from Task Manager.
**Solutions**
- Wrap the command with [**~~winpty~~**](https://github.com/rprichard/winpty), which ships with Git for Windows:
```bash
winpty npx unlighthouse --site example.com
```
- Or run the command from **PowerShell**, **Command Prompt**, or **Windows Terminal** instead of Git Bash, where `**Ctrl+C**` works as expected.
[Edit this page](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/common-errors.md)
[Markdown For LLMs](https://unlighthouse.dev/guide/guides/common-errors.md)
**Did this page help you? **
Anything that could be done better? :)
Help us improve this page. You can [~~edit this page ~~](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/common-errors.md) on GitHub or provide anonymous feedback below.
### **Related **
[**Debugging**](https://unlighthouse.dev/guide/guides/debugging) [**Chrome Dependency**](https://unlighthouse.dev/guide/guides/chrome-dependency) [**Puppeteer Configuration**](https://unlighthouse.dev/guide/guides/puppeteer)
[**Chrome Dependency** Configure Chrome browser settings for Unlighthouse scanning, including system Chrome usage and custom installations.](https://unlighthouse.dev/guide/guides/chrome-dependency) [**Device Configuration** Run Unlighthouse in desktop mode with the --desktop flag: npx unlighthouse --site \ --desktop. Configure mobile, desktop, or custom viewports with throttling.](https://unlighthouse.dev/guide/guides/device)
**On this page **
- [connect ECONNREFUSED 127.0.0.1:\](#connect-econnrefused-127001port)
- [Can't stop the scan with Ctrl+C](#cant-stop-the-scan-with-ctrlc)
---
- **Page:** URL Discovery · Unlighthouse
- **Source:** https://unlighthouse.dev/guide/guides/url-discovery
- **Description:** How Unlighthouse discovers pages using sitemaps, robots.txt, and internal link crawling. Configure URL sources and filters.
Star Unlighthouse on GitHubUnlighthouse on GitHub
[**User Guide **](https://unlighthouse.dev/guide/getting-started/installation)
[**Integrations **](https://unlighthouse.dev/integrations/cli)
[**API **](https://unlighthouse.dev/api-doc/config)
**Guides**
# **URL Discovery**
[Copy for LLMs](https://unlighthouse.dev/guide/guides/url-discovery.md)
Unlighthouse automatically finds all pages on your site using multiple discovery methods. Configure which sources to use and filter results to scan exactly what you need.
Unlighthouse discovers URLs through multiple methods:
1. Add the specified `**site**` from `**--site**` or config
2. Manually providing URLs via the `**--urls**` flag or `**urls**` on the provider.
3. `**robotsTxt**` - Reading robots.txt, if it exists. Provides sitemap URLs and disallowed paths.
4. `**sitemap**` - Reading sitemap.xml, if it exists
5. `**crawler**` - Inspecting internal links
6. Using provided static [**~~route definitions~~**](https://unlighthouse.dev/api-doc/glossary#route-definition)
## Robots.txt
When a robots.txt is found, it will attempt to read the sitemap and disallowed paths.
### Disabling robots
You may not want to use the robots.txt in all occasions. For example if you want to scan URLs which are disallowed.
```ts
import { defineUnlighthouseConfig } from 'unlighthouse/config'
export default defineUnlighthouseConfig({
scanner: {
// disable robots.txt scanning
robotsTxt: false,
},
})
```
## Sitemap.xml
By default, the sitemap config will be read from your `**/robots.txt**`. Otherwise, it will fall back to using `**/sitemap.xml**`.
Note: When a sitemap exists with over 50 paths, it will disable the crawler.
### Manual sitemap paths
You may provide an array of sitemap paths to scan.
```ts
export default defineUnlighthouseConfig({
scanner: {
sitemap: [
'/sitemap.xml',
'/sitemap2.xml',
],
},
})
```
### Disabling scan
If you know your site doesn't have a sitemap, it may make sense to disable it.
```ts
export default defineUnlighthouseConfig({
scanner: {
// disable sitemap scanning
sitemap: false,
},
})
```
## Crawler
When enabled, the crawler will inspect the HTML payload of a page and extract internal links. These internal links will be queued up and scanned if they haven't already been scanned.
## Disable crawling
If you have many pages with many internal links, it may be a good idea to disable the crawling.
```ts
export default defineUnlighthouseConfig({
scanner: {
crawler: false,
},
})
```
## Manually Providing URLs
While not recommended for most use cases, you may provide relative URLs within your configuration file, or use the `**--urls**` flag.
This will disable the crawler and sitemap scanning.
Can be provided statically.
```ts
export default defineUnlighthouseConfig({
urls: [
'/about',
'/other-page',
],
})
```
Or you can return a function or promise.
```ts
export default defineUnlighthouseConfig({
urls: async () => await getUrls(),
})
```
Specify explicit relative URLs as a comma-separated list.
```bash
unlighthouse --site https://example.com --urls /about,/other-page
```
[Edit this page](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/url-discovery.md)
[Markdown For LLMs](https://unlighthouse.dev/guide/guides/url-discovery.md)
**Did this page help you? **
Anything that could be done better? :)
Help us improve this page. You can [~~edit this page ~~](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/guides/url-discovery.md) on GitHub or provide anonymous feedback below.
### **Related **
[**Route Definitions**](https://unlighthouse.dev/guide/guides/route-definitions) [**Large Sites**](https://unlighthouse.dev/guide/recipes/large-sites) [**Configuration**](https://unlighthouse.dev/guide/guides/config)
[**Route Definitions** Configure route discovery and custom sampling patterns for better page organization and intelligent scanning.](https://unlighthouse.dev/guide/guides/route-definitions) [**UI Customization** Modify Unlighthouse client interface columns and display to show custom metrics and data.](https://unlighthouse.dev/guide/recipes/client)
**On this page **
- [Robots.txt](#robotstxt)
- [Sitemap.xml](#sitemapxml)
- [Crawler](#crawler)
- [Disable crawling](#disable-crawling)
- [Manually Providing URLs](#manually-providing-urls)
---
- **Page:** Improving Lighthouse Accuracy · Unlighthouse
- **Source:** https://unlighthouse.dev/guide/recipes/improving-accuracy
- **Description:** Optimize Lighthouse scan accuracy with multiple samples and reduced concurrency for more reliable, consistent Core Web Vitals results.
Star Unlighthouse on GitHubUnlighthouse on GitHub
[**User Guide **](https://unlighthouse.dev/guide/getting-started/installation)
[**Integrations **](https://unlighthouse.dev/integrations/cli)
[**API **](https://unlighthouse.dev/api-doc/config)
**Recipes**
# **Improving Lighthouse Accuracy**
[Copy for LLMs](https://unlighthouse.dev/guide/recipes/improving-accuracy.md)
Lighthouse scores can vary 5-10 points between runs due to network conditions, CPU load, and browser state. These techniques improve consistency for reliable [**~~Core Web Vitals~~**](https://unlighthouse.dev/glossary) measurement.
## Why Scores Vary
Single Lighthouse runs can fluctuate by 5-10 points due to:
- CPU load from other browser tabs or processes
- Network latency variations
- Memory pressure
- Background service workers
For reliable performance monitoring, use multiple samples.
## Multiple Samples Per URL
Run Lighthouse multiple times and average the results for better accuracy:
```ts
import { defineUnlighthouseConfig } from 'unlighthouse/config'
export default defineUnlighthouseConfig({
scanner: {
samples: 3, // Run 3 scans per URL and average results
},
})
```
Use `**samples: 3**` for development, `**samples: 5**` for CI/production audits.
## Reduce Parallel Scans
Limit concurrent workers to reduce CPU contention and improve score consistency:
```ts
import { defineUnlighthouseConfig } from 'unlighthouse/config'
export default defineUnlighthouseConfig({
puppeteerClusterOptions: {
maxConcurrency: 1, // Single worker for maximum accuracy
},
})
```
## Enable Throttling
Network throttling simulates real-world conditions and reduces score variability:
```ts
export default defineUnlighthouseConfig({
scanner: {
throttle: true, // Simulate 4G network
},
})
```
## Recommended Production Config
For the most accurate results:
```ts
export default defineUnlighthouseConfig({
scanner: {
samples: 5,
throttle: true,
},
puppeteerClusterOptions: {
maxConcurrency: 1,
},
})
```
Higher accuracy increases scan time significantly. Balance accuracy needs with scan duration.
[Edit this page](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/recipes/improving-accuracy.md)
[Markdown For LLMs](https://unlighthouse.dev/guide/recipes/improving-accuracy.md)
**Did this page help you? **
Anything that could be done better? :)
Help us improve this page. You can [~~edit this page ~~](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/recipes/improving-accuracy.md) on GitHub or provide anonymous feedback below.
### **Related **
[**Configuration**](https://unlighthouse.dev/guide/guides/config) [**Device Configuration**](https://unlighthouse.dev/guide/guides/device) [**Core Web Vitals Glossary**](https://unlighthouse.dev/glossary)
[**UI Customization** Modify Unlighthouse client interface columns and display to show custom metrics and data.](https://unlighthouse.dev/guide/recipes/client) [**Large Sites** Scan large websites with thousands of pages efficiently. Configure sampling, URL filtering, and optimization strategies for bulk Lighthouse testing.](https://unlighthouse.dev/guide/recipes/large-sites)
**On this page **
- [Why Scores Vary](#why-scores-vary)
- [Multiple Samples Per URL](#multiple-samples-per-url)
- [Reduce Parallel Scans](#reduce-parallel-scans)
- [Enable Throttling](#enable-throttling)
- [Recommended Production Config](#recommended-production-config)
---
- **Page:** Single-Page Applications · Unlighthouse
- **Source:** https://unlighthouse.dev/guide/recipes/spa
- **Description:** Configure Unlighthouse to scan single-page applications (SPAs) with client-side routing like React, Vue, and Angular apps.
Star Unlighthouse on GitHubUnlighthouse on GitHub
[**User Guide **](https://unlighthouse.dev/guide/getting-started/installation)
[**Integrations **](https://unlighthouse.dev/integrations/cli)
[**API **](https://unlighthouse.dev/api-doc/config)
**Recipes**
# **Single-Page Applications**
[Copy for LLMs](https://unlighthouse.dev/guide/recipes/spa.md)
Scan React, Vue, Angular, and other SPAs with client-side routing. SPAs require JavaScript execution for link discovery and accurate [**~~Core Web Vitals~~**](https://unlighthouse.dev/glossary) measurement.
## Enable JavaScript Execution
Allow Puppeteer to execute JavaScript before extracting page content:
```ts
import { defineUnlighthouseConfig } from 'unlighthouse/config'
export default defineUnlighthouseConfig({
scanner: {
skipJavascript: false, // Enable JS execution for SPAs
},
})
```
## Wait for Hydration
SPAs often need time to hydrate. Configure wait conditions:
```ts
export default defineUnlighthouseConfig({
scanner: {
skipJavascript: false,
},
lighthouseOptions: {
maxWaitForLoad: 45000, // Wait up to 45s for page load
},
})
```
## Provide URLs Manually
If automatic crawling misses routes, provide them explicitly:
```ts
export default defineUnlighthouseConfig({
urls: [
'/',
'/about',
'/products',
'/contact',
],
scanner: {
skipJavascript: false,
},
})
```
## SPA Performance Considerations
SPAs typically have worse [**~~LCP~~**](https://unlighthouse.dev/glossary/lcp) scores because:
- Content renders after JavaScript execution
- Initial HTML is often empty or minimal
- Hydration adds to [**~~INP~~**](https://unlighthouse.dev/glossary/inp) delays
Consider SSR or SSG for content-heavy pages to improve Core Web Vitals.
Enabling JavaScript execution increases scan time but is necessary for accurate SPA scanning.
[Edit this page](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/recipes/spa.md)
[Markdown For LLMs](https://unlighthouse.dev/guide/recipes/spa.md)
**Did this page help you? **
Anything that could be done better? :)
Help us improve this page. You can [~~edit this page ~~](https://github.com/harlan-zw/unlighthouse/edit/main/docs/1.guide/recipes/spa.md) on GitHub or provide anonymous feedback below.
### **Related **
[**URL Discovery**](https://unlighthouse.dev/guide/guides/url-discovery) [**Puppeteer Configuration**](https://unlighthouse.dev/guide/guides/puppeteer) [**LCP for SPAs**](https://unlighthouse.dev/glossary/lcp)
[**Large Sites** Scan large websites with thousands of pages efficiently. Configure sampling, URL filtering, and optimization strategies for bulk Lighthouse testing.](https://unlighthouse.dev/guide/recipes/large-sites) [**CLI** Scan your entire website with Lighthouse from the command line. Alternative to lighthouse CLI that audits all pages automatically.](https://unlighthouse.dev/integrations/cli)
**On this page **
- [Enable JavaScript Execution](#enable-javascript-execution)
- [Wait for Hydration](#wait-for-hydration)
- [Provide URLs Manually](#provide-urls-manually)
- [SPA Performance Considerations](#spa-performance-considerations)
---
- **Page:** Lighthouse on Authenticated Pages with Playwright · Unlighthouse
- **Source:** https://unlighthouse.dev/learn-lighthouse/playwright/authentication
- **Description:** Run Lighthouse audits on pages behind login. Learn how to preserve authentication state when Lighthouse opens a new browser context.
Star Unlighthouse on GitHubUnlighthouse on GitHub
# **Lighthouse on Authenticated Pages with Playwright**
Run Lighthouse audits on pages behind login. Learn how to preserve authentication state when Lighthouse opens a new browser context.
[Harlan Wilton](https://x.com/harlan-zw) Published **Jan 27, 2026** Updated **Aug 9, 2026**
Running Lighthouse on pages that require authentication is tricky. Lighthouse opens a fresh page context, which means your login state doesn't automatically carry over.
## The Problem
When you navigate to a protected page with [**~~Playwright~~**](https://playwright.dev) and then run Lighthouse:
```js
// This won't work as expected
await page.goto('https://app.example.com/login')
await page.fill('#email', 'user@example.com')
await page.fill('#password', 'password')
await page.click('button[type="submit"]')
await page.waitForURL('**/dashboard')
// Lighthouse opens a NEW page — login state is lost
const result = await lighthouse('https://app.example.com/dashboard', { port: PORT })
// ❌ Redirects to login page
```
Lighthouse creates its own page to run audits, discarding Playwright's authenticated session.
## Solution: Storage State
Playwright can save and restore session state (cookies, localStorage, sessionStorage). Save it after login, then configure Lighthouse to use the same browser context.
### Step 1: Save Authentication State
```js
import { writeFileSync } from 'node:fs'
import { chromium } from '@playwright/test'
const PORT = 9222
async function saveAuthState() {
const browser = await chromium.launch({
args: [`--remote-debugging-port=${PORT}`],
})
const context = await browser.newContext()
const page = await context.newPage()
// Perform login
await page.goto('https://app.example.com/login')
await page.fill('#email', 'user@example.com')
await page.fill('#password', 'password')
await page.click('button[type="submit"]')
await page.waitForURL('**/dashboard')
// Save storage state
const storageState = await context.storageState()
writeFileSync('auth.json', JSON.stringify(storageState))
await browser.close()
}
```
### Step 2: Run Lighthouse with Saved State
```js
import { readFileSync } from 'node:fs'
import { chromium } from '@playwright/test'
import lighthouse from 'lighthouse'
const PORT = 9222
async function auditAuthenticatedPage(url) {
const browser = await chromium.launch({
args: [`--remote-debugging-port=${PORT}`],
})
// Load saved authentication state
const storageState = JSON.parse(readFileSync('auth.json', 'utf-8'))
const context = await browser.newContext({ storageState })
const page = await context.newPage()
// Navigate to authenticated page
await page.goto(url, { waitUntil: 'networkidle' })
// Run Lighthouse with storage reset disabled
const result = await lighthouse(url, {
port: PORT,
disableStorageReset: true, // Critical: preserves cookies/storage
logLevel: 'error',
})
await browser.close()
return result.lhr
}
auditAuthenticatedPage('https://app.example.com/dashboard')
```
**Critical**: Always set `**disableStorageReset: true**`. Without this, Lighthouse clears cookies and storage before the audit, logging you out.
## Complete Working Example
Full script that handles login and audit in one flow:
```js
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { chromium } from '@playwright/test'
import lighthouse from 'lighthouse'
const PORT = 9222
const AUTH_FILE = 'auth.json'
async function login(context) {
const page = await context.newPage()
await page.goto('https://app.example.com/login')
await page.fill('#email', process.env.TEST_USER_EMAIL)
await page.fill('#password', process.env.TEST_USER_PASSWORD)
await page.click('button[type="submit"]')
await page.waitForURL('**/dashboard')
await page.close()
}
async function auditWithAuth(url) {
const browser = await chromium.launch({
args: [`--remote-debugging-port=${PORT}`],
})
let context
// Reuse saved auth if available
if (existsSync(AUTH_FILE)) {
const storageState = JSON.parse(readFileSync(AUTH_FILE, 'utf-8'))
context = await browser.newContext({ storageState })
}
else {
context = await browser.newContext()
await login(context)
// Save for future runs
const state = await context.storageState()
writeFileSync(AUTH_FILE, JSON.stringify(state))
}
const page = await context.newPage()
await page.goto(url, { waitUntil: 'networkidle' })
const result = await lighthouse(url, {
port: PORT,
disableStorageReset: true,
output: 'html',
})
writeFileSync('lighthouse-report.html', result.report)
await browser.close()
return result.lhr
}
auditWithAuth('https://app.example.com/dashboard')
```
## Simplifying with Playwright Project Dependencies
Modern Playwright (v1.31+) allows defining [**~~setup projects~~**](https://playwright.dev/docs/test-global-setup-teardown#project-dependencies). This separates login logic from your tests.
**1. Configure `**playwright.config.ts**`:**
```ts
import { defineConfig } from '@playwright/test'
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'lighthouse',
use: {
// Automatically load auth state
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'], // Run setup first
},
],
})
```
**2. Create setup file `**auth.setup.ts**`:**
```ts
import { test as setup } from '@playwright/test'
setup('authenticate', async ({ page }) => {
await page.goto('https://app.example.com/login')
// ... perform login ...
await page.context().storageState({ path: 'playwright/.auth/user.json' })
})
```
**3. Run audit (no login logic needed):**
```ts
test('dashboard performance', async ({ page }) => {
// Page is already authenticated!
await page.goto('https://app.example.com/dashboard')
// Connect Lighthouse to this authenticated session
// (Use the port and disableStorageReset pattern)
})
```
## Alternative: Cookie Injection via CDP
For token-based auth where `**storageState**` doesn't work (e.g., custom auth headers), inject cookies directly:
```js
import { chromium } from '@playwright/test'
import lighthouse from 'lighthouse'
const PORT = 9222
async function auditWithCookies(url, cookies) {
const browser = await chromium.launch({
args: [`--remote-debugging-port=${PORT}`],
})
const context = await browser.newContext()
// Add cookies to context
await context.addCookies(cookies)
const page = await context.newPage()
await page.goto(url, { waitUntil: 'networkidle' })
const result = await lighthouse(url, {
port: PORT,
disableStorageReset: true,
})
await browser.close()
return result.lhr
}
// Usage
auditWithCookies('https://app.example.com/dashboard', [
{
name: 'session_token',
value: 'your-token-here',
domain: 'app.example.com',
path: '/',
},
])
```
## Handling Session Expiry
Long test runs cause session tokens to expire. Add a check:
```js
async function verifyAuthenticated(page, context) {
await page.goto('https://app.example.com/dashboard')
// Check if redirected to login
if (page.url().includes('/login')) {
await login(context)
const state = await context.storageState()
writeFileSync(AUTH_FILE, JSON.stringify(state))
await page.goto('https://app.example.com/dashboard')
}
}
```
## Common Pitfalls
| **Issue** | **Solution** |
| --- | --- |
| Session lost after audit | Add `**disableStorageReset: true**` |
| CSRF token invalid | Re-login before each audit |
| Cookies not applying | Check `**domain**` matches exactly |
| Auth works in Playwright but not Lighthouse | Lighthouse uses a new page - check that cookies are on the context, not just the page |
### **Related **
[**Troubleshooting**](https://unlighthouse.dev/learn-lighthouse/playwright/troubleshooting) [**CI/CD Integration**](https://unlighthouse.dev/learn-lighthouse/playwright/ci-cd) [**Playwright Guide**](https://unlighthouse.dev/learn-lighthouse/playwright)
[**Playwright** Run Google Lighthouse audits with Playwright for automated performance, accessibility, and SEO testing. Complete setup guide with code examples.](https://unlighthouse.dev/learn-lighthouse/playwright) [**CI/CD** Automate Lighthouse audits with Playwright in GitHub Actions. Run performance tests on every PR with thresholds and artifact reports.](https://unlighthouse.dev/learn-lighthouse/playwright/ci-cd)
**On this page **
- [The Problem](#the-problem)
- [Solution: Storage State](#solution-storage-state)
- [Complete Working Example](#complete-working-example)
- [Simplifying with Playwright Project Dependencies](#simplifying-with-playwright-project-dependencies)
- [Alternative: Cookie Injection via CDP](#alternative-cookie-injection-via-cdp)
- [Handling Session Expiry](#handling-session-expiry)
- [Common Pitfalls](#common-pitfalls)
---
- **Page:** Playwright Lighthouse in GitHub Actions · Unlighthouse
- **Source:** https://unlighthouse.dev/learn-lighthouse/playwright/ci-cd
- **Description:** Automate Lighthouse audits with Playwright in GitHub Actions. Run performance tests on every PR with thresholds and artifact reports.
Star Unlighthouse on GitHubUnlighthouse on GitHub
# **Playwright Lighthouse in GitHub Actions**
Automate Lighthouse audits with Playwright in GitHub Actions. Run performance tests on every PR with thresholds and artifact reports.
[Harlan Wilton](https://x.com/harlan-zw) Published **Jan 27, 2026** Updated **Aug 9, 2026**
Run Lighthouse audits automatically on every pull request. This guide covers GitHub Actions setup with Playwright-based Lighthouse integration.
**Alternative**: For dedicated CI tooling, consider [**~~Lighthouse CI~~**](https://unlighthouse.dev/learn-lighthouse/lighthouse-ci/github-actions) which is purpose-built for CI pipelines with historical tracking and [**~~GitHub~~**](https://github.com) status checks.
## Basic Workflow (Script)
Create `**.github/workflows/lighthouse.yml**`:
```yaml
name: Lighthouse Audit
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install chromium --with-deps
- name: Run Lighthouse audit
run: node scripts/lighthouse-audit.js
- name: Upload report
uses: actions/upload-artifact@v4
if: always()
with:
name: lighthouse-report
path: lighthouse-report.html
retention-days: 14
```
Create `**scripts/lighthouse-audit.js**`:
```js
import { writeFileSync } from 'node:fs'
import { chromium } from '@playwright/test'
import lighthouse from 'lighthouse'
const PORT = 9222
const URL = process.env.AUDIT_URL || 'https://example.com'
const THRESHOLDS = {
'performance': 80,
'accessibility': 90,
'best-practices': 80,
'seo': 80,
}
async function audit() {
const browser = await chromium.launch({
args: [`--remote-debugging-port=${PORT}`],
})
const page = await browser.newPage()
await page.goto(URL, { waitUntil: 'networkidle' })
const result = await lighthouse(URL, {
port: PORT,
output: 'html',
logLevel: 'error',
})
writeFileSync('lighthouse-report.html', result.report)
const { categories } = result.lhr
const scores = {
performance: Math.round(categories.performance.score * 100),
accessibility: Math.round(categories.accessibility.score * 100),
bestPractices: Math.round(categories['best-practices'].score * 100),
seo: Math.round(categories.seo.score * 100),
}
// Save scores for PR comment
writeFileSync('lighthouse-results.json', JSON.stringify(scores, null, 2))
let failed = false
for (const [key, threshold] of Object.entries(THRESHOLDS)) {
const score = scores[key === 'best-practices' ? 'bestPractices' : key]
const status = score >= threshold ? '✅' : '❌'
console.log(`${status} ${key}: ${score} (threshold: ${threshold})`)
if (score < threshold)
failed = true
}
await browser.close()
if (failed) {
console.error('\nLighthouse audit failed to meet thresholds')
process.exit(1)
}
}
audit()
```
## Testing Preview Deployments
For [**~~Vercel~~**](https://vercel.com), [**~~Netlify~~**](https://netlify.com), or Cloudflare Pages preview URLs (using the script approach):
```yaml
# ... (same as above)
- name: Run Lighthouse
run: node scripts/lighthouse-audit.js
env:
AUDIT_URL: ${{ github.event.deployment_status.target_url }}
```
### Testing Preview Deployments (Playwright Test)
If you are using the `**playwright.config.ts**` setup, override the `**baseURL**`:
```yaml
- name: Run Playwright Lighthouse
run: npx playwright test
env:
PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }}
```
## Testing Multiple URLs
Audit several pages in one workflow:
```js
// scripts/lighthouse-audit.js
const URLS = [
'https://example.com/',
'https://example.com/pricing',
'https://example.com/docs',
]
async function auditAll() {
const browser = await chromium.launch({
args: [`--remote-debugging-port=${PORT}`],
})
const results = []
for (const url of URLS) {
const page = await browser.newPage()
await page.goto(url, { waitUntil: 'networkidle' })
const result = await lighthouse(url, {
port: PORT,
logLevel: 'error',
})
results.push({
url,
performance: Math.round(result.lhr.categories.performance.score * 100),
accessibility: Math.round(result.lhr.categories.accessibility.score * 100),
})
await page.close()
}
await browser.close()
console.table(results)
// Fail if any page is below threshold
const failed = results.some(r => r.performance < 80)
if (failed)
process.exit(1)
}
```
## With Authentication
For protected pages, use saved auth state:
```yaml
- name: Run Lighthouse on authenticated pages
run: node scripts/lighthouse-auth-audit.js
env:
TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
```
See [**~~Authentication Guide~~**](https://unlighthouse.dev/learn-lighthouse/playwright/authentication) for the full script.
## Reducing Variance
Lighthouse scores vary between runs. Run multiple times and use median:
```js
async function auditWithMedian(url, runs = 3) {
const scores = []
for (let i = 0; i < runs; i++) {
const result = await lighthouse(url, { port: PORT, logLevel: 'error' })
scores.push(result.lhr.categories.performance.score * 100)
}
scores.sort((a, b) => a - b)
return scores[Math.floor(scores.length / 2)] // Median
}
```
Run at least three audits and compare the median. Keep the runner size, Chrome version, Lighthouse version, and throttling settings fixed between builds. Playwright recommends one worker in CI when reproducibility matters.
## PR Comments with Results
Post Lighthouse scores as a PR comment:
```yaml
- name: Comment PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
import fs from 'fs'
const results = JSON.parse(fs.readFileSync('lighthouse-results.json'))
const body = `## Lighthouse Results
| Metric | Score |
|--------|-------|
| Performance | ${results.performance} |
| Accessibility | ${results.accessibility} |
| Best Practices | ${results.bestPractices} |
| SEO | ${results.seo} | `
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body
})
```
## Installing browsers in CI
[**~~Playwright does not recommend caching browser binaries~~**](https://playwright.dev/docs/ci#caching-browsers). Restoring the cache often takes as long as downloading the browser, and Linux system dependencies still need a separate install. Install only Chromium for this job:
```yaml
- name: Install Chromium and system dependencies
run: npx playwright install chromium --with-deps
```
## Full Production Workflow
Use Playwright's [`**webServer**`](https://playwright.dev/docs/test-webserver) configuration to handle starting/stopping your app automatically.
**1. Configure `**playwright.config.ts**`:**
```ts
import { defineConfig } from '@playwright/test'
export default defineConfig({
webServer: {
command: 'npm run preview',
port: 4173,
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
},
})
```
**2. Update GitHub Actions Workflow:**
```yaml
name: Lighthouse CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx playwright install chromium --with-deps
- run: npm run build
# Playwright starts the server automatically!
- name: Run Lighthouse
run: npx playwright test
```
## Scaling with Sharding
Lighthouse audits are slow. For large sites, run tests in parallel across multiple machines using [**~~Playwright~~**](https://playwright.dev) [**~~Sharding~~**](https://playwright.dev/docs/test-sharding).
```yaml
jobs:
lighthouse:
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
runs-on: ubuntu-latest
steps:
# ... setup steps ...
- name: Run Lighthouse (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- name: Upload Blob Report
if: always()
uses: actions/upload-artifact@v4
with:
name: blob-report-${{ matrix.shardIndex }}
path: blob-report
retention-days: 1
```
You can then merge these reports in a separate job using `**npx playwright merge-reports**`.
## When to Use Lighthouse CI Instead
For advanced CI features, [**~~Lighthouse CI~~**](https://unlighthouse.dev/learn-lighthouse/lighthouse-ci) provides:
- Historical tracking across builds
- GitHub status checks (not just comments)
- Baseline comparisons
- Built-in assertion presets
The Playwright approach works well for simpler setups or when you need custom control over the browser context (like authentication).
### **Related **
[**Lighthouse CI with GitHub Actions**](https://unlighthouse.dev/learn-lighthouse/lighthouse-ci/github-actions) [**Authentication**](https://unlighthouse.dev/learn-lighthouse/playwright/authentication) [**Troubleshooting**](https://unlighthouse.dev/learn-lighthouse/playwright/troubleshooting)
[**Authentication** Run Lighthouse audits on pages behind login. Learn how to preserve authentication state when Lighthouse opens a new browser context.](https://unlighthouse.dev/learn-lighthouse/playwright/authentication) [**Troubleshooting** Fix common issues when running Lighthouse with Playwright: port conflicts, authentication problems, flaky scores, and Chrome version mismatches.](https://unlighthouse.dev/learn-lighthouse/playwright/troubleshooting)
**On this page **
- [Basic Workflow (Script)](#basic-workflow-script)
- [Testing Preview Deployments](#testing-preview-deployments)
- [Testing Multiple URLs](#testing-multiple-urls)
- [With Authentication](#with-authentication)
- [Reducing Variance](#reducing-variance)
- [PR Comments with Results](#pr-comments-with-results)
- [Installing browsers in CI](#installing-browsers-in-ci)
- [Full Production Workflow](#full-production-workflow)
- [Scaling with Sharding](#scaling-with-sharding)
- [When to Use Lighthouse CI Instead](#when-to-use-lighthouse-ci-instead)
---
- **Page:** Integration Deprecations · Unlighthouse
- **Source:** https://unlighthouse.dev/integration-deprecations
- **Description:** Build tool integrations are deprecated in v1.0. Learn about migration paths and alternatives.
Star Unlighthouse on GitHubUnlighthouse on GitHub
# **Integration Deprecations**
[Copy for LLMs](https://unlighthouse.dev/integration-deprecations.md)
The following build tool integrations are deprecated and will be removed in v1.0:
- `**@unlighthouse/nuxt**`
- `**@unlighthouse/vite**`
- `**@unlighthouse/webpack**`
Start migrating to [**~~CLI~~**](https://unlighthouse.dev/integrations/cli) or [**~~CI~~**](https://unlighthouse.dev/integrations/ci) integrations for continued support.
## Background
When Unlighthouse was being developed, the goal was to make it as simple as possible to use with your development site.
To allow for this, integrations where added that set up Unlighthouse automatically for you.
This provided the site URL, automatic rescans on page updates and route discovery, which allowed for smarter sampling of dynamic routes.
## Why Deprecate?
Simply, the integrations are too difficult to maintain, error-prone and provide low-value.
In nearly all raised issues related to integration, they weren't needed and the CLI could be used instead.
## Upgrading
You should remove any of the following packages from your project.
- `**@unlighthouse/nuxt**`
- `**@unlighthouse/vite**`
- `**@unlighthouse/webpack**`
Instead, you should simply use the CLI.
```bash
npx unlighthouse --site localhost:3000
```
The HMR integration be solved by manually rescanning routes using the UI.
The route discovery will still work when scanned in the root directory or an app with `**pages**`.
[Edit this page](https://github.com/harlan-zw/unlighthouse/edit/main/docs/integration-deprecations.md)
[Markdown For LLMs](https://unlighthouse.dev/integration-deprecations.md)
**Did this page help you? **
Anything that could be done better? :)
Help us improve this page. You can [~~edit this page ~~](https://github.com/harlan-zw/unlighthouse/edit/main/docs/integration-deprecations.md) on GitHub or provide anonymous feedback below.
### **Related **
[**CLI Integration**](https://unlighthouse.dev/integrations/cli) [**CI Integration**](https://unlighthouse.dev/integrations/ci) [**Configuration**](https://unlighthouse.dev/guide/guides/config)
**On this page **
- [Background](#background)
- [Why Deprecate?](#why-deprecate)
- [Upgrading](#upgrading)