diff --git a/.github/workflows/toolify-live-index.yml b/.github/workflows/toolify-live-index.yml
new file mode 100644
index 00000000..81f9a69e
--- /dev/null
+++ b/.github/workflows/toolify-live-index.yml
@@ -0,0 +1,38 @@
+name: Toolify live indexability recovery (12 URLs)
+on:
+ pull_request:
+ paths:
+ - 'scripts/toolify-live-index-gate.mjs'
+ - 'scripts/tests/toolify-live-index-gate.test.mjs'
+ - '.github/workflows/toolify-live-index.yml'
+ push:
+ branches: [main]
+ paths:
+ - 'scripts/toolify-live-index-gate.mjs'
+ - '.github/workflows/toolify-live-index.yml'
+ workflow_dispatch:
+jobs:
+ source:
+ runs-on: ubuntu-latest
+ timeout-minutes: 4
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ - run: node --test scripts/tests/toolify-live-index-gate.test.mjs
+ live:
+ needs: source
+ if: github.event_name != 'pull_request'
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ - run: node scripts/toolify-live-index-gate.mjs
diff --git a/scripts/tests/toolify-live-index-gate.test.mjs b/scripts/tests/toolify-live-index-gate.test.mjs
new file mode 100644
index 00000000..4c9113c0
--- /dev/null
+++ b/scripts/tests/toolify-live-index-gate.test.mjs
@@ -0,0 +1,38 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+import {readFileSync} from "node:fs";
+import {fileURLToPath} from "node:url";
+import {targets,urlFor,inspectHTML} from "../toolify-live-index-gate.mjs";
+
+const source=readFileSync(fileURLToPath(new URL("../../site/src/lib/tools/registry.ts",import.meta.url)),"utf8");
+function sourceBlock(start,end) {
+ const a=source.indexOf(start),b=source.indexOf(end,a+start.length);
+ assert.ok(a>=0&&b>a,"registry source declarations absent");
+ return source.slice(a,b);
+}
+test("12 nonduplicated source-indexable official-inspection URLs",()=>{
+ assert.equal(targets.length,12);
+ assert.equal(new Set(targets.map(urlFor)).size,12);
+ const core=sourceBlock("export const CORE_TOOLS_BY_DEMAND =","] as const;");
+ const exceptions=sourceBlock("export const INDEX_EXCEPTIONS:", "\n};");
+ for(const [loc,slug,type] of targets) {
+ if(type==="core") assert.ok(core.includes('"'+slug+'"'),loc+"/"+slug+" missing core");
+ else assert.ok(new RegExp("\\b"+loc+":\\s*\\[[^\\]]*\\\""+slug+"\\\"").test(exceptions),
+ loc+"/"+slug+" missing locale exception");
+ }
+});
+test("existing old Google noindex status must not override valid current HTTP+canonical",()=>{
+ const url=urlFor(targets[6]);
+ const html='
Date CalculatorTool';
+ assert.equal(inspectHTML(html,{get:()=>null},url,200,url).pass,true);
+});
+test("reject noindex, missing canonical, bad HTTP and redirect",()=>{
+ const url=urlFor(targets[0]);
+ const html='Workdays';
+ assert.ok(inspectHTML(html,{get:()=>null},url,200,url).pass);
+ assert.ok(!inspectHTML(html.replace('',''),{get:()=>null},url,200,url).pass);
+ assert.ok(!inspectHTML(html,{get:()=> "noindex"},url,200,url).pass);
+ assert.ok(!inspectHTML(html,{get:()=>null},url,404,url).pass);
+ assert.ok(!inspectHTML(html,{get:()=>null},url,200,url+"/").pass);
+});
diff --git a/scripts/toolify-live-index-gate.mjs b/scripts/toolify-live-index-gate.mjs
new file mode 100644
index 00000000..636098ea
--- /dev/null
+++ b/scripts/toolify-live-index-gate.mjs
@@ -0,0 +1,74 @@
+#!/usr/bin/env node
+/**
+ * Read-only exact live indexability diagnosis for the fixed 12 URL cohort
+ * Google URL Inspection inspected on 2026-10-26.
+ * It NEVER submits URLs to Google, updates robots, or touches credentials.
+ * Google cached 4 noindex statuses from July; the source's October policy
+ * intends all 12 to be indexable. Verify what is really PUBLIC today.
+ */
+export const targets = [
+ ["en", "workdays-calculator", "core"],
+ ["en", "date-calculator", "core"],
+ ["en", "pressure-converter", "core"],
+ ["en", "unit-price-calculator", "core"],
+ ["ja", "workdays-calculator", "core"],
+ ["ja", "pressure-converter", "core"],
+ ["th", "date-calculator", "exception"],
+ ["ru", "pressure-converter", "exception"],
+ ["tr", "grade-calculator", "exception"],
+ ["ar", "length-converter", "exception"],
+ ["fr", "speed-converter", "exception"],
+ ["es", "due-date-calculator", "exception"],
+];
+export const base="https://toolify365.com";
+export const urlFor=([loc,slug])=>base+"/"+loc+"/tools/"+slug;
+export function attr(tag,name){
+ const m=tag.match(new RegExp("\\b"+name+"\\s*=\\s*([\"'])(.*?)\\1","i"));
+ if(m)return m[2];
+ const loose=tag.match(new RegExp("\\b"+name+"\\s*=\\s*([^\\s>]+)","i"));
+ return loose?.[1]??null;
+}
+export function inspectHTML(html,headers,url,status,finalUrl){
+ const head=html.split(/<\/head>/i)[0]??"";
+ const metaTags=[...head.matchAll(/]*>/gi)].map(x=>x[0]);
+ const robotsMeta=metaTags.filter(t=>["robots","googlebot"].includes((attr(t,"name")??"").toLowerCase()))
+ .map(t=>attr(t,"content")??"").join(",");
+ const xRobots=headers?.get?.("x-robots-tag")??null;
+ const tags=[...head.matchAll(/]*>/gi)].map(x=>x[0]);
+ const canonicals=tags.filter(t=>(attr(t,"rel")??"").toLowerCase().split(/\s+/).includes("canonical"))
+ .map(t=>attr(t,"href"));
+ const noindex=/(^|[\s,;])noindex($|[\s,;])/i.test(robotsMeta+","+String(xRobots??""));
+ const canonical=canonicals[0]??null;
+ return {url,httpStatus:status,finalUrl,canonical,canonicalCount:canonicals.length,
+ robotsMeta,xRobots,noindex,pass:status===200&&finalUrl===url&&canonicals.length===1&&
+ canonical===url&&!noindex&&/]*>[^<]+<\/title>/i.test(head)};
+}
+export async function main(){
+ const results=await Promise.all(targets.map(async t=>{
+ const url=urlFor(t);
+ try {
+ const r=await fetch(url,{redirect:"follow",
+ headers:{"Cache-Control":"no-cache","Pragma":"no-cache","User-Agent":"ToolifyIndexSmoke/1.0"},
+ signal:AbortSignal.timeout(20000)});
+ return {...inspectHTML(await r.text(),r.headers,url,r.status,r.url),locale:t[0],slug:t[1],indexIntent:t[2]};
+ }catch(e){return {url,locale:t[0],slug:t[1],indexIntent:t[2],pass:false,error:String(e.message??e)};}
+ }));
+ const bad=results.filter(r=>!r.pass);
+ const summary={checked:results.length,liveIndexable:results.length-bad.length,problems:bad.length,
+ note:"Source intended indexability and live HTML are distinct from Google's cached index verdict. All 12 URL Inspection rows are a selected test cohort, not the whole site.",
+ checkedAtUtc:new Date().toISOString(),results};
+ console.log(JSON.stringify(summary,null,2));
+ if (process.env.GITHUB_STEP_SUMMARY) {
+ const {appendFileSync}=await import("node:fs");
+ appendFileSync(process.env.GITHUB_STEP_SUMMARY,
+ "## Toolify 12URL live indexability\n\n"+
+ "- Pass: "+(results.length-bad.length)+"/12\n"+
+ "- Live failures: "+bad.length+"\n\n"+
+ "| URL | HTTP | canonical | noindex | Result |\n|---|---|---|---|---|\n"+
+ results.map(x=>"| "+x.url+" | "+(x.httpStatus??"ERROR")+" | "+(x.canonical??"missing")+
+ " | "+(x.noindex??"unknown")+" | "+(x.pass?"PASS":"FAIL")+" |").join("\n")+"\n");
+ }
+ if(bad.length)process.exitCode=1;
+}
+if(process.argv[1]?.endsWith("toolify-live-index-gate.mjs"))
+ main().catch(e=>{console.error(e);process.exitCode=2});