@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
|
@@ -5,8 +5,13 @@
|
|
|
5
5
|
* anything urgent, and keeps itself out of search (`noindex, follow`) so it never ranks and never counts as a lead.
|
|
6
6
|
* The library's `ThankYou` block and `thankYouMetadata()` do all three. A site with a quote form but no thank-you page
|
|
7
7
|
* among the sampled pages fails: the form has nowhere to send an accepted enquiry.
|
|
8
|
+
*
|
|
9
|
+
* A site whose owner decided to promise no reply time records it in `siteSystem.replyTime` (3.15.0): the page then
|
|
10
|
+
* carries the recorded wording instead of a time, and must not promise a time anyway. Without the record, vague
|
|
11
|
+
* words such as "shortly" or "as soon as we can" still fail.
|
|
8
12
|
*/
|
|
9
13
|
import type { SiteSystemFinding } from "../../checks.js";
|
|
14
|
+
import type { SiteSystemReplyTime } from "../../config.js";
|
|
10
15
|
import { type ParsedPage } from "../enquiry/common.js";
|
|
11
16
|
import { type HtmlElement } from "../enquiry/html.js";
|
|
12
17
|
/** A sentence that says how soon the business replies: "within one business day", "in 1 to 2 hours", "within the hour". */
|
|
@@ -16,5 +21,6 @@ export declare const C7_REPLY_TIME: RegExp;
|
|
|
16
21
|
* leaves Bing and every other crawler free to index the page, which is why `thankYouMetadata()` writes both.
|
|
17
22
|
*/
|
|
18
23
|
export declare function robotsDirectives(root: HtmlElement): string[];
|
|
19
|
-
|
|
24
|
+
/** C7 over the sampled pages. `replyTime` is the site's `siteSystem.replyTime` record, when it has one. */
|
|
25
|
+
export declare function checkC7(pages: readonly ParsedPage[], missing?: readonly string[], replyTime?: SiteSystemReplyTime | null): SiteSystemFinding[];
|
|
20
26
|
//# sourceMappingURL=c7-thank-you.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"c7-thank-you.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/form/c7-thank-you.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"c7-thank-you.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/form/c7-thank-you.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAyC,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAE9F,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAMjF,2HAA2H;AAC3H,eAAO,MAAM,aAAa,QASzB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,EAAE,CAK5D;AA6BD,2GAA2G;AAC3G,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,EAAE,OAAO,GAAE,SAAS,MAAM,EAAO,EAAE,SAAS,GAAE,mBAAmB,GAAG,IAAW,GAAG,iBAAiB,EAAE,CAwCxJ"}
|
|
@@ -31,7 +31,20 @@ function telLinks(scope) {
|
|
|
31
31
|
function isThankYouPage(page) {
|
|
32
32
|
return isThankYouPath(page.path) || byHook(page.root, "thank-you").length > 0;
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
/** Lower case, curly quotes straightened, apostrophes dropped, other punctuation and runs of space as one space. */
|
|
35
|
+
function normalised(text) {
|
|
36
|
+
return text
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/[\u2018\u2019']/g, "")
|
|
39
|
+
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
40
|
+
.trim();
|
|
41
|
+
}
|
|
42
|
+
function decision(record) {
|
|
43
|
+
return record.decidedBy === "owner" ? `the owner recorded on ${record.decidedOn}` : `Romie recorded for the owner on ${record.decidedOn}`;
|
|
44
|
+
}
|
|
45
|
+
/** C7 over the sampled pages. `replyTime` is the site's `siteSystem.replyTime` record, when it has one. */
|
|
46
|
+
export function checkC7(pages, missing = [], replyTime = null) {
|
|
47
|
+
const noPromise = replyTime?.promise === false ? replyTime : null;
|
|
35
48
|
const thankYouPages = pages.filter(isThankYouPage);
|
|
36
49
|
if (thankYouPages.length === 0) {
|
|
37
50
|
const named = missing.find((path) => isThankYouPath(path));
|
|
@@ -48,7 +61,16 @@ export function checkC7(pages, missing = []) {
|
|
|
48
61
|
const scope = block ?? mainOf(page.root);
|
|
49
62
|
const reply = descendants(scope).find((element) => attr(element, "data-slot") === "reply");
|
|
50
63
|
const replyText = reply ? textOf(reply) : textOf(scope);
|
|
51
|
-
if (
|
|
64
|
+
if (noPromise) {
|
|
65
|
+
if (!` ${normalised(replyText)} `.includes(` ${normalised(noPromise.wording)} `)) {
|
|
66
|
+
failures.push(fail("C7", page.path, `The thank-you page ${page.path} does not use the reply wording ${decision(noPromise)} ("${noPromise.wording}").`));
|
|
67
|
+
}
|
|
68
|
+
else if (C7_REPLY_TIME.test(replyText)) {
|
|
69
|
+
const by = noPromise.decidedBy === "owner" ? "the owner" : "Romie, for the owner,";
|
|
70
|
+
failures.push(fail("C7", page.path, `The thank-you page ${page.path} promises a reply time although ${by} decided on ${noPromise.decidedOn} to promise none; use "${noPromise.wording}".`));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else if (!C7_REPLY_TIME.test(replyText)) {
|
|
52
74
|
failures.push(fail("C7", page.path, `The thank-you page ${page.path} does not say when the business will reply (for example "within one business day").`));
|
|
53
75
|
}
|
|
54
76
|
if (telLinks(scope).length === 0) {
|
|
@@ -62,6 +84,7 @@ export function checkC7(pages, missing = []) {
|
|
|
62
84
|
failures.push(fail("C7", page.path, `The thank-you page ${page.path} is noindex but also nofollow; its links should still be followed ("noindex, follow").`));
|
|
63
85
|
}
|
|
64
86
|
}
|
|
65
|
-
|
|
87
|
+
const reply = noPromise ? `the reply wording ${decision(noPromise)} instead of a reply time ("${noPromise.wording}")` : "a reply time";
|
|
88
|
+
return resultOf("C7", failures, `Checked ${plural(thankYouPages.length, "thank-you page")}: each gives ${reply} and a tel: link and is noindex, follow.`);
|
|
66
89
|
}
|
|
67
90
|
//# sourceMappingURL=c7-thank-you.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"c7-thank-you.js","sourceRoot":"","sources":["../../../../src/site-system/rules/form/c7-thank-you.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * C7: the thank-you page (Site System v2 §13.2, CONV §14 item 10).\n *\n * After an accepted enquiry the visitor lands on a page that says when they will hear back, offers a `tel:` link for\n * anything urgent, and keeps itself out of search (`noindex, follow`) so it never ranks and never counts as a lead.\n * The library's `ThankYou` block and `thankYouMetadata()` do all three. A site with a quote form but no thank-you page\n * among the sampled pages fails: the form has nowhere to send an accepted enquiry.\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { fail, notApplicable, plural, resultOf, type ParsedPage } from \"../enquiry/common.js\";\nimport { isQuoteForm } from \"../enquiry/c2-quote-form.js\";\nimport { attr, descendants, textOf, type HtmlElement } from \"../enquiry/html.js\";\nimport { byHook, isThankYouPath, mainOf } from \"../library/common.js\";\n\nconst NUMBER = String.raw`(?:(?:a\\s+)?(?:few|couple(?:\\s+of)?|half(?:\\s+an)?)|one|two|three|four|five|six|seven|eight|nine|ten|twelve|an?|\\d+)`;\nconst UNIT = String.raw`(?:minutes?|mins?|hours?|hrs?|business\\s+days?|working\\s+days?|days?)`;\n\n/** A sentence that says how soon the business replies: \"within one business day\", \"in 1 to 2 hours\", \"within the hour\". */\nexport const C7_REPLY_TIME = new RegExp(\n [\n String.raw`\\b(?:within|in|inside|under)\\s+(?:the\\s+)?(?:next\\s+)?${NUMBER}(?:\\s*(?:-|–|to|or)\\s*${NUMBER})?\\s+${UNIT}\\b`,\n String.raw`\\b(?:within|in|inside|under)\\s+(?:the\\s+)?(?:next\\s+)?(?:hour|day)\\b`,\n String.raw`\\b(?:within|during|in)\\s+(?:our\\s+)?(?:normal\\s+)?(?:business|working|office)\\s+hours\\b`,\n String.raw`\\b(?:same|next)(?:[-\\s]+business)?[-\\s]+day\\b`,\n String.raw`\\bby\\s+(?:the\\s+)?end\\s+of\\s+(?:the\\s+)?(?:business\\s+)?day\\b`,\n ].join(\"|\"),\n \"i\",\n);\n\n/**\n * The robots directives of a page, from every `<meta name=\"robots\">` tag. A `googlebot` tag alone does not count: it\n * leaves Bing and every other crawler free to index the page, which is why `thankYouMetadata()` writes both.\n */\nexport function robotsDirectives(root: HtmlElement): string[] {\n return descendants(root)\n .filter((element) => element.tag === \"meta\" && (attr(element, \"name\") ?? \"\").toLowerCase() === \"robots\")\n .flatMap((element) => (attr(element, \"content\") ?? \"\").toLowerCase().split(/[,\\s]+/))\n .filter(Boolean);\n}\n\nfunction hasQuoteForm(page: ParsedPage): boolean {\n return descendants(page.root).some((element) => element.tag === \"form\" && isQuoteForm(element));\n}\n\nfunction telLinks(scope: HtmlElement): HtmlElement[] {\n return descendants(scope).filter((element) => element.tag === \"a\" && (attr(element, \"href\") ?? \"\").trim().toLowerCase().startsWith(\"tel:\"));\n}\n\nfunction isThankYouPage(page: ParsedPage): boolean {\n return isThankYouPath(page.path) || byHook(page.root, \"thank-you\").length > 0;\n}\n\nexport function checkC7(pages: readonly ParsedPage[], missing: readonly string[] = []): SiteSystemFinding[] {\n const thankYouPages = pages.filter(isThankYouPage);\n if (thankYouPages.length === 0) {\n const named = missing.find((path) => isThankYouPath(path));\n if (named !== undefined) {\n return [fail(\"C7\", named, `The thank-you page ${named} could not be read, so its reply time, call link and noindex were not checked.`)];\n }\n if (!pages.some(hasQuoteForm)) return [notApplicable(\"C7\", \"None of the sampled pages has a quote form or a thank-you page.\")];\n return [fail(\"C7\", undefined, \"The site has a quote form but no thank-you page for an accepted enquiry to open.\")];\n }\n\n const failures: SiteSystemFinding[] = [];\n for (const page of thankYouPages) {\n const block = byHook(page.root, \"thank-you\")[0];\n const scope = block ?? mainOf(page.root);\n const reply = descendants(scope).find((element) => attr(element, \"data-slot\") === \"reply\");\n const replyText = reply ? textOf(reply) : textOf(scope);\n if (!C7_REPLY_TIME.test(replyText)) {\n failures.push(fail(\"C7\", page.path, `The thank-you page ${page.path} does not say when the business will reply (for example \"within one business day\").`));\n }\n if (telLinks(scope).length === 0) {\n failures.push(fail(\"C7\", page.path, `The thank-you page ${page.path} has no tel: link for anything urgent.`));\n }\n const directives = robotsDirectives(page.root);\n if (!directives.includes(\"noindex\") && !directives.includes(\"none\")) {\n failures.push(fail(\"C7\", page.path, `The thank-you page ${page.path} is not noindex; it carries robots \"${directives.join(\", \") || \"none\"}\" instead of \"noindex, follow\".`));\n } else if (directives.includes(\"nofollow\") || directives.includes(\"none\")) {\n failures.push(fail(\"C7\", page.path, `The thank-you page ${page.path} is noindex but also nofollow; its links should still be followed (\"noindex, follow\").`));\n }\n }\n return resultOf(\"C7\", failures, `Checked ${plural(thankYouPages.length, \"thank-you page\")}: each gives a reply time and a tel: link and is noindex, follow.`);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"c7-thank-you.js","sourceRoot":"","sources":["../../../../src/site-system/rules/form/c7-thank-you.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * C7: the thank-you page (Site System v2 §13.2, CONV §14 item 10).\n *\n * After an accepted enquiry the visitor lands on a page that says when they will hear back, offers a `tel:` link for\n * anything urgent, and keeps itself out of search (`noindex, follow`) so it never ranks and never counts as a lead.\n * The library's `ThankYou` block and `thankYouMetadata()` do all three. A site with a quote form but no thank-you page\n * among the sampled pages fails: the form has nowhere to send an accepted enquiry.\n *\n * A site whose owner decided to promise no reply time records it in `siteSystem.replyTime` (3.15.0): the page then\n * carries the recorded wording instead of a time, and must not promise a time anyway. Without the record, vague\n * words such as \"shortly\" or \"as soon as we can\" still fail.\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport type { SiteSystemReplyTime } from \"../../config.js\";\nimport { fail, notApplicable, plural, resultOf, type ParsedPage } from \"../enquiry/common.js\";\nimport { isQuoteForm } from \"../enquiry/c2-quote-form.js\";\nimport { attr, descendants, textOf, type HtmlElement } from \"../enquiry/html.js\";\nimport { byHook, isThankYouPath, mainOf } from \"../library/common.js\";\n\nconst NUMBER = String.raw`(?:(?:a\\s+)?(?:few|couple(?:\\s+of)?|half(?:\\s+an)?)|one|two|three|four|five|six|seven|eight|nine|ten|twelve|an?|\\d+)`;\nconst UNIT = String.raw`(?:minutes?|mins?|hours?|hrs?|business\\s+days?|working\\s+days?|days?)`;\n\n/** A sentence that says how soon the business replies: \"within one business day\", \"in 1 to 2 hours\", \"within the hour\". */\nexport const C7_REPLY_TIME = new RegExp(\n [\n String.raw`\\b(?:within|in|inside|under)\\s+(?:the\\s+)?(?:next\\s+)?${NUMBER}(?:\\s*(?:-|–|to|or)\\s*${NUMBER})?\\s+${UNIT}\\b`,\n String.raw`\\b(?:within|in|inside|under)\\s+(?:the\\s+)?(?:next\\s+)?(?:hour|day)\\b`,\n String.raw`\\b(?:within|during|in)\\s+(?:our\\s+)?(?:normal\\s+)?(?:business|working|office)\\s+hours\\b`,\n String.raw`\\b(?:same|next)(?:[-\\s]+business)?[-\\s]+day\\b`,\n String.raw`\\bby\\s+(?:the\\s+)?end\\s+of\\s+(?:the\\s+)?(?:business\\s+)?day\\b`,\n ].join(\"|\"),\n \"i\",\n);\n\n/**\n * The robots directives of a page, from every `<meta name=\"robots\">` tag. A `googlebot` tag alone does not count: it\n * leaves Bing and every other crawler free to index the page, which is why `thankYouMetadata()` writes both.\n */\nexport function robotsDirectives(root: HtmlElement): string[] {\n return descendants(root)\n .filter((element) => element.tag === \"meta\" && (attr(element, \"name\") ?? \"\").toLowerCase() === \"robots\")\n .flatMap((element) => (attr(element, \"content\") ?? \"\").toLowerCase().split(/[,\\s]+/))\n .filter(Boolean);\n}\n\nfunction hasQuoteForm(page: ParsedPage): boolean {\n return descendants(page.root).some((element) => element.tag === \"form\" && isQuoteForm(element));\n}\n\nfunction telLinks(scope: HtmlElement): HtmlElement[] {\n return descendants(scope).filter((element) => element.tag === \"a\" && (attr(element, \"href\") ?? \"\").trim().toLowerCase().startsWith(\"tel:\"));\n}\n\nfunction isThankYouPage(page: ParsedPage): boolean {\n return isThankYouPath(page.path) || byHook(page.root, \"thank-you\").length > 0;\n}\n\n/** Lower case, curly quotes straightened, apostrophes dropped, other punctuation and runs of space as one space. */\nfunction normalised(text: string): string {\n return text\n .toLowerCase()\n .replace(/[\\u2018\\u2019']/g, \"\")\n .replace(/[^\\p{L}\\p{N}]+/gu, \" \")\n .trim();\n}\n\ntype NoPromise = Extract<SiteSystemReplyTime, { promise: false }>;\n\nfunction decision(record: NoPromise): string {\n return record.decidedBy === \"owner\" ? `the owner recorded on ${record.decidedOn}` : `Romie recorded for the owner on ${record.decidedOn}`;\n}\n\n/** C7 over the sampled pages. `replyTime` is the site's `siteSystem.replyTime` record, when it has one. */\nexport function checkC7(pages: readonly ParsedPage[], missing: readonly string[] = [], replyTime: SiteSystemReplyTime | null = null): SiteSystemFinding[] {\n const noPromise = replyTime?.promise === false ? replyTime : null;\n const thankYouPages = pages.filter(isThankYouPage);\n if (thankYouPages.length === 0) {\n const named = missing.find((path) => isThankYouPath(path));\n if (named !== undefined) {\n return [fail(\"C7\", named, `The thank-you page ${named} could not be read, so its reply time, call link and noindex were not checked.`)];\n }\n if (!pages.some(hasQuoteForm)) return [notApplicable(\"C7\", \"None of the sampled pages has a quote form or a thank-you page.\")];\n return [fail(\"C7\", undefined, \"The site has a quote form but no thank-you page for an accepted enquiry to open.\")];\n }\n\n const failures: SiteSystemFinding[] = [];\n for (const page of thankYouPages) {\n const block = byHook(page.root, \"thank-you\")[0];\n const scope = block ?? mainOf(page.root);\n const reply = descendants(scope).find((element) => attr(element, \"data-slot\") === \"reply\");\n const replyText = reply ? textOf(reply) : textOf(scope);\n if (noPromise) {\n if (!` ${normalised(replyText)} `.includes(` ${normalised(noPromise.wording)} `)) {\n failures.push(fail(\"C7\", page.path, `The thank-you page ${page.path} does not use the reply wording ${decision(noPromise)} (\"${noPromise.wording}\").`));\n } else if (C7_REPLY_TIME.test(replyText)) {\n const by = noPromise.decidedBy === \"owner\" ? \"the owner\" : \"Romie, for the owner,\";\n failures.push(fail(\"C7\", page.path, `The thank-you page ${page.path} promises a reply time although ${by} decided on ${noPromise.decidedOn} to promise none; use \"${noPromise.wording}\".`));\n }\n } else if (!C7_REPLY_TIME.test(replyText)) {\n failures.push(fail(\"C7\", page.path, `The thank-you page ${page.path} does not say when the business will reply (for example \"within one business day\").`));\n }\n if (telLinks(scope).length === 0) {\n failures.push(fail(\"C7\", page.path, `The thank-you page ${page.path} has no tel: link for anything urgent.`));\n }\n const directives = robotsDirectives(page.root);\n if (!directives.includes(\"noindex\") && !directives.includes(\"none\")) {\n failures.push(fail(\"C7\", page.path, `The thank-you page ${page.path} is not noindex; it carries robots \"${directives.join(\", \") || \"none\"}\" instead of \"noindex, follow\".`));\n } else if (directives.includes(\"nofollow\") || directives.includes(\"none\")) {\n failures.push(fail(\"C7\", page.path, `The thank-you page ${page.path} is noindex but also nofollow; its links should still be followed (\"noindex, follow\").`));\n }\n }\n const reply = noPromise ? `the reply wording ${decision(noPromise)} instead of a reply time (\"${noPromise.wording}\")` : \"a reply time\";\n return resultOf(\"C7\", failures, `Checked ${plural(thankYouPages.length, \"thank-you page\")}: each gives ${reply} and a tel: link and is noindex, follow.`);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/form/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/form/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAMhE,OAAO,EAAE,oBAAoB,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAErG,2BAA2B;AAC3B,wBAAsB,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,uBAAuB,EAAE,SAAS,GAAG,OAAO,GAAG,aAAa,CAAC,GAAG;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAAG,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAG9L"}
|
|
@@ -2,12 +2,22 @@ import { parsePages } from "../enquiry/common.js";
|
|
|
2
2
|
import { checkC11 } from "./c11-contact-route.js";
|
|
3
3
|
import { checkC7 } from "./c7-thank-you.js";
|
|
4
4
|
import { checkC8 } from "./c8-forgiving-form.js";
|
|
5
|
+
import { readSiteSystemProjectConfig } from "../../config.js";
|
|
5
6
|
export { C11_ROUTE_CANDIDATES, checkC11 } from "./c11-contact-route.js";
|
|
6
7
|
export { C7_REPLY_TIME, checkC7, robotsDirectives } from "./c7-thank-you.js";
|
|
7
8
|
export { C8_PHONE_SAMPLES, checkC8, formProblems, phoneInputProblems } from "./c8-forgiving-form.js";
|
|
8
9
|
/** Runs C7, C8 and C11. */
|
|
9
10
|
export async function runFormRules(input) {
|
|
10
11
|
const pages = parsePages(input.pages);
|
|
11
|
-
return [...checkC7(pages, input.missing ?? []), ...checkC8(pages), ...checkC11(input.projectRoot, pages)];
|
|
12
|
+
return [...checkC7(pages, input.missing ?? [], recordedReplyTime(input.projectRoot)), ...checkC8(pages), ...checkC11(input.projectRoot, pages)];
|
|
13
|
+
}
|
|
14
|
+
/** The site's `siteSystem.replyTime` record; null when there is none or the config cannot be read (C7 then needs a time). */
|
|
15
|
+
function recordedReplyTime(projectRoot) {
|
|
16
|
+
try {
|
|
17
|
+
return readSiteSystemProjectConfig(projectRoot).siteSystem?.replyTime ?? null;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
12
22
|
}
|
|
13
23
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/site-system/rules/form/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAElD,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/site-system/rules/form/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAElD,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,2BAA2B,EAA4B,MAAM,iBAAiB,CAAC;AAExF,OAAO,EAAE,oBAAoB,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAErG,2BAA2B;AAC3B,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,KAAoH;IACrJ,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACtC,OAAO,CAAC,GAAG,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,OAAO,IAAI,EAAE,EAAE,iBAAiB,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC;AAClJ,CAAC;AAED,6HAA6H;AAC7H,SAAS,iBAAiB,CAAC,WAAmB;IAC5C,IAAI,CAAC;QACH,OAAO,2BAA2B,CAAC,WAAW,CAAC,CAAC,UAAU,EAAE,SAAS,IAAI,IAAI,CAAC;IAChF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC","sourcesContent":["/**\n * The form rule module (release 3.10.0): C7 the thank-you page, C8 the forgiving form and C11 the contact route's\n * protections. C7 and C8 read the sampled pages' server HTML; C11 reads the project's `/api/contact` route.\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { parsePages } from \"../enquiry/common.js\";\nimport type { SiteSystemRenderedInput } from \"../../runners.js\";\nimport { checkC11 } from \"./c11-contact-route.js\";\nimport { checkC7 } from \"./c7-thank-you.js\";\nimport { checkC8 } from \"./c8-forgiving-form.js\";\nimport { readSiteSystemProjectConfig, type SiteSystemReplyTime } from \"../../config.js\";\n\nexport { C11_ROUTE_CANDIDATES, checkC11 } from \"./c11-contact-route.js\";\nexport { C7_REPLY_TIME, checkC7, robotsDirectives } from \"./c7-thank-you.js\";\nexport { C8_PHONE_SAMPLES, checkC8, formProblems, phoneInputProblems } from \"./c8-forgiving-form.js\";\n\n/** Runs C7, C8 and C11. */\nexport async function runFormRules(input: Pick<SiteSystemRenderedInput, \"baseUrl\" | \"pages\" | \"projectRoot\"> & { readonly missing?: readonly string[] }): Promise<readonly SiteSystemFinding[]> {\n const pages = parsePages(input.pages);\n return [...checkC7(pages, input.missing ?? [], recordedReplyTime(input.projectRoot)), ...checkC8(pages), ...checkC11(input.projectRoot, pages)];\n}\n\n/** The site's `siteSystem.replyTime` record; null when there is none or the config cannot be read (C7 then needs a time). */\nfunction recordedReplyTime(projectRoot: string): SiteSystemReplyTime | null {\n try {\n return readSiteSystemProjectConfig(projectRoot).siteSystem?.replyTime ?? null;\n } catch {\n return null;\n }\n}\n"]}
|
|
@@ -12,6 +12,8 @@ export interface FullContext {
|
|
|
12
12
|
readonly geographyProblem: string;
|
|
13
13
|
readonly strategy: FullStrategy;
|
|
14
14
|
readonly projectRoot: string;
|
|
15
|
+
/** Path → kind for the route catalogue's literal entries (`src/data/routes.ts`); absent or empty when unread. */
|
|
16
|
+
readonly catalogueKinds?: ReadonlyMap<string, string>;
|
|
15
17
|
/** Estate families brandlab.config.ts allows to refresh more often than weekly (W3); absent when none. */
|
|
16
18
|
readonly refreshExceptions?: readonly SiteSystemRefreshException[];
|
|
17
19
|
/** Today in Sydney, YYYY-MM-DD: the day waves are judged on. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/common.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAE5E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAClE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAEzE,yCAAyC;AACzC,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,0FAA0F;IAC1F,QAAQ,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI,CAAC;IACzC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,0GAA0G;IAC1G,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACnE,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,wHAAwH;IACxH,QAAQ,CAAC,YAAY,EACjB,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,SAAS;YAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;KAAE,GAAG,SAAS,CAAC,GAC9J,MAAM,CAAC;CACZ;AAED,4FAA4F;AAC5F,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC,4FAA4F;AAC5F,wBAAgB,OAAO,CAAC,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAE,SAAS,iBAAiB,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAkBpI;AAED,sFAAsF;AACtF,wBAAgB,OAAO,CAAC,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,iBAAiB,CAEtG;AAED,2HAA2H;AAC3H,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAG/F;AAED,oBAAoB;AACpB,wBAAgB,MAAM,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,SAAI,GAAG,MAAM,CAKlE;AAED,mEAAmE;AACnE,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAI1D,CAAC"}
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/common.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAE5E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAClE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAEzE,yCAAyC;AACzC,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,0FAA0F;IAC1F,QAAQ,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI,CAAC;IACzC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,iHAAiH;IACjH,QAAQ,CAAC,cAAc,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtD,0GAA0G;IAC1G,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACnE,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,wHAAwH;IACxH,QAAQ,CAAC,YAAY,EACjB,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,SAAS;YAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;KAAE,GAAG,SAAS,CAAC,GAC9J,MAAM,CAAC;CACZ;AAED,4FAA4F;AAC5F,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC,4FAA4F;AAC5F,wBAAgB,OAAO,CAAC,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAE,SAAS,iBAAiB,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAkBpI;AAED,sFAAsF;AACtF,wBAAgB,OAAO,CAAC,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,iBAAiB,CAEtG;AAED,2HAA2H;AAC3H,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAG/F;AAED,oBAAoB;AACpB,wBAAgB,MAAM,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,SAAI,GAAG,MAAM,CAKlE;AAED,mEAAmE;AACnE,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAI1D,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/common.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAMlD,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"common.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/common.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SiteSystemCheckId, SiteSystemFinding } from \"../../checks.js\";\nimport { fail, pass } from \"../enquiry/common.js\";\nimport type { SiteSystemRefreshException } from \"../../config.js\";\nimport type { Estate } from \"./estate.js\";\nimport type { FullGeography } from \"./geography.js\";\nimport type { FullStrategy } from \"./strategy.js\";\n\nexport { fail, notApplicable, pass, plural } from \"../enquiry/common.js\";\n\n/** Everything a full-tier rule reads. */\nexport interface FullContext {\n readonly estate: Estate;\n /** Null when the site-core geography could not be loaded; `geographyProblem` says why. */\n readonly geography: FullGeography | null;\n readonly geographyProblem: string;\n readonly strategy: FullStrategy;\n readonly projectRoot: string;\n /** Path → kind for the route catalogue's literal entries (`src/data/routes.ts`); absent or empty when unread. */\n readonly catalogueKinds?: ReadonlyMap<string, string>;\n /** Estate families brandlab.config.ts allows to refresh more often than weekly (W3); absent when none. */\n readonly refreshExceptions?: readonly SiteSystemRefreshException[];\n /** Today in Sydney, YYYY-MM-DD: the day waves are judged on. */\n readonly today: string;\n /** A family's canonical sections and cap (site-core `defaultSectionOrder`), or a string saying why they are unknown. */\n readonly defaultOrder:\n | ((family: string, trade?: string) => { readonly cap: number; readonly entries: readonly { readonly id: string; readonly requirement: string }[] } | undefined)\n | string;\n}\n\n/** The most failures one check lists one by one; the rest are counted in a last finding. */\nexport const FULL_LISTED_FAILURES = 25;\n\n/** The failures (at most `FULL_LISTED_FAILURES`, then a count of the rest), or one pass. */\nexport function outcome(checkId: SiteSystemCheckId, failures: readonly SiteSystemFinding[], passMessage: string): SiteSystemFinding[] {\n const failed = failures.filter((finding) => finding.status === \"fail\");\n const allWarnings = failures.filter((finding) => finding.status !== \"fail\");\n const warnings = allWarnings.length <= FULL_LISTED_FAILURES\n ? allWarnings\n : [\n ...allWarnings.slice(0, FULL_LISTED_FAILURES),\n { checkId, status: \"pass\" as const, severity: \"warning\" as const, message: `${allWarnings.length - FULL_LISTED_FAILURES} more warnings of the same kind.` },\n ];\n if (failed.length === 0) return [...warnings, pass(checkId, passMessage)];\n if (failed.length <= FULL_LISTED_FAILURES) return [...failed, ...warnings];\n const rest = failed.slice(FULL_LISTED_FAILURES);\n const examples = [...new Set(rest.flatMap((finding) => finding.target ?? []))].slice(0, 3);\n return [\n ...failed.slice(0, FULL_LISTED_FAILURES),\n fail(checkId, undefined, `${rest.length} more ${rest.length === 1 ? \"problem\" : \"problems\"} of the same kind${examples.length > 0 ? `, for example on ${examples.join(\", \")}` : \"\"}.`),\n ...warnings,\n ];\n}\n\n/** A finding that never blocks: the check passes and the report shows the warning. */\nexport function warning(checkId: SiteSystemCheckId, target: string, message: string): SiteSystemFinding {\n return { checkId, status: \"pass\", target, message, severity: \"warning\" };\n}\n\n/** The estate-wide checks need the whole build, not a sample: without it they fail, so the gate never passes unchecked. */\nexport function needsBuild(checkId: SiteSystemCheckId, estate: Estate): SiteSystemFinding | null {\n if (estate.complete) return null;\n return fail(checkId, undefined, \"The production build could not be read (no .next/prerender-manifest.json), so the whole estate could not be checked; run next build first.\");\n}\n\n/** \"a, b and c\". */\nexport function listOf(items: readonly string[], limit = 4): string {\n const shown = items.slice(0, limit);\n const more = items.length - shown.length;\n if (more > 0) return `${shown.join(\", \")} and ${more} more`;\n return shown.length <= 1 ? shown.join(\"\") : `${shown.slice(0, -1).join(\", \")} and ${shown.at(-1)}`;\n}\n\n/** \"the suburb page /locations/ryde\" style labels for a family. */\nexport const FAMILY_LABELS: Readonly<Record<string, string>> = {\n suburb: \"suburb page\",\n \"service-in-suburb\": \"service-in-suburb page\",\n region: \"region page\",\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"e1-pattern.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/e1-pattern.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAoD,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAUjG,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,iBAAiB,EAAE,CAqBxE"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E1-pattern (3.14.0): the title pattern half of E1 on every built location page (Site System v2 §5.2; correction
|
|
3
|
+
* #27). A suburb, region or service-in-suburb page's H1 is "{Service} {Place}": the short exact phrase ending with the
|
|
4
|
+
* place, with no "in" and no "the"; its title starts with that H1, at most 60 characters with at most two pipes. The
|
|
5
|
+
* place is the one the page's path names. Pages whose titles the site recorded (`docs/recorded-titles.json`) keep them
|
|
6
|
+
* word for word under E1 and are not judged here. site-core's `buildPageTitle` writes titles that pass.
|
|
7
|
+
*/
|
|
8
|
+
import { locationHeadingProblem } from "@brandlab-digital/site-core/page-titles";
|
|
9
|
+
import { readRecordedTitles } from "../enquiry/e1-recorded-titles.js";
|
|
10
|
+
import { fail, needsBuild, notApplicable, outcome, plural } from "./common.js";
|
|
11
|
+
import { isLocationFamily } from "./estate.js";
|
|
12
|
+
/** The names a page's place may be written as: a suburb's name, or a region's name, search name and alternates. */
|
|
13
|
+
function placeNames(slug, context) {
|
|
14
|
+
const suburb = context.geography?.suburbs.get(slug);
|
|
15
|
+
if (suburb)
|
|
16
|
+
return [suburb.name];
|
|
17
|
+
return [...(context.geography?.regions.get(slug)?.names ?? [])];
|
|
18
|
+
}
|
|
19
|
+
export function checkE1Pattern(context) {
|
|
20
|
+
const skip = needsBuild("E1-pattern", context.estate);
|
|
21
|
+
if (skip)
|
|
22
|
+
return [skip];
|
|
23
|
+
if (!context.geography)
|
|
24
|
+
return [fail("E1-pattern", undefined, `The places location pages are for could not be read (${context.geographyProblem}).`)];
|
|
25
|
+
const recorded = readRecordedTitles(context.projectRoot);
|
|
26
|
+
const kept = recorded instanceof Map ? recorded : new Map();
|
|
27
|
+
const failures = [];
|
|
28
|
+
let checked = 0;
|
|
29
|
+
for (const page of context.estate.pages) {
|
|
30
|
+
if (!isLocationFamily(page.family) || !page.place || kept.has(page.path))
|
|
31
|
+
continue;
|
|
32
|
+
const names = placeNames(page.place.slug, context);
|
|
33
|
+
if (names.length === 0)
|
|
34
|
+
continue;
|
|
35
|
+
checked += 1;
|
|
36
|
+
const title = page.title ?? "";
|
|
37
|
+
// The name the H1 ends with when there is one; otherwise the place's main name.
|
|
38
|
+
const name = names.find((candidate) => page.h1.trim().toLowerCase().endsWith(candidate.toLowerCase())) ?? names[0];
|
|
39
|
+
const problem = page.h1.trim() ? locationHeadingProblem({ h1: page.h1, title, place: name }) : "the page has no H1.";
|
|
40
|
+
if (problem)
|
|
41
|
+
failures.push(fail("E1-pattern", page.path, `${page.path}: ${problem}`));
|
|
42
|
+
}
|
|
43
|
+
if (checked === 0)
|
|
44
|
+
return [notApplicable("E1-pattern", "The build has no location page outside the recorded titles.")];
|
|
45
|
+
return outcome("E1-pattern", failures, `${plural(checked, "location page")} carry "{Service} {Place}" as their H1, and their titles lead with it.`);
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=e1-pattern.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"e1-pattern.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/e1-pattern.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * E1-pattern (3.14.0): the title pattern half of E1 on every built location page (Site System v2 §5.2; correction\n * #27). A suburb, region or service-in-suburb page's H1 is \"{Service} {Place}\": the short exact phrase ending with the\n * place, with no \"in\" and no \"the\"; its title starts with that H1, at most 60 characters with at most two pipes. The\n * place is the one the page's path names. Pages whose titles the site recorded (`docs/recorded-titles.json`) keep them\n * word for word under E1 and are not judged here. site-core's `buildPageTitle` writes titles that pass.\n */\nimport { locationHeadingProblem } from \"@brandlab-digital/site-core/page-titles\";\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { readRecordedTitles } from \"../enquiry/e1-recorded-titles.js\";\nimport { fail, needsBuild, notApplicable, outcome, plural, type FullContext } from \"./common.js\";\nimport { isLocationFamily } from \"./estate.js\";\n\n/** The names a page's place may be written as: a suburb's name, or a region's name, search name and alternates. */\nfunction placeNames(slug: string, context: FullContext): string[] {\n const suburb = context.geography?.suburbs.get(slug);\n if (suburb) return [suburb.name];\n return [...(context.geography?.regions.get(slug)?.names ?? [])];\n}\n\nexport function checkE1Pattern(context: FullContext): SiteSystemFinding[] {\n const skip = needsBuild(\"E1-pattern\", context.estate);\n if (skip) return [skip];\n if (!context.geography) return [fail(\"E1-pattern\", undefined, `The places location pages are for could not be read (${context.geographyProblem}).`)];\n const recorded = readRecordedTitles(context.projectRoot);\n const kept = recorded instanceof Map ? recorded : new Map<string, string>();\n const failures: SiteSystemFinding[] = [];\n let checked = 0;\n for (const page of context.estate.pages) {\n if (!isLocationFamily(page.family) || !page.place || kept.has(page.path)) continue;\n const names = placeNames(page.place.slug, context);\n if (names.length === 0) continue;\n checked += 1;\n const title = page.title ?? \"\";\n // The name the H1 ends with when there is one; otherwise the place's main name.\n const name = names.find((candidate) => page.h1.trim().toLowerCase().endsWith(candidate.toLowerCase())) ?? names[0]!;\n const problem = page.h1.trim() ? locationHeadingProblem({ h1: page.h1, title, place: name }) : \"the page has no H1.\";\n if (problem) failures.push(fail(\"E1-pattern\", page.path, `${page.path}: ${problem}`));\n }\n if (checked === 0) return [notApplicable(\"E1-pattern\", \"The build has no location page outside the recorded titles.\")];\n return outcome(\"E1-pattern\", failures, `${plural(checked, \"location page\")} carry \"{Service} {Place}\" as their H1, and their titles lead with it.`);\n}\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SiteSystemPage } from "../../samples.js";
|
|
2
|
-
import
|
|
2
|
+
import type { SiteEstatePaths } from "@brandlab-digital/site-core/strategy";
|
|
3
|
+
import { type FullGeography, type PathAliases, type PathPlace } from "./geography.js";
|
|
3
4
|
/** The section manifest as SectionSequence writes it (site-core `BrandlabSectionManifest`). */
|
|
4
5
|
export interface ManifestData {
|
|
5
6
|
readonly version: number;
|
|
@@ -53,7 +54,7 @@ export interface LocationCopy {
|
|
|
53
54
|
readonly answer: string;
|
|
54
55
|
}[];
|
|
55
56
|
/** The job cards of the nearby-jobs section; null when the page has none. */
|
|
56
|
-
readonly jobs: readonly
|
|
57
|
+
readonly jobs: readonly LocationJobCard[] | null;
|
|
57
58
|
/** The price-guide section's text; null when the page has none. */
|
|
58
59
|
readonly prices: string | null;
|
|
59
60
|
readonly reviews: {
|
|
@@ -68,6 +69,11 @@ export interface LocationCopy {
|
|
|
68
69
|
readonly telLink: boolean;
|
|
69
70
|
readonly callBar: boolean;
|
|
70
71
|
}
|
|
72
|
+
/** A nearby-job card: its text, and the job's suburb slug from the card's `data-suburb` (NearbyJobs) when it carries one. */
|
|
73
|
+
export interface LocationJobCard {
|
|
74
|
+
readonly text: string;
|
|
75
|
+
readonly place?: string;
|
|
76
|
+
}
|
|
71
77
|
export interface EstatePage {
|
|
72
78
|
readonly path: string;
|
|
73
79
|
/** The app route Next built the page from (`/locations/[suburb]`), when known. */
|
|
@@ -85,10 +91,23 @@ export interface EstatePage {
|
|
|
85
91
|
readonly manifest: ManifestState;
|
|
86
92
|
/** Top-level `data-bl-section` ids, in document order. */
|
|
87
93
|
readonly sections: readonly string[];
|
|
94
|
+
/**
|
|
95
|
+
* Per top-level section (3.14.0): the `data-bl-slot` values inside it and the `slot:reason` pairs of its
|
|
96
|
+
* `data-bl-slot-skip` attribute.
|
|
97
|
+
*/
|
|
98
|
+
readonly slots?: ReadonlyMap<string, {
|
|
99
|
+
readonly present: readonly string[];
|
|
100
|
+
readonly skipped: readonly {
|
|
101
|
+
readonly slot: string;
|
|
102
|
+
readonly reason: string;
|
|
103
|
+
}[];
|
|
104
|
+
}>;
|
|
88
105
|
readonly h1: string;
|
|
106
|
+
/** The page's `<title>` text (3.14.0); absent when it has none. */
|
|
107
|
+
readonly title?: string;
|
|
89
108
|
readonly place: PathPlace | undefined;
|
|
90
109
|
family: string | undefined;
|
|
91
|
-
familySource: "manifest" | "sitemap" | "route" | "stem" | "path" | undefined;
|
|
110
|
+
familySource: "manifest" | "plan" | "sitemap" | "route" | "stem" | "path" | undefined;
|
|
92
111
|
copy: LocationCopy | undefined;
|
|
93
112
|
}
|
|
94
113
|
export interface Estate {
|
|
@@ -116,6 +135,10 @@ export declare function readEstate(input: {
|
|
|
116
135
|
readonly geography: FullGeography | null;
|
|
117
136
|
/** Families known from elsewhere, by path (the representative service route). */
|
|
118
137
|
readonly seeds?: Readonly<Record<string, string>>;
|
|
138
|
+
/** The estate plan's public slugs (3.14.0). */
|
|
139
|
+
readonly aliases?: PathAliases;
|
|
140
|
+
/** The estate plan's family paths (3.14.0): they type region, suburb and service-in-suburb pages without a manifest. */
|
|
141
|
+
readonly paths?: SiteEstatePaths;
|
|
119
142
|
}): Estate;
|
|
120
143
|
/** An internal link's normalised path, or null for other sites, anchors on the same page, mail and phone links. */
|
|
121
144
|
export declare function internalPath(href: string, from: string, hosts: ReadonlySet<string>): string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"estate.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/estate.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAIvD,OAAO,EAAe,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"estate.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/estate.ts"],"names":[],"mappings":"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"}
|
|
@@ -96,7 +96,7 @@ export function readEstate(input) {
|
|
|
96
96
|
// A relative canonical names no host.
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
-
const { summary, hrefs } = summarise({ path, html, ...(source.srcRoute ? { srcRoute: source.srcRoute } : {}), ...(source.headers ? { headers: source.headers } : {}), ...(source.revalidate !== undefined ? { revalidate: source.revalidate } : {}) }, sitemapOf, input.geography);
|
|
99
|
+
const { summary, hrefs } = summarise({ path, html, ...(source.srcRoute ? { srcRoute: source.srcRoute } : {}), ...(source.headers ? { headers: source.headers } : {}), ...(source.revalidate !== undefined ? { revalidate: source.revalidate } : {}) }, sitemapOf, input.geography, input.aliases);
|
|
100
100
|
read.push({ summary, hrefs, groups: extractLinkGroups(html), source });
|
|
101
101
|
}
|
|
102
102
|
const pages = [];
|
|
@@ -114,7 +114,7 @@ export function readEstate(input) {
|
|
|
114
114
|
});
|
|
115
115
|
}
|
|
116
116
|
const byPath = new Map(pages.map((page) => [page.path, page]));
|
|
117
|
-
inferFamilies(pages, input.seeds ?? {});
|
|
117
|
+
inferFamilies(pages, input.seeds ?? {}, input.paths);
|
|
118
118
|
// Second pass: the copy of location pages found without a manifest.
|
|
119
119
|
for (const [index, page] of pages.entries()) {
|
|
120
120
|
if (!isLocationFamily(page.family) || page.copy)
|
|
@@ -125,7 +125,7 @@ export function readEstate(input) {
|
|
|
125
125
|
const locationRoutes = new Set(pages.flatMap((page) => (isLocationFamily(page.family) && page.srcRoute && isGenerated(page.srcRoute) ? [page.srcRoute] : [])));
|
|
126
126
|
const locationStems = new Set(pages.flatMap((page) => (isLocationFamily(page.family) && page.place ? [page.place.stem] : [])));
|
|
127
127
|
const missingHtml = missing.map((item) => {
|
|
128
|
-
const stem = input.geography ? placeOfPath(item.path, input.geography)?.stem : undefined;
|
|
128
|
+
const stem = input.geography ? placeOfPath(item.path, input.geography, input.aliases)?.stem : undefined;
|
|
129
129
|
return { path: item.path, location: (item.srcRoute !== undefined && locationRoutes.has(item.srcRoute)) || (stem !== undefined && locationStems.has(stem)) };
|
|
130
130
|
});
|
|
131
131
|
return {
|
|
@@ -139,7 +139,7 @@ export function readEstate(input) {
|
|
|
139
139
|
missingHtml,
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
|
-
function summarise(item, sitemapOf, geography) {
|
|
142
|
+
function summarise(item, sitemapOf, geography, aliases) {
|
|
143
143
|
const root = parseHtml(item.html);
|
|
144
144
|
const elements = descendants(root);
|
|
145
145
|
const robots = elements
|
|
@@ -150,7 +150,7 @@ function summarise(item, sitemapOf, geography) {
|
|
|
150
150
|
const indexable = !/noindex/i.test(`${robots} ${typeof header === "string" ? header : ""}`);
|
|
151
151
|
const hrefs = elements.filter((element) => element.tag === "a").map((element) => attr(element, "href") ?? "");
|
|
152
152
|
const h1Element = elements.find((element) => element.tag === "h1");
|
|
153
|
-
const place = geography ? placeOfPath(item.path, geography) : undefined;
|
|
153
|
+
const place = geography ? placeOfPath(item.path, geography, aliases) : undefined;
|
|
154
154
|
const page = {
|
|
155
155
|
path: item.path,
|
|
156
156
|
srcRoute: item.srcRoute,
|
|
@@ -162,7 +162,9 @@ function summarise(item, sitemapOf, geography) {
|
|
|
162
162
|
linkGroups: [],
|
|
163
163
|
manifest: readManifest(elements),
|
|
164
164
|
sections: topLevelSections(root).map(sectionId),
|
|
165
|
+
slots: sectionSlots(root),
|
|
165
166
|
h1: h1Element ? textOf(h1Element) : "",
|
|
167
|
+
...(titleOf(elements) !== undefined ? { title: titleOf(elements) } : {}),
|
|
166
168
|
place,
|
|
167
169
|
family: undefined,
|
|
168
170
|
familySource: undefined,
|
|
@@ -176,6 +178,23 @@ function summarise(item, sitemapOf, geography) {
|
|
|
176
178
|
}
|
|
177
179
|
return { summary: page, hrefs };
|
|
178
180
|
}
|
|
181
|
+
function sectionSlots(root) {
|
|
182
|
+
const slots = new Map();
|
|
183
|
+
for (const section of topLevelSections(root)) {
|
|
184
|
+
const present = descendants(section).flatMap((element) => attr(element, "data-bl-slot") ?? []);
|
|
185
|
+
const skipped = (attr(section, "data-bl-slot-skip") ?? "").split(/\s+/).flatMap((pair) => {
|
|
186
|
+
const [slot, reason] = pair.split(":");
|
|
187
|
+
return slot && reason ? [{ slot, reason }] : [];
|
|
188
|
+
});
|
|
189
|
+
if (present.length > 0 || skipped.length > 0)
|
|
190
|
+
slots.set(sectionId(section), { present, skipped });
|
|
191
|
+
}
|
|
192
|
+
return slots;
|
|
193
|
+
}
|
|
194
|
+
function titleOf(elements) {
|
|
195
|
+
const title = elements.find((element) => element.tag === "title");
|
|
196
|
+
return title ? normaliseText(textOf(title)) : undefined;
|
|
197
|
+
}
|
|
179
198
|
function readManifest(elements) {
|
|
180
199
|
const scripts = elements.filter((element) => element.tag === "script" && attr(element, "data-bl-manifest") !== undefined);
|
|
181
200
|
if (scripts.length === 0)
|
|
@@ -231,7 +250,26 @@ function isGenerated(route) {
|
|
|
231
250
|
* Families for pages without a manifest: the sitemap file, then the family of the other pages of their dynamic route,
|
|
232
251
|
* the seeds (the representative service route), the family of their stem, then `/` as home.
|
|
233
252
|
*/
|
|
234
|
-
function inferFamilies(pages, seeds) {
|
|
253
|
+
function inferFamilies(pages, seeds, paths) {
|
|
254
|
+
// The estate plan's family paths come first: region and suburb pages can share a dynamic route with service pages
|
|
255
|
+
// (AA's /tree-services/[slug]), so the route alone cannot type them.
|
|
256
|
+
const parent = (template) => template.slice(0, template.lastIndexOf("/"));
|
|
257
|
+
const suburbParents = new Map([
|
|
258
|
+
...(paths?.serviceInSuburb ?? []).map((template) => [parent(template), "service-in-suburb"]),
|
|
259
|
+
...(paths?.suburb !== undefined ? [[parent(paths.suburb), "suburb"]] : []),
|
|
260
|
+
]);
|
|
261
|
+
const regionParents = new Set([paths?.region, ...(paths?.serviceInSuburb ?? [])].flatMap((template) => (template === undefined ? [] : [parent(template)])));
|
|
262
|
+
for (const page of pages) {
|
|
263
|
+
const at = parent(page.path);
|
|
264
|
+
// The whole last segment is the place: "/tree-services/tree-removal-sydney" is a service page, not a suburb page.
|
|
265
|
+
if (page.family !== undefined || !page.place || page.place.stem !== `${at}/{place}`)
|
|
266
|
+
continue;
|
|
267
|
+
const family = page.place.kind === "region" ? (regionParents.has(at) ? "region" : undefined) : suburbParents.get(at);
|
|
268
|
+
if (family) {
|
|
269
|
+
page.family = family;
|
|
270
|
+
page.familySource = "plan";
|
|
271
|
+
}
|
|
272
|
+
}
|
|
235
273
|
for (const page of pages) {
|
|
236
274
|
if (page.family !== undefined || !page.sitemapFile)
|
|
237
275
|
continue;
|
|
@@ -267,7 +305,8 @@ function inferFamilies(pages, seeds) {
|
|
|
267
305
|
for (const page of pages) {
|
|
268
306
|
if (page.family !== undefined || !page.srcRoute || !isGenerated(page.srcRoute))
|
|
269
307
|
continue;
|
|
270
|
-
|
|
308
|
+
// Only pages naming the same kind of place: a route that builds service pages and suburb pages types them apart.
|
|
309
|
+
const seeded = pages.find((other) => other.familySource === "route" && routeKey(other) === routeKey(page));
|
|
271
310
|
if (seeded) {
|
|
272
311
|
page.family = seeded.family;
|
|
273
312
|
page.familySource = "route";
|
|
@@ -353,9 +392,19 @@ function locationCopy(root) {
|
|
|
353
392
|
if (jobsSection) {
|
|
354
393
|
const cards = descendants(jobsSection).filter((element) => (element.tag === "li" || element.tag === "article") && !descendants(element).some((inner) => inner.tag === "li" || inner.tag === "article"));
|
|
355
394
|
const headings = new Set(descendants(jobsSection).filter((element) => /^h[1-6]$/.test(element.tag)).map((element) => textOf(element)));
|
|
395
|
+
// The job's place is structured data on the card (NearbyJobs renders `data-suburb`), so its outcome text is never
|
|
396
|
+
// read for place names when the card says where the job was.
|
|
397
|
+
const placeOf = (card) => {
|
|
398
|
+
const carrier = [card, ...descendants(card)].find((element) => attr(element, "data-suburb"));
|
|
399
|
+
return carrier ? attr(carrier, "data-suburb").trim() : undefined;
|
|
400
|
+
};
|
|
356
401
|
jobs = cards.length > 0
|
|
357
|
-
? cards.
|
|
358
|
-
|
|
402
|
+
? cards.flatMap((card) => {
|
|
403
|
+
const text = textOf(card, true);
|
|
404
|
+
const place = placeOf(card);
|
|
405
|
+
return text ? [place ? { text, place } : { text }] : [];
|
|
406
|
+
})
|
|
407
|
+
: readableLines(jobsSection).map((line) => line.text).filter((text) => !headings.has(text)).map((text) => ({ text }));
|
|
359
408
|
}
|
|
360
409
|
const pricesSection = sectionOf("price-guide");
|
|
361
410
|
const reviewsSection = sectionOf("reviews");
|