wiki-formant 0.81.0 → 0.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,6 +27,11 @@
27
27
  // URLs come from /sitemap.xml, so a route the sitemap omits is not checked —
28
28
  // which is the right default, since an unlisted route is one nobody asked a
29
29
  // crawler to read.
30
+ //
31
+ // Once per site it also checks the icon. miow.me changed its icon in October
32
+ // 2026 and every address of it 404'd at /favicon.ico, the one place a client
33
+ // that never reads the page's <link rel="icon"> looks; so /favicon.ico must
34
+ // answer an image, and the same image the home page declares.
30
35
  import { outlineIssues } from '../dist/headings.js';
31
36
  import { readSitemap } from '../dist/indexnow.js';
32
37
  import { decodeEntities } from '../dist/markdown.js';
@@ -125,6 +130,36 @@ for (const url of checked) {
125
130
  if (descriptionOf(page.body) === null) faults.push({ url, fault: 'noDescription', detail: '' });
126
131
  }
127
132
 
133
+ // The icon, once per site: what the home page declares (a page that declares
134
+ // none is shown /favicon.ico) against what /favicon.ico answers, redirects
135
+ // followed.
136
+ const iconHref = html => {
137
+ for (const tag of html.match(/<link\b[^>]*>/gi) ?? []) {
138
+ if (/\brel=["'](?:shortcut )?icon["']/i.test(tag)) return decodeEntities(tag.match(/\bhref=["']([^"']+)["']/i)?.[1] ?? '');
139
+ }
140
+ return '';
141
+ };
142
+ const image = async url => {
143
+ try {
144
+ const res = await fetch(url, { headers: { 'user-agent': UA } });
145
+ const type = res.headers.get('content-type') ?? '';
146
+ return res.ok && /^image\//i.test(type) ? Buffer.from(await res.arrayBuffer()) : `${res.status} ${type}`.trim();
147
+ } catch (e) {
148
+ return e.message;
149
+ }
150
+ };
151
+ {
152
+ const home = await get(`${site}/`).catch(() => ({ body: '' }));
153
+ const declared = new URL(iconHref(home.body) || '/favicon.ico', `${site}/`).href;
154
+ const favicon = await image(`${site}/favicon.ico`);
155
+ const shown = declared === `${site}/favicon.ico` ? favicon : await image(declared);
156
+ if (!Buffer.isBuffer(favicon)) faults.push({ url: `${site}/favicon.ico`, fault: 'noFavicon', detail: favicon });
157
+ if (!Buffer.isBuffer(shown)) faults.push({ url: declared, fault: 'iconDead', detail: shown });
158
+ else if (Buffer.isBuffer(favicon) && !favicon.equals(shown)) {
159
+ faults.push({ url: `${site}/favicon.ico`, fault: 'faviconStale', detail: `not the icon the home page declares, ${declared.replace(site, '')}` });
160
+ }
161
+ }
162
+
128
163
  // Reported once per title rather than once per page, or a shared title on forty
129
164
  // routes drowns everything else.
130
165
  for (const [title, sharing] of titles) {
@@ -149,7 +184,7 @@ if (JSON_OUT) {
149
184
  const byFault = new Map();
150
185
  for (const f of faults) byFault.set(f.fault, [...(byFault.get(f.fault) ?? []), f]);
151
186
  console.log(`check-pages: ${checked.length} of ${urls.length} sitemap URLs on ${site}\n`);
152
- if (!faults.length) console.log(' clean — one h1 per page, no skipped levels, every title inside the budget.');
187
+ if (!faults.length) console.log(' clean — one h1 per page, no skipped levels, every title inside the budget, and /favicon.ico the icon the page declares.');
153
188
  else if (!failing.length) console.log(' no failures; everything below is advisory.\n');
154
189
  for (const [fault, list] of [...byFault].sort((a, b) => b[1].length - a[1].length)) {
155
190
  console.log(` ${fault} (${list.length})${ADVISORY.has(fault) ? ' — advisory, does not fail' : ''}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-formant",
3
- "version": "0.81.0",
3
+ "version": "0.82.0",
4
4
  "description": "The portable half of a wiki: derived taxonomy and facet controls, a version-negotiating MCP transport, markdown twins, block rendering, a rich-text editor engine, conditional-GET plumbing, and the headers and CSP every origin sends.",
5
5
  "type": "module",
6
6
  "license": "MIT",