Articles

cf-indexnow: Submit URLs to search engines from ColdFusion

IndexNow for ColdFusion: Submit new, changed and deleted URLs to Bing, Yandex, Seznam, Naver and Yep the moment your content changes.
August 19, 2026

IndexNow is an open protocol introduced by Microsoft Bing and Yandex in 2021. It lets a website notify search engines the moment a URL is added, changed, or removed instead of waiting for the next crawl. You POST a list of URLs along with a key that proves you control the host, and the submission is automatically shared with every participating engine: Bing, Yandex, Seznam, Naver, and Yep. (Google doesn't participate, but Bing results also feed DuckDuckGo, Yahoo, and others, so one ping still covers a lot of ground.)

I've published cf-indexnow, a CFC that implements the protocol for Adobe ColdFusion 2016+ and Lucee 5+. It's MIT licensed and I'm using it in production. IndexNow.cfc is the only required file:

indexNow = new IndexNow( host="www.example.com", key="yourindexnowkey" );
result = indexNow.submitUrl( "https://www.example.com/new-article" );
if ( !result.success ) {
	writeLog( file="indexnow", text=result.message );
}

Network problems and API rejections never throw. Every submission returns a struct with success, the HTTP statusCode, a plain-English message, how many URLs were submitted, which input URLs were skipped, and the raw per-batch responses. Invalid constructor arguments do throw (with typed exceptions like IndexNow.InvalidKey), so configuration mistakes surface during development rather than in a production log.

submitUrls() accepts an array and automatically splits anything over 10,000 URLs into multiple POSTs, which is the protocol's per-request limit. submitSitemap() fetches a sitemap.xml, recurses into sitemap index files, and submits every URL it finds. In both cases, URLs that don't belong to the configured host are filtered out and reported in the result instead of being submitted, because a single foreign URL will get an entire batch rejected with a 422.

The key file

Ownership is proven with a plain text file at https://yourhost/{key}.txt containing nothing but the key. The component handles the lifecycle: generateKey() mints a spec-compliant key, writeKeyFile() writes the file to your webroot, and verifyKeyFile() fetches it over HTTP and confirms the content matches before you submit anything.

You don't need to store the key anywhere. I derive it from the hostname so every site gets a stable key with zero configuration:

indexNowKey = "indexnow-" & lcase( hash( cgi.server_name ) );

My scheduled task calls verifyKeyFile() first and regenerates the file if it's missing or wrong. It costs one HTTP request.

Only submit what changed

The spec asks you not to resubmit unchanged URLs, so you need to track state somewhere. I added a nullable IndexNowDate column to the content table. Inserts and edits clear the column; a scheduled task submits whatever is pending and stamps the date on success:

qry = queryExecute( "SELECT ID, 'https://#websiteHost#' + Permalink AS Permalink
	FROM Posts WHERE IndexNowDate IS NULL", {}, {} );

batchResult = indexNow.submitUrls( urls=valueArray( qry, "Permalink" ) );

if ( batchResult.success ) {
	queryExecute( "UPDATE Posts SET IndexNowDate = SYSDATETIME() WHERE ID IN (:ids)",
		{ "ids": { "value": valueList( qry.ID ), "list": true, "cfsqltype": "cf_sql_integer" } }, {} );
}

Two behaviors worth knowing. Deleted pages should be resubmitted, not skipped: the spiders re-fetch the URL, see the 404 or 410, and drop it from the index. And when a URL gets a 301/302 redirect, submit the old URL too so the engines learn about the move.

A ColdFusion 2016 workaround

An IndexNow key is allowed to be all digits, and that's how I learned something I hadn't run into before. On ColdFusion 2016, serializeJSON() converts a numeric-looking string into a JSON number even when it's wrapped in toString() or javacast("string", ...). A key of "12345678" goes over the wire as "key":12345678 and the API rejects the submission with a 403. CF2016 is the only platform that does this. ColdFusion 2018+, Lucee, and BoxLang all keep the string quoted. (I verified this on ACF 2016.0.17 by asserting against the raw request body; if you round-trip through deserializeJSON() to test it, the coercion is invisible.)

The fix is Nathan Mische's JSONUtil, which serializes values by their actual underlying Java type. Rather than making it a hard dependency, it's a constructor option:

indexNow = new IndexNow( host="www.example.com", key="12345678", useJSONUtil=true );

The default uses native serializeJSON(), which behaves correctly on every current engine. I enable useJSONUtil anyway. It's a little slower, but JSONUtil is stricter, which I like: deserializeJSON( JSONvar=body, strictMapping=true ) throws when the JSON contains duplicate keys (BoxLang behaves the same way, while Adobe ColdFusion silently accepts them), and it avoids another long-standing Adobe frustration when debugging or transforming data: every ACF version re-orders keys alphabetically when deserializing, while Lucee and BoxLang honor the original key order.

Testing

The repo includes a framework-free test harness: 38 tests that run against a local mock endpoint, so the suite never contacts the real API. It's verified green on Adobe ColdFusion 2016 and Lucee 5.4. There's also a demo page that exercises the whole flow against the mock, plus a localhost-only smoke test page for a one-time check against the live endpoint with a real key.

Grab it at github.com/JamoCA/cf-indexnow. If you hit an engine quirk I missed, open an issue and let me know.