Skip to content
16 changes: 15 additions & 1 deletion scripts/check-public-live-deploy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@
// Used by the release lane after a Pages deployment. Runs against the live
// site only; set SKIP_LIVE_CHECKS=1 to skip (exit 0) on offline machines.
//
// The four live proofs (all neutral, all merged on main before this lane):
// The live proofs (all neutral, all merged on main before this lane):
// 1. /promptly/support/ renders H2 after H1 (PRs #18/#20)
// 2. /contact/ carries application/ld+json (PR #19)
// 3. unknown URLs get a real 404, not the homepage (PR #34)
// 4. homepage stays portfolio-only: brand-disambiguation copy (#29) live,
// and no managed-service buyer-path content (#10/#11, snoozed).
// 5. /llms.txt lists every public page (PR #68)
// Proof 2b covers the 2026-08-08 dogfood finding page:
// 2b. /contact/ renders H2 after H1 (the heading-hierarchy repair, PR #18).
import { join } from "node:path"
import { fileURLToPath } from "node:url"
import { dirname } from "node:path"

import { PUBLIC_PAGE_URLS } from "./lib/public-pages.mjs"

const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..")

const BASE = "https://tinystudio.in"
Expand Down Expand Up @@ -99,6 +102,17 @@ try {
ok(!body.includes(marker), `homepage has no ${marker}`)
}
}

console.log("E. /llms.txt lists every public page (PR #68 live)")
{
const { status, body } = await get("/llms.txt")
ok(status === 200, `/llms.txt returns 200 (got ${status})`)
const missing = PUBLIC_PAGE_URLS.filter((url) => !body.includes(url))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match complete llms.txt URL entries

Using body.includes(url) treats parent URLs as present whenever a child URL is listed: for example, https://tinystudio.in/promptly/support/ satisfies checks for both the homepage and /promptly/. Consequently, both live-site guards can pass even if the Home, Promptly, or Drishti entries are absent, defeating the coverage check this commit adds. Parse the file into complete URL tokens or lines and compare exact entries instead.

Useful? React with 👍 / 👎.

ok(
missing.length === 0,
`llms.txt lists all ${PUBLIC_PAGE_URLS.length} public pages (missing: ${missing.join(", ") || "none"})`
)
}
} catch (error) {
failures++
console.error(` FAIL live request error: ${error.message}`)
Expand Down
12 changes: 12 additions & 0 deletions scripts/check-public-live-soft-404.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { fileURLToPath } from "node:url"
import { dirname, join } from "node:path"
import { randomBytes } from "node:crypto"

import { PUBLIC_PAGE_URLS } from "./lib/public-pages.mjs"

if (process.env.SKIP_LIVE_CHECKS === "1") {
console.log("check-public-live-soft-404: SKIP_LIVE_CHECKS=1, skipping live site checks")
process.exit(0)
Expand Down Expand Up @@ -73,6 +75,7 @@ try {
unknown: await fetchWithRetry(`${SITE}${UNKNOWN_PATH}`),
notFoundAsset: await fetchWithRetry(`${SITE}/404.html`),
realPage: await fetchWithRetry(`${SITE}/promptly/`),
llmsTxt: await fetchWithRetry(`${SITE}/llms.txt`),
}
} catch (err) {
console.error(` FAIL could not reach ${SITE}: ${err.message}`)
Expand All @@ -84,6 +87,7 @@ const { res: homeRes, body: homeBody } = results.home
const { res: unknownRes, body: unknownBody } = results.unknown
const { res: notFoundRes, body: notFoundBody } = results.notFoundAsset
const { res: realRes } = results.realPage
const { res: llmsTxtRes, body: llmsTxtBody } = results.llmsTxt

console.log("A. the homepage is reachable and intact")
ok(homeRes.status === 200, `GET / returns HTTP ${homeRes.status}`)
Expand All @@ -102,6 +106,14 @@ ok(!notFoundBody.includes(HOME_TITLE), "/404.html body is not the homepage")
console.log("D. a real page still serves")
ok(realRes.status === 200, `GET /promptly/ returns HTTP ${realRes.status}`)

console.log("E. the deployed llms.txt lists every public page (PR #68 live)")
ok(llmsTxtRes.status === 200, `GET /llms.txt returns HTTP ${llmsTxtRes.status}`)
const missingFromLlmsTxt = PUBLIC_PAGE_URLS.filter((url) => !llmsTxtBody.includes(url))
ok(
missingFromLlmsTxt.length === 0,
`llms.txt lists all ${PUBLIC_PAGE_URLS.length} public pages (missing: ${missingFromLlmsTxt.join(", ") || "none"})`
)

console.log(`\n${checks} checks, ${failures} failures`)
if (failures > 0) {
console.error("\nThe live site is soft-404ing or serving a stale bundle. Re-deploy the public site from origin/main and re-run this check.")
Expand Down
43 changes: 43 additions & 0 deletions scripts/lib/public-pages.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Single source of truth for the tinystudio.in public page set.
//
// Every consumer that needs to know "what are all the public pages" must
// import from here instead of hard-coding its own list:
// - scripts/prepare-static-site-bundle.mjs (bundle generator + its
// llms.txt coverage assertion)
// - scripts/check-public-live-deploy.mjs (release-lane live verifier)
// - scripts/check-public-live-soft-404.mjs (nightly stale-bundle net)
//
// Drift between these lists is how the live llms.txt ended up listing only
// 7 of the 12 public URLs (the five per-app support/privacy trust pages
// were missing from the June-20 bundle). Keep the lists in sync here only.

export const PUBLIC_HTML_FILES = [
"index.html",
"404.html",
"support/index.html",
"contact/index.html",
"privacy/index.html",
"privacy-choices/index.html",
"terms/index.html",
"promptly/index.html",
"promptly/support/index.html",
"promptly/privacy/index.html",
"drishti/index.html",
"drishti/support/index.html",
"drishti/privacy/index.html",
]

export const pageUrlFor = (relativeFile) =>
relativeFile === "index.html"
? "https://tinystudio.in/"
: `https://tinystudio.in/${relativeFile.split("/").slice(0, -1).join("/")}/`

// The 12 canonical public URLs (every page except the 404 catch-all).
export const PUBLIC_PAGE_URLS = PUBLIC_HTML_FILES.filter(
(relativeFile) => relativeFile !== "404.html"
).map(pageUrlFor)

// Assert that llms.txt content lists every public page; returns the
// missing URLs (empty array when coverage is complete).
export const missingFromLlmsTxt = (content) =>
PUBLIC_PAGE_URLS.filter((url) => !content.includes(url))
29 changes: 4 additions & 25 deletions scripts/prepare-static-site-bundle.mjs
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
import { promises as fs } from "node:fs";
import path from "node:path";

import { PUBLIC_HTML_FILES, missingFromLlmsTxt } from "./lib/public-pages.mjs";

const root = path.resolve("public");
const cssPath = path.join(root, "styles.css");
const iconPath = path.join(root, "favicon.svg");
const appleIconPath = path.join(root, "apple-touch-icon.svg");

const htmlFiles = [
"index.html",
"404.html",
"support/index.html",
"contact/index.html",
"privacy/index.html",
"privacy-choices/index.html",
"terms/index.html",
"promptly/index.html",
"promptly/support/index.html",
"promptly/privacy/index.html",
"drishti/index.html",
"drishti/support/index.html",
"drishti/privacy/index.html"
];
const htmlFiles = PUBLIC_HTML_FILES;

const supportSchema = {
"@context": "https://schema.org",
Expand Down Expand Up @@ -149,17 +137,8 @@ function addSupportSchema(html) {
return html.replace(/(\s*<link rel="apple-touch-icon")/, `\n${script}$1`);
}

function pageUrlFor(relativeFile) {
return relativeFile === "index.html"
? "https://tinystudio.in/"
: `https://tinystudio.in/${path.posix.dirname(relativeFile)}/`;
}

function assertLlmsTxtCoversPublicPages(content) {
const publicFiles = htmlFiles.filter((relativeFile) => relativeFile !== "404.html");
const missing = publicFiles
.map(pageUrlFor)
.filter((url) => !content.includes(url));
const missing = missingFromLlmsTxt(content);
if (missing.length) {
throw new Error(`llms.txt does not list every public page; missing: ${missing.join(", ")}`);
}
Expand Down