From 370bc80bee4199f15e40f9f523ffbc3446d71c0d Mon Sep 17 00:00:00 2001 From: Sami Ahmed Siddiqui Date: Mon, 19 Oct 2026 18:09:53 +0500 Subject: [PATCH 1/3] Record H1, meta description, asset type and image alt text Crawled pages get h1, h1Count and metaDescription. Assets get assetType, and images get their alt texts, title attributes and missingAlt, per page and in total. Excel and CSV reports have the new columns. --- README.md | 6 ++-- index.d.ts | 17 +++++++++++ lib/checker.js | 69 ++++++++++++++++++++++++++++++++++++------- lib/report/csv.js | 7 +++++ lib/report/excel.js | 7 +++++ lib/report/rows.js | 10 +++++++ test/checker.test.js | 31 +++++++++++++++++++ test/fixtures/site.js | 8 ++--- 8 files changed, 138 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 9492e6a..03dd1a6 100644 --- a/README.md +++ b/README.md @@ -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: ``, 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 diff --git a/index.d.ts b/index.d.ts index df79f9a..c1684f2 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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 { @@ -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[]; @@ -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; diff --git a/lib/checker.js b/lib/checker.js index ed3ef5e..2ac8b94 100644 --- a/lib/checker.js +++ b/lib/checker.js @@ -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; @@ -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, @@ -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); + } + } } } @@ -434,8 +451,8 @@ class Checker { this.discover(target.href, { 'anchor': '(redirect)', + 'assetType': record.assetType, 'from': record, - 'isAsset': record.linkType === 'asset', 'kind': 'redirect' }); return; @@ -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'); @@ -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 }); }); } @@ -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, @@ -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, diff --git a/lib/report/csv.js b/lib/report/csv.js index e8f902d..a3388fa 100644 --- a/lib/report/csv.js +++ b/lib/report/csv.js @@ -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)'], diff --git a/lib/report/excel.js b/lib/report/excel.js index 861ac6d..ef7ea1d 100644 --- a/lib/report/excel.js +++ b/lib/report/excel.js @@ -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 }, diff --git a/lib/report/rows.js b/lib/report/rows.js index b566ac1..cd4839a 100644 --- a/lib/report/rows.js +++ b/lib/report/rows.js @@ -44,8 +44,13 @@ 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, @@ -53,8 +58,13 @@ function toRow(result) { '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, diff --git a/test/checker.test.js b/test/checker.test.js index 9cb8b41..060f0a5 100644 --- a/test/checker.test.js +++ b/test/checker.test.js @@ -118,6 +118,37 @@ describe('crawl mode', () => { assert.strictEqual(image.linkType, 'asset'); assert.strictEqual(image.status, 404); assert.strictEqual(find(report, '/assets/style.css').status, 200); + assert.strictEqual(find(report, '/assets/style.css').assetType, 'stylesheet'); + assert.strictEqual(find(report, '/favicon.ico').assetType, 'icon'); + assert.strictEqual(find(report, '/').assetType, ''); + }); + + test('records H1 and meta description for crawled pages', () => { + const home = find(report, '/'); + assert.strictEqual(home.h1, 'Gear for every trail'); + assert.strictEqual(home.h1Count, 1); + assert.strictEqual(home.metaDescription, 'Tents, backpacks and stoves.'); + + const about = find(report, '/about-us'); + assert.strictEqual(about.h1, 'About'); + assert.strictEqual(about.h1Count, 2); + assert.strictEqual(about.metaDescription, ''); + assert.strictEqual(find(report, '/privacy').h1Count, 0); + }); + + test('records alt text and title for images, per page', () => { + const hero = find(report, '/images/hero.jpg'); + assert.strictEqual(hero.assetType, 'image'); + assert.deepStrictEqual(hero.alt, ['Hero']); + assert.deepStrictEqual(hero.imageTitle, ['Hero banner']); + assert.strictEqual(hero.missingAlt, true); + + const onHome = hero.foundOn.find((source) => source.page === site.origin + '/'); + assert.deepStrictEqual([onHome.alt, onHome.missingAlt], [['Hero'], 0]); + const onAbout = hero.foundOn.find((source) => source.page === site.origin + '/about-us'); + assert.deepStrictEqual([onAbout.alt, onAbout.imageTitle, onAbout.missingAlt], [[], ['Hero banner'], 1]); + + assert.strictEqual(find(report, '/images/missing-trail.jpg').missingAlt, false); }); test('reads sitemap indexes and gzipped sitemaps, and flags orphan pages', () => { diff --git a/test/fixtures/site.js b/test/fixtures/site.js index 0717d2b..2701606 100644 --- a/test/fixtures/site.js +++ b/test/fixtures/site.js @@ -4,8 +4,8 @@ const http = require('http'); const zlib = require('zlib'); // Fake "Acme Outdoor" site with every kind of problem, plus a fake external site. -function layout(title, body) { - return `<!doctype html><html><head><title>${title} | Acme Outdoor` + +function layout(title, body, head = '') { + return `${title} | Acme Outdoor${head}` + '' + '' + `
${body}
` + @@ -14,8 +14,8 @@ function layout(title, body) { } const PAGES = { - '/': layout('Home', '

Gear for every trail

Tents Backpacks Summer sale Help centre Hero'), - '/about-us': layout('About us', '

Founded in 2009.

Meet the team Our partners Careers'), + '/': layout('Home', '

Gear for every trail

Tents Backpacks Summer sale Help centre Hero', ''), + '/about-us': layout('About us', '

About

Our story

Founded in 2009.

Meet the team Our partners Careers'), '/blog/': layout('Blog', 'Best hiking trails Packing list Old news'), '/blog/best-hiking-trails': layout('Best hiking trails', '

Our picks.

Get a tent Trail guide Source article Trail'), '/blog/packing-list': layout('Packing list', 'Backpacks Stoves Checklist PDF'), From ae20a6227ec1666b028f420e4e53e214e406b33c Mon Sep 17 00:00:00 2001 From: Sami Ahmed Siddiqui Date: Mon, 19 Oct 2026 18:09:54 +0500 Subject: [PATCH 2/3] Fix HTML report search; add SEO and asset filters Search only looked inside the selected filter (All issues by default), so URLs without an issue were never found. Filter counts now follow the search, and a search with no matches in the filter offers to show all. Search no longer matches every link on a page whose URL contains the text. --- lib/report/template.html | 76 +++++++++++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 17 deletions(-) diff --git a/lib/report/template.html b/lib/report/template.html index 8555c1d..aeb45b1 100644 --- a/lib/report/template.html +++ b/lib/report/template.html @@ -60,6 +60,7 @@ .anchor { color: var(--muted); } .pager { display: flex; gap: 8px; align-items: center; justify-content: flex-end; margin-top: 10px; color: var(--muted); } .empty { text-align: center; color: var(--muted); padding: 30px; } + .empty button { margin-top: 8px; } footer { color: var(--muted); font-size: 12px; text-align: center; margin-top: 24px; } @@ -72,7 +73,7 @@

HTTP status report

- +
@@ -126,11 +127,48 @@

HTTP status report

['external', 'External', function (r) { return r.type === 'external'; }], ['excluded', 'Excluded', function (r) { return r.scope === 'excluded'; }], ['asset', 'Assets', function (r) { return r.linkType === 'asset'; }], + ['image', 'Images', function (r) { return assetType(r) === 'image'; }], + ['script', 'Scripts', function (r) { return assetType(r) === 'script'; }], + ['stylesheet', 'Stylesheets', function (r) { return assetType(r) === 'stylesheet'; }], + ['noTitle', 'Missing title', function (r) { return isPage(r) && !r.title; }], + ['noH1', 'Missing H1', function (r) { return isPage(r) && r.h1Count === 0; }], + ['multiH1', 'Multiple H1', function (r) { return isPage(r) && r.h1Count > 1; }], + ['noDescription', 'Missing meta description', function (r) { return isPage(r) && r.h1Count !== undefined && !r.metaDescription; }], + ['noAlt', 'Missing alt text', function (r) { return r.missingAlt; }], ['orphan', 'Orphan pages', function (r) { return r.orphan; }], ['robots', 'Blocked by robots.txt', function (r) { return r.scope === 'robots'; }], ['notchecked', 'Not checked', function (r) { return r.category === 'Not checked'; }] ]; var PAGE_SIZE = 100; + + function isPage(r) { return r.crawled && r.linkType === 'page' && r.category === 'OK'; } + // Reports from before 2.2 have no assetType; the link text says "(image)". + function assetType(r) { + if (r.linkType !== 'asset') return ''; + if (r.assetType) return r.assetType; + for (var i = 0; i < r.foundOn.length; i++) { + var m = r.foundOn[i].anchors.join(' ').match(/\((image|script|stylesheet|icon|media|iframe)\)/); + if (m) return m[1]; + } + return ''; + } + + // The text a search looks in. Pages a URL was found on are left out: they would match every link on a page. + function matches(r, q) { + if (!q) return true; + var fields = [r.url, r.redirectTo, r.finalUrl, r.title, r.h1].concat(r.alt || [], r.imageTitle || []); + for (var i = 0; i < fields.length; i++) { + if (fields[i] && String(fields[i]).toLowerCase().indexOf(q) !== -1) return true; + } + return r.foundOn.some(function (s) { + return s.anchors.some(function (a) { return a.toLowerCase().indexOf(q) !== -1; }); + }); + } + + function searched() { + var q = state.query.toLowerCase(); + return q ? results.filter(function (r) { return matches(r, q); }) : results; + } var state = { filter: countOf('issues') ? 'issues' : 'all', query: '', page: 0, sort: null, dir: 1 }; var expanded = {}; @@ -167,16 +205,7 @@

HTTP status report

function filtered() { var fn = FILTERS.filter(function (f) { return f[0] === state.filter; })[0][2]; - var q = state.query.toLowerCase(); - var rows = results.filter(fn); - if (q) { - rows = rows.filter(function (r) { - if (r.url.toLowerCase().indexOf(q) !== -1 || (r.redirectTo || '').toLowerCase().indexOf(q) !== -1) return true; - return r.foundOn.some(function (s) { - return s.page.toLowerCase().indexOf(q) !== -1 || s.anchors.some(function (a) { return a.toLowerCase().indexOf(q) !== -1; }); - }); - }); - } + var rows = searched().filter(fn); if (state.sort) { var val = { status: function (r) { return r.status == null ? (r.error ? 999 : 1000) : r.status; }, @@ -206,7 +235,8 @@

HTTP status report

if (r.error) sub += '
' + esc(r.error) + '
'; var tags = '' + esc(r.type) + '' + esc(SCOPES[r.scope] || r.scope) + ''; - if (r.linkType === 'asset') tags += 'asset'; + if (r.linkType === 'asset') tags += '' + esc(assetType(r) || 'asset') + ''; + if (r.missingAlt) tags += 'no alt'; if (r.inSitemap) tags += 'sitemap'; if (r.orphan) tags += 'orphan'; @@ -228,12 +258,17 @@

HTTP status report

detail += '

Found on

    ' + r.foundOn.map(function (s) { var via = s.kind === 'redirect' ? ' redirect' : s.kind === 'sitemap' ? ' sitemap' : ''; var anchors = s.anchors.filter(function (a) { return a.charAt(0) !== '('; }); + var alt = s.alt ? (s.alt.length ? ' — alt “' + esc(s.alt.join('”, “')) + '”' : '') + (s.missingAlt ? ' no alt' : '') : ''; return '
  • ' + esc(s.page) + '' + via + - (anchors.length ? ' — “' + esc(anchors.join('”, “')) + '”' : '') + + (anchors.length ? ' — “' + esc(anchors.join('”, “')) + '”' : '') + alt + (s.count > 1 ? ' ×' + s.count + '' : '') + '
  • '; }).join('') + '
'; } if (r.title) detail += '
Title: ' + esc(r.title) + '
'; + if (r.h1) detail += '
H1: ' + esc(r.h1) + (r.h1Count > 1 ? ' (' + r.h1Count + ' H1s)' : '') + '
'; + if (r.metaDescription) detail += '
Meta description: ' + esc(r.metaDescription) + '
'; + if (r.alt && r.alt.length) detail += '
Alt text: “' + esc(r.alt.join('”, “')) + '”
'; + if (r.imageTitle && r.imageTitle.length) detail += '
Image title: “' + esc(r.imageTitle.join('”, “')) + '”
'; if (r.responseTime != null) detail += '
Response: ' + r.responseTime + ' ms' + (r.contentType ? ' · ' + esc(r.contentType) : '') + '
'; html += '' + detail + ''; } @@ -241,8 +276,9 @@

HTTP status report

} function render() { + var pool = searched(); $('chips').innerHTML = FILTERS.map(function (f) { - var n = results.filter(f[2]).length; + var n = pool.filter(f[2]).length; if (!n && ['all', 'issues', state.filter].indexOf(f[0]) === -1) return ''; return ''; }).join(''); @@ -252,7 +288,12 @@

HTTP status report

if (state.page >= pages) state.page = pages - 1; var slice = rows.slice(state.page * PAGE_SIZE, (state.page + 1) * PAGE_SIZE); - $('tbody').innerHTML = slice.length ? slice.map(rowHtml).join('') : 'No URLs match this filter.'; + var empty = 'No URLs match this filter.'; + // The search found URLs, just not under the selected filter. + if (state.query && state.filter !== 'all' && pool.length) { + empty = 'No matches in this filter.
'; + } + $('tbody').innerHTML = slice.length ? slice.map(rowHtml).join('') : '' + empty + ''; $('pager').innerHTML = rows.length > PAGE_SIZE ? 'Page ' + (state.page + 1) + ' of ' + pages + ' · ' + rows.length.toLocaleString() + ' URLs' : '' + rows.length.toLocaleString() + ' URLs'; @@ -273,10 +314,10 @@

HTTP status report

} $('csvBtn').addEventListener('click', function () { - var lines = [['URL', 'Found On', 'Redirect To', 'Status', 'Category', 'Type', 'Final URL', 'Final Status'].join(',')]; + var lines = [['URL', 'Found On', 'Redirect To', 'Status', 'Category', 'Type', 'Final URL', 'Final Status', 'Title', 'H1', 'Meta Description', 'Alt Text'].join(',')]; filtered().forEach(function (r) { var foundOn = r.foundOn.map(function (s) { return s.page; }).join('\n'); - lines.push([r.url, foundOn, r.redirectTo, r.error || r.status, r.category, r.type, r.finalUrl, r.finalStatus].map(csvCell).join(',')); + lines.push([r.url, foundOn, r.redirectTo, r.error || r.status, r.category, r.type, r.finalUrl, r.finalStatus, r.title, r.h1, r.metaDescription, (r.alt || []).join(' | ')].map(csvCell).join(',')); }); download('http-status-report.csv', 'text/csv', '\ufeff' + lines.join('\r\n')); }); @@ -301,6 +342,7 @@

HTTP status report

render(); }); $('tbody').addEventListener('click', function (e) { + if (e.target.closest('[data-filter]')) return setFilter(e); if (e.target.closest('a')) return; var tr = e.target.closest('tr.row'); if (!tr) return; From 5a8ba0104a330d7e9e625643e34832b751393aeb Mon Sep 17 00:00:00 2001 From: Sami Ahmed Siddiqui Date: Mon, 19 Oct 2026 18:09:55 +0500 Subject: [PATCH 3/3] Release v2.2.0 --- CHANGELOG.md | 15 +++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e55fa7a..42ab514 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `` 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 diff --git a/package-lock.json b/package-lock.json index b17484b..d4c8ea6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "check-http-status", - "version": "2.1.0", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "check-http-status", - "version": "2.1.0", + "version": "2.2.0", "license": "MIT", "dependencies": { "cheerio": "^1.0.0", diff --git a/package.json b/package.json index 11bbc7e..58dea81 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "check-http-status", "description": "Crawl a website or check a list of URLs: HTTP status codes, redirect chains, broken links and the pages they were found on. Export to Excel, CSV, JSON or an HTML report.", - "version": "2.1.0", + "version": "2.2.0", "main": "index.js", "types": "index.d.ts", "bin": {