@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.
Files changed (152) hide show
  1. package/README.md +68 -2
  2. package/dist/analytics-e2e.d.ts +10 -16
  3. package/dist/analytics-e2e.d.ts.map +1 -1
  4. package/dist/analytics-e2e.js +61 -3
  5. package/dist/analytics-e2e.js.map +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +4 -1
  8. package/dist/cli.js.map +1 -1
  9. package/dist/current-release.js +5 -5
  10. package/dist/current-release.js.map +1 -1
  11. package/dist/engineering.d.ts.map +1 -1
  12. package/dist/engineering.js +18 -2
  13. package/dist/engineering.js.map +1 -1
  14. package/dist/estate-snapshot.d.ts +54 -0
  15. package/dist/estate-snapshot.d.ts.map +1 -0
  16. package/dist/estate-snapshot.js +164 -0
  17. package/dist/estate-snapshot.js.map +1 -0
  18. package/dist/estate.d.ts +38 -0
  19. package/dist/estate.d.ts.map +1 -1
  20. package/dist/estate.js +71 -28
  21. package/dist/estate.js.map +1 -1
  22. package/dist/media-snapshot.d.ts +53 -0
  23. package/dist/media-snapshot.d.ts.map +1 -0
  24. package/dist/media-snapshot.js +66 -0
  25. package/dist/media-snapshot.js.map +1 -0
  26. package/dist/project.d.ts +20 -0
  27. package/dist/project.d.ts.map +1 -1
  28. package/dist/project.js +92 -2
  29. package/dist/project.js.map +1 -1
  30. package/dist/ready-cli.d.ts.map +1 -1
  31. package/dist/ready-cli.js +8 -3
  32. package/dist/ready-cli.js.map +1 -1
  33. package/dist/rule-registry.d.ts +5 -0
  34. package/dist/rule-registry.d.ts.map +1 -1
  35. package/dist/rule-registry.js +1 -0
  36. package/dist/rule-registry.js.map +1 -1
  37. package/dist/site-system/checks.d.ts +28 -3
  38. package/dist/site-system/checks.d.ts.map +1 -1
  39. package/dist/site-system/checks.js +5 -2
  40. package/dist/site-system/checks.js.map +1 -1
  41. package/dist/site-system/config.d.ts +13 -0
  42. package/dist/site-system/config.d.ts.map +1 -1
  43. package/dist/site-system/config.js +30 -1
  44. package/dist/site-system/config.js.map +1 -1
  45. package/dist/site-system/index.d.ts +1 -1
  46. package/dist/site-system/index.d.ts.map +1 -1
  47. package/dist/site-system/index.js +1 -1
  48. package/dist/site-system/index.js.map +1 -1
  49. package/dist/site-system/journeys/conversion.d.ts.map +1 -1
  50. package/dist/site-system/journeys/conversion.js +15 -2
  51. package/dist/site-system/journeys/conversion.js.map +1 -1
  52. package/dist/site-system/journeys/menu.d.ts.map +1 -1
  53. package/dist/site-system/journeys/menu.js +24 -1
  54. package/dist/site-system/journeys/menu.js.map +1 -1
  55. package/dist/site-system/journeys/page-helpers.d.ts.map +1 -1
  56. package/dist/site-system/journeys/page-helpers.js +1 -1
  57. package/dist/site-system/journeys/page-helpers.js.map +1 -1
  58. package/dist/site-system/journeys/plan.d.ts +13 -3
  59. package/dist/site-system/journeys/plan.d.ts.map +1 -1
  60. package/dist/site-system/journeys/plan.js +12 -2
  61. package/dist/site-system/journeys/plan.js.map +1 -1
  62. package/dist/site-system/journeys/shell.d.ts +1 -1
  63. package/dist/site-system/journeys/shell.js +2 -2
  64. package/dist/site-system/journeys/shell.js.map +1 -1
  65. package/dist/site-system/report.d.ts.map +1 -1
  66. package/dist/site-system/report.js +42 -8
  67. package/dist/site-system/report.js.map +1 -1
  68. package/dist/site-system/rule-modules.d.ts.map +1 -1
  69. package/dist/site-system/rule-modules.js +5 -0
  70. package/dist/site-system/rule-modules.js.map +1 -1
  71. package/dist/site-system/rules/delivery/h13-inbox.d.ts +23 -0
  72. package/dist/site-system/rules/delivery/h13-inbox.d.ts.map +1 -0
  73. package/dist/site-system/rules/delivery/h13-inbox.js +208 -0
  74. package/dist/site-system/rules/delivery/h13-inbox.js.map +1 -0
  75. package/dist/site-system/rules/delivery/index.d.ts +12 -0
  76. package/dist/site-system/rules/delivery/index.d.ts.map +1 -0
  77. package/dist/site-system/rules/delivery/index.js +7 -0
  78. package/dist/site-system/rules/delivery/index.js.map +1 -0
  79. package/dist/site-system/rules/form/c7-thank-you.d.ts +7 -1
  80. package/dist/site-system/rules/form/c7-thank-you.d.ts.map +1 -1
  81. package/dist/site-system/rules/form/c7-thank-you.js +26 -3
  82. package/dist/site-system/rules/form/c7-thank-you.js.map +1 -1
  83. package/dist/site-system/rules/form/index.d.ts.map +1 -1
  84. package/dist/site-system/rules/form/index.js +11 -1
  85. package/dist/site-system/rules/form/index.js.map +1 -1
  86. package/dist/site-system/rules/full/common.d.ts +2 -0
  87. package/dist/site-system/rules/full/common.d.ts.map +1 -1
  88. package/dist/site-system/rules/full/common.js.map +1 -1
  89. package/dist/site-system/rules/full/e1-pattern.d.ts +4 -0
  90. package/dist/site-system/rules/full/e1-pattern.d.ts.map +1 -0
  91. package/dist/site-system/rules/full/e1-pattern.js +47 -0
  92. package/dist/site-system/rules/full/e1-pattern.js.map +1 -0
  93. package/dist/site-system/rules/full/estate.d.ts +26 -3
  94. package/dist/site-system/rules/full/estate.d.ts.map +1 -1
  95. package/dist/site-system/rules/full/estate.js +58 -9
  96. package/dist/site-system/rules/full/estate.js.map +1 -1
  97. package/dist/site-system/rules/full/geography.d.ts +15 -2
  98. package/dist/site-system/rules/full/geography.d.ts.map +1 -1
  99. package/dist/site-system/rules/full/geography.js +15 -2
  100. package/dist/site-system/rules/full/geography.js.map +1 -1
  101. package/dist/site-system/rules/full/index.d.ts +4 -3
  102. package/dist/site-system/rules/full/index.d.ts.map +1 -1
  103. package/dist/site-system/rules/full/index.js +20 -5
  104. package/dist/site-system/rules/full/index.js.map +1 -1
  105. package/dist/site-system/rules/full/k12-slots.d.ts +4 -0
  106. package/dist/site-system/rules/full/k12-slots.d.ts.map +1 -0
  107. package/dist/site-system/rules/full/k12-slots.js +40 -0
  108. package/dist/site-system/rules/full/k12-slots.js.map +1 -0
  109. package/dist/site-system/rules/full/l10-location-copy.d.ts +1 -1
  110. package/dist/site-system/rules/full/l10-location-copy.d.ts.map +1 -1
  111. package/dist/site-system/rules/full/l10-location-copy.js +5 -2
  112. package/dist/site-system/rules/full/l10-location-copy.js.map +1 -1
  113. package/dist/site-system/rules/full/l3-neighbours.d.ts +3 -2
  114. package/dist/site-system/rules/full/l3-neighbours.d.ts.map +1 -1
  115. package/dist/site-system/rules/full/l3-neighbours.js +4 -1
  116. package/dist/site-system/rules/full/l3-neighbours.js.map +1 -1
  117. package/dist/site-system/rules/full/l4-local-copy.d.ts +2 -1
  118. package/dist/site-system/rules/full/l4-local-copy.d.ts.map +1 -1
  119. package/dist/site-system/rules/full/l4-local-copy.js +8 -3
  120. package/dist/site-system/rules/full/l4-local-copy.js.map +1 -1
  121. package/dist/site-system/rules/full/l9-data-contract.js +2 -2
  122. package/dist/site-system/rules/full/l9-data-contract.js.map +1 -1
  123. package/dist/site-system/rules/full/links.d.ts +3 -1
  124. package/dist/site-system/rules/full/links.d.ts.map +1 -1
  125. package/dist/site-system/rules/full/links.js +15 -5
  126. package/dist/site-system/rules/full/links.js.map +1 -1
  127. package/dist/site-system/rules/full/loc-index.d.ts +5 -0
  128. package/dist/site-system/rules/full/loc-index.d.ts.map +1 -1
  129. package/dist/site-system/rules/full/loc-index.js +43 -2
  130. package/dist/site-system/rules/full/loc-index.js.map +1 -1
  131. package/dist/site-system/rules/full/route-kinds.d.ts +15 -0
  132. package/dist/site-system/rules/full/route-kinds.d.ts.map +1 -0
  133. package/dist/site-system/rules/full/route-kinds.js +52 -0
  134. package/dist/site-system/rules/full/route-kinds.js.map +1 -0
  135. package/dist/site-system/rules/full/strategy.d.ts +6 -0
  136. package/dist/site-system/rules/full/strategy.d.ts.map +1 -1
  137. package/dist/site-system/rules/full/strategy.js +98 -10
  138. package/dist/site-system/rules/full/strategy.js.map +1 -1
  139. package/dist/site-system/rules/sections/k2-skin-coverage.d.ts +2 -2
  140. package/dist/site-system/rules/sections/k2-skin-coverage.d.ts.map +1 -1
  141. package/dist/site-system/rules/sections/k2-skin-coverage.js +13 -2
  142. package/dist/site-system/rules/sections/k2-skin-coverage.js.map +1 -1
  143. package/dist/site-system/rules/sections/t6-media-entries.d.ts.map +1 -1
  144. package/dist/site-system/rules/sections/t6-media-entries.js +69 -5
  145. package/dist/site-system/rules/sections/t6-media-entries.js.map +1 -1
  146. package/dist/site-system/runners.js +1 -1
  147. package/dist/site-system/runners.js.map +1 -1
  148. package/dist/site-system-e2e.d.ts +1 -1
  149. package/dist/site-system-e2e.d.ts.map +1 -1
  150. package/dist/site-system-e2e.js +17 -4
  151. package/dist/site-system-e2e.js.map +1 -1
  152. 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
- export declare function checkC7(pages: readonly ParsedPage[], missing?: readonly string[]): SiteSystemFinding[];
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;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,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;AAcD,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,EAAE,OAAO,GAAE,SAAS,MAAM,EAAO,GAAG,iBAAiB,EAAE,CA+B1G"}
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
- export function checkC7(pages, missing = []) {
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 (!C7_REPLY_TIME.test(replyText)) {
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
- return resultOf("C7", failures, `Checked ${plural(thankYouPages.length, "thank-you page")}: each gives a reply time and a tel: link and is noindex, follow.`);
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;AAKhE,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"}
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;AAEjD,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,CAAC,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC;AAC5G,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\";\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 ?? []), ...checkC8(pages), ...checkC11(input.projectRoot, pages)];\n}\n"]}
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":"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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 /** 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"]}
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,4 @@
1
+ import type { SiteSystemFinding } from "../../checks.js";
2
+ import { type FullContext } from "./common.js";
3
+ export declare function checkE1Pattern(context: FullContext): SiteSystemFinding[];
4
+ //# sourceMappingURL=e1-pattern.d.ts.map
@@ -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 { type FullGeography, type PathPlace } from "./geography.js";
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 string[] | null;
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":"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"}
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
- const seeded = pages.find((other) => other.familySource === "route" && other.srcRoute === page.srcRoute);
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.map((card) => textOf(card, true)).filter(Boolean)
358
- : readableLines(jobsSection).map((line) => line.text).filter((text) => !headings.has(text));
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");