From e3e3e5682faefcca6fe124672f26f74694eec5ce Mon Sep 17 00:00:00 2001 From: Nish <257724087+nish3451@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:36:53 +0530 Subject: [PATCH 1/2] 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 a stale bundle, 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. It runs as npm run site:check-live-social-preview and from the nightly live-site-check workflow, following the #84 pattern for live-site guards (live-only, network-tolerant, SKIP_LIVE_CHECKS escape hatch, deliberately NOT part of npm run ci so the blocking chain stays green on repo state). Verified against the live site: the guard fails with exactly the 7 stale pages (21 failures) while all three social images are served, so the staleness alarm is live. All public-site tests pass; npm test's unrelated retention failure pre-exists on clean origin/main. Co-authored-by: CommandCodeBot --- .github/workflows/live-site-check.yml | 15 ++- package.json | 1 + scripts/check-public-live-social-preview.mjs | 135 +++++++++++++++++++ 3 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 scripts/check-public-live-social-preview.mjs diff --git a/.github/workflows/live-site-check.yml b/.github/workflows/live-site-check.yml index 40c5d581..216fdfd8 100644 --- a/.github/workflows/live-site-check.yml +++ b/.github/workflows/live-site-check.yml @@ -8,8 +8,9 @@ name: Live Site Check # blocking chain must stay green on repo state alone. # # This job is expected to FAIL while the deployment is stale (currently the -# June-20 bundle); the red run is the loud signal to re-deploy from -# origin/main, not a reason to disable the workflow. +# 2026-06-20 bundle, which still serves pre-2026-08-07 public fixes); the red +# run is the loud signal to re-deploy from origin/main, not a reason to +# disable the workflow. on: schedule: @@ -46,3 +47,13 @@ jobs: cache: npm - run: npm ci --ignore-scripts - run: node scripts/test-public-live-contact-heading-hierarchy.mjs + 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 c9b7c4d8..9c5306d2 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "site:prepare": "node scripts/prepare-static-site-bundle.mjs", "site:check-live": "node scripts/check-public-live-soft-404.mjs", "site:check-live-contact-heading-hierarchy": "node scripts/test-public-live-contact-heading-hierarchy.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", diff --git a/scripts/check-public-live-social-preview.mjs b/scripts/check-public-live-social-preview.mjs new file mode 100644 index 00000000..64d0ade4 --- /dev/null +++ b/scripts/check-public-live-social-preview.mjs @@ -0,0 +1,135 @@ +// Guard the LIVE tinystudio.in against the missing social preview imagery +// that was repaired in source but still ships on the deployed site. +// +// The class-gap item "Social preview imagery missing on 7 of 12 public +// pages" (the 7 pages being /privacy/, /privacy-choices/, /terms/, and the +// Promptly/Drishti support+privacy pairs) was repaired in source by PR #26 +// (commit ffe6e1f), and scripts/test-public-social-preview.mjs guards the +// worktree HTML. This check re-asserts the full og:image + twitter:image +// blocks against what the live site actually serves, so a stale deployment +// (the June-20 bundle still serving 5 of 12 pages with no social preview +// imagery) fails loudly instead of silently re-opening the finding. +// +// It runs as `npm run site:check-live-social-preview`, from the nightly +// live-site-check workflow (the loud staleness alarm while the site is +// stale) and on demand. It is deliberately NOT part of `npm run test` / +// `npm run ci`: those blocking chains must stay green on repo state alone, +// while the live site is deployed by an external mechanism (Cloudflare +// Pages). Blocking CI on the live site would keep every pull request red +// whenever the deployment is stale, and would deadlock the deploy lane's +// pre-deploy `npm run check` gate. +// +// Network-tolerant: a page or image that is unreachable is skipped (ok, no +// assertion); a page that is reachable but stale fails loudly. +// +// Escape hatch for machines without network access: +// SKIP_LIVE_CHECKS=1 npm run site:check-live-social-preview +// +// Only public tinystudio.in URLs and the local source files are referenced +// here; there is no per-environment configuration. +import { readFileSync } from "node:fs" +import { fileURLToPath } from "node:url" +import { dirname, join } from "node:path" + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..") +const read = (p) => readFileSync(join(ROOT, p), "utf8") + +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) +} + +let failures = 0 +let checks = 0 +const ok = (cond, msg) => { + checks++ + if (cond) console.log(` ok ${msg}`) + else { + failures++ + console.error(` FAIL ${msg}`) + } +} + +// Every public page (canonical list, mirrors scripts/test-public-social-preview.mjs). +const PUBLIC_PAGES = [ + { name: "Homepage", path: "", image: "tiny-studio-social.png" }, + { name: "Contact", path: "contact/", image: "tiny-studio-social.png" }, + { name: "Promptly", path: "promptly/", image: "promptly-social.png" }, + { name: "Promptly support", path: "promptly/support/", image: "promptly-social.png" }, + { name: "Promptly privacy", path: "promptly/privacy/", image: "promptly-social.png" }, + { name: "Drishti", path: "drishti/", image: "drishti-social.png" }, + { name: "Drishti support", path: "drishti/support/", image: "drishti-social.png" }, + { name: "Drishti privacy", path: "drishti/privacy/", image: "drishti-social.png" }, + { name: "Studio support", path: "support/", image: "tiny-studio-social.png" }, + { name: "Privacy", path: "privacy/", image: "tiny-studio-social.png" }, + { name: "Privacy choices", path: "privacy-choices/", image: "tiny-studio-social.png" }, + { name: "Terms", path: "terms/", image: "tiny-studio-social.png" } +] + +const BASE = "https://tinystudio.in" +const imageUrl = (file) => `${BASE}/social/${file}` +const FETCH_TIMEOUT_MS = 10_000 + +const metaOf = (html, name) => { + const match = html.match(new RegExp(` { + try { + const res = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }) + if (!res.ok) { + console.log(` ok skipped: ${url} answered ${res.status}, deployment not reachable - no assertions run for it`) + return null + } + return await res.text() + } catch (err) { + console.log(` ok skipped: ${url} unreachable (${err?.cause?.code ?? err?.name ?? "network error"}) - no assertions run for it`) + return null + } +} + +console.log("check-public-live-social-preview: every deployed tinystudio.in page keeps its social preview imagery") + +console.log("A. every live public page declares the complete og:image block") +for (const page of PUBLIC_PAGES) { + const html = await fetchLive(`${BASE}/${page.path}`) + if (html === null) continue + const expected = imageUrl(page.image) + const image = metaOf(html, "og:image") + ok(image === expected, `live /${page.path} og:image points at ${page.image}`) + if (image !== null) { + ok(metaOf(html, "og:image:secure_url") === image, `live /${page.path} og:image:secure_url matches og:image`) + ok(metaOf(html, "og:image:type") === "image/png", `live /${page.path} og:image:type is image/png`) + ok(metaOf(html, "og:image:width") === "1200", `live /${page.path} og:image:width is 1200`) + ok(metaOf(html, "og:image:height") === "630", `live /${page.path} og:image:height is 630`) + ok((metaOf(html, "og:image:alt") ?? "") !== "", `live /${page.path} has non-empty og:image:alt`) + } +} + +console.log("B. every live public page declares the matching twitter image card") +for (const page of PUBLIC_PAGES) { + const html = await fetchLive(`${BASE}/${page.path}`) + if (html === null) continue + ok(metaOf(html, "twitter:card") === "summary_large_image", `live /${page.path} twitter:card is summary_large_image`) + ok(metaOf(html, "twitter:image") === imageUrl(page.image), `live /${page.path} twitter:image matches og:image`) +} + +console.log("C. every referenced social image is served") +for (const file of ["tiny-studio-social.png", "promptly-social.png", "drishti-social.png"]) { + const url = imageUrl(file) + const res = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }).catch(() => null) + if (res === null) { + console.log(` ok skipped: ${url} unreachable - no assertion run for it`) + } else { + ok(res.ok, `${url} is served (${res.status})`) + } +} + +if (failures > 0) { + console.error(" the deployed site is stale: it misses the social preview imagery that public/ already has on every page. Refresh the live deployment from origin/main.") +} + +console.log("\nLive social-preview guard result: the finding stays open against tinystudio.in until a refresh of the live deployment lands on origin/main.") +console.log(`\n${checks} checks, ${failures} failures`) +process.exit(failures === 0 ? 0 : 1) From 3445884e39d56b740b9b0d23151acfbdba14147d Mon Sep 17 00:00:00 2001 From: Nish <257724087+nish3451@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:37:20 +0530 Subject: [PATCH 2/2] =?UTF-8?q?docs(lane1):=20report=20social-preview=20li?= =?UTF-8?q?ve-guard=20item=20=E2=80=94=20source=20merged,=20live=20blocked?= =?UTF-8?q?=20on=20missing=20Cloudflare=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lane 1 verification report: the 7-page social preview imagery fix is already merged to main (PR #26, ffe6e1f) and test-public-social-preview.mjs guards the static files. The live tinystudio.in still serves the stale bundle with exactly the 7 pages from the item missing og:image, and live delivery is blocked only by the missing CLOUDFLARE_API_TOKEN secret (NEEDS-NISH, deploy-pipeline item). Co-authored-by: CommandCodeBot --- ...ane1-live-social-preview-guard-20260814.md | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .lane/reports/fix-lane1-live-social-preview-guard-20260814.md diff --git a/.lane/reports/fix-lane1-live-social-preview-guard-20260814.md b/.lane/reports/fix-lane1-live-social-preview-guard-20260814.md new file mode 100644 index 00000000..90f3f77d --- /dev/null +++ b/.lane/reports/fix-lane1-live-social-preview-guard-20260814.md @@ -0,0 +1,73 @@ +# Lane 1 report: social preview imagery missing on 7 of 12 public pages + +Lane: tinystudio-in lane 1 (worktree tinystudio-in-lane1-20260814-181032) +Branch: `fix/lane1-live-social-preview-guard-20260814` +PR: https://github.com/nish3451/tinystudio-in/pull/153 +Date: 2026-08-14 +Outcome: **source fix already merged (PR #26); live delivery blocked by missing `CLOUDFLARE_API_TOKEN` (NEEDS-NISH). Landed the missing live-site guard so the stale deployment fails loudly instead of silently re-opening the item.** + +## The one item + +> Social preview imagery missing on 7 of 12 public pages - class gap after the closed /promptly + +## Verification performed + +### 1. Source state (branch from origin/main HEAD 2796b33) + +All 12 pages in `public/` carry the full social preview block; `scripts/test-public-social-preview.mjs` passes (exit 0). The fix commit `ffe6e1f` "add social preview imagery to the 7 pages missing it" is already in main (merged via PR #26). + +### 2. Live state — exactly the 7 pages from the item are still stale + +Fetched all 12 live routes from https://tinystudio.in: + +| Page | live og:image | +|---|---| +| / | tiny-studio-social.png | +| /contact/ | tiny-studio-social.png | +| /promptly/ | promptly-social.png | +| /promptly/support/ | **MISSING** | +| /promptly/privacy/ | **MISSING** | +| /drishti/ | drishti-social.png | +| /drishti/support/ | **MISSING** | +| /drishti/privacy/ | **MISSING** | +| /support/ | tiny-studio-social.png | +| /privacy/ | **MISSING** | +| /privacy-choices/ | **MISSING** | +| /terms/ | **MISSING** | + +All three social image files are served (HTTP 200). The stale pages also carry `twitter:card=summary` instead of `summary_large_image`. + +### 3. Root cause: deploy pipeline blocked (NEEDS-NISH) + +Identical to the closed meta-descriptions item (report `fix-lane1-meta-descriptions-live-gate.md`, PR #150): +- Live production is the stale 2026-06-20 bundle; every merged public fix since 2026-08-07 has not reached production. +- `.github/workflows/deploy-public-site.yml` is fail-closed on `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID`; the secret `CLOUDFLARE_API_TOKEN` is not provisioned. No code change can unblock the byte-level delivery. + +## What landed (PR #153) + +The detection gap: nothing guarded the DEPLOYED site against missing social preview imagery (the static guard `test-public-social-preview.mjs` passes on repo state alone). Added: + +- `scripts/check-public-live-social-preview.mjs` — fetches all 12 public pages from the live site, requires the complete `og:image` block (secure_url, type, 1200x630, alt), `summary_large_image` twitter card, matching `twitter:image`, and that every referenced image is served. Network-tolerant, `SKIP_LIVE_CHECKS=1` escape hatch, deliberately NOT in `npm run ci` (follows the #84 live-guard pattern). +- `package.json` — `site:check-live-social-preview` script. +- `.github/workflows/live-site-check.yml` — new `live-social-preview` nightly job; header comment updated from "June-20 bundle" to the accurate current stale state. + +## Verification + +- Guard against the live site: exit 1 with exactly the 7 stale pages (21 failures), images served (200) — proves the alarm is live and accurate. +- `node --check` passes; all 10 public-site tests pass (conversion-signal, structured-data, brand-disambiguation, heading-hierarchy, link-targets, social-preview, soft-404, deploy-bundle, deploy-workflow, pages-release). +- `npm test`'s retention failure (`check-retention-automation.mjs`) is pre-existing and unrelated: reproduces on a clean origin/main checkout (exit 1). It inspects the canonical main worktree's private state and freshness, not this lane's files. +- `git diff --check` clean. + +## What unblocks live delivery + +1. Nish provisions a Cloudflare Pages:Edit token: `gh secret set CLOUDFLARE_API_TOKEN -R nish3451/tinystudio-in` (account id `f670a698e17bf160c8e4679823e68916`). +2. The deploy lane runs on the next main push (or workflow_dispatch); the 7 pages then serve imagery with the other merged public fixes. +3. This item can be ticked on live proof — the new nightly guard goes green automatically. + +## Files touched + +- `scripts/check-public-live-social-preview.mjs` (new) +- `package.json` +- `.github/workflows/live-site-check.yml` +- `.lane/reports/fix-lane1-live-social-preview-guard-20260814.md` (this report) +- Lane record `/home/nish/workspaces/agent-state/lanes/tinystudio-in/lane-1.json` — claims list only.