Skip to content
This repository was archived by the owner on Oct 12, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e3e3e56
fix(public): guard the live site against missing social preview imagery
nish3451 Aug 14, 2026
3445884
docs(lane1): report social-preview live-guard item — source merged, l…
nish3451 Aug 14, 2026
5fde7dc
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 14, 2026
148e00a
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 14, 2026
027b54a
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 14, 2026
d18a429
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 14, 2026
7450e6c
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 14, 2026
0a33cc4
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 14, 2026
3979fe6
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 14, 2026
03e5215
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 14, 2026
5d1958c
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 15, 2026
f59f8d2
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 15, 2026
b12f8b0
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 15, 2026
ec6d1e3
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 15, 2026
c8781c5
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 15, 2026
ede3e0e
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 15, 2026
b75b210
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 15, 2026
0b710be
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 15, 2026
d242504
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 15, 2026
621a145
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 15, 2026
86cbc85
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 15, 2026
c5b0d73
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 15, 2026
7b9cf30
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 15, 2026
3ea702d
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 15, 2026
b0fc7eb
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 17, 2026
832a8ba
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 17, 2026
f8daa4a
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 17, 2026
3de8a71
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 17, 2026
5870f8e
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 17, 2026
4215bbe
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 17, 2026
4056225
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 17, 2026
d165b44
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 18, 2026
6bfe82f
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 19, 2026
e598180
Merge origin/main into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 19, 2026
80de20f
Merge branch 'main' into fix/lane1-live-social-preview-guard-20260814
nish3451 Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions .github/workflows/live-site-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
73 changes: 73 additions & 0 deletions .lane/reports/fix-lane1-live-social-preview-guard-20260814.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
135 changes: 135 additions & 0 deletions scripts/check-public-live-social-preview.mjs
Original file line number Diff line number Diff line change
@@ -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(`<meta\\s+(?:name|property)="${name}"\\s+content="([^"]*)"`))
return match ? match[1] : null
}

const fetchLive = async (url) => {
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)