Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 33 additions & 0 deletions public/agents.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,39 @@
<meta name="twitter:title" content="TinyStudio — The Desk">
<meta name="twitter:description" content="Seven specialists, one human signature. Each does a single job on your actual pages and numbers; nothing reaches you until a person has read and signed it.">
<meta name="twitter:image" content="https://tinystudio.io/og-image.png">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://tinystudio.io/#organization",
"name": "TinyStudio",
"url": "https://tinystudio.io/",
"logo": "https://tinystudio.io/apple-touch-icon.png",
"description": "TinyStudio: the free leak audit of high-ticket service homepages. Each fault named in order of what it costs you, with the fix beside it. Six a month."
},
{
"@type": "WebSite",
"@id": "https://tinystudio.io/#website",
"url": "https://tinystudio.io/",
"name": "TinyStudio",
"inLanguage": "en",
"publisher": { "@id": "https://tinystudio.io/#organization" }
},
{
"@type": "WebPage",
"@id": "https://tinystudio.io/agents.html#webpage",
"url": "https://tinystudio.io/agents.html",
"name": "TinyStudio — The Desk",
"description": "Seven specialists, one human signature. Each does a single job on your actual pages and numbers; nothing reaches you until a person has read and signed it.",
"inLanguage": "en",
"isPartOf": { "@id": "https://tinystudio.io/#website" },
"about": { "@id": "https://tinystudio.io/#organization" }
}
]
}
</script>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" data-fonts-css href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,200;0,9..144,300;0,9..144,400;1,9..144,200;1,9..144,300&family=Karla:wght@300;400;500;600;700&display=swap">
<script src="fonts.js" defer></script>
Expand Down
33 changes: 33 additions & 0 deletions public/audit.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,39 @@
<meta name="twitter:title" content="TinyStudio — What the appraisal checks">
<meta name="twitter:description" content="What the appraisal actually checks: four passes by hand, the evidence embedded — including a controlled AI-search test with verbatim answers and cited pages.">
<meta name="twitter:image" content="https://tinystudio.io/og-image.png">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://tinystudio.io/#organization",
"name": "TinyStudio",
"url": "https://tinystudio.io/",
"logo": "https://tinystudio.io/apple-touch-icon.png",
"description": "TinyStudio: the free leak audit of high-ticket service homepages. Each fault named in order of what it costs you, with the fix beside it. Six a month."
},
{
"@type": "WebSite",
"@id": "https://tinystudio.io/#website",
"url": "https://tinystudio.io/",
"name": "TinyStudio",
"inLanguage": "en",
"publisher": { "@id": "https://tinystudio.io/#organization" }
},
{
"@type": "WebPage",
"@id": "https://tinystudio.io/audit.html#webpage",
"url": "https://tinystudio.io/audit.html",
"name": "TinyStudio — What the appraisal checks",
"description": "What the appraisal actually checks: four passes by hand, the evidence embedded — including a controlled AI-search test with verbatim answers and cited pages.",
"inLanguage": "en",
"isPartOf": { "@id": "https://tinystudio.io/#website" },
"about": { "@id": "https://tinystudio.io/#organization" }
}
]
}
</script>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" data-fonts-css href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,200;0,9..144,300;0,9..144,400;1,9..144,200;1,9..144,300&family=Karla:wght@300;400;500;600;700&display=swap">
<script src="fonts.js" defer></script>
Expand Down
33 changes: 33 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,39 @@
<meta name="twitter:title" content="TinyStudio — The Website Appraisal">
<meta name="twitter:description" content="TinyStudio: the free leak audit of high-ticket service homepages. Each fault named in order of what it costs you, with the fix beside it. Six a month.">
<meta name="twitter:image" content="https://tinystudio.io/og-image.png">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://tinystudio.io/#organization",
"name": "TinyStudio",

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 Reconcile the legacy Agent Desk organization identity

When crawlers reach the publicly served /agent-desk route, public/agent-desk.html:36-44 still declares an Organization named The Tiny Studio with the same https://tinystudio.io/ URL, while this new graph declares it as TinyStudio. This gives machine readers two conflicting identities for the organization the structured data is meant to clarify; update or remove the legacy block and include that served page in the consistency guard.

Useful? React with 👍 / 👎.

"url": "https://tinystudio.io/",
"logo": "https://tinystudio.io/apple-touch-icon.png",
"description": "TinyStudio: the free leak audit of high-ticket service homepages. Each fault named in order of what it costs you, with the fix beside it. Six a month."
},
{
"@type": "WebSite",
"@id": "https://tinystudio.io/#website",
"url": "https://tinystudio.io/",
"name": "TinyStudio",
"inLanguage": "en",
"publisher": { "@id": "https://tinystudio.io/#organization" }
},
{
"@type": "WebPage",
"@id": "https://tinystudio.io/#webpage",
"url": "https://tinystudio.io/",
"name": "TinyStudio — The Website Appraisal",
"description": "TinyStudio: the free leak audit of high-ticket service homepages. Each fault named in order of what it costs you, with the fix beside it. Six a month.",
"inLanguage": "en",
"isPartOf": { "@id": "https://tinystudio.io/#website" },
"about": { "@id": "https://tinystudio.io/#organization" }
}
]
}
</script>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" data-fonts-css href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,200;0,9..144,300;0,9..144,400;1,9..144,200;1,9..144,300&family=Karla:wght@300;400;500;600;700&display=swap">
<script src="fonts.js" defer></script>
Expand Down
33 changes: 33 additions & 0 deletions public/pricing.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,39 @@
<meta name="twitter:title" content="TinyStudio — Pricing &amp; terms">
<meta name="twitter:description" content="One price, plainly stated: the appraisal is free, the desk is $2,500 a month on a three-month minimum, and the delivery guarantee is written out in full.">
<meta name="twitter:image" content="https://tinystudio.io/og-image.png">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://tinystudio.io/#organization",
"name": "TinyStudio",
"url": "https://tinystudio.io/",
"logo": "https://tinystudio.io/apple-touch-icon.png",
"description": "TinyStudio: the free leak audit of high-ticket service homepages. Each fault named in order of what it costs you, with the fix beside it. Six a month."
},
{
"@type": "WebSite",
"@id": "https://tinystudio.io/#website",
"url": "https://tinystudio.io/",
"name": "TinyStudio",
"inLanguage": "en",
"publisher": { "@id": "https://tinystudio.io/#organization" }
},
{
"@type": "WebPage",
"@id": "https://tinystudio.io/pricing.html#webpage",
"url": "https://tinystudio.io/pricing.html",
"name": "TinyStudio — Pricing & terms",
"description": "One price, plainly stated: the appraisal is free, the desk is $2,500 a month on a three-month minimum, and the delivery guarantee is written out in full.",
"inLanguage": "en",
"isPartOf": { "@id": "https://tinystudio.io/#website" },
"about": { "@id": "https://tinystudio.io/#organization" }
}
]
}
</script>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" data-fonts-css href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,200;0,9..144,300;0,9..144,400;1,9..144,200;1,9..144,300&family=Karla:wght@300;400;500;600;700&display=swap">
<script src="fonts.js" defer></script>
Expand Down
33 changes: 33 additions & 0 deletions public/specimen.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,39 @@
<meta name="twitter:title" content="TinyStudio — The Website Appraisal, specimen">
<meta name="twitter:description" content="The appraisal, with the client's name removed: a real audit of a clinic homepage, four findings with evidence and fixes, read and signed by Nish.">
<meta name="twitter:image" content="https://tinystudio.io/og-image.png">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://tinystudio.io/#organization",
"name": "TinyStudio",
"url": "https://tinystudio.io/",
"logo": "https://tinystudio.io/apple-touch-icon.png",
"description": "TinyStudio: the free leak audit of high-ticket service homepages. Each fault named in order of what it costs you, with the fix beside it. Six a month."
},
{
"@type": "WebSite",
"@id": "https://tinystudio.io/#website",
"url": "https://tinystudio.io/",
"name": "TinyStudio",
"inLanguage": "en",
"publisher": { "@id": "https://tinystudio.io/#organization" }
},
{
"@type": "WebPage",
"@id": "https://tinystudio.io/specimen.html#webpage",
"url": "https://tinystudio.io/specimen.html",
"name": "TinyStudio — The Website Appraisal, specimen",
"description": "The appraisal, with the client's name removed: a real audit of a clinic homepage, four findings with evidence and fixes, read and signed by Nish.",
"inLanguage": "en",
"isPartOf": { "@id": "https://tinystudio.io/#website" },
"about": { "@id": "https://tinystudio.io/#organization" }
}
]
}
</script>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" data-fonts-css href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,200;0,9..144,300;0,9..144,400;1,9..144,200;1,9..144,300&family=Karla:wght@300;400;500;600;700&display=swap">
<script src="fonts.js" defer></script>
Expand Down
158 changes: 158 additions & 0 deletions scripts/check-site.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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(/<meta\b[^>]*\bname="description"[^>]*\bcontent="([^"]*)"/i)?.[1] ?? "";

// The page node name is bound to the og:title, whose HTML entities (e.g. the
// pricing page's "&amp;") must be decoded before comparison.
const decodeEntities = (text) =>
text.replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">");

const jsonLdBlocksIn = (html) =>
[...html.matchAll(/<script\b[^>]*\btype="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/gi)]
.map((match) => match[1]);

const ogTitleOf = (html) =>
html.match(/<meta\b[^>]*\bproperty="og:title"[^>]*\bcontent="([^"]*)"/i)?.[1] ?? "";

for (const [pageName, pageHtml, pageUrl] of structuredDataPages) {
const blocks = jsonLdBlocksIn(pageHtml);
if (blocks.length !== 1) {
failures.push(`Structured data must appear exactly once on ${pageName} (found ${blocks.length}).`);
continue;
}
const head = pageHtml.match(/<head\b[\s\S]*?<\/head>/i)?.[0] ?? "";
if (jsonLdBlocksIn(head).length !== 1) {
failures.push(`Structured data on ${pageName} must sit inside <head>.`);
}

let graph = null;
try {
graph = JSON.parse(blocks[0]);
} catch (error) {
failures.push(`Structured data on ${pageName} must be valid JSON (${error.message}).`);
continue;
}
if (graph["@context"] !== "https://schema.org") {
failures.push(`Structured data on ${pageName} must use the schema.org context.`);
}
if (!Array.isArray(graph["@graph"])) {
failures.push(`Structured data on ${pageName} must use an @graph array.`);
continue;
}

const nodes = graph["@graph"];
const nodeIds = nodes.map((node) => node["@id"]).filter(Boolean);
if (new Set(nodeIds).size !== nodeIds.length) {
failures.push(`Structured data on ${pageName} must use unique @id values within the graph.`);
}

const orgNodes = nodes.filter((node) => node["@type"] === "Organization");
const siteNodes = nodes.filter((node) => node["@type"] === "WebSite");
const pageNodes = nodes.filter((node) => node["@type"] === "WebPage");
Comment on lines +1125 to +1133

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Reject extra graph nodes.

The validator accepts an @graph that contains the required three nodes plus an arbitrary fourth node. This does not enforce the stated restriction against extra nodes. Require exactly three nodes before validating the node types.

Proposed fix
 const nodes = graph["`@graph`"];
+if (nodes.length !== 3) {
+  failures.push(`Structured data on ${pageName} must contain exactly three nodes.`);
+}
 const nodeIds = nodes.map((node) => node["`@id`"]).filter(Boolean);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const nodes = graph["@graph"];
const nodeIds = nodes.map((node) => node["@id"]).filter(Boolean);
if (new Set(nodeIds).size !== nodeIds.length) {
failures.push(`Structured data on ${pageName} must use unique @id values within the graph.`);
}
const orgNodes = nodes.filter((node) => node["@type"] === "Organization");
const siteNodes = nodes.filter((node) => node["@type"] === "WebSite");
const pageNodes = nodes.filter((node) => node["@type"] === "WebPage");
const nodes = graph["`@graph`"];
if (nodes.length !== 3) {
failures.push(`Structured data on ${pageName} must contain exactly three nodes.`);
}
const nodeIds = nodes.map((node) => node["`@id`"]).filter(Boolean);
if (new Set(nodeIds).size !== nodeIds.length) {
failures.push(`Structured data on ${pageName} must use unique `@id` values within the graph.`);
}
const orgNodes = nodes.filter((node) => node["`@type`"] === "Organization");
const siteNodes = nodes.filter((node) => node["`@type`"] === "WebSite");
const pageNodes = nodes.filter((node) => node["`@type`"] === "WebPage");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-site.mjs` around lines 1125 - 1133, Update the structured-data
validation around the nodes array to reject graphs whose node count is not
exactly three before validating node types. Preserve the existing uniqueness
check and required Organization, WebSite, and WebPage node validation for valid
three-node graphs.

if (orgNodes.length !== 1) {
failures.push(`Structured data on ${pageName} must carry exactly one Organization node (found ${orgNodes.length}).`);
} else {
const org = orgNodes[0];
if (org["@id"] !== ORGANIZATION_ID) {
failures.push(`Organization on ${pageName} must use the stable @id ${ORGANIZATION_ID}.`);
}
if (org.name !== ORGANIZATION_NAME) {
failures.push(`Organization on ${pageName} must be named ${ORGANIZATION_NAME} (found ${JSON.stringify(org.name)}).`);
}
if (org.url !== "https://tinystudio.io/") {
failures.push(`Organization on ${pageName} must point at https://tinystudio.io/ (found ${JSON.stringify(org.url)}).`);
}
if (org.logo !== ORGANIZATION_LOGO) {
failures.push(`Organization on ${pageName} must use the served logo ${ORGANIZATION_LOGO} (found ${JSON.stringify(org.logo)}).`);
}
if (org.description !== ORGANIZATION_DESCRIPTION) {
failures.push(`Organization description on ${pageName} must match the homepage meta description (stable entity).`);
}
}

if (siteNodes.length !== 1) {
failures.push(`Structured data on ${pageName} must carry exactly one WebSite node (found ${siteNodes.length}).`);
} else {
const site = siteNodes[0];
if (site["@id"] !== WEBSITE_ID) {
failures.push(`WebSite on ${pageName} must use the stable @id ${WEBSITE_ID}.`);
}
if (site.url !== "https://tinystudio.io/" || site.name !== ORGANIZATION_NAME) {
failures.push(`WebSite on ${pageName} must carry the site url and the TinyStudio name.`);
}
if (site.inLanguage !== "en") {
failures.push(`WebSite on ${pageName} must declare inLanguage "en".`);
}
if (site.publisher?.["@id"] !== ORGANIZATION_ID) {
failures.push(`WebSite on ${pageName} must name the Organization as its publisher.`);
}
}

const metaDescription = pageHtml.match(/<meta\b[^>]*\bname="description"[^>]*\bcontent="([^"]*)"/i)?.[1] ?? "";
const ogTitle = ogTitleOf(pageHtml);
if (pageNodes.length !== 1) {
failures.push(`Structured data on ${pageName} must carry exactly one WebPage node (found ${pageNodes.length}).`);
} else {
const page = pageNodes[0];
if (page["@id"] !== `${pageUrl}#webpage`) {
failures.push(`WebPage on ${pageName} must use the @id ${pageUrl}#webpage.`);
}
if (page.url !== pageUrl) {
failures.push(`WebPage on ${pageName} must carry its own url ${pageUrl} (found ${JSON.stringify(page.url)}).`);
}
if (page.name !== decodeEntities(ogTitle)) {
failures.push(`WebPage name on ${pageName} must equal the og:title (${JSON.stringify(decodeEntities(ogTitle))}).`);
}
if (page.description !== metaDescription) {
failures.push(`WebPage description on ${pageName} must equal the meta description.`);
}
if (page.inLanguage !== "en") {
failures.push(`WebPage on ${pageName} must declare inLanguage "en".`);
}
if (page.isPartOf?.["@id"] !== WEBSITE_ID) {
failures.push(`WebPage on ${pageName} must belong to the WebSite node.`);
}
if (page.about?.["@id"] !== ORGANIZATION_ID) {
failures.push(`WebPage on ${pageName} must be about the Organization node.`);
}
}

// The price is pricing.html's to state; no other page's structured data may
// restate a dollar amount (the pricing page mirrors its own meta
// description, which legitimately carries the price).
if (pageName !== "pricing page" && /\$\s?\d/.test(blocks[0])) {
failures.push(`Structured data on ${pageName} must not restate a dollar amount; pricing.html owns the price.`);
}
for (const claim of forbiddenClaims) {
if (blocks[0].toLowerCase().includes(claim.toLowerCase())) {
failures.push(`Structured data on ${pageName} must not promise: ${claim}`);
}
}
if (/\+\d[\d\s()-]{6,}\d/.test(blocks[0])) {
failures.push(`Structured data on ${pageName} must not capture phone numbers.`);
}
}

for (const migration of ["migrations/0002_agent_runs.sql", "migrations/0003_agent_usage_limits.sql"]) { if (!existsSync(new URL(`../${migration}`, import.meta.url))) {
failures.push(`Missing migration: ${migration}`);
continue;
Expand Down
Loading