Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# check-http-status changelog

## 2.2.0 - Unreleased

### New

- Crawled pages record the first `h1`, the number of H1s (`h1Count`) and the `metaDescription`
- Assets record where they came from (`assetType`: `image`, `script`, `stylesheet`, `icon`, `media` or `iframe`)
- Images record their `alt` texts and `title` attributes, per page and in total, and `missingAlt` when an `<img>` has no `alt` attribute
- Excel and CSV reports have Asset Type, H1, # H1, Meta Description, Alt Text, Image Title and Missing Alt columns
- HTML report: filters for images, scripts, stylesheets, missing title, missing or multiple H1, missing meta description and missing alt text

### Fixed

- HTML report: search only looked inside the selected filter (*All issues* by default), so a URL without an issue was never found. Filter counts now follow the search, and a search with no matches in the selected filter offers to show all matches
- HTML report: search no longer matches every link on a page whose URL contains the search text; it looks in the URL, redirect, title, H1, link text and alt text

## 2.1.0 - Oct 2, 2026

### New
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,10 @@ await writeReport(report, 'report.xlsx', 'xlsx');
| `error` | Connection error (timeout, DNS, TLS…) if the request failed. |
| `type`, `linkType`, `scope` | `internal`/`external`, `page`/`asset`, and whether it was crawled, excluded, checked or skipped. |
| `redirectTo`, `redirectChain`, `finalUrl`, `finalStatus`, `redirectCount` | Redirect details. `note` is set for loops or too many redirects. |
| `foundOn` | `[{ page, anchors, count, kind }]`, where `kind` is `link`, `redirect` or `sitemap`. |
| `title`, `contentType`, `responseTime`, `depth`, `inSitemap`, `orphan` | Page details. |
| `foundOn` | `[{ page, anchors, count, kind }]`, where `kind` is `link`, `redirect` or `sitemap`. Images also have `alt`, `imageTitle` and `missingAlt` for that page. |
| `title`, `h1`, `h1Count`, `metaDescription` | Crawled pages: `<title>`, the first `<h1>` and how many there are, and the meta description. |
| `assetType`, `alt`, `imageTitle`, `missingAlt` | Assets: `image`, `script`, `stylesheet`, `icon`, `media` or `iframe`. Images: every alt text and `title` used for it, and whether an `<img>` has no `alt` attribute (`alt=""` counts as set). |
| `contentType`, `responseTime`, `depth`, `inSitemap`, `orphan` | Page details. |

## Config file

Expand Down
17 changes: 17 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ declare namespace checkHttpStatus {
/** Times the link appears on that page. */
count: number;
kind: 'link' | 'redirect' | 'sitemap' | 'seed';
/** Images only: `alt` texts used on that page. */
alt?: string[];
/** Images only: `title` attributes used on that page. */
imageTitle?: string[];
/** Images only: `<img>` tags on that page without an `alt` attribute. */
missingAlt?: number;
}

interface RedirectHop {
Expand All @@ -167,6 +173,8 @@ declare namespace checkHttpStatus {
error: string;
type: 'internal' | 'external';
linkType: 'page' | 'asset';
/** Assets only: the tag it was found in. */
assetType: '' | 'image' | 'script' | 'stylesheet' | 'icon' | 'media' | 'iframe';
scope: Scope;
redirectTo: string;
redirectChain: RedirectHop[];
Expand All @@ -179,6 +187,15 @@ declare namespace checkHttpStatus {
note: string;
foundOn: FoundOn[];
title: string;
/** Crawled pages: first `<h1>` and how many there are. */
h1: string;
h1Count: number;
metaDescription: string;
/** Images: every `alt` text and `title` attribute used for it. */
alt: string[];
imageTitle: string[];
/** Images: some `<img>` has no `alt` attribute (`alt=""` is fine). */
missingAlt: boolean;
contentType: string;
responseTime: number | null;
depth: number;
Expand Down
69 changes: 58 additions & 11 deletions lib/checker.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ class Checker {
return { depth, 'scope': 'crawl' };
}

discover(raw, { from = null, fromUrl = null, kind = 'link', anchor = '', base, isAsset = false, seed = null } = {}) {
discover(raw, { from = null, fromUrl = null, kind = 'link', anchor = '', base, assetType = '', image = null, seed = null } = {}) {
const isAsset = !!assetType;
const url = this.normalize(raw, base || (from && from.url) || undefined);
if (!url) {
return;
Expand All @@ -206,13 +207,17 @@ class Checker {
}

record = {
'assetType': assetType,
'contentType': '',
'crawled': false,
'depth': depth,
'error': '',
'h1': '',
'h1Count': 0,
'internal': this.isInternal(url),
'isStart': seed === 'crawl',
'linkType': isAsset ? 'asset' : 'page',
'metaDescription': '',
'note': '',
'queued': false,
'redirectHops': kind === 'redirect' ? from.redirectHops + 1 : 0,
Expand Down Expand Up @@ -249,6 +254,18 @@ class Checker {
if (anchor) {
source.anchors.add(anchor);
}

if (image) {
source.alt = source.alt || new Set();
source.imageTitle = source.imageTitle || new Set();
source.missingAlt = (source.missingAlt || 0) + (image.alt === null ? 1 : 0);
if (image.alt) {
source.alt.add(image.alt);
}
if (image.title) {
source.imageTitle.add(image.title);
}
}
}
}

Expand Down Expand Up @@ -434,8 +451,8 @@ class Checker {

this.discover(target.href, {
'anchor': '(redirect)',
'assetType': record.assetType,
'from': record,
'isAsset': record.linkType === 'asset',
'kind': 'redirect'
});
return;
Expand All @@ -450,7 +467,12 @@ class Checker {

extractLinks(record, html) {
const $ = cheerio.load(html);
record.title = $('title').first().text().replace(/\s+/g, ' ').trim().slice(0, 300);
const text = (node) => node.text().replace(/\s+/g, ' ').trim().slice(0, 300);
record.title = text($('title').first());
record.h1 = text($('h1').first());
record.h1Count = $('h1').length;
record.metaDescription = ($('meta[name]').filter((_, el) => $(el).attr('name').toLowerCase() === 'description').first().attr('content') || '')
.replace(/\s+/g, ' ').trim().slice(0, 500);

let base = record.url;
const baseHref = $('base[href]').attr('href');
Expand Down Expand Up @@ -489,11 +511,19 @@ class Checker {

for (const [selector, attr, label] of assets) {
$(selector).each((_, el) => {
this.discover($(el).attr(attr), {
const $el = $(el);
// `alt=""` marks a decorative image; only a missing attribute counts as missing.
const image = label === 'image' ? {
'alt': $el.attr('alt') === undefined ? null : $el.attr('alt').replace(/\s+/g, ' ').trim().slice(0, 300),
'title': ($el.attr('title') || '').replace(/\s+/g, ' ').trim().slice(0, 300)
} : null;

this.discover($el.attr(attr), {
'anchor': `(${label})`,
'assetType': label,
'base': base,
'from': record,
'isAsset': true
image
});
});
}
Expand Down Expand Up @@ -573,15 +603,27 @@ class Checker {
toResult(record) {
const { chain, loop } = record.redirectTo ? this.redirectChain(record) : { 'chain': [], 'loop': false };
const final = chain[chain.length - 1];
const foundOn = [...record.sources].map(([page, source]) => ({
'anchors': [...source.anchors],
'count': source.count,
'kind': source.kind,
'page': page
}));
const foundOn = [...record.sources].map(([page, source]) => {
const entry = {
'anchors': [...source.anchors],
'count': source.count,
'kind': source.kind,
'page': page
};
if (source.alt) {
entry.alt = [...source.alt];
entry.imageTitle = [...source.imageTitle];
entry.missingAlt = source.missingAlt;
}

return entry;
});
const unique = (key) => [...new Set(foundOn.flatMap((source) => source[key] || []))];
const inSitemap = foundOn.some((source) => source.kind === 'sitemap');

return {
'alt': unique('alt'),
'assetType': record.assetType,
'botProtection': record.botProtection || '',
'category': categorize(record),
'contentType': record.contentType,
Expand All @@ -591,8 +633,13 @@ class Checker {
'finalStatus': final ? final.status : null,
'finalUrl': final ? final.url : '',
'foundOn': foundOn,
'h1': record.h1,
'h1Count': record.h1Count,
'imageTitle': unique('imageTitle'),
'inSitemap': inSitemap,
'linkType': record.linkType,
'metaDescription': record.metaDescription,
'missingAlt': foundOn.some((source) => source.missingAlt > 0),
'note': loop ? 'Redirect loop' : record.note,
'orphan': this.crawlMode && inSitemap && !record.isStart && !this.isLinked(record),
'redirectChain': chain,
Expand Down
7 changes: 7 additions & 0 deletions lib/report/csv.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,18 @@ const COLUMNS = [
['category', 'Category'],
['type', 'Type'],
['linkType', 'Link Type'],
['assetType', 'Asset Type'],
['scope', 'Crawl'],
['redirectCount', '# Redirects'],
['finalUrl', 'Final URL'],
['finalStatus', 'Final Status'],
['title', 'Title'],
['h1', 'H1'],
['h1Count', '# H1'],
['metaDescription', 'Meta Description'],
['alt', 'Alt Text'],
['imageTitle', 'Image Title'],
['missingAlt', 'Missing Alt'],
['contentType', 'Content Type'],
['depth', 'Depth'],
['responseTime', 'Response (ms)'],
Expand Down
7 changes: 7 additions & 0 deletions lib/report/excel.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,18 @@ const URL_COLUMNS = [
CATEGORY,
{ 'header': 'Type', 'key': 'type', 'width': 10 },
{ 'header': 'Link Type', 'key': 'linkType', 'width': 10 },
{ 'header': 'Asset Type', 'key': 'assetType', 'width': 11 },
{ 'header': 'Crawl', 'key': 'scope', 'width': 22 },
{ 'header': '# Redirects', 'key': 'redirectCount', 'width': 11 },
FINAL_URL,
FINAL_STATUS,
{ 'header': 'Title', 'key': 'title', 'width': 40 },
{ 'header': 'H1', 'key': 'h1', 'width': 40 },
{ 'header': '# H1', 'key': 'h1Count', 'width': 6 },
{ 'header': 'Meta Description', 'key': 'metaDescription', 'width': 60 },
{ 'header': 'Alt Text', 'key': 'alt', 'width': 40 },
{ 'header': 'Image Title', 'key': 'imageTitle', 'width': 30 },
{ 'header': 'Missing Alt', 'key': 'missingAlt', 'width': 11 },
{ 'header': 'Content Type', 'key': 'contentType', 'width': 20 },
{ 'header': 'Depth', 'key': 'depth', 'width': 7 },
{ 'header': 'Response (ms)', 'key': 'responseTime', 'width': 13 },
Expand Down
10 changes: 10 additions & 0 deletions lib/report/rows.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,27 @@ function finalUrlLabel(result) {
const byTypeThenUrl = (a, b) => (a.type === b.type ? 0 : a.type === 'internal' ? -1 : 1) || a.url.localeCompare(b.url);
const byIssueThenUrl = (a, b) => ISSUE_ORDER.indexOf(a.category) - ISSUE_ORDER.indexOf(b.category) || a.url.localeCompare(b.url);

// Older reports have no SEO fields.
const list = (values) => (values || []).join(' | ');

function toRow(result) {
return {
'alt': list(result.alt),
'assetType': result.assetType || '',
'category': result.category,
'contentType': result.contentType,
'depth': result.depth,
'finalStatus': result.finalStatus === null ? '' : result.finalStatus,
'finalUrl': finalUrlLabel(result),
'foundOn': foundOnLabel(result),
'foundOnCount': result.foundOn.length,
'h1': result.h1 || '',
'h1Count': result.crawled ? result.h1Count || 0 : '',
'imageTitle': list(result.imageTitle),
'inSitemap': result.inSitemap ? 'Yes' : '',
'linkType': result.linkType,
'metaDescription': result.metaDescription || '',
'missingAlt': result.missingAlt ? 'Yes' : '',
'orphan': result.orphan ? 'Yes' : '',
'redirectCount': result.redirectCount || '',
'redirectTo': result.redirectTo,
Expand Down
Loading
Loading