@reddoorla/maintenance 0.45.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
@@ -287,8 +287,8 @@ type ResolvedCopy = {
287
287
  announceCadenceHeading: string;
288
288
  announceTestingLabel: string;
289
289
  announceMaintenanceLabel: string;
290
- announceMonitorItems: string[];
291
290
  announcePreviewLabel: string;
291
+ announceScoreNote: string;
292
292
  announceImprovementResend: string;
293
293
  announceImprovementSvelte5: string;
294
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.",
@@ -2788,8 +2788,8 @@ var DEFAULT_COPY = {
2788
2788
  announceCadenceHeading: "WHAT TO EXPECT",
2789
2789
  announceTestingLabel: "Full site testing",
2790
2790
  announceMaintenanceLabel: "Routine maintenance",
2791
- announceMonitorItems: ["Performance", "Accessibility", "Security", "Uptime"],
2792
2791
  announcePreviewLabel: "From your latest full site test:",
2792
+ announceScoreNote: "These are independent Google Lighthouse scores, each out of 100 \u2014 higher is better.",
2793
2793
  announceImprovementResend: "Your contact forms now deliver straight to your inbox through reliable infrastructure, so no inquiry slips through the cracks.",
2794
2794
  announceImprovementSvelte5: "We've modernized your site to the latest framework \u2014 it's faster, more secure, and built to last.",
2795
2795
  announceCadence: "After each one we'll send you a short report like this \u2014 there's nothing you need to do.",
@@ -3031,7 +3031,7 @@ function buildMjml(data) {
3031
3031
  <mj-text color="#C00" font-size="44px" font-weight="400" padding-top="0px">${data.lighthouse.performance}</mj-text>
3032
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>
3033
3033
  <mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
3034
- <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>
3035
3035
  <mj-text color="#C00" font-size="44px" font-weight="400" padding-top="0px">${data.lighthouse.accessibility}</mj-text>
3036
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>
3037
3037
  <mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
@@ -3136,9 +3136,26 @@ var FREQ_PHRASE = {
3136
3136
  };
3137
3137
  var RED2 = "#C00";
3138
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
+ }
3139
3156
  var SCORE_PREVIEW = [
3140
3157
  { label: "Performance", key: "performance" },
3141
- { label: "Readability", key: "accessibility" },
3158
+ { label: "Readability (A11y)", key: "accessibility" },
3142
3159
  { label: "Best Practices", key: "bestPractices" },
3143
3160
  { label: "Site Structure", key: "seo" }
3144
3161
  ];
@@ -3159,31 +3176,53 @@ function buildAnnouncementMjml(data) {
3159
3176
  </mj-column>
3160
3177
  </mj-section>` : "";
3161
3178
  const cad = data.cadence;
3162
- const cadenceLines = [];
3179
+ const cadenceBlocks = [];
3163
3180
  if (cad && cad.testing !== "None")
3164
- cadenceLines.push(`${copy.announceTestingLabel} \u2014 ${FREQ_PHRASE[cad.testing]}`);
3181
+ cadenceBlocks.push({
3182
+ line: `${copy.announceTestingLabel} \u2014 ${FREQ_PHRASE[cad.testing]}`,
3183
+ checks: copy.testingChecklist
3184
+ });
3165
3185
  if (cad && cad.maintenance !== "None")
3166
- cadenceLines.push(`${copy.announceMaintenanceLabel} \u2014 ${FREQ_PHRASE[cad.maintenance]}`);
3167
- const cadenceSection = cadenceLines.length > 0 ? `
3186
+ cadenceBlocks.push({
3187
+ line: `${copy.announceMaintenanceLabel} \u2014 ${FREQ_PHRASE[cad.maintenance]}`,
3188
+ checks: copy.maintenanceChecks
3189
+ });
3190
+ const cadenceSection = cadenceBlocks.length > 0 ? `
3168
3191
  <mj-section background-color="white">
3169
3192
  <mj-column>
3170
3193
  <mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="36px">${escapeXml(copy.announceCadenceHeading)}</mj-text>
3171
- ${cadenceLines.map(
3172
- (line) => `
3173
- <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="4px" padding-bottom="4px">\u2022 ${escapeXml(line)}</mj-text>`
3194
+ ${cadenceBlocks.map(
3195
+ (b) => `
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("")}`
3174
3200
  ).join("")}
3175
- <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="12px">${escapeXml(copy.announceCadence)}</mj-text>
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>
3176
3202
  </mj-column>
3177
3203
  </mj-section>` : "";
3178
- const monitorRows = copy.announceMonitorItems.map(
3179
- (item) => `
3180
- <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="4px" padding-bottom="4px">\u2022 ${escapeXml(item)}</mj-text>`
3181
- ).join("");
3182
3204
  const scoreRows = SCORE_PREVIEW.map(
3183
3205
  ({ label, key }) => `
3184
3206
  <mj-text color="${RED2}" font-size="20px" font-weight="300" padding-top="25px">${label}</mj-text>
3185
3207
  <mj-text color="${RED2}" font-size="44px" font-weight="400" padding-top="0px">${data.lighthouse[key]}</mj-text>`
3186
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>` : "";
3187
3226
  const contactRows = copy.contact.map(
3188
3227
  (line) => `
3189
3228
  <mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml(line)}</mj-text>`
@@ -3215,18 +3254,13 @@ function buildAnnouncementMjml(data) {
3215
3254
  </mj-section>
3216
3255
  ${cadenceSection}
3217
3256
  ${improvementsSection}
3218
- <mj-section background-color="white">
3219
- <mj-column>
3220
- <mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="36px">WHAT WE MONITOR</mj-text>
3221
- ${monitorRows}
3222
- </mj-column>
3223
- </mj-section>
3224
3257
  <mj-section background-color="#F4F4F4">
3225
3258
  <mj-column>
3226
3259
  <mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="55px">${escapeXml(copy.announcePreviewLabel)}</mj-text>
3227
- ${scoreRows}
3260
+ ${scoreRows}${scoreNote}
3228
3261
  </mj-column>
3229
3262
  </mj-section>
3263
+ ${trafficSection}
3230
3264
  <mj-section background-color="white">
3231
3265
  <mj-column>
3232
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>
@@ -3274,7 +3308,10 @@ var TESTING_CHECKLIST = [
3274
3308
  label: "Interactions & Animations",
3275
3309
  field: "Test: Interactions & Animations"
3276
3310
  },
3277
- { 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" }
3278
3315
  ];
3279
3316
  var ALL_CHECKLIST_FIELDS = [...MAINTENANCE_CHECKLIST, ...TESTING_CHECKLIST].map(
3280
3317
  (i) => i.field
@@ -3515,6 +3552,7 @@ import { JWT as JWT2 } from "google-auth-library";
3515
3552
  var WEBMASTERS_READONLY = "https://www.googleapis.com/auth/webmasters.readonly";
3516
3553
  var SC_BASE = "https://searchconsole.googleapis.com/webmasters/v3";
3517
3554
  var PAGE_1_MAX_POSITION = 10;
3555
+ var BRAND_QUERY_ROW_LIMIT = 100;
3518
3556
  function bareHost(s) {
3519
3557
  return s.trim().replace(/^sc-domain:/i, "").replace(/^https?:\/\//i, "").split("/")[0].replace(/^www\./i, "").toLowerCase();
3520
3558
  }
@@ -3528,6 +3566,25 @@ function resolvePropertyCandidates(entries, host) {
3528
3566
  function ymd3(d) {
3529
3567
  return d.toISOString().slice(0, 10);
3530
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
+ }
3531
3588
  async function fetchSearchPresence(q, periodStart, periodEnd) {
3532
3589
  const key = JSON.parse(readFileSync4(q.keyPath, "utf8"));
3533
3590
  const jwt = new JWT2({
@@ -3559,14 +3616,14 @@ async function fetchSearchPresence(q, periodStart, periodEnd) {
3559
3616
  dimensionFilterGroups: [
3560
3617
  {
3561
3618
  filters: [
3562
- { dimension: "query", operator: "equals", expression: q.query.toLowerCase() }
3619
+ { dimension: "query", operator: "contains", expression: q.query.toLowerCase() }
3563
3620
  ]
3564
3621
  }
3565
3622
  ],
3566
- rowLimit: 1
3623
+ rowLimit: BRAND_QUERY_ROW_LIMIT
3567
3624
  }
3568
3625
  });
3569
- const pos = res.data.rows?.[0]?.position;
3626
+ const pos = selectBrandPosition(res.data.rows ?? [], q.query.toLowerCase());
3570
3627
  if (typeof pos === "number") {
3571
3628
  return { foundOnPage1: pos <= PAGE_1_MAX_POSITION, position: Math.max(1, Math.round(pos)) };
3572
3629
  }
@@ -3914,7 +3971,12 @@ async function sendOne(client, base, site, report) {
3914
3971
  headerImageCid: cidName,
3915
3972
  headerWidth: header.displayWidth,
3916
3973
  headerHeight: header.displayHeight,
3917
- 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) : {}
3918
3980
  });
3919
3981
  const reportDate = report.completedOn ? new Date(report.completedOn) : /* @__PURE__ */ new Date();
3920
3982
  const subject = report.subjectOverride ?? `${site.name} \u2014 ${monthYear(reportDate)} ${report.reportType} Report`;