diff --git a/public/agents.html b/public/agents.html
index 13f2d5de..426110e9 100644
--- a/public/agents.html
+++ b/public/agents.html
@@ -18,6 +18,39 @@
+
diff --git a/public/audit.html b/public/audit.html
index b1f9848c..11e66e51 100644
--- a/public/audit.html
+++ b/public/audit.html
@@ -18,6 +18,39 @@
+
diff --git a/public/index.html b/public/index.html
index 71af6123..9cf7c09d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -18,6 +18,39 @@
+
diff --git a/public/pricing.html b/public/pricing.html
index dd1b9abf..6117eddf 100644
--- a/public/pricing.html
+++ b/public/pricing.html
@@ -18,6 +18,39 @@
+
diff --git a/public/specimen.html b/public/specimen.html
index 9e233ab8..d8f5122b 100644
--- a/public/specimen.html
+++ b/public/specimen.html
@@ -18,6 +18,39 @@
+
diff --git a/scripts/check-site.mjs b/scripts/check-site.mjs
index 2cd04d1c..84b5bae3 100644
--- a/scripts/check-site.mjs
+++ b/scripts/check-site.mjs
@@ -1057,6 +1057,164 @@ for (const [pageName, pageHtml, pageUrl] of socialSharePages) {
}
}
+// ---- Structured data (dogfood 975fdb784275) --------------------------------
+// The leak audit this site sells flags a homepage whose served HTML gives a
+// machine reader nothing to hold onto — no schema.org markup at all — so the
+// site's own five public pages must not carry that fault either. Each page
+// keeps exactly one application/ld+json block in its head: a @graph with a
+// stable Organization node (the same entity on every page), a WebSite node,
+// and the page's own WebPage node. Every value is bound to the page's own
+// head metadata — name to the og:title, description to the meta description,
+// url to the og:url — so the structured data cannot drift from what the page
+// actually says. The Organization node is identical on all five pages, and
+// the price stays where it belongs: pricing.html owns it, so no other page's
+// block may restate a dollar amount.
+const structuredDataPages = [
+ ["homepage", siteHome, "https://tinystudio.io/"],
+ ["audit page", siteAudit, "https://tinystudio.io/audit.html"],
+ ["desk page", read("public/agents.html"), "https://tinystudio.io/agents.html"],
+ ["pricing page", read("public/pricing.html"), "https://tinystudio.io/pricing.html"],
+ ["specimen page", read("public/specimen.html"), "https://tinystudio.io/specimen.html"]
+];
+
+const ORGANIZATION_ID = "https://tinystudio.io/#organization";
+const WEBSITE_ID = "https://tinystudio.io/#website";
+const ORGANIZATION_NAME = "TinyStudio";
+const ORGANIZATION_LOGO = "https://tinystudio.io/apple-touch-icon.png";
+const ORGANIZATION_DESCRIPTION =
+ siteHome.match(/]*\bname="description"[^>]*\bcontent="([^"]*)"/i)?.[1] ?? "";
+
+// The page node name is bound to the og:title, whose HTML entities (e.g. the
+// pricing page's "&") must be decoded before comparison.
+const decodeEntities = (text) =>
+ text.replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
+
+const jsonLdBlocksIn = (html) =>
+ [...html.matchAll(/