From 74a954b32d85c59686aa263216f12eafcf27e308 Mon Sep 17 00:00:00 2001 From: Nish <257724087+nish3451@users.noreply.github.com> Date: Tue, 11 Aug 2026 06:38:08 +0530 Subject: [PATCH] fix(public): guard the live site against missing social preview imagery The repo fix for the 7 pages missing og:image/twitter:image is already in main (#26), and test-public-social-preview.mjs guards the static files. But nothing guards the DEPLOYED site: the live tinystudio.in still serves the June 20 bundle (07acd07), and exactly the 7 pages from the class-gap item (privacy, privacy-choices, terms, promptly/support, promptly/privacy, drishti/support, drishti/privacy) ship with no og:image and a summary-only twitter card, so shared links render as bare text cards. Add scripts/check-public-live-social-preview.mjs, which fetches all 12 public pages from the live site and requires the complete og:image block (secure_url, type, 1200x630, alt), the summary_large_image card, matching twitter:image, and that every referenced image file is served. Wired into npm test and npm run ci (SKIP_LIVE_CHECKS=1 escape hatch for machines without network) plus a nightly vps-verify workflow, so a stale deployment fails loudly within a day. Current live result (proof the detector works): 72 checks, 7 failures - exactly the 7 pages the item names, all failing on missing og:image. The check goes green once the site is redeployed from origin/main. verify: SKIP_LIVE_CHECKS=1 npm test; node scripts/check-public-live-social-preview.mjs; git diff --check --- .../workflows/live-social-preview-check.yml | 28 ++++ package.json | 5 +- scripts/check-public-live-social-preview.mjs | 125 ++++++++++++++++++ 3 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/live-social-preview-check.yml create mode 100644 scripts/check-public-live-social-preview.mjs diff --git a/.github/workflows/live-social-preview-check.yml b/.github/workflows/live-social-preview-check.yml new file mode 100644 index 00000000..815bb24a --- /dev/null +++ b/.github/workflows/live-social-preview-check.yml @@ -0,0 +1,28 @@ +name: Live Social Preview Check + +on: + schedule: + # Nightly: catch stale deployments that dropped social preview imagery + # (og:image / twitter:image) within a day even when no pull requests are + # open. + - cron: "37 3 * * *" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: live-social-preview-check + cancel-in-progress: false + +jobs: + live-social-preview: + runs-on: [self-hosted, linux, x64, vps-verify] + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 20 + cache: npm + - run: npm ci --ignore-scripts + - run: node scripts/check-public-live-social-preview.mjs diff --git a/package.json b/package.json index 340ea7d9..1eb67d39 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "client:proof-review": "node scripts/review-client-proof.mjs", "retention:automation-check": "node scripts/check-retention-automation.mjs", "site:prepare": "node scripts/prepare-static-site-bundle.mjs", + "site:check-live-social-preview": "node scripts/check-public-live-social-preview.mjs", "claims:check": "node scripts/check-outbound-claim-safety.mjs", "config:check": "node scripts/check-agency-defaults.mjs", "send:configure": "node scripts/configure-sender-setup.mjs", @@ -85,9 +86,9 @@ "product:truth": "node scripts/check-product-truth.mjs", "mobbin:run": "node scripts/run-design-system-proving-lab.mjs", "mobbin:check": "node scripts/run-design-system-proving-lab.mjs --dry-run", - "ci": "node scripts/test-service-engine.mjs && node scripts/test-sales-intake-contract.mjs && node scripts/test-active-offer-projection.mjs && node scripts/test-active-operator-surfaces.mjs && node scripts/test-direction-proof-gate.mjs && node scripts/test-client-readiness-contract.mjs && node scripts/test-validated-service-client.mjs && node scripts/test-client-acceptance-gates.mjs && node scripts/check-product-truth.mjs && node scripts/check-human-service-kit.mjs && node scripts/test-design-system-proving-lab.mjs && node scripts/check-retention-automation.mjs && node scripts/test-retention-automation.mjs && node scripts/check-agency-defaults.mjs && node scripts/check-outbound-claim-safety.mjs && node scripts/check-outbound-send-readiness.mjs && node scripts/test-outbound-send-readiness.mjs && node scripts/test-public-conversion-signal.mjs && node scripts/test-public-structured-data.mjs && node scripts/test-public-brand-disambiguation.mjs && node scripts/test-public-heading-hierarchy.mjs && node scripts/test-public-link-targets.mjs && node scripts/test-public-social-preview.mjs && node scripts/test-public-soft-404.mjs && find scripts -name '*.mjs' -print0 | xargs -0 -n1 node --check", + "ci": "node scripts/test-service-engine.mjs && node scripts/test-sales-intake-contract.mjs && node scripts/test-active-offer-projection.mjs && node scripts/test-active-operator-surfaces.mjs && node scripts/test-direction-proof-gate.mjs && node scripts/test-client-readiness-contract.mjs && node scripts/test-validated-service-client.mjs && node scripts/test-client-acceptance-gates.mjs && node scripts/check-product-truth.mjs && node scripts/check-human-service-kit.mjs && node scripts/test-design-system-proving-lab.mjs && node scripts/check-retention-automation.mjs && node scripts/test-retention-automation.mjs && node scripts/check-agency-defaults.mjs && node scripts/check-outbound-claim-safety.mjs && node scripts/check-outbound-send-readiness.mjs && node scripts/test-outbound-send-readiness.mjs && node scripts/test-public-conversion-signal.mjs && node scripts/test-public-structured-data.mjs && node scripts/test-public-brand-disambiguation.mjs && node scripts/test-public-heading-hierarchy.mjs && node scripts/test-public-link-targets.mjs && node scripts/test-public-social-preview.mjs && node scripts/test-public-soft-404.mjs && node scripts/check-public-live-social-preview.mjs && find scripts -name '*.mjs' -print0 | xargs -0 -n1 node --check", "check": "npm test", - "test": "node scripts/test-service-engine.mjs && node scripts/test-sales-intake-contract.mjs && node scripts/test-active-offer-projection.mjs && node scripts/test-active-operator-surfaces.mjs && node scripts/test-direction-proof-gate.mjs && node scripts/test-client-readiness-contract.mjs && node scripts/test-validated-service-client.mjs && node scripts/test-client-acceptance-gates.mjs && node scripts/check-product-truth.mjs && node scripts/check-human-service-kit.mjs && node scripts/test-design-system-proving-lab.mjs && node scripts/check-retention-automation.mjs && node scripts/test-retention-automation.mjs && node scripts/check-agency-defaults.mjs && node scripts/check-outbound-claim-safety.mjs && node scripts/check-outbound-send-readiness.mjs && node scripts/test-outbound-send-readiness.mjs && node scripts/test-public-conversion-signal.mjs && node scripts/test-public-structured-data.mjs && node scripts/test-public-brand-disambiguation.mjs && node scripts/test-public-heading-hierarchy.mjs && node scripts/test-public-link-targets.mjs && node scripts/test-public-social-preview.mjs && node scripts/test-public-soft-404.mjs && find scripts -name '*.mjs' -print0 | xargs -0 -n1 node --check" + "test": "node scripts/test-service-engine.mjs && node scripts/test-sales-intake-contract.mjs && node scripts/test-active-offer-projection.mjs && node scripts/test-active-operator-surfaces.mjs && node scripts/test-direction-proof-gate.mjs && node scripts/test-client-readiness-contract.mjs && node scripts/test-validated-service-client.mjs && node scripts/test-client-acceptance-gates.mjs && node scripts/check-product-truth.mjs && node scripts/check-human-service-kit.mjs && node scripts/test-design-system-proving-lab.mjs && node scripts/check-retention-automation.mjs && node scripts/test-retention-automation.mjs && node scripts/check-agency-defaults.mjs && node scripts/check-outbound-claim-safety.mjs && node scripts/check-outbound-send-readiness.mjs && node scripts/test-outbound-send-readiness.mjs && node scripts/test-public-conversion-signal.mjs && node scripts/test-public-structured-data.mjs && node scripts/test-public-brand-disambiguation.mjs && node scripts/test-public-heading-hierarchy.mjs && node scripts/test-public-link-targets.mjs && node scripts/test-public-social-preview.mjs && node scripts/test-public-soft-404.mjs && node scripts/check-public-live-social-preview.mjs && find scripts -name '*.mjs' -print0 | xargs -0 -n1 node --check" }, "engines": { "node": ">=20" diff --git a/scripts/check-public-live-social-preview.mjs b/scripts/check-public-live-social-preview.mjs new file mode 100644 index 00000000..1018f615 --- /dev/null +++ b/scripts/check-public-live-social-preview.mjs @@ -0,0 +1,125 @@ +// Guard the LIVE public site against missing social preview imagery: every +// one of the 12 public pages on tinystudio.in must declare the full +// og:image block and the matching twitter summary_large_image card. +// +// The static test (test-public-social-preview.mjs) only proves the repo files +// carry the meta tags; it cannot catch a stale deployment that still serves +// the old pages. This check hits the deployed site so a regression is +// detected the moment it ships. It is wired into `npm run ci` and a nightly +// workflow. +// +// Escape hatch for machines without network access: +// SKIP_LIVE_CHECKS=1 npm run ci +// +// Only the live site name and the local family-to-image mapping are +// referenced here; there is no per-environment configuration. + +if (process.env.SKIP_LIVE_CHECKS === "1") { + console.log("check-public-live-social-preview: SKIP_LIVE_CHECKS=1, skipping live site checks") + process.exit(0) +} + +const SITE = "https://tinystudio.in" + +// Every public page (canonical list, mirrors test-public-social-preview.mjs). +const PUBLIC_PAGES = [ + "/", + "/contact/", + "/promptly/", + "/promptly/support/", + "/promptly/privacy/", + "/drishti/", + "/drishti/support/", + "/drishti/privacy/", + "/support/", + "/privacy/", + "/privacy-choices/", + "/terms/" +] + +// Which social image each page family must carry. +const expectedImage = (page) => + page.startsWith("/promptly/") + ? "promptly-social.png" + : page.startsWith("/drishti/") + ? "drishti-social.png" + : "tiny-studio-social.png" + +const metaOf = (html, name) => { + const match = html.match(new RegExp(` { + checks++ + if (cond) console.log(` ok ${msg}`) + else { + failures++ + console.error(` FAIL ${msg}`) + } +} + +const fetchWithRetry = async (url, attempts = 2) => { + let lastError + for (let i = 0; i < attempts; i++) { + try { + const res = await fetch(url, { redirect: "follow", headers: { "user-agent": "tinystudio-social-preview-check" } }) + return { res, body: await res.text() } + } catch (err) { + lastError = err + if (i < attempts - 1) await new Promise((r) => setTimeout(r, 1000)) + } + } + throw lastError +} + +console.log("check-public-live-social-preview: every live public page must declare a social preview image") + +let results +try { + results = await Promise.all( + PUBLIC_PAGES.map(async (page) => ({ page, ...(await fetchWithRetry(`${SITE}${page}`)) })) + ) +} catch (err) { + console.error(` FAIL could not reach ${SITE}: ${err.message}`) + console.error(" (transient network problems and live deploy state both fail here; retry once manually before assuming a deploy issue)") + process.exit(1) +} + +for (const { page, res, body } of results) { + ok(res.status === 200, `GET ${page} returns HTTP ${res.status}`) + const image = metaOf(body, "og:image") + ok(image !== null && image !== "", `${page} declares `) + if (image) { + const imageUrl = `${SITE}/social/${expectedImage(page)}` + ok(image === imageUrl, `${page} og:image points at ${expectedImage(page)}`) + ok(metaOf(body, "og:image:secure_url") === image, `${page} og:image:secure_url matches og:image`) + ok(metaOf(body, "og:image:type") === "image/png", `${page} og:image:type is image/png`) + ok(metaOf(body, "og:image:width") === "1200", `${page} og:image:width is 1200`) + ok(metaOf(body, "og:image:height") === "630", `${page} og:image:height is 630`) + ok((metaOf(body, "og:image:alt") ?? "") !== "", `${page} has non-empty og:image:alt`) + ok(metaOf(body, "twitter:card") === "summary_large_image", `${page} twitter:card is summary_large_image`) + ok(metaOf(body, "twitter:image") === image, `${page} twitter:image matches og:image`) + ok((metaOf(body, "twitter:image:alt") ?? "") !== "", `${page} has non-empty twitter:image:alt`) + } +} + +console.log("B. every referenced social image file is served") +try { + const uniqueImages = [...new Set(PUBLIC_PAGES.map((page) => `${SITE}/social/${expectedImage(page)}`))] + for (const url of uniqueImages) { + const { res } = await fetchWithRetry(url) + ok(res.status === 200, `GET ${url} returns HTTP ${res.status}`) + } +} catch (err) { + console.error(` FAIL could not reach a social image: ${err.message}`) + process.exit(1) +} + +console.log(`\n${checks} checks, ${failures} failures`) +if (failures > 0) { + console.error("\nThe live site is missing social preview imagery (or serving a stale bundle). Re-deploy the public site from origin/main and re-run this check.") +} +process.exit(failures === 0 ? 0 : 1)