@brandlab-digital/conformance 3.13.0 → 3.15.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/README.md +68 -2
- package/dist/analytics-e2e.d.ts +10 -16
- package/dist/analytics-e2e.d.ts.map +1 -1
- package/dist/analytics-e2e.js +61 -3
- package/dist/analytics-e2e.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -1
- package/dist/cli.js.map +1 -1
- package/dist/current-release.js +5 -5
- package/dist/current-release.js.map +1 -1
- package/dist/engineering.d.ts.map +1 -1
- package/dist/engineering.js +18 -2
- package/dist/engineering.js.map +1 -1
- package/dist/estate-snapshot.d.ts +54 -0
- package/dist/estate-snapshot.d.ts.map +1 -0
- package/dist/estate-snapshot.js +164 -0
- package/dist/estate-snapshot.js.map +1 -0
- package/dist/estate.d.ts +38 -0
- package/dist/estate.d.ts.map +1 -1
- package/dist/estate.js +71 -28
- package/dist/estate.js.map +1 -1
- package/dist/media-snapshot.d.ts +53 -0
- package/dist/media-snapshot.d.ts.map +1 -0
- package/dist/media-snapshot.js +66 -0
- package/dist/media-snapshot.js.map +1 -0
- package/dist/project.d.ts +20 -0
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +92 -2
- package/dist/project.js.map +1 -1
- package/dist/ready-cli.d.ts.map +1 -1
- package/dist/ready-cli.js +8 -3
- package/dist/ready-cli.js.map +1 -1
- package/dist/rule-registry.d.ts +5 -0
- package/dist/rule-registry.d.ts.map +1 -1
- package/dist/rule-registry.js +1 -0
- package/dist/rule-registry.js.map +1 -1
- package/dist/site-system/checks.d.ts +28 -3
- package/dist/site-system/checks.d.ts.map +1 -1
- package/dist/site-system/checks.js +5 -2
- package/dist/site-system/checks.js.map +1 -1
- package/dist/site-system/config.d.ts +13 -0
- package/dist/site-system/config.d.ts.map +1 -1
- package/dist/site-system/config.js +30 -1
- package/dist/site-system/config.js.map +1 -1
- package/dist/site-system/index.d.ts +1 -1
- package/dist/site-system/index.d.ts.map +1 -1
- package/dist/site-system/index.js +1 -1
- package/dist/site-system/index.js.map +1 -1
- package/dist/site-system/journeys/conversion.d.ts.map +1 -1
- package/dist/site-system/journeys/conversion.js +15 -2
- package/dist/site-system/journeys/conversion.js.map +1 -1
- package/dist/site-system/journeys/menu.d.ts.map +1 -1
- package/dist/site-system/journeys/menu.js +24 -1
- package/dist/site-system/journeys/menu.js.map +1 -1
- package/dist/site-system/journeys/page-helpers.d.ts.map +1 -1
- package/dist/site-system/journeys/page-helpers.js +1 -1
- package/dist/site-system/journeys/page-helpers.js.map +1 -1
- package/dist/site-system/journeys/plan.d.ts +13 -3
- package/dist/site-system/journeys/plan.d.ts.map +1 -1
- package/dist/site-system/journeys/plan.js +12 -2
- package/dist/site-system/journeys/plan.js.map +1 -1
- package/dist/site-system/journeys/shell.d.ts +1 -1
- package/dist/site-system/journeys/shell.js +2 -2
- package/dist/site-system/journeys/shell.js.map +1 -1
- package/dist/site-system/report.d.ts.map +1 -1
- package/dist/site-system/report.js +42 -8
- package/dist/site-system/report.js.map +1 -1
- package/dist/site-system/rule-modules.d.ts.map +1 -1
- package/dist/site-system/rule-modules.js +5 -0
- package/dist/site-system/rule-modules.js.map +1 -1
- package/dist/site-system/rules/delivery/h13-inbox.d.ts +23 -0
- package/dist/site-system/rules/delivery/h13-inbox.d.ts.map +1 -0
- package/dist/site-system/rules/delivery/h13-inbox.js +208 -0
- package/dist/site-system/rules/delivery/h13-inbox.js.map +1 -0
- package/dist/site-system/rules/delivery/index.d.ts +12 -0
- package/dist/site-system/rules/delivery/index.d.ts.map +1 -0
- package/dist/site-system/rules/delivery/index.js +7 -0
- package/dist/site-system/rules/delivery/index.js.map +1 -0
- package/dist/site-system/rules/form/c7-thank-you.d.ts +7 -1
- package/dist/site-system/rules/form/c7-thank-you.d.ts.map +1 -1
- package/dist/site-system/rules/form/c7-thank-you.js +26 -3
- package/dist/site-system/rules/form/c7-thank-you.js.map +1 -1
- package/dist/site-system/rules/form/index.d.ts.map +1 -1
- package/dist/site-system/rules/form/index.js +11 -1
- package/dist/site-system/rules/form/index.js.map +1 -1
- package/dist/site-system/rules/full/common.d.ts +2 -0
- package/dist/site-system/rules/full/common.d.ts.map +1 -1
- package/dist/site-system/rules/full/common.js.map +1 -1
- package/dist/site-system/rules/full/e1-pattern.d.ts +4 -0
- package/dist/site-system/rules/full/e1-pattern.d.ts.map +1 -0
- package/dist/site-system/rules/full/e1-pattern.js +47 -0
- package/dist/site-system/rules/full/e1-pattern.js.map +1 -0
- package/dist/site-system/rules/full/estate.d.ts +26 -3
- package/dist/site-system/rules/full/estate.d.ts.map +1 -1
- package/dist/site-system/rules/full/estate.js +58 -9
- package/dist/site-system/rules/full/estate.js.map +1 -1
- package/dist/site-system/rules/full/geography.d.ts +15 -2
- package/dist/site-system/rules/full/geography.d.ts.map +1 -1
- package/dist/site-system/rules/full/geography.js +15 -2
- package/dist/site-system/rules/full/geography.js.map +1 -1
- package/dist/site-system/rules/full/index.d.ts +4 -3
- package/dist/site-system/rules/full/index.d.ts.map +1 -1
- package/dist/site-system/rules/full/index.js +20 -5
- package/dist/site-system/rules/full/index.js.map +1 -1
- package/dist/site-system/rules/full/k12-slots.d.ts +4 -0
- package/dist/site-system/rules/full/k12-slots.d.ts.map +1 -0
- package/dist/site-system/rules/full/k12-slots.js +40 -0
- package/dist/site-system/rules/full/k12-slots.js.map +1 -0
- package/dist/site-system/rules/full/l10-location-copy.d.ts +1 -1
- package/dist/site-system/rules/full/l10-location-copy.d.ts.map +1 -1
- package/dist/site-system/rules/full/l10-location-copy.js +5 -2
- package/dist/site-system/rules/full/l10-location-copy.js.map +1 -1
- package/dist/site-system/rules/full/l3-neighbours.d.ts +3 -2
- package/dist/site-system/rules/full/l3-neighbours.d.ts.map +1 -1
- package/dist/site-system/rules/full/l3-neighbours.js +4 -1
- package/dist/site-system/rules/full/l3-neighbours.js.map +1 -1
- package/dist/site-system/rules/full/l4-local-copy.d.ts +2 -1
- package/dist/site-system/rules/full/l4-local-copy.d.ts.map +1 -1
- package/dist/site-system/rules/full/l4-local-copy.js +8 -3
- package/dist/site-system/rules/full/l4-local-copy.js.map +1 -1
- package/dist/site-system/rules/full/l9-data-contract.js +2 -2
- package/dist/site-system/rules/full/l9-data-contract.js.map +1 -1
- package/dist/site-system/rules/full/links.d.ts +3 -1
- package/dist/site-system/rules/full/links.d.ts.map +1 -1
- package/dist/site-system/rules/full/links.js +15 -5
- package/dist/site-system/rules/full/links.js.map +1 -1
- package/dist/site-system/rules/full/loc-index.d.ts +5 -0
- package/dist/site-system/rules/full/loc-index.d.ts.map +1 -1
- package/dist/site-system/rules/full/loc-index.js +43 -2
- package/dist/site-system/rules/full/loc-index.js.map +1 -1
- package/dist/site-system/rules/full/route-kinds.d.ts +15 -0
- package/dist/site-system/rules/full/route-kinds.d.ts.map +1 -0
- package/dist/site-system/rules/full/route-kinds.js +52 -0
- package/dist/site-system/rules/full/route-kinds.js.map +1 -0
- package/dist/site-system/rules/full/strategy.d.ts +6 -0
- package/dist/site-system/rules/full/strategy.d.ts.map +1 -1
- package/dist/site-system/rules/full/strategy.js +98 -10
- package/dist/site-system/rules/full/strategy.js.map +1 -1
- package/dist/site-system/rules/sections/k2-skin-coverage.d.ts +2 -2
- package/dist/site-system/rules/sections/k2-skin-coverage.d.ts.map +1 -1
- package/dist/site-system/rules/sections/k2-skin-coverage.js +13 -2
- package/dist/site-system/rules/sections/k2-skin-coverage.js.map +1 -1
- package/dist/site-system/rules/sections/t6-media-entries.d.ts.map +1 -1
- package/dist/site-system/rules/sections/t6-media-entries.js +69 -5
- package/dist/site-system/rules/sections/t6-media-entries.js.map +1 -1
- package/dist/site-system/runners.js +1 -1
- package/dist/site-system/runners.js.map +1 -1
- package/dist/site-system-e2e.d.ts +1 -1
- package/dist/site-system-e2e.d.ts.map +1 -1
- package/dist/site-system-e2e.js +17 -4
- package/dist/site-system-e2e.js.map +1 -1
- package/package.json +10 -2
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The estate plan snapshot (3.15.0): a site whose waves or estate plan are computed (from its location catalogue,
|
|
3
|
+
* for example) records them as plain JSON, so the ready gate's LOC-INDEX can read them without running the site's
|
|
4
|
+
* code. The snapshot is deterministic (sorted routes and keys, no timestamp) and carries a hash of the source files it
|
|
5
|
+
* was written from (`src/data` by default), so the gate fails a snapshot older than those files.
|
|
6
|
+
*
|
|
7
|
+
* The site writes it from its own tests, where its module aliases resolve:
|
|
8
|
+
*
|
|
9
|
+
* // tests/data/estatePlanSnapshot.test.ts
|
|
10
|
+
* import { writeEstatePlanSnapshot } from '@brandlab-digital/conformance/estate-snapshot'
|
|
11
|
+
* import { siteStrategy } from '@/data/strategy'
|
|
12
|
+
*
|
|
13
|
+
* it('records the estate plan for the ready gate', () => {
|
|
14
|
+
* const { changed } = writeEstatePlanSnapshot(siteStrategy, { projectRoot: process.cwd() })
|
|
15
|
+
* if (process.env.CI) expect(changed).toBe(false)
|
|
16
|
+
* })
|
|
17
|
+
*
|
|
18
|
+
* Node only: it reads and writes files.
|
|
19
|
+
*/
|
|
20
|
+
import { createHash } from "node:crypto";
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { dirname, isAbsolute, join, normalize, relative, sep } from "node:path";
|
|
23
|
+
/** Where the snapshot lives, relative to the project root. */
|
|
24
|
+
export const ESTATE_PLAN_SNAPSHOT_FILE = "src/data/estate-plan.generated.json";
|
|
25
|
+
/** The folders hashed by default: the snapshot is stale once any file in them changes. */
|
|
26
|
+
export const ESTATE_PLAN_SNAPSHOT_SOURCES = Object.freeze(["src/data"]);
|
|
27
|
+
export const ESTATE_PLAN_SNAPSHOT_KIND = "brandlab-estate-plan-snapshot";
|
|
28
|
+
export const ESTATE_PLAN_SNAPSHOT_VERSION = 1;
|
|
29
|
+
/** The source files hashed: code and data, never tests or generated files. */
|
|
30
|
+
const SOURCE_FILE = /\.(?:ts|tsx|mts|cts|js|mjs|cjs|json|mdx?|csv)$/;
|
|
31
|
+
const SKIPPED_FILE = /(?:\.generated\.json|\.(?:test|spec)\.[a-z]+)$/;
|
|
32
|
+
function sortedUnique(values) {
|
|
33
|
+
return [...new Set(values)].sort();
|
|
34
|
+
}
|
|
35
|
+
function sortedRecord(record) {
|
|
36
|
+
return Object.fromEntries(Object.entries(record).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The waves and estate plan in their canonical form: waves in date order (stable), each wave's routes sorted, the
|
|
40
|
+
* plan's lists sorted and its keys in a fixed order. Two plans that mean the same thing serialise the same.
|
|
41
|
+
*/
|
|
42
|
+
export function canonicalEstatePlan(strategy) {
|
|
43
|
+
const result = {};
|
|
44
|
+
if (strategy.waves !== undefined) {
|
|
45
|
+
result.waves = [...strategy.waves]
|
|
46
|
+
.map((wave, index) => ({ wave, index }))
|
|
47
|
+
.sort((left, right) => (left.wave.releaseOn < right.wave.releaseOn ? -1 : left.wave.releaseOn > right.wave.releaseOn ? 1 : left.index - right.index))
|
|
48
|
+
.map(({ wave }) => ({
|
|
49
|
+
releaseOn: wave.releaseOn,
|
|
50
|
+
...(wave.kind !== undefined ? { kind: wave.kind } : {}),
|
|
51
|
+
routes: sortedUnique(wave.routes),
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
const plan = strategy.estate;
|
|
55
|
+
if (plan !== undefined) {
|
|
56
|
+
const paths = plan.paths;
|
|
57
|
+
const slugs = plan.slugs;
|
|
58
|
+
result.estate = {
|
|
59
|
+
indexedNow: sortedUnique(plan.indexedNow),
|
|
60
|
+
...(paths !== undefined
|
|
61
|
+
? {
|
|
62
|
+
paths: {
|
|
63
|
+
...(paths.region !== undefined ? { region: paths.region } : {}),
|
|
64
|
+
...(paths.suburb !== undefined ? { suburb: paths.suburb } : {}),
|
|
65
|
+
...(paths.serviceInSuburb !== undefined ? { serviceInSuburb: sortedUnique(paths.serviceInSuburb) } : {}),
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
: {}),
|
|
69
|
+
...(slugs !== undefined
|
|
70
|
+
? {
|
|
71
|
+
slugs: {
|
|
72
|
+
...(slugs.regions !== undefined ? { regions: sortedRecord(slugs.regions) } : {}),
|
|
73
|
+
...(slugs.suburbs !== undefined ? { suburbs: sortedRecord(slugs.suburbs) } : {}),
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
: {}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
function assertSourcePath(path) {
|
|
82
|
+
if (typeof path !== "string" || path === "" || isAbsolute(path)) {
|
|
83
|
+
throw new Error(`The snapshot source "${String(path)}" must be a folder or file inside the project.`);
|
|
84
|
+
}
|
|
85
|
+
const clean = normalize(path).split(sep).join("/").replace(/\/$/, "");
|
|
86
|
+
if (clean === "." || clean === ".." || clean.startsWith("../")) {
|
|
87
|
+
throw new Error(`The snapshot source "${path}" must be a folder or file inside the project.`);
|
|
88
|
+
}
|
|
89
|
+
return clean;
|
|
90
|
+
}
|
|
91
|
+
function collectFiles(root, path, files) {
|
|
92
|
+
const absolute = join(root, path);
|
|
93
|
+
if (!existsSync(absolute))
|
|
94
|
+
return;
|
|
95
|
+
if (statSync(absolute).isFile()) {
|
|
96
|
+
if (!SKIPPED_FILE.test(path))
|
|
97
|
+
files.push(path);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
for (const name of readdirSync(absolute)) {
|
|
101
|
+
if (name === "node_modules" || name.startsWith("."))
|
|
102
|
+
continue;
|
|
103
|
+
const child = `${path}/${name}`;
|
|
104
|
+
// statSync follows symbolic links, so a linked file or folder is hashed by what it points to.
|
|
105
|
+
const stats = statSync(join(root, child), { throwIfNoEntry: false });
|
|
106
|
+
if (stats?.isDirectory())
|
|
107
|
+
collectFiles(root, child, files);
|
|
108
|
+
else if (stats?.isFile() && SOURCE_FILE.test(name) && !SKIPPED_FILE.test(name))
|
|
109
|
+
files.push(child);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The hash of the files the plan is computed from: every code and data file under `sources` (tests and generated
|
|
114
|
+
* files left out), by path and content in path order. The ready gate recomputes it to tell a stale snapshot.
|
|
115
|
+
*/
|
|
116
|
+
export function estatePlanSourceHash(projectRoot, sources = ESTATE_PLAN_SNAPSHOT_SOURCES) {
|
|
117
|
+
const files = [];
|
|
118
|
+
for (const source of sortedUnique(sources.map(assertSourcePath)))
|
|
119
|
+
collectFiles(projectRoot, source, files);
|
|
120
|
+
const hash = createHash("sha256");
|
|
121
|
+
for (const file of sortedUnique(files)) {
|
|
122
|
+
hash.update(`${file}\n`);
|
|
123
|
+
hash.update(readFileSync(join(projectRoot, file)));
|
|
124
|
+
hash.update("\n");
|
|
125
|
+
}
|
|
126
|
+
return hash.digest("hex");
|
|
127
|
+
}
|
|
128
|
+
/** The snapshot for a strategy record: its waves and estate plan in canonical form, with the sources' hash. */
|
|
129
|
+
export function estatePlanSnapshot(strategy, options) {
|
|
130
|
+
const paths = sortedUnique((options.sources ?? ESTATE_PLAN_SNAPSHOT_SOURCES).map(assertSourcePath));
|
|
131
|
+
return {
|
|
132
|
+
kind: ESTATE_PLAN_SNAPSHOT_KIND,
|
|
133
|
+
version: ESTATE_PLAN_SNAPSHOT_VERSION,
|
|
134
|
+
sources: { paths, sha256: estatePlanSourceHash(options.projectRoot, paths) },
|
|
135
|
+
...canonicalEstatePlan(strategy),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/** The snapshot as the file holds it: two-space JSON and a final newline. */
|
|
139
|
+
export function serialiseEstatePlanSnapshot(snapshot) {
|
|
140
|
+
return `${JSON.stringify(snapshot, null, 2)}\n`;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Writes `src/data/estate-plan.generated.json` when its content differs; `changed` says whether it did. Run it from the
|
|
144
|
+
* site's tests or build so the snapshot follows the strategy record, and commit the file.
|
|
145
|
+
*/
|
|
146
|
+
export function writeEstatePlanSnapshot(strategy, options) {
|
|
147
|
+
const snapshot = estatePlanSnapshot(strategy, options);
|
|
148
|
+
return { ...writeSnapshotFile(options.projectRoot, ESTATE_PLAN_SNAPSHOT_FILE, serialiseEstatePlanSnapshot(snapshot)), snapshot };
|
|
149
|
+
}
|
|
150
|
+
/** Writes a snapshot file when its text differs; `changed` says whether it did. Shared with the media snapshot. */
|
|
151
|
+
export function writeSnapshotFile(projectRoot, file, text) {
|
|
152
|
+
const path = join(projectRoot, file);
|
|
153
|
+
const current = existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
154
|
+
if (current === text)
|
|
155
|
+
return { path: relative(projectRoot, path), changed: false };
|
|
156
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
157
|
+
writeFileSync(path, text);
|
|
158
|
+
return { path: relative(projectRoot, path), changed: true };
|
|
159
|
+
}
|
|
160
|
+
/** The folder list a snapshot records, checked and sorted. */
|
|
161
|
+
export function snapshotSourcePaths(sources = ESTATE_PLAN_SNAPSHOT_SOURCES) {
|
|
162
|
+
return sortedUnique(sources.map(assertSourcePath));
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=estate-snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"estate-snapshot.js","sourceRoot":"","sources":["../src/estate-snapshot.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The estate plan snapshot (3.15.0): a site whose waves or estate plan are computed (from its location catalogue,\n * for example) records them as plain JSON, so the ready gate's LOC-INDEX can read them without running the site's\n * code. The snapshot is deterministic (sorted routes and keys, no timestamp) and carries a hash of the source files it\n * was written from (`src/data` by default), so the gate fails a snapshot older than those files.\n *\n * The site writes it from its own tests, where its module aliases resolve:\n *\n * // tests/data/estatePlanSnapshot.test.ts\n * import { writeEstatePlanSnapshot } from '@brandlab-digital/conformance/estate-snapshot'\n * import { siteStrategy } from '@/data/strategy'\n *\n * it('records the estate plan for the ready gate', () => {\n * const { changed } = writeEstatePlanSnapshot(siteStrategy, { projectRoot: process.cwd() })\n * if (process.env.CI) expect(changed).toBe(false)\n * })\n *\n * Node only: it reads and writes files.\n */\nimport { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from \"node:fs\";\nimport { dirname, isAbsolute, join, normalize, relative, sep } from \"node:path\";\nimport type { SiteEstatePlan, SiteReleaseWave, SiteStrategy } from \"@brandlab-digital/site-core/strategy\";\n\n/** Where the snapshot lives, relative to the project root. */\nexport const ESTATE_PLAN_SNAPSHOT_FILE = \"src/data/estate-plan.generated.json\";\n/** The folders hashed by default: the snapshot is stale once any file in them changes. */\nexport const ESTATE_PLAN_SNAPSHOT_SOURCES: readonly string[] = Object.freeze([\"src/data\"]);\nexport const ESTATE_PLAN_SNAPSHOT_KIND = \"brandlab-estate-plan-snapshot\";\nexport const ESTATE_PLAN_SNAPSHOT_VERSION = 1;\n\n/** The source files hashed: code and data, never tests or generated files. */\nconst SOURCE_FILE = /\\.(?:ts|tsx|mts|cts|js|mjs|cjs|json|mdx?|csv)$/;\nconst SKIPPED_FILE = /(?:\\.generated\\.json|\\.(?:test|spec)\\.[a-z]+)$/;\n\nexport interface EstatePlanSnapshot {\n readonly kind: typeof ESTATE_PLAN_SNAPSHOT_KIND;\n readonly version: typeof ESTATE_PLAN_SNAPSHOT_VERSION;\n /** The folders hashed and the hash of their files when the snapshot was written. */\n readonly sources: { readonly paths: readonly string[]; readonly sha256: string };\n readonly waves?: readonly SiteReleaseWave[];\n readonly estate?: SiteEstatePlan;\n}\n\nexport interface EstatePlanSnapshotOptions {\n readonly projectRoot: string;\n /** Folders (or files) relative to the project root the plan is computed from. Defaults to `src/data`. */\n readonly sources?: readonly string[];\n}\n\nfunction sortedUnique(values: readonly string[]): string[] {\n return [...new Set(values)].sort();\n}\n\nfunction sortedRecord(record: Readonly<Record<string, string>>): Record<string, string> {\n return Object.fromEntries(Object.entries(record).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)));\n}\n\n/**\n * The waves and estate plan in their canonical form: waves in date order (stable), each wave's routes sorted, the\n * plan's lists sorted and its keys in a fixed order. Two plans that mean the same thing serialise the same.\n */\nexport function canonicalEstatePlan(strategy: Pick<SiteStrategy, \"waves\" | \"estate\">): Pick<EstatePlanSnapshot, \"waves\" | \"estate\"> {\n const result: { waves?: SiteReleaseWave[]; estate?: SiteEstatePlan } = {};\n if (strategy.waves !== undefined) {\n result.waves = [...strategy.waves]\n .map((wave, index) => ({ wave, index }))\n .sort((left, right) => (left.wave.releaseOn < right.wave.releaseOn ? -1 : left.wave.releaseOn > right.wave.releaseOn ? 1 : left.index - right.index))\n .map(({ wave }) => ({\n releaseOn: wave.releaseOn,\n ...(wave.kind !== undefined ? { kind: wave.kind } : {}),\n routes: sortedUnique(wave.routes),\n }));\n }\n const plan = strategy.estate;\n if (plan !== undefined) {\n const paths = plan.paths;\n const slugs = plan.slugs;\n result.estate = {\n indexedNow: sortedUnique(plan.indexedNow),\n ...(paths !== undefined\n ? {\n paths: {\n ...(paths.region !== undefined ? { region: paths.region } : {}),\n ...(paths.suburb !== undefined ? { suburb: paths.suburb } : {}),\n ...(paths.serviceInSuburb !== undefined ? { serviceInSuburb: sortedUnique(paths.serviceInSuburb) } : {}),\n },\n }\n : {}),\n ...(slugs !== undefined\n ? {\n slugs: {\n ...(slugs.regions !== undefined ? { regions: sortedRecord(slugs.regions) } : {}),\n ...(slugs.suburbs !== undefined ? { suburbs: sortedRecord(slugs.suburbs) } : {}),\n },\n }\n : {}),\n };\n }\n return result;\n}\n\nfunction assertSourcePath(path: string): string {\n if (typeof path !== \"string\" || path === \"\" || isAbsolute(path)) {\n throw new Error(`The snapshot source \"${String(path)}\" must be a folder or file inside the project.`);\n }\n const clean = normalize(path).split(sep).join(\"/\").replace(/\\/$/, \"\");\n if (clean === \".\" || clean === \"..\" || clean.startsWith(\"../\")) {\n throw new Error(`The snapshot source \"${path}\" must be a folder or file inside the project.`);\n }\n return clean;\n}\n\nfunction collectFiles(root: string, path: string, files: string[]): void {\n const absolute = join(root, path);\n if (!existsSync(absolute)) return;\n if (statSync(absolute).isFile()) {\n if (!SKIPPED_FILE.test(path)) files.push(path);\n return;\n }\n for (const name of readdirSync(absolute)) {\n if (name === \"node_modules\" || name.startsWith(\".\")) continue;\n const child = `${path}/${name}`;\n // statSync follows symbolic links, so a linked file or folder is hashed by what it points to.\n const stats = statSync(join(root, child), { throwIfNoEntry: false });\n if (stats?.isDirectory()) collectFiles(root, child, files);\n else if (stats?.isFile() && SOURCE_FILE.test(name) && !SKIPPED_FILE.test(name)) files.push(child);\n }\n}\n\n/**\n * The hash of the files the plan is computed from: every code and data file under `sources` (tests and generated\n * files left out), by path and content in path order. The ready gate recomputes it to tell a stale snapshot.\n */\nexport function estatePlanSourceHash(projectRoot: string, sources: readonly string[] = ESTATE_PLAN_SNAPSHOT_SOURCES): string {\n const files: string[] = [];\n for (const source of sortedUnique(sources.map(assertSourcePath))) collectFiles(projectRoot, source, files);\n const hash = createHash(\"sha256\");\n for (const file of sortedUnique(files)) {\n hash.update(`${file}\\n`);\n hash.update(readFileSync(join(projectRoot, file)));\n hash.update(\"\\n\");\n }\n return hash.digest(\"hex\");\n}\n\n/** The snapshot for a strategy record: its waves and estate plan in canonical form, with the sources' hash. */\nexport function estatePlanSnapshot(strategy: Pick<SiteStrategy, \"waves\" | \"estate\">, options: EstatePlanSnapshotOptions): EstatePlanSnapshot {\n const paths = sortedUnique((options.sources ?? ESTATE_PLAN_SNAPSHOT_SOURCES).map(assertSourcePath));\n return {\n kind: ESTATE_PLAN_SNAPSHOT_KIND,\n version: ESTATE_PLAN_SNAPSHOT_VERSION,\n sources: { paths, sha256: estatePlanSourceHash(options.projectRoot, paths) },\n ...canonicalEstatePlan(strategy),\n };\n}\n\n/** The snapshot as the file holds it: two-space JSON and a final newline. */\nexport function serialiseEstatePlanSnapshot(snapshot: EstatePlanSnapshot): string {\n return `${JSON.stringify(snapshot, null, 2)}\\n`;\n}\n\n/**\n * Writes `src/data/estate-plan.generated.json` when its content differs; `changed` says whether it did. Run it from the\n * site's tests or build so the snapshot follows the strategy record, and commit the file.\n */\nexport function writeEstatePlanSnapshot(\n strategy: Pick<SiteStrategy, \"waves\" | \"estate\">,\n options: EstatePlanSnapshotOptions\n): { readonly path: string; readonly changed: boolean; readonly snapshot: EstatePlanSnapshot } {\n const snapshot = estatePlanSnapshot(strategy, options);\n return { ...writeSnapshotFile(options.projectRoot, ESTATE_PLAN_SNAPSHOT_FILE, serialiseEstatePlanSnapshot(snapshot)), snapshot };\n}\n\n/** Writes a snapshot file when its text differs; `changed` says whether it did. Shared with the media snapshot. */\nexport function writeSnapshotFile(projectRoot: string, file: string, text: string): { readonly path: string; readonly changed: boolean } {\n const path = join(projectRoot, file);\n const current = existsSync(path) ? readFileSync(path, \"utf8\") : null;\n if (current === text) return { path: relative(projectRoot, path), changed: false };\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, text);\n return { path: relative(projectRoot, path), changed: true };\n}\n\n/** The folder list a snapshot records, checked and sorted. */\nexport function snapshotSourcePaths(sources: readonly string[] = ESTATE_PLAN_SNAPSHOT_SOURCES): string[] {\n return sortedUnique(sources.map(assertSourcePath));\n}\n"]}
|
package/dist/estate.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface BrandlabEstateRoute {
|
|
|
9
9
|
/** The route kind (a catalogue route has it): sitemap families and location checks read it. */
|
|
10
10
|
kind?: string;
|
|
11
11
|
id?: string;
|
|
12
|
+
/** The catalogue parent: a location page other location pages sit under is a region page. */
|
|
13
|
+
parentId?: string;
|
|
12
14
|
/** The content date the sitemap's lastmod must carry. */
|
|
13
15
|
lastSignificantChange?: string;
|
|
14
16
|
locationDecision?: {
|
|
@@ -79,6 +81,11 @@ export interface BrandlabEstateTestOptions {
|
|
|
79
81
|
};
|
|
80
82
|
/** A place's centre for neighbour order (default: the registry's suburb centres). */
|
|
81
83
|
centreOf?: SuburbCentreLookup;
|
|
84
|
+
/**
|
|
85
|
+
* The site's estate plan (site-core `siteStrategy.estate`, 3.14.0): its public slugs map a location page to the
|
|
86
|
+
* registry suburb it names (`ryde-suburb` → `ryde`) and mark region pages (`ryde`), which are not ranked by distance.
|
|
87
|
+
*/
|
|
88
|
+
estate?: BrandlabEstateSlugPlan | null;
|
|
82
89
|
/** The most inline markup one service-area map may carry (default 60 000 bytes, 3.6.0). */
|
|
83
90
|
areaMapMaxBytes?: number;
|
|
84
91
|
/**
|
|
@@ -98,6 +105,8 @@ export interface BrandlabPageQualityTestOptions {
|
|
|
98
105
|
businessId?: string | false;
|
|
99
106
|
/** A place's centre for similarity siblings (default: the registry's suburb centres). */
|
|
100
107
|
centreOf?: SuburbCentreLookup;
|
|
108
|
+
/** The site's estate plan, as for the estate journey: public slugs resolve to the places they name. */
|
|
109
|
+
estate?: BrandlabEstateSlugPlan | null;
|
|
101
110
|
concurrency?: number;
|
|
102
111
|
timeout?: number;
|
|
103
112
|
}
|
|
@@ -106,6 +115,25 @@ export interface BrandlabPageQualityFinding {
|
|
|
106
115
|
kind: 'headings' | 'title' | 'title-format' | 'title-duplicate' | 'title-h1' | 'description' | 'description-length' | 'description-duplicate' | 'html-size' | 'similarity' | 'location-review' | 'orphan' | 'noindex-link' | 'og-url' | 'implementation-wording' | 'structured-data';
|
|
107
116
|
detail: string;
|
|
108
117
|
}
|
|
118
|
+
/** The public-slug part of site-core's SiteEstatePlan (mirrored: conformance compiles without site-core). */
|
|
119
|
+
export interface BrandlabEstateSlugPlan {
|
|
120
|
+
slugs?: {
|
|
121
|
+
/** Registry region slug → public slug, for example `{ 'ryde-area': 'ryde' }`. */
|
|
122
|
+
regions?: Readonly<Record<string, string>>;
|
|
123
|
+
/** Registry suburb slug → public slug, for example `{ ryde: 'ryde-suburb' }`. */
|
|
124
|
+
suburbs?: Readonly<Record<string, string>>;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* A location route's place: the registry suburb slug its last path segment names (through the estate plan's public
|
|
129
|
+
* slugs, as site-core's `estatePublicSlug`), and whether it is a region page. A region page has no centre, so its
|
|
130
|
+
* nearby lists are not ranked by distance: it is a region when other location pages sit under it, when its slug is a
|
|
131
|
+
* region's public slug, or when its slug is a suburb's registry slug that the plan moved elsewhere (AA: `ryde`).
|
|
132
|
+
*/
|
|
133
|
+
export declare function estatePlaceResolver(routes: readonly BrandlabEstateRoute[], estate?: BrandlabEstateSlugPlan | null): (route: BrandlabEstateRoute | undefined) => {
|
|
134
|
+
slug: string;
|
|
135
|
+
region: boolean;
|
|
136
|
+
} | undefined;
|
|
109
137
|
export declare function defineBrandlabEstateTests(options: BrandlabEstateTestOptions): void;
|
|
110
138
|
/**
|
|
111
139
|
* Why a page's BreadcrumbList JSON-LD would fail Google's breadcrumb rich result (3.3.3): every
|
|
@@ -184,6 +212,16 @@ export declare function pageQualityRunFromEnv(env?: NodeJS.ProcessEnv): {
|
|
|
184
212
|
reason: string;
|
|
185
213
|
directory?: string;
|
|
186
214
|
};
|
|
215
|
+
/**
|
|
216
|
+
* The internal pages a page's server HTML links, once each, without the page itself. Only anchors count: an area named
|
|
217
|
+
* as plain text (a `Coverage` entry with no `href`, 3.15.0) is no link.
|
|
218
|
+
*/
|
|
219
|
+
export declare function pageLinkTargets(html: string, path: string, origin: string, base?: string): Set<string>;
|
|
220
|
+
/**
|
|
221
|
+
* Indexable pages that link a noindex location page (D4: list a place whose page waits for its wave as plain text,
|
|
222
|
+
* as site-core's `coverageEntries` does). `linksFrom` maps each page to the pages it links.
|
|
223
|
+
*/
|
|
224
|
+
export declare function noindexLinkFindings(linksFrom: ReadonlyMap<string, ReadonlySet<string>>, routes: readonly Pick<BrandlabEstateRoute, 'path' | 'indexing' | 'kind'>[]): BrandlabPageQualityFinding[];
|
|
187
225
|
/**
|
|
188
226
|
* Absolute (`https://host`) and protocol-relative (`//host`) destinations leave
|
|
189
227
|
* the site; browsers also read `/\host` and `\\host` as protocol-relative.
|
package/dist/estate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"estate.d.ts","sourceRoot":"","sources":["../src/estate.ts"],"names":[],"mappings":"AA+CA,OAAO,EAAgB,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAmBL,KAAK,sBAAsB,EAE3B,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACxB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAA6E,KAAK,2BAA2B,EAAE,MAAM,oBAAoB,CAAC;AAEjJ,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,YAAY,EACZ,kBAAkB,EAClB,KAAK,oBAAoB,EACzB,KAAK,2BAA2B,EAChC,KAAK,yBAAyB,EAC9B,KAAK,mBAAmB,GACzB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,OAAO,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;IAC1D,+FAA+F;IAC/F,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,yDAAyD;IACzD,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gBAAgB,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACzF;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IACpC,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,yFAAyF;IACzF,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,2DAA2D;IAC3D,QAAQ,EAAE,mBAAmB,CAAC,UAAU,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE;QAAE,YAAY,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,CAAC;IACrC,wFAAwF;IACxF,MAAM,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACvC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAC9C,6FAA6F;IAC7F,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,oGAAoG;IACpG,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;IAC3C;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,sBAAsB,GAAG,IAAI,CAAC;IACjD,8FAA8F;IAC9F,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC5B,uFAAuF;IACvF,QAAQ,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACjD,qFAAqF;IACrF,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,2FAA2F;IAC3F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,MAAM,CAAC,EAAE,2BAA2B,CAAC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,8BAA8B;IAC7C,IAAI,EAAE;QAAE,YAAY,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,CAAC;IACrC,MAAM,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACvC,2GAA2G;IAC3G,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC5B,yFAAyF;IACzF,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EACA,UAAU,GACV,OAAO,GACP,cAAc,GACd,iBAAiB,GACjB,UAAU,GACV,aAAa,GACb,oBAAoB,GACpB,uBAAuB,GACvB,WAAW,GACX,YAAY,GACZ,iBAAiB,GACjB,QAAQ,GACR,cAAc,GACd,QAAQ,GACR,wBAAwB,GACxB,iBAAiB,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB;
|
|
1
|
+
{"version":3,"file":"estate.d.ts","sourceRoot":"","sources":["../src/estate.ts"],"names":[],"mappings":"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"}
|
package/dist/estate.js
CHANGED
|
@@ -51,11 +51,29 @@ import { extractSearchCertificationHtmlSnapshot } from './search-certification.j
|
|
|
51
51
|
import { changedFilesFromEnv, estateScopeFromEnv, sampleBySeed, sampleEstateRoutes } from './estate-sample.js';
|
|
52
52
|
export { changedFilesFromEnv, changedFileStem, defaultRoutesForFile, estateScopeFromEnv, sampleBySeed, sampleEstateRoutes, } from './estate-sample.js';
|
|
53
53
|
const LOCATION_KINDS = new Set(['location', 'service-location']);
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
/**
|
|
55
|
+
* A location route's place: the registry suburb slug its last path segment names (through the estate plan's public
|
|
56
|
+
* slugs, as site-core's `estatePublicSlug`), and whether it is a region page. A region page has no centre, so its
|
|
57
|
+
* nearby lists are not ranked by distance: it is a region when other location pages sit under it, when its slug is a
|
|
58
|
+
* region's public slug, or when its slug is a suburb's registry slug that the plan moved elsewhere (AA: `ryde`).
|
|
59
|
+
*/
|
|
60
|
+
export function estatePlaceResolver(routes, estate) {
|
|
61
|
+
const parents = new Set(routes.filter((route) => LOCATION_KINDS.has(route.kind ?? '')).map((route) => route.parentId));
|
|
62
|
+
const regionSlugs = new Set(Object.values(estate?.slugs?.regions ?? {}));
|
|
63
|
+
const suburbRenames = estate?.slugs?.suburbs ?? {};
|
|
64
|
+
const suburbByPublic = new Map(Object.entries(suburbRenames).map(([registry, publicSlug]) => [publicSlug, registry]));
|
|
65
|
+
return (route) => {
|
|
66
|
+
if (!route || !LOCATION_KINDS.has(route.kind ?? ''))
|
|
67
|
+
return undefined;
|
|
68
|
+
const publicSlug = normalisePath(route.path).split('/').filter(Boolean).at(-1);
|
|
69
|
+
if (!publicSlug)
|
|
70
|
+
return undefined;
|
|
71
|
+
const suburb = suburbByPublic.get(publicSlug);
|
|
72
|
+
const region = (route.id !== undefined && parents.has(route.id)) ||
|
|
73
|
+
regionSlugs.has(publicSlug) ||
|
|
74
|
+
(suburb === undefined && Object.hasOwn(suburbRenames, publicSlug));
|
|
75
|
+
return { slug: suburb ?? publicSlug, region };
|
|
76
|
+
};
|
|
59
77
|
}
|
|
60
78
|
const BUSINESS_TYPES = new Set([
|
|
61
79
|
'LocalBusiness', 'HomeAndConstructionBusiness', 'GeneralContractor', 'Plumber', 'Electrician',
|
|
@@ -81,6 +99,7 @@ export function defineBrandlabEstateTests(options) {
|
|
|
81
99
|
const families = options.families ?? [];
|
|
82
100
|
const familyPaths = new Set(families.flatMap((family) => family.paths.map(normalisePath)));
|
|
83
101
|
const routeByPath = new Map(routes.map((route) => [normalisePath(route.path), route]));
|
|
102
|
+
const placeOfRoute = estatePlaceResolver(routes, options.estate);
|
|
84
103
|
const homePath = normalisePath(routes.find((route) => route.kind === 'home')?.path ?? '/');
|
|
85
104
|
// A draft pull request crawls a sample (BRANDLAB_ESTATE_SCOPE=sample); ready for review crawls every route.
|
|
86
105
|
const scope = estateScopeFromEnv();
|
|
@@ -174,13 +193,16 @@ export function defineBrandlabEstateTests(options) {
|
|
|
174
193
|
business: options.business,
|
|
175
194
|
}))
|
|
176
195
|
failures.push(`${route.path}: ${problem}`);
|
|
177
|
-
const place =
|
|
178
|
-
if (place && centreOf(place)) {
|
|
196
|
+
const place = placeOfRoute(route);
|
|
197
|
+
if (place && !place.region && centreOf(place.slug)) {
|
|
179
198
|
for (const problem of neighbourOrderProblems({
|
|
180
199
|
html,
|
|
181
|
-
origin: place,
|
|
200
|
+
origin: place.slug,
|
|
182
201
|
pageUrl: expectedCanonical,
|
|
183
|
-
placeOf: (path) =>
|
|
202
|
+
placeOf: (path) => {
|
|
203
|
+
const linked = placeOfRoute(routeByPath.get(path));
|
|
204
|
+
return linked && !linked.region ? linked.slug : undefined;
|
|
205
|
+
},
|
|
184
206
|
centreOf,
|
|
185
207
|
}))
|
|
186
208
|
failures.push(`${route.path}: ${problem}`);
|
|
@@ -575,6 +597,7 @@ export function defineBrandlabPageQualityTests(options) {
|
|
|
575
597
|
const timeout = options.timeout ?? 240_000;
|
|
576
598
|
const routes = options.routes.filter((route) => normalisePath(route.path) !== NOT_FOUND_ROUTE);
|
|
577
599
|
const routeByPath = new Map(routes.map((route) => [normalisePath(route.path), route]));
|
|
600
|
+
const placeOfRoute = estatePlaceResolver(routes, options.estate);
|
|
578
601
|
const run = pageQualityRunFromEnv();
|
|
579
602
|
test.describe('Brandlab page quality (advisory)', () => {
|
|
580
603
|
test('reports page and structured-data quality without failing', async ({ request }, testInfo) => {
|
|
@@ -608,9 +631,9 @@ export function defineBrandlabPageQualityTests(options) {
|
|
|
608
631
|
if (formatProblems.length > 0)
|
|
609
632
|
add('title-format', `"${snapshot.title}": ${formatProblems.join('; ')}`);
|
|
610
633
|
titles.set(snapshot.title, [...(titles.get(snapshot.title) ?? []), route.path]);
|
|
611
|
-
const place =
|
|
634
|
+
const place = placeOfRoute(route);
|
|
612
635
|
if (h1s.length === 1) {
|
|
613
|
-
const problems = titleHeadingProblems(snapshot.title, h1s[0], place ? geography.placeNames.get(place) : undefined);
|
|
636
|
+
const problems = titleHeadingProblems(snapshot.title, h1s[0], place && !place.region ? geography.placeNames.get(place.slug) : undefined);
|
|
614
637
|
if (problems.length > 0)
|
|
615
638
|
add('title-h1', problems.join('; '));
|
|
616
639
|
}
|
|
@@ -633,29 +656,17 @@ export function defineBrandlabPageQualityTests(options) {
|
|
|
633
656
|
for (const failure of inspectStructuredData(route, snapshot.jsonLd, businessId)) {
|
|
634
657
|
add('structured-data', failure.slice(route.path.length + 2));
|
|
635
658
|
}
|
|
636
|
-
const place =
|
|
659
|
+
const place = placeOfRoute(route)?.slug;
|
|
637
660
|
if (indexable && place)
|
|
638
661
|
similarityPages.push({ path: route.path, place, blocks: textBlocks(html).map(geography.maskPlaces) });
|
|
639
|
-
|
|
640
|
-
for (const href of extractLinks(html)) {
|
|
641
|
-
const path = internalPath(href, expectedCanonical, origin);
|
|
642
|
-
if (path && path !== normalisePath(route.path))
|
|
643
|
-
targets.add(path);
|
|
644
|
-
}
|
|
645
|
-
linksFrom.set(normalisePath(route.path), targets);
|
|
662
|
+
linksFrom.set(normalisePath(route.path), pageLinkTargets(html, route.path, origin, expectedCanonical));
|
|
646
663
|
});
|
|
647
664
|
// Internal links: every indexable page needs a link from another page, and indexable pages link no noindex location page.
|
|
648
665
|
const inbound = new Map();
|
|
649
|
-
for (const
|
|
650
|
-
const
|
|
651
|
-
for (const target of targets) {
|
|
666
|
+
for (const targets of linksFrom.values())
|
|
667
|
+
for (const target of targets)
|
|
652
668
|
inbound.set(target, (inbound.get(target) ?? 0) + 1);
|
|
653
|
-
|
|
654
|
-
if (source?.indexing === 'index' && linked && linked.indexing !== 'index' && LOCATION_KINDS.has(linked.kind ?? '')) {
|
|
655
|
-
findings.push({ path: from, kind: 'noindex-link', detail: `links to ${target}, a noindex location page (list unindexed places as plain text)` });
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
669
|
+
findings.push(...noindexLinkFindings(linksFrom, routes));
|
|
659
670
|
for (const route of routes) {
|
|
660
671
|
const path = normalisePath(route.path);
|
|
661
672
|
if (route.indexing === 'index' && route.kind !== 'home' && path !== '/' && linksFrom.has(path) && !inbound.get(path)) {
|
|
@@ -766,6 +777,38 @@ function inspectStructuredData(route, payloads, businessId) {
|
|
|
766
777
|
failures.push(`${route.path}: JSON-LD does not reference ${businessId}`);
|
|
767
778
|
return failures;
|
|
768
779
|
}
|
|
780
|
+
/**
|
|
781
|
+
* The internal pages a page's server HTML links, once each, without the page itself. Only anchors count: an area named
|
|
782
|
+
* as plain text (a `Coverage` entry with no `href`, 3.15.0) is no link.
|
|
783
|
+
*/
|
|
784
|
+
export function pageLinkTargets(html, path, origin, base = new URL(path, `${origin}/`).href) {
|
|
785
|
+
const targets = new Set();
|
|
786
|
+
for (const href of extractLinks(html)) {
|
|
787
|
+
const target = internalPath(href, base, origin);
|
|
788
|
+
if (target && target !== normalisePath(path))
|
|
789
|
+
targets.add(target);
|
|
790
|
+
}
|
|
791
|
+
return targets;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Indexable pages that link a noindex location page (D4: list a place whose page waits for its wave as plain text,
|
|
795
|
+
* as site-core's `coverageEntries` does). `linksFrom` maps each page to the pages it links.
|
|
796
|
+
*/
|
|
797
|
+
export function noindexLinkFindings(linksFrom, routes) {
|
|
798
|
+
const routeByPath = new Map(routes.map((route) => [normalisePath(route.path), route]));
|
|
799
|
+
const findings = [];
|
|
800
|
+
for (const [from, targets] of linksFrom) {
|
|
801
|
+
if (routeByPath.get(from)?.indexing !== 'index')
|
|
802
|
+
continue;
|
|
803
|
+
for (const target of targets) {
|
|
804
|
+
const linked = routeByPath.get(target);
|
|
805
|
+
if (linked && linked.indexing !== 'index' && LOCATION_KINDS.has(linked.kind ?? '')) {
|
|
806
|
+
findings.push({ path: from, kind: 'noindex-link', detail: `links to ${target}, a noindex location page (list unindexed places as plain text)` });
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return findings;
|
|
811
|
+
}
|
|
769
812
|
function extractLinks(html) {
|
|
770
813
|
return [...html.matchAll(/<a\b[^>]*\bhref=["']([^"']+)["']/gi)].map((match) => decodeXml(match[1]));
|
|
771
814
|
}
|