@reddoorla/maintenance 0.46.0 → 0.47.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/dist/index.d.ts CHANGED
@@ -288,6 +288,7 @@ type ResolvedCopy = {
288
288
  announceTestingLabel: string;
289
289
  announceMaintenanceLabel: string;
290
290
  announcePreviewLabel: string;
291
+ announceScoreNote: string;
291
292
  announceImprovementResend: string;
292
293
  announceImprovementSvelte5: string;
293
294
  announceCadence: string;
package/dist/index.js CHANGED
@@ -2769,7 +2769,7 @@ var DEFAULT_COPY = {
2769
2769
  "Links & Navigation",
2770
2770
  "Form Functionality",
2771
2771
  "Interactions & Animations",
2772
- "Verified After Updates"
2772
+ "Tested After Updates"
2773
2773
  ],
2774
2774
  notesHeader: "NOTES",
2775
2775
  seoCta: "Contact us if you are interested in more in-depth data or have questions about SEO.",
@@ -2789,6 +2789,7 @@ var DEFAULT_COPY = {
2789
2789
  announceTestingLabel: "Full site testing",
2790
2790
  announceMaintenanceLabel: "Routine maintenance",
2791
2791
  announcePreviewLabel: "From your latest full site test:",
2792
+ announceScoreNote: "These are independent Google Lighthouse scores, each out of 100 \u2014 higher is better.",
2792
2793
  announceImprovementResend: "Your contact forms now deliver straight to your inbox through reliable infrastructure, so no inquiry slips through the cracks.",
2793
2794
  announceImprovementSvelte5: "We've modernized your site to the latest framework \u2014 it's faster, more secure, and built to last.",
2794
2795
  announceCadence: "After each one we'll send you a short report like this \u2014 there's nothing you need to do.",
@@ -3030,7 +3031,7 @@ function buildMjml(data) {
3030
3031
  <mj-text color="#C00" font-size="44px" font-weight="400" padding-top="0px">${data.lighthouse.performance}</mj-text>
3031
3032
  <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="0px" padding-bottom="36px">Acceptable 50\u201389 // Ideal 90\u2013100</mj-text>
3032
3033
  <mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
3033
- <mj-text color="#C00" font-size="20px" font-weight="300" padding-top="25px">Readability</mj-text>
3034
+ <mj-text color="#C00" font-size="20px" font-weight="300" padding-top="25px">Readability (A11y)</mj-text>
3034
3035
  <mj-text color="#C00" font-size="44px" font-weight="400" padding-top="0px">${data.lighthouse.accessibility}</mj-text>
3035
3036
  <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="0px" padding-bottom="36px">Acceptable 80\u201399 // Ideal 100</mj-text>
3036
3037
  <mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
@@ -3135,9 +3136,26 @@ var FREQ_PHRASE = {
3135
3136
  };
3136
3137
  var RED2 = "#C00";
3137
3138
  var GREY2 = "#757575";
3139
+ var CHECK_PNG2 = `cid:${CHECK_CID}`;
3140
+ function fmtVisitors(n) {
3141
+ return n.toLocaleString("en-US");
3142
+ }
3143
+ function visitorTrend(cur, prev) {
3144
+ if (cur === void 0 || prev === void 0 || prev === 0) return null;
3145
+ const pct = Math.round((cur - prev) / prev * 100);
3146
+ if (pct > 0) return `\u25B2 ${pct}% vs the previous month`;
3147
+ if (pct < 0) return `\u25BC ${Math.abs(pct)}% vs the previous month`;
3148
+ return "No change vs the previous month";
3149
+ }
3150
+ function announcementSiteExtras(site) {
3151
+ return {
3152
+ cadence: { maintenance: site.maintenanceFreq, testing: site.testingFreq },
3153
+ improvements: { resendForms: true, svelte5: true }
3154
+ };
3155
+ }
3138
3156
  var SCORE_PREVIEW = [
3139
3157
  { label: "Performance", key: "performance" },
3140
- { label: "Readability", key: "accessibility" },
3158
+ { label: "Readability (A11y)", key: "accessibility" },
3141
3159
  { label: "Best Practices", key: "bestPractices" },
3142
3160
  { label: "Site Structure", key: "seo" }
3143
3161
  ];
@@ -3175,8 +3193,10 @@ function buildAnnouncementMjml(data) {
3175
3193
  <mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="36px">${escapeXml(copy.announceCadenceHeading)}</mj-text>
3176
3194
  ${cadenceBlocks.map(
3177
3195
  (b) => `
3178
- <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="400" line-height="24px" padding-top="12px" padding-bottom="0px">\u2022 ${escapeXml(b.line)}</mj-text>
3179
- <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="13px" font-weight="300" line-height="20px" padding-top="2px" padding-bottom="0px" padding-left="16px">${escapeXml(b.checks.join(" \xB7 "))}</mj-text>`
3196
+ <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="400" line-height="24px" padding-top="12px" padding-bottom="2px">\u2022 ${escapeXml(b.line)}</mj-text>${b.checks.map(
3197
+ (c) => `
3198
+ <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="14px" font-weight="300" line-height="22px" padding-top="1px" padding-bottom="0px" padding-left="16px">${escapeXml(c)} <img src="${CHECK_PNG2}" alt="\u2713" width="14" height="14" style="vertical-align:middle;display:inline-block;margin-left:2px;" /></mj-text>`
3199
+ ).join("")}`
3180
3200
  ).join("")}
3181
3201
  <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="14px">${escapeXml(copy.announceCadence)}</mj-text>
3182
3202
  </mj-column>
@@ -3186,6 +3206,23 @@ function buildAnnouncementMjml(data) {
3186
3206
  <mj-text color="${RED2}" font-size="20px" font-weight="300" padding-top="25px">${label}</mj-text>
3187
3207
  <mj-text color="${RED2}" font-size="44px" font-weight="400" padding-top="0px">${data.lighthouse[key]}</mj-text>`
3188
3208
  ).join("");
3209
+ const scoreNote = copy.announceScoreNote ? `
3210
+ <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" font-style="italic" line-height="18px" padding-top="16px">${escapeXml(copy.announceScoreNote)}</mj-text>` : "";
3211
+ const trend = visitorTrend(data.gaUsersCurrent, data.gaUsersPrevious);
3212
+ const trafficRows = [];
3213
+ if (data.gaUsersCurrent !== void 0)
3214
+ trafficRows.push(`
3215
+ <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="8px"><span style="color:${RED2};font-size:22px;font-weight:400;">${escapeXml(fmtVisitors(data.gaUsersCurrent))}</span> visitors in the last month${trend ? ` \u2014 ${escapeXml(trend)}` : ""}</mj-text>`);
3216
+ if (data.searchPosition !== void 0)
3217
+ trafficRows.push(`
3218
+ <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="4px">Page 1 Google result (#${data.searchPosition}) for your brand search</mj-text>`);
3219
+ const trafficSection = trafficRows.length > 0 ? `
3220
+ <mj-section background-color="white">
3221
+ <mj-column>
3222
+ <mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="36px">TRAFFIC &amp; SEARCH</mj-text>
3223
+ ${trafficRows.join("")}
3224
+ </mj-column>
3225
+ </mj-section>` : "";
3189
3226
  const contactRows = copy.contact.map(
3190
3227
  (line) => `
3191
3228
  <mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml(line)}</mj-text>`
@@ -3220,9 +3257,10 @@ function buildAnnouncementMjml(data) {
3220
3257
  <mj-section background-color="#F4F4F4">
3221
3258
  <mj-column>
3222
3259
  <mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="55px">${escapeXml(copy.announcePreviewLabel)}</mj-text>
3223
- ${scoreRows}
3260
+ ${scoreRows}${scoreNote}
3224
3261
  </mj-column>
3225
3262
  </mj-section>
3263
+ ${trafficSection}
3226
3264
  <mj-section background-color="white">
3227
3265
  <mj-column>
3228
3266
  <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="36px">${escapeXml(copy.announceOpenDoor)}</mj-text>
@@ -3270,7 +3308,10 @@ var TESTING_CHECKLIST = [
3270
3308
  label: "Interactions & Animations",
3271
3309
  field: "Test: Interactions & Animations"
3272
3310
  },
3273
- { key: "updates", label: "Verified After Updates", field: "Test: Verified After Updates" }
3311
+ // `field` keeps its original Airtable column name ("Verified After Updates") even though
3312
+ // the client-facing label is now "Tested After Updates" — the column holds operator data,
3313
+ // so renaming the label is display-only and avoids a live-base column migration.
3314
+ { key: "updates", label: "Tested After Updates", field: "Test: Verified After Updates" }
3274
3315
  ];
3275
3316
  var ALL_CHECKLIST_FIELDS = [...MAINTENANCE_CHECKLIST, ...TESTING_CHECKLIST].map(
3276
3317
  (i) => i.field
@@ -3511,6 +3552,7 @@ import { JWT as JWT2 } from "google-auth-library";
3511
3552
  var WEBMASTERS_READONLY = "https://www.googleapis.com/auth/webmasters.readonly";
3512
3553
  var SC_BASE = "https://searchconsole.googleapis.com/webmasters/v3";
3513
3554
  var PAGE_1_MAX_POSITION = 10;
3555
+ var BRAND_QUERY_ROW_LIMIT = 100;
3514
3556
  function bareHost(s) {
3515
3557
  return s.trim().replace(/^sc-domain:/i, "").replace(/^https?:\/\//i, "").split("/")[0].replace(/^www\./i, "").toLowerCase();
3516
3558
  }
@@ -3524,6 +3566,25 @@ function resolvePropertyCandidates(entries, host) {
3524
3566
  function ymd3(d) {
3525
3567
  return d.toISOString().slice(0, 10);
3526
3568
  }
3569
+ function pickBrandQuery(rows) {
3570
+ let best;
3571
+ for (const r of rows) {
3572
+ if (typeof r.position !== "number") continue;
3573
+ const impressions = r.impressions ?? 0;
3574
+ if (best === void 0 || impressions > best.impressions || impressions === best.impressions && r.position < best.position) {
3575
+ best = { position: r.position, impressions };
3576
+ }
3577
+ }
3578
+ return best?.position;
3579
+ }
3580
+ function selectBrandPosition(rows, exactQuery) {
3581
+ const want = exactQuery.toLowerCase();
3582
+ const exact = rows.find(
3583
+ (r) => typeof r.position === "number" && (r.keys?.[0] ?? "").toLowerCase() === want
3584
+ );
3585
+ if (exact) return exact.position;
3586
+ return pickBrandQuery(rows);
3587
+ }
3527
3588
  async function fetchSearchPresence(q, periodStart, periodEnd) {
3528
3589
  const key = JSON.parse(readFileSync4(q.keyPath, "utf8"));
3529
3590
  const jwt = new JWT2({
@@ -3555,14 +3616,14 @@ async function fetchSearchPresence(q, periodStart, periodEnd) {
3555
3616
  dimensionFilterGroups: [
3556
3617
  {
3557
3618
  filters: [
3558
- { dimension: "query", operator: "equals", expression: q.query.toLowerCase() }
3619
+ { dimension: "query", operator: "contains", expression: q.query.toLowerCase() }
3559
3620
  ]
3560
3621
  }
3561
3622
  ],
3562
- rowLimit: 1
3623
+ rowLimit: BRAND_QUERY_ROW_LIMIT
3563
3624
  }
3564
3625
  });
3565
- const pos = res.data.rows?.[0]?.position;
3626
+ const pos = selectBrandPosition(res.data.rows ?? [], q.query.toLowerCase());
3566
3627
  if (typeof pos === "number") {
3567
3628
  return { foundOnPage1: pos <= PAGE_1_MAX_POSITION, position: Math.max(1, Math.round(pos)) };
3568
3629
  }
@@ -3910,7 +3971,12 @@ async function sendOne(client, base, site, report) {
3910
3971
  headerImageCid: cidName,
3911
3972
  headerWidth: header.displayWidth,
3912
3973
  headerHeight: header.displayHeight,
3913
- headerBgColor: header.placeholderColor
3974
+ headerBgColor: header.placeholderColor,
3975
+ // Announcement-only: re-derive cadence + improvements from the site row so the SENT email
3976
+ // keeps its WHAT TO EXPECT section + improvement callouts. Without this the send-time
3977
+ // re-render drops them entirely (they're not stored on the Reports row). Ignored by the
3978
+ // other report templates.
3979
+ ...report.reportType === "Announcement" ? announcementSiteExtras(site) : {}
3914
3980
  });
3915
3981
  const reportDate = report.completedOn ? new Date(report.completedOn) : /* @__PURE__ */ new Date();
3916
3982
  const subject = report.subjectOverride ?? `${site.name} \u2014 ${monthYear(reportDate)} ${report.reportType} Report`;