import * as env from "$app/env/private";
import type { Fetch } from "#lib/fetch.ts";
export const DEFAULT_INDEXNOW_KEY = "d76fae270607bfc761b35c14f3182c9e";
export const DEFAULT_INDEXNOW_HOST = "tangled.org";
export const INDEXNOW_ENDPOINT = "https://api.indexnow.org/indexnow";
export interface IndexNowPayload {
host: string;
key: string;
keyLocation: string;
urlList: string[];
}
export interface IndexNowOptions {
host?: string;
key?: string;
keyLocation?: string;
fetch?: Fetch;
}
export interface IndexNowResult {
success: boolean;
status?: number;
error?: string;
}
/**
* Submits a URL or list of URLs to IndexNow (Bing, Yandex, Naver, Seznam, etc.).
* Guarantees silent error handling: network or API errors are logged to console
* without throwing, ensuring user or client flows are never disrupted.
*/
export async function submitToIndexNow(
urls: string | string[],
options: IndexNowOptions = {}
): Promise<IndexNowResult> {
const rawUrls = Array.isArray(urls) ? urls : [urls];
const validUrls = rawUrls.filter((u) => typeof u === "string" && u.trim().length > 0);
if (validUrls.length === 0) {
return { success: false, error: "No URLs provided" };
}
const host = options.host || env.INDEXNOW_HOST || DEFAULT_INDEXNOW_HOST;
const key = options.key || env.INDEXNOW_KEY || DEFAULT_INDEXNOW_KEY;
const keyLocation = options.keyLocation || `https://${host}/${key}.txt`;
const customFetch = options.fetch ?? globalThis.fetch;
// IndexNow allows maximum 10,000 URLs per request
const MAX_BATCH_SIZE = 10000;
const urlBatches: string[][] = [];
for (let i = 0; i < validUrls.length; i += MAX_BATCH_SIZE) {
urlBatches.push(validUrls.slice(i, i + MAX_BATCH_SIZE));
}
let allSuccess = true;
let lastStatus: number | undefined;
for (const batch of urlBatches) {
const payload: IndexNowPayload = {
host,
key,
keyLocation,
urlList: batch
};
try {
const res = await customFetch(INDEXNOW_ENDPOINT, {
method: "POST",
headers: {
"content-type": "application/json; charset=utf-8"
},
body: JSON.stringify(payload)
});
lastStatus = res.status;
// 200 = OK, 202 = Accepted (key validation pending)
if (!res.ok && res.status !== 202) {
const body = await res.text().catch(() => "");
console.warn(
`[IndexNow] Non-OK response (${res.status}): ${body || res.statusText}`
);
allSuccess = false;
}
} catch (err) {
console.warn("[IndexNow] Failed to submit URLs:", err);
allSuccess = false;
}
}
return {
success: allSuccess,
status: lastStatus,
error: allSuccess
? undefined
: `IndexNow submission encountered errors (HTTP ${lastStatus ?? "unknown"})`
};
}
/**
* Convenience helper to ping IndexNow when a new profile is created.
*/
export async function notifyProfileCreated(
handle: string,
options?: IndexNowOptions
): Promise<IndexNowResult> {
const host = options?.host || DEFAULT_INDEXNOW_HOST;
return submitToIndexNow(`https://${host}/${handle}`, options);
}
/**
* Convenience helper to ping IndexNow when a new repository is created.
*/
export async function notifyRepoCreated(
handle: string,
repoSlug: string,
options?: IndexNowOptions
): Promise<IndexNowResult> {
const host = options?.host || DEFAULT_INDEXNOW_HOST;
return submitToIndexNow(`https://${host}/${handle}/${repoSlug}`, options);
}