Files
doc.rustdesk.com/scripts/indexnow.mjs
T
rustdeskandClaude Opus 5.5 1ad15e22b7 seo: bring meta descriptions into Bing's range, and add IndexNow
Bing Webmaster Tools flags meta descriptions under roughly 90-100
characters. It counts characters, so most Chinese, Japanese and Korean
pages failed even when the text read fine.

- Hand-written descriptions for the key site pages in 11 languages:
  home, pricing, support, team, the blog index and its categories. Plus
  16 docs pages whose text is too short to extend from.
- scripts/fit-meta-descriptions.mjs runs after both builds (v3/build.sh,
  deploy.sh and the docs workflow). It brings every other description to
  120-160 characters, using the page's opening sentences.
- Category descriptions now resolve for the transliterated zh/ja/ko
  slugs. Before, those pages only ever showed the generic text.
- IndexNow: the key file ships with the site from v3/public, and
  scripts/indexnow.mjs submits the live sitemaps. After deploying, each
  workflow submits its own half: the site or the docs.
- Translate the bodies of the Spanish team page and the German support
  page, which were still in English.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:07:55 +08:00

78 lines
3.1 KiB
JavaScript

#!/usr/bin/env node
/**
* Tells Bing (and the other IndexNow engines) to recrawl rustdesk.com now,
* instead of waiting for its own schedule. ChatGPT search leans on Bing's
* index, so this is how a fix reaches AI answers quickly.
*
* node scripts/indexnow.mjs # the site and the docs
* node scripts/indexnow.mjs site # rustdesk.com only (v3)
* node scripts/indexnow.mjs docs # rustdesk.com/docs only (Hugo)
*
* Run it AFTER a deploy: the engines verify the request by fetching the key
* file from the live site, and judge the pages as they are served. The URL
* list is read from the live sitemaps for the same reason. The two deploy
* workflows run it as their last step, each for its own half.
*
* The key is the name of the one file v3/public/<32 hex>.txt, published at
* https://rustdesk.com/<key>.txt. One key covers the whole host, docs
* included. Google does not take part in IndexNow.
*/
import { readdir } from "node:fs/promises";
import path from "node:path";
const ROOT = path.resolve(import.meta.dirname, "..");
const ORIGIN = "https://rustdesk.com";
const SITEMAPS = {
site: `${ORIGIN}/sitemap-index.xml`,
docs: `${ORIGIN}/docs/sitemap.xml`,
};
const which = process.argv[2] ?? "all";
if (!["all", "site", "docs"].includes(which)) {
console.error("usage: indexnow.mjs [all|site|docs]");
process.exit(2);
}
const keyFile = (await readdir(path.join(ROOT, "v3/public"))).find((f) => /^[0-9a-f]{32}\.txt$/.test(f));
if (!keyFile) {
console.error("[indexnow] no v3/public/<key>.txt found");
process.exit(1);
}
const key = keyFile.replace(/\.txt$/, "");
const keyLocation = `${ORIGIN}/${keyFile}`;
const live = await fetch(keyLocation).then((r) => (r.ok ? r.text() : null)).catch(() => null);
if (live?.trim() !== key) {
console.error(`[indexnow] ${keyLocation} is not live yet. Deploy the site first.`);
process.exit(1);
}
/** Every <loc> under a sitemap, following sitemap indexes. */
async function urlsOf(sitemap) {
const xml = await fetch(sitemap).then((r) => r.text());
const locs = [...xml.matchAll(/<loc>\s*([^<\s]+)\s*<\/loc>/g)].map((m) => m[1]);
if (!/<sitemapindex/.test(xml)) return locs;
// One at a time: a burst of parallel fetches trips Cloudflare's timeouts.
const out = [];
for (const loc of locs) out.push(...(await urlsOf(loc)));
return out;
}
const sources = which === "all" ? Object.values(SITEMAPS) : [SITEMAPS[which]];
const found = [];
for (const s of sources) found.push(...(await urlsOf(s)));
const urlList = [...new Set(found)].filter((u) => u.startsWith(`${ORIGIN}/`));
// One request takes up to 10,000 URLs.
for (let i = 0; i < urlList.length; i += 10000) {
const batch = urlList.slice(i, i + 10000);
const res = await fetch("https://api.indexnow.org/indexnow", {
method: "POST",
headers: { "Content-Type": "application/json; charset=utf-8" },
body: JSON.stringify({ host: new URL(ORIGIN).host, key, keyLocation, urlList: batch }),
});
// 200 and 202 both mean accepted; 202 is "key validation pending".
console.log(`[indexnow] ${batch.length} URLs (${which}) submitted: HTTP ${res.status}`);
if (!res.ok) process.exit(1);
}