Skip to content
Open
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
36 changes: 36 additions & 0 deletions apps/landing/src/__tests__/indexnow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { existsSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { INDEXNOW_KEY, INDEXNOW_KEY_PATH } from '@/lib/indexnow'
import { GET } from '../app/5cb30cbda540958e8d033652400e59e3.txt/route'

// IndexNow verification hinges on one brittle coupling: the App Router
// directory that serves the key file is NAMED after the key, so a change to
// the constant without the matching directory rename (or vice versa) leaves
// the site serving one key while claiming another — and every submission an
// engine receives fails ownership. These pin the two to each other, plus the
// response shape indexnow.org requires (exact body, text/plain).
describe('IndexNow key file served by the landing app', () => {
it('key constant is 32 lowercase hexadecimal characters as IndexNow requires', () => {
expect(INDEXNOW_KEY).toMatch(/^[0-9a-f]{32}$/)
})

it('key path constant is the key served as a root-level .txt file', () => {
expect(INDEXNOW_KEY_PATH).toBe(`/${INDEXNOW_KEY}.txt`)
})

it('route directory on disk is named exactly after the key constant', () => {
const appDir = fileURLToPath(new URL('../app/', import.meta.url))
expect(existsSync(`${appDir}${INDEXNOW_KEY}.txt/route.ts`)).toBe(true)
})

it('GET responds with the bare key as plain text and nothing else', async () => {
const response = GET()

expect(response.status).toBe(200)
expect(response.headers.get('content-type')).toBe(
'text/plain; charset=utf-8',
)
await expect(response.text()).resolves.toBe(INDEXNOW_KEY)
})
})
15 changes: 15 additions & 0 deletions apps/landing/src/app/5cb30cbda540958e8d033652400e59e3.txt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { INDEXNOW_KEY } from '@/lib/indexnow'

// The IndexNow ownership key file. The directory name above is the key and
// must match INDEXNOW_KEY exactly — src/__tests__/indexnow.test.ts pins it.
// Static: the body is a constant, so there is nothing to compute per request.
export const dynamic = 'force-static'

export function GET() {
return new Response(INDEXNOW_KEY, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'public, max-age=86400',
},
})
}
17 changes: 17 additions & 0 deletions apps/landing/src/lib/indexnow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// IndexNow (https://www.indexnow.org/documentation) lets a site push URL
// changes to participating engines (Bing, Yandex, Seznam, Naver) instead of
// waiting to be recrawled. Ownership is proved by hosting a key file: a plain
// text file at `https://<host>/<key>.txt` whose body is EXACTLY the key, served
// as `text/plain`. An engine fetches it when a submission arrives; a missing,
// redirected, or differently-bodied file makes every submission fail.
//
// This module is the single source of truth for that key. The route directory
// `src/app/<key>.txt/` must stay byte-identical to INDEXNOW_KEY — a rename in
// one place and not the other silently breaks verification, so
// `src/__tests__/indexnow.test.ts` pins the two together.
//
// Submissions are NOT made from this app. Serving the key is the prerequisite;
// pushing URLs is an operational step run separately, after this deploys.
export const INDEXNOW_KEY = '5cb30cbda540958e8d033652400e59e3'

export const INDEXNOW_KEY_PATH = `/${INDEXNOW_KEY}.txt`
Loading