ssrwire 0.2.0 → 0.3.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/CHANGELOG.md +24 -1
- package/CONTRIBUTING.md +1 -0
- package/PUBLISHING.md +15 -15
- package/README.md +70 -23
- package/dist/analyze.d.ts.map +1 -1
- package/dist/analyze.js +162 -6
- package/dist/analyze.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/http-probe.d.ts.map +1 -1
- package/dist/http-probe.js +4 -0
- package/dist/http-probe.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/redact.d.ts.map +1 -1
- package/dist/redact.js +5 -0
- package/dist/redact.js.map +1 -1
- package/dist/reporters.d.ts.map +1 -1
- package/dist/reporters.js +27 -0
- package/dist/reporters.js.map +1 -1
- package/dist/social.d.ts +14 -0
- package/dist/social.d.ts.map +1 -0
- package/dist/social.js +88 -0
- package/dist/social.js.map +1 -0
- package/dist/stability.d.ts.map +1 -1
- package/dist/stability.js +84 -4
- package/dist/stability.js.map +1 -1
- package/dist/stream-parser.d.ts.map +1 -1
- package/dist/stream-parser.js +21 -1
- package/dist/stream-parser.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/examples/github-actions.yml +2 -2
- package/examples/ssrwire.config.yml +4 -0
- package/package.json +5 -2
- package/src/analyze.ts +208 -7
- package/src/cli.ts +3 -1
- package/src/config.ts +4 -0
- package/src/http-probe.ts +4 -0
- package/src/index.ts +2 -0
- package/src/redact.ts +5 -0
- package/src/reporters.ts +46 -0
- package/src/social.ts +116 -0
- package/src/stability.ts +123 -4
- package/src/stream-parser.ts +31 -1
- package/src/types.ts +21 -0
package/src/stability.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
effectiveTwitterCardSignal,
|
|
3
|
+
firstSocialSignal,
|
|
4
|
+
normalizeSocialValue,
|
|
5
|
+
OPEN_GRAPH_PROPERTIES,
|
|
6
|
+
OPEN_GRAPH_REQUIRED_PROPERTIES,
|
|
7
|
+
socialSignals,
|
|
8
|
+
TWITTER_CARD_REQUIRED_FIELDS,
|
|
9
|
+
} from "./social.js";
|
|
1
10
|
import type {
|
|
2
11
|
AgentProfile,
|
|
3
12
|
AgentStability,
|
|
@@ -7,6 +16,7 @@ import type {
|
|
|
7
16
|
ProbeResult,
|
|
8
17
|
RobotsAudience,
|
|
9
18
|
RobotsSignal,
|
|
19
|
+
SocialMetadataProperty,
|
|
10
20
|
TimingStats,
|
|
11
21
|
} from "./types.js";
|
|
12
22
|
|
|
@@ -25,6 +35,11 @@ export interface StabilityAnalysis {
|
|
|
25
35
|
readonly findings: readonly Finding[];
|
|
26
36
|
}
|
|
27
37
|
|
|
38
|
+
interface SocialSignature {
|
|
39
|
+
openGraph?: unknown;
|
|
40
|
+
twitterCard?: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
function normalizeText(value: string): string {
|
|
29
44
|
return value.trim().replace(/\s+/g, " ");
|
|
30
45
|
}
|
|
@@ -117,8 +132,97 @@ function robotsValueLocations(probe: ProbeResult): readonly (readonly [string, s
|
|
|
117
132
|
);
|
|
118
133
|
}
|
|
119
134
|
|
|
120
|
-
function
|
|
135
|
+
function socialValuesForSignature(
|
|
136
|
+
probe: ProbeResult,
|
|
137
|
+
property: SocialMetadataProperty,
|
|
138
|
+
): readonly string[] {
|
|
139
|
+
const values = [
|
|
140
|
+
...new Set(
|
|
141
|
+
socialSignals(probe.signals, property)
|
|
142
|
+
.map((signal) => normalizeSocialValue(property, signal.value, probe.finalUrl))
|
|
143
|
+
.filter((value) => value.length > 0),
|
|
144
|
+
),
|
|
145
|
+
];
|
|
146
|
+
return property === "og:image" ? values : values.sort();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function socialLocationsForSignature(
|
|
150
|
+
probe: ProbeResult,
|
|
151
|
+
property: SocialMetadataProperty,
|
|
152
|
+
): readonly (readonly [string, string])[] {
|
|
153
|
+
if (property !== "og:image") {
|
|
154
|
+
return signalValueLocations(socialSignals(probe.signals, property), (value) =>
|
|
155
|
+
normalizeSocialValue(property, value, probe.finalUrl),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return socialSignals(probe.signals, property)
|
|
159
|
+
.map(
|
|
160
|
+
(signal) =>
|
|
161
|
+
[normalizeSocialValue(property, signal.value, probe.finalUrl), signal.location] as const,
|
|
162
|
+
)
|
|
163
|
+
.filter(([value]) => value.length > 0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function socialValueSignature(target: AuditTarget, probe: ProbeResult): unknown {
|
|
167
|
+
const signature: SocialSignature = {};
|
|
168
|
+
if (target.expectations.requireOpenGraph === true) {
|
|
169
|
+
signature.openGraph = Object.fromEntries(
|
|
170
|
+
OPEN_GRAPH_PROPERTIES.map((property) => [
|
|
171
|
+
property,
|
|
172
|
+
socialValuesForSignature(probe, property),
|
|
173
|
+
]),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (target.expectations.requireTwitterCard === true) {
|
|
177
|
+
signature.twitterCard = Object.fromEntries(
|
|
178
|
+
TWITTER_CARD_REQUIRED_FIELDS.map((field) => {
|
|
179
|
+
const signal = effectiveTwitterCardSignal(probe.signals, field);
|
|
180
|
+
return [
|
|
181
|
+
field,
|
|
182
|
+
signal === undefined
|
|
183
|
+
? "<missing>"
|
|
184
|
+
: normalizeSocialValue(signal.property, signal.value, probe.finalUrl) || "<missing>",
|
|
185
|
+
];
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return Object.keys(signature).length === 0 ? undefined : signature;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function socialLocationSignature(target: AuditTarget, probe: ProbeResult): unknown {
|
|
193
|
+
const signature: SocialSignature = {};
|
|
194
|
+
if (target.expectations.requireOpenGraph === true) {
|
|
195
|
+
signature.openGraph = Object.fromEntries(
|
|
196
|
+
OPEN_GRAPH_PROPERTIES.map((property) => [
|
|
197
|
+
property,
|
|
198
|
+
socialLocationsForSignature(probe, property),
|
|
199
|
+
]),
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
if (target.expectations.requireTwitterCard === true) {
|
|
203
|
+
signature.twitterCard = Object.fromEntries(
|
|
204
|
+
TWITTER_CARD_REQUIRED_FIELDS.map((field) => {
|
|
205
|
+
const signal = effectiveTwitterCardSignal(probe.signals, field);
|
|
206
|
+
return [
|
|
207
|
+
field,
|
|
208
|
+
signal === undefined
|
|
209
|
+
? []
|
|
210
|
+
: [
|
|
211
|
+
[
|
|
212
|
+
normalizeSocialValue(signal.property, signal.value, probe.finalUrl),
|
|
213
|
+
signal.location,
|
|
214
|
+
],
|
|
215
|
+
],
|
|
216
|
+
];
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return Object.keys(signature).length === 0 ? undefined : signature;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function metadataValueSignature(target: AuditTarget, probe: ProbeResult): string {
|
|
121
224
|
const titles = probe.signals.titles ?? (probe.signals.title ? [probe.signals.title] : []);
|
|
225
|
+
const social = socialValueSignature(target, probe);
|
|
122
226
|
return JSON.stringify({
|
|
123
227
|
title: normalizedSignalValues(titles),
|
|
124
228
|
description: normalizedSignalValues(probe.signals.descriptions),
|
|
@@ -126,11 +230,13 @@ function metadataValueSignature(probe: ProbeResult): string {
|
|
|
126
230
|
normalizeCanonical(value, probe.finalUrl),
|
|
127
231
|
),
|
|
128
232
|
robots: effectiveRobotsValue(probe),
|
|
233
|
+
...(social === undefined ? {} : { social }),
|
|
129
234
|
});
|
|
130
235
|
}
|
|
131
236
|
|
|
132
|
-
function metadataLocationSignature(probe: ProbeResult): string {
|
|
237
|
+
function metadataLocationSignature(target: AuditTarget, probe: ProbeResult): string {
|
|
133
238
|
const titles = probe.signals.titles ?? (probe.signals.title ? [probe.signals.title] : []);
|
|
239
|
+
const social = socialLocationSignature(target, probe);
|
|
134
240
|
return JSON.stringify({
|
|
135
241
|
title: signalValueLocations(titles),
|
|
136
242
|
description: signalValueLocations(probe.signals.descriptions),
|
|
@@ -138,6 +244,7 @@ function metadataLocationSignature(probe: ProbeResult): string {
|
|
|
138
244
|
normalizeCanonical(value, probe.finalUrl),
|
|
139
245
|
),
|
|
140
246
|
robots: robotsValueLocations(probe),
|
|
247
|
+
...(social === undefined ? {} : { social }),
|
|
141
248
|
});
|
|
142
249
|
}
|
|
143
250
|
|
|
@@ -213,6 +320,16 @@ export function criticalSignalsArrivalMs(
|
|
|
213
320
|
}
|
|
214
321
|
if (expectations.requireH1 && !addRequired(firstNonEmpty(probe.signals.h1s))) return undefined;
|
|
215
322
|
if (expectations.requireMainText && !addRequired(probe.signals.firstMainText)) return undefined;
|
|
323
|
+
if (expectations.requireOpenGraph === true) {
|
|
324
|
+
for (const property of OPEN_GRAPH_REQUIRED_PROPERTIES) {
|
|
325
|
+
if (!addRequired(firstSocialSignal(probe.signals, property))) return undefined;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (expectations.requireTwitterCard === true) {
|
|
329
|
+
for (const field of TWITTER_CARD_REQUIRED_FIELDS) {
|
|
330
|
+
if (!addRequired(effectiveTwitterCardSignal(probe.signals, field))) return undefined;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
216
333
|
|
|
217
334
|
return required === 0 ? undefined : Math.max(...marks);
|
|
218
335
|
}
|
|
@@ -275,8 +392,10 @@ function summarizeAgent(target: AuditTarget, probes: readonly ProbeResult[]): Ag
|
|
|
275
392
|
finalUrl: uniqueCount(finalResponses.map((probe) => normalizeUrl(probe.finalUrl))),
|
|
276
393
|
redirectChain: uniqueCount(probes.map(redirectChainSignature)),
|
|
277
394
|
bodySha256: uniqueCount(complete.map((probe) => probe.bodySha256 ?? "<missing>")),
|
|
278
|
-
metadataValues: uniqueCount(complete.map(metadataValueSignature)),
|
|
279
|
-
metadataLocations: uniqueCount(
|
|
395
|
+
metadataValues: uniqueCount(complete.map((probe) => metadataValueSignature(target, probe))),
|
|
396
|
+
metadataLocations: uniqueCount(
|
|
397
|
+
complete.map((probe) => metadataLocationSignature(target, probe)),
|
|
398
|
+
),
|
|
280
399
|
},
|
|
281
400
|
};
|
|
282
401
|
}
|
package/src/stream-parser.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Parser } from "htmlparser2";
|
|
2
2
|
|
|
3
|
+
import { isSocialMetadataProperty } from "./social.js";
|
|
4
|
+
|
|
3
5
|
import type {
|
|
4
6
|
DocumentSignals,
|
|
5
7
|
ElementLocation,
|
|
@@ -7,6 +9,8 @@ import type {
|
|
|
7
9
|
JsonLdSignal,
|
|
8
10
|
RobotsAudience,
|
|
9
11
|
RobotsSignal,
|
|
12
|
+
SocialMetadataProperty,
|
|
13
|
+
SocialMetadataSignal,
|
|
10
14
|
TimingMark,
|
|
11
15
|
} from "./types.js";
|
|
12
16
|
|
|
@@ -93,6 +97,8 @@ class HtmlStreamInspector implements StreamInspector {
|
|
|
93
97
|
readonly #descriptions: ElementSignal[] = [];
|
|
94
98
|
readonly #canonicals: ElementSignal[] = [];
|
|
95
99
|
readonly #robots: RobotsSignal[] = [];
|
|
100
|
+
readonly #socialMetadata: SocialMetadataSignal[] = [];
|
|
101
|
+
readonly #socialMetadataCounts = new Map<SocialMetadataProperty, number>();
|
|
96
102
|
readonly #h1s: ElementSignal[] = [];
|
|
97
103
|
readonly #jsonLd: JsonLdSignal[] = [];
|
|
98
104
|
readonly #titles: ElementSignal[] = [];
|
|
@@ -170,6 +176,7 @@ class HtmlStreamInspector implements StreamInspector {
|
|
|
170
176
|
descriptions: [...this.#descriptions],
|
|
171
177
|
canonicals: [...this.#canonicals],
|
|
172
178
|
robots: [...this.#robots],
|
|
179
|
+
socialMetadata: [...this.#socialMetadata],
|
|
173
180
|
h1s: [...this.#h1s],
|
|
174
181
|
...(firstMainText === undefined || firstMainText.value.length === 0 ? {} : { firstMainText }),
|
|
175
182
|
jsonLd: [...this.#jsonLd],
|
|
@@ -213,7 +220,14 @@ class HtmlStreamInspector implements StreamInspector {
|
|
|
213
220
|
return;
|
|
214
221
|
}
|
|
215
222
|
|
|
216
|
-
const {
|
|
223
|
+
const {
|
|
224
|
+
name: metaNameAttribute,
|
|
225
|
+
property: metaPropertyAttribute,
|
|
226
|
+
content,
|
|
227
|
+
rel,
|
|
228
|
+
href,
|
|
229
|
+
type: scriptType,
|
|
230
|
+
} = attributes;
|
|
217
231
|
if (name === "head") this.#inHead = true;
|
|
218
232
|
if (name === "body") {
|
|
219
233
|
if (this.#bodyStarted === undefined) this.#bodyStarted = this.#mark();
|
|
@@ -232,6 +246,7 @@ class HtmlStreamInspector implements StreamInspector {
|
|
|
232
246
|
|
|
233
247
|
if (name === "meta") {
|
|
234
248
|
const metaName = metaNameAttribute?.trim().toLowerCase();
|
|
249
|
+
const metaProperty = metaPropertyAttribute?.trim().toLowerCase();
|
|
235
250
|
if (
|
|
236
251
|
content !== undefined &&
|
|
237
252
|
metaName === "description" &&
|
|
@@ -249,6 +264,21 @@ class HtmlStreamInspector implements StreamInspector {
|
|
|
249
264
|
audience: metaName,
|
|
250
265
|
});
|
|
251
266
|
}
|
|
267
|
+
const socialProperty = isSocialMetadataProperty(metaProperty)
|
|
268
|
+
? metaProperty
|
|
269
|
+
: isSocialMetadataProperty(metaName)
|
|
270
|
+
? metaName
|
|
271
|
+
: undefined;
|
|
272
|
+
if (content !== undefined && socialProperty !== undefined) {
|
|
273
|
+
const count = this.#socialMetadataCounts.get(socialProperty) ?? 0;
|
|
274
|
+
if (count < SIGNAL_LIMIT) {
|
|
275
|
+
this.#socialMetadata.push({
|
|
276
|
+
...this.#elementSignal(content, location),
|
|
277
|
+
property: socialProperty,
|
|
278
|
+
});
|
|
279
|
+
this.#socialMetadataCounts.set(socialProperty, count + 1);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
252
282
|
}
|
|
253
283
|
|
|
254
284
|
if (name === "link") {
|
package/src/types.ts
CHANGED
|
@@ -18,6 +18,21 @@ export interface RobotsSignal extends ElementSignal {
|
|
|
18
18
|
readonly audience: RobotsAudience;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export type SocialMetadataProperty =
|
|
22
|
+
| "og:title"
|
|
23
|
+
| "og:type"
|
|
24
|
+
| "og:url"
|
|
25
|
+
| "og:image"
|
|
26
|
+
| "og:description"
|
|
27
|
+
| "twitter:card"
|
|
28
|
+
| "twitter:title"
|
|
29
|
+
| "twitter:description"
|
|
30
|
+
| "twitter:image";
|
|
31
|
+
|
|
32
|
+
export interface SocialMetadataSignal extends ElementSignal {
|
|
33
|
+
readonly property: SocialMetadataProperty;
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
export interface JsonLdSignal extends TimingMark {
|
|
22
37
|
readonly location: ElementLocation;
|
|
23
38
|
readonly valid?: boolean;
|
|
@@ -33,6 +48,8 @@ export interface DocumentSignals {
|
|
|
33
48
|
readonly descriptions: readonly ElementSignal[];
|
|
34
49
|
readonly canonicals: readonly ElementSignal[];
|
|
35
50
|
readonly robots: readonly RobotsSignal[];
|
|
51
|
+
/** Present on probes produced by SSRWire 0.3.0 and newer. */
|
|
52
|
+
readonly socialMetadata?: readonly SocialMetadataSignal[];
|
|
36
53
|
readonly h1s: readonly ElementSignal[];
|
|
37
54
|
readonly firstMainText?: ElementSignal;
|
|
38
55
|
readonly jsonLd: readonly JsonLdSignal[];
|
|
@@ -109,6 +126,10 @@ export interface TargetExpectations {
|
|
|
109
126
|
readonly requireCanonical: boolean;
|
|
110
127
|
readonly requireH1: boolean;
|
|
111
128
|
readonly requireMainText: boolean;
|
|
129
|
+
/** Require the four Open Graph protocol basic metadata properties. Defaults to false. */
|
|
130
|
+
readonly requireOpenGraph?: boolean;
|
|
131
|
+
/** Require SSRWire's Twitter Card readiness contract. Defaults to false. */
|
|
132
|
+
readonly requireTwitterCard?: boolean;
|
|
112
133
|
readonly maxFirstByteMs?: number;
|
|
113
134
|
readonly maxCriticalMs?: number;
|
|
114
135
|
}
|