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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# check-http-status changelog

## 2.2.0 - Unreleased
## 2.2.0 - Oct 19, 2026

### New

Expand All @@ -13,6 +13,7 @@
### 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: every value is now HTML-escaped, including status codes and counts, so an edited or crafted JSON report can't inject markup
- 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
Expand Down
94 changes: 68 additions & 26 deletions docs/demo.html

Large diffs are not rendered by default.

Binary file modified docs/sample-report.xlsx
Binary file not shown.
18 changes: 9 additions & 9 deletions lib/report/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ <h1 id="title">HTTP status report</h1>
];
if (summary.orphans) tiles.push(['warn', 'orphan', summary.orphans, 'Orphan pages']);
$('stats').innerHTML = tiles.map(function (t) {
return '<button type="button" class="stat ' + t[0] + '" data-filter="' + esc(t[1]) + '"><b>' + t[2].toLocaleString() + '</b><span>' + esc(t[3]) + '</span></button>';
return '<button type="button" class="stat ' + t[0] + '" data-filter="' + esc(t[1]) + '"><b>' + esc(t[2].toLocaleString()) + '</b><span>' + esc(t[3]) + '</span></button>';
}).join('');

function filtered() {
Expand All @@ -222,14 +222,14 @@ <h1 id="title">HTTP status report</h1>
}

function rowHtml(r) {
var cls = 'c-' + r.category.replace(/\s/g, '');
var cls = esc('c-' + r.category.replace(/\s/g, ''));
var label = r.error ? 'Error' : (r.status == null ? '—' : r.status);
var sub = '';
if (r.botProtection) sub += '<div class="sub">' + esc(r.note) + '</div>';
if (r.redirectTo) {
sub += '<div class="sub">→ ' + esc(r.redirectTo) + '</div>';
if (r.redirectCount > 1 || r.note) {
sub += '<div class="sub">⇒ ' + esc(r.finalUrl) + (r.finalStatus != null ? ' (' + r.finalStatus + ')' : '') + ' · ' + r.redirectCount + ' hops' + (r.note ? ' · ' + esc(r.note) : '') + '</div>';
sub += '<div class="sub">⇒ ' + esc(r.finalUrl) + (r.finalStatus != null ? ' (' + esc(r.finalStatus) + ')' : '') + ' · ' + esc(r.redirectCount) + ' hops' + (r.note ? ' · ' + esc(r.note) : '') + '</div>';
}
}
if (r.error) sub += '<div class="sub">' + esc(r.error) + '</div>';
Expand All @@ -245,13 +245,13 @@ <h1 id="title">HTTP status report</h1>
'<td><span class="pill ' + cls + '" title="' + esc(r.error || (r.status + ' ' + r.statusText)) + '">' + esc(label) + '</span><div class="sub">' + esc(r.category === 'Not checked' ? 'not checked' : r.category) + '</div></td>' +
'<td class="url"><a href="' + esc(r.url) + '" target="_blank" rel="noopener noreferrer">' + esc(r.url) + '</a>' + sub + '</td>' +
'<td>' + tags + '</td>' +
'<td>' + (r.foundOn.length ? (open ? '▾ ' : '▸ ') + r.foundOn.length + ' page' + (r.foundOn.length > 1 ? 's' : '') : '<span class="tag">start</span>') + '</td></tr>';
'<td>' + (r.foundOn.length ? (open ? '▾ ' : '▸ ') + esc(r.foundOn.length) + ' page' + (r.foundOn.length > 1 ? 's' : '') : '<span class="tag">start</span>') + '</td></tr>';

if (open) {
var detail = '';
if (r.redirectChain.length) {
detail += '<h4>Redirect chain</h4><ul><li>' + esc(r.url) + ' (' + r.status + ')</li>' + r.redirectChain.map(function (h) {
return '<li>→ ' + esc(h.url) + ' (' + (h.status == null ? '?' : h.status) + ')</li>';
detail += '<h4>Redirect chain</h4><ul><li>' + esc(r.url) + ' (' + esc(r.status) + ')</li>' + r.redirectChain.map(function (h) {
return '<li>→ ' + esc(h.url) + ' (' + (h.status == null ? '?' : esc(h.status)) + ')</li>';
}).join('') + '</ul>';
}
if (r.foundOn.length) {
Expand All @@ -261,15 +261,15 @@ <h1 id="title">HTTP status report</h1>
var alt = s.alt ? (s.alt.length ? ' <span class="anchor">— alt “' + esc(s.alt.join('”, “')) + '”</span>' : '') + (s.missingAlt ? ' <span class="tag">no alt</span>' : '') : '';
return '<li><a href="' + esc(s.page) + '" target="_blank" rel="noopener noreferrer">' + esc(s.page) + '</a>' + via +
(anchors.length ? ' <span class="anchor">— “' + esc(anchors.join('”, “')) + '”</span>' : '') + alt +
(s.count > 1 ? ' <span class="tag">×' + s.count + '</span>' : '') + '</li>';
(s.count > 1 ? ' <span class="tag">×' + esc(s.count) + '</span>' : '') + '</li>';
}).join('') + '</ul>';
}
if (r.title) detail += '<div class="sub">Title: ' + esc(r.title) + '</div>';
if (r.h1) detail += '<div class="sub">H1: ' + esc(r.h1) + (r.h1Count > 1 ? ' (' + r.h1Count + ' H1s)' : '') + '</div>';
if (r.h1) detail += '<div class="sub">H1: ' + esc(r.h1) + (r.h1Count > 1 ? ' (' + esc(r.h1Count) + ' H1s)' : '') + '</div>';
if (r.metaDescription) detail += '<div class="sub">Meta description: ' + esc(r.metaDescription) + '</div>';
if (r.alt && r.alt.length) detail += '<div class="sub">Alt text: “' + esc(r.alt.join('”, “')) + '”</div>';
if (r.imageTitle && r.imageTitle.length) detail += '<div class="sub">Image title: “' + esc(r.imageTitle.join('”, “')) + '”</div>';
if (r.responseTime != null) detail += '<div class="sub">Response: ' + r.responseTime + ' ms' + (r.contentType ? ' · ' + esc(r.contentType) : '') + '</div>';
if (r.responseTime != null) detail += '<div class="sub">Response: ' + esc(r.responseTime) + ' ms' + (r.contentType ? ' · ' + esc(r.contentType) : '') + '</div>';
html += '<tr class="detail"><td></td><td colspan="3">' + detail + '</td></tr>';
}
return html;
Expand Down
2 changes: 1 addition & 1 deletion test/checker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ describe('crawl mode', () => {
});

test('reports DNS failures as errors', () => {
const careers = report.results.find((result) => result.url.startsWith('https://jobs.example.invalid'));
const careers = report.results.find((result) => new URL(result.url).hostname === 'jobs.example.invalid');
assert.strictEqual(careers.category, 'Error');
assert.ok(careers.error);
});
Expand Down
Loading