@nurkamol/seo-audit 1.37.0 → 1.38.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurkamol/seo-audit",
3
- "version": "1.37.0",
3
+ "version": "1.38.0",
4
4
  "description": "Crawl a site's sitemap and check every page for SEO, metadata and structured-data problems that single-page graders miss. Zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/kept.mjs CHANGED
@@ -38,3 +38,98 @@ export function keptSince(rows, at) {
38
38
  return Number.isNaN(when) ? true : when >= at;
39
39
  });
40
40
  }
41
+
42
+ /** The columns a kept-runs list can be ordered by.
43
+ *
44
+ * Named here rather than in the page so the sentence in a header link, the
45
+ * value in a URL and the comparison below cannot drift apart. `descFirst` is
46
+ * which direction a column should take when it is first clicked: dates and
47
+ * numbers are most useful largest-first, a site name is not.
48
+ */
49
+ export const SORTS = {
50
+ site: { label: 'Site', descFirst: false, of: (r) => String(r?.host ?? r?.site ?? '').toLowerCase() },
51
+ when: { label: 'When', descFirst: true, of: (r) => String(r?.finishedAt ?? '') },
52
+ pages: { label: 'Pages', descFirst: true, of: (r) => Number(r?.pages ?? 0) },
53
+ causes: { label: 'To change', descFirst: true, of: (r) => Number(r?.causes ?? 0) },
54
+ score: { label: 'Score', descFirst: true, of: (r) => (typeof r?.score === 'number' ? r.score : -1) },
55
+ };
56
+
57
+ /** Kept runs in the order asked for. Unknown columns fall back to the newest
58
+ * first, which is the order the library already hands them over in.
59
+ *
60
+ * A copy, never in place: the caller's array is the store's own list, and
61
+ * sorting it underneath them would reorder a list somebody else is holding.
62
+ * Stable on ties by date then id, so two runs of one site at the same minute
63
+ * do not swap places between one request and the next. */
64
+ export function sortKept(rows, sort = 'when', dir = 'desc') {
65
+ const column = SORTS[sort] ?? SORTS.when;
66
+ const way = dir === 'asc' ? 1 : -1;
67
+ return [...(rows ?? [])].sort((a, b) => {
68
+ const left = column.of(a);
69
+ const right = column.of(b);
70
+ if (left < right) return -1 * way;
71
+ if (left > right) return 1 * way;
72
+ return (
73
+ String(b?.finishedAt ?? '').localeCompare(String(a?.finishedAt ?? '')) ||
74
+ String(a?.id ?? '').localeCompare(String(b?.id ?? ''))
75
+ );
76
+ });
77
+ }
78
+
79
+ /** What a `sort`/`dir` pair means, with anything unrecognised refused rather
80
+ * than carried. These arrive from a URL and from a cookie, and a cookie is
81
+ * just a URL somebody kept — neither may name a column that does not exist. */
82
+ export function sortAsked(sort, dir) {
83
+ const column = Object.hasOwn(SORTS, sort ?? '') ? sort : 'when';
84
+ const way = dir === 'asc' || dir === 'desc' ? dir : SORTS[column].descFirst ? 'desc' : 'asc';
85
+ return { sort: column, dir: way };
86
+ }
87
+
88
+ /** The name the remembered view is kept under. */
89
+ const VIEW_COOKIE = 'seo_audit_view';
90
+
91
+ /** What this browser last asked the kept-runs list to show.
92
+ *
93
+ * A cookie rather than `localStorage` so the page needs no JavaScript to
94
+ * honour it — the filter and the sort are links and a form, and a preference
95
+ * that only works once a script has run would be the one control on the page
96
+ * that does not.
97
+ *
98
+ * Everything is put back through the same validators the query string goes
99
+ * through. A cookie is a URL somebody kept, and it may have been edited since:
100
+ * it names a column or it is ignored. */
101
+ export function rememberedView(cookieHeader) {
102
+ const raw = String(cookieHeader ?? '')
103
+ .split(';')
104
+ .map((part) => part.trim())
105
+ .find((part) => part.startsWith(`${VIEW_COOKIE}=`));
106
+ if (!raw) return {};
107
+ let asked;
108
+ try {
109
+ asked = new URLSearchParams(decodeURIComponent(raw.slice(VIEW_COOKIE.length + 1)));
110
+ } catch {
111
+ return {};
112
+ }
113
+ const since = asked.get('since') ?? '';
114
+ const { sort, dir } = sortAsked(asked.get('sort'), asked.get('dir'));
115
+ return {
116
+ // A date that no longer parses is dropped rather than shown back as a
117
+ // filter nobody can see the effect of.
118
+ ...(since && !sinceWhen(since).error ? { since } : {}),
119
+ sort,
120
+ dir,
121
+ };
122
+ }
123
+
124
+ /** That view, as a `Set-Cookie` value.
125
+ *
126
+ * `SameSite=Lax` and no `Secure`, because the server this is served from is
127
+ * usually `http://127.0.0.1`. It holds a date and a column name — nothing
128
+ * worth protecting, and nothing worth sending anywhere else. */
129
+ export function rememberView({ since = '', sort = 'when', dir = 'desc' } = {}) {
130
+ const value = new URLSearchParams();
131
+ if (since) value.set('since', since);
132
+ value.set('sort', sort);
133
+ value.set('dir', dir);
134
+ return `${VIEW_COOKIE}=${encodeURIComponent(value.toString())}; Path=/; Max-Age=31536000; SameSite=Lax`;
135
+ }
package/src/report.mjs CHANGED
@@ -920,7 +920,17 @@ export function reportParts(findings, meta, { backHref, backLabel = 'New audit',
920
920
  padding: 0 1.25rem 6rem;
921
921
  background: var(--bg);
922
922
  color: var(--fg);
923
- font: 400 15px/1.65 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
923
+ /* The interface face each system actually uses, in the order a system is
924
+ likely to have one. Windows 11's UI face is Segoe UI Variable Text, not
925
+ Segoe UI, and desktop Linux has Cantarell or Noto long before it has
926
+ Roboto, which is an Android face. system-ui catches what the list does
927
+ not name. This is most of what makes a window feel like it belongs to
928
+ the machine it is on — more than any border radius does. */
929
+ font: 400 15px/1.65 -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text",
930
+ "Segoe UI", system-ui, Cantarell, "Noto Sans", Ubuntu, Roboto, Helvetica, Arial, sans-serif;
931
+ /* A variable face has real weights; letting the renderer fake them is the
932
+ blurry bold that reads as "web page in a window". */
933
+ font-synthesis: none;
924
934
  -webkit-font-smoothing: antialiased;
925
935
  }
926
936
  main { max-width: 62rem; margin-inline: auto; }