SEO + AEO
This commit is contained in:
223
scripts/ping-indexnow.ts
Normal file
223
scripts/ping-indexnow.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
/**
|
||||
* Manual IndexNow ping script for PassMaster
|
||||
* Usage: npx tsx scripts/ping-indexnow.ts [urls...]
|
||||
* Example: npx tsx scripts/ping-indexnow.ts https://passmaster.app/offline https://passmaster.app/client-side
|
||||
*/
|
||||
|
||||
import { config } from 'dotenv'
|
||||
import { writeFileSync, existsSync, mkdirSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
|
||||
// Load environment variables
|
||||
config({ path: '.env.local' })
|
||||
config({ path: '.env' })
|
||||
|
||||
interface IndexNowRequest {
|
||||
host: string
|
||||
key: string
|
||||
keyLocation: string
|
||||
urlList: string[]
|
||||
}
|
||||
|
||||
class IndexNowPinger {
|
||||
private readonly key: string
|
||||
private readonly host: string
|
||||
private readonly enabled: boolean
|
||||
|
||||
constructor() {
|
||||
this.key = process.env.INDEXNOW_KEY || ''
|
||||
this.host = process.env.SITE_HOST || 'passmaster.app'
|
||||
this.enabled = Boolean(this.key)
|
||||
|
||||
if (!this.enabled) {
|
||||
throw new Error('INDEXNOW_KEY is required in environment variables')
|
||||
}
|
||||
|
||||
this.ensureKeyFile()
|
||||
}
|
||||
|
||||
private ensureKeyFile() {
|
||||
try {
|
||||
const publicDir = join(process.cwd(), 'public')
|
||||
if (!existsSync(publicDir)) {
|
||||
mkdirSync(publicDir, { recursive: true })
|
||||
}
|
||||
|
||||
const keyFilePath = join(publicDir, `${this.key}.txt`)
|
||||
if (!existsSync(keyFilePath)) {
|
||||
writeFileSync(keyFilePath, this.key)
|
||||
console.log(`✅ Created key file: public/${this.key}.txt`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to create key file:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private validateUrls(urls: string[]): string[] {
|
||||
return urls.filter(url => {
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
if (parsed.hostname !== this.host) {
|
||||
console.warn(`⚠️ Skipping URL with wrong hostname: ${url} (expected: ${this.host})`)
|
||||
return false
|
||||
}
|
||||
if (parsed.protocol !== 'https:') {
|
||||
console.warn(`⚠️ Skipping non-HTTPS URL: ${url}`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
console.warn(`⚠️ Skipping invalid URL: ${url}`)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async ping(urls: string[]): Promise<boolean> {
|
||||
const validUrls = this.validateUrls(urls)
|
||||
|
||||
if (validUrls.length === 0) {
|
||||
console.error('❌ No valid URLs to ping')
|
||||
return false
|
||||
}
|
||||
|
||||
if (validUrls.length > 10000) {
|
||||
console.warn(`⚠️ Too many URLs (${validUrls.length}), limiting to 10,000`)
|
||||
validUrls.splice(10000)
|
||||
}
|
||||
|
||||
const payload: IndexNowRequest = {
|
||||
host: this.host,
|
||||
key: this.key,
|
||||
keyLocation: `https://${this.host}/${this.key}.txt`,
|
||||
urlList: validUrls
|
||||
}
|
||||
|
||||
console.log(`🚀 Pinging IndexNow with ${validUrls.length} URLs...`)
|
||||
console.log(' URLs:', validUrls.join(', '))
|
||||
|
||||
try {
|
||||
const response = await fetch('https://api.indexnow.org/indexnow', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'PassMaster IndexNow Manual Client'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
|
||||
console.log(`📡 Response status: ${response.status} ${response.statusText}`)
|
||||
|
||||
if (response.ok || response.status === 202) {
|
||||
console.log('✅ IndexNow ping successful!')
|
||||
return true
|
||||
} else {
|
||||
const errorText = await response.text().catch(() => 'Unknown error')
|
||||
console.error('❌ IndexNow ping failed:', errorText)
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Network error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return {
|
||||
enabled: this.enabled,
|
||||
key: this.key ? `${this.key.substring(0, 8)}...` : 'Not set',
|
||||
host: this.host,
|
||||
keyFileUrl: `https://${this.host}/${this.key}.txt`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CLI Interface
|
||||
async function main() {
|
||||
console.log('🔧 PassMaster IndexNow Manual Ping Tool\n')
|
||||
|
||||
try {
|
||||
const pinger = new IndexNowPinger()
|
||||
const status = pinger.getStatus()
|
||||
|
||||
console.log('📊 Configuration:')
|
||||
console.log(` Host: ${status.host}`)
|
||||
console.log(` Key: ${status.key}`)
|
||||
console.log(` Key File: ${status.keyFileUrl}`)
|
||||
console.log(` Enabled: ${status.enabled}\n`)
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
|
||||
if (args.length === 0) {
|
||||
console.log('ℹ️ Usage: npx tsx scripts/ping-indexnow.ts [urls...]')
|
||||
console.log(' Example: npx tsx scripts/ping-indexnow.ts https://passmaster.app/offline')
|
||||
console.log('\n🎯 Common URLs to ping:')
|
||||
console.log(' • https://passmaster.app/')
|
||||
console.log(' • https://passmaster.app/offline')
|
||||
console.log(' • https://passmaster.app/client-side')
|
||||
console.log(' • https://passmaster.app/exclude-similar')
|
||||
console.log(' • https://passmaster.app/privacy')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Special commands
|
||||
if (args[0] === '--all') {
|
||||
const allUrls = [
|
||||
`https://${status.host}/`,
|
||||
`https://${status.host}/offline`,
|
||||
`https://${status.host}/client-side`,
|
||||
`https://${status.host}/exclude-similar`,
|
||||
`https://${status.host}/privacy`
|
||||
]
|
||||
|
||||
console.log('🎯 Pinging all main pages...')
|
||||
const success = await pinger.ping(allUrls)
|
||||
process.exit(success ? 0 : 1)
|
||||
}
|
||||
|
||||
if (args[0] === '--test') {
|
||||
console.log('🧪 Testing with homepage only...')
|
||||
const testUrl = `https://${status.host}/`
|
||||
const success = await pinger.ping([testUrl])
|
||||
process.exit(success ? 0 : 1)
|
||||
}
|
||||
|
||||
if (args[0] === '--help' || args[0] === '-h') {
|
||||
console.log('📖 IndexNow Ping Commands:')
|
||||
console.log(' --all Ping all main pages')
|
||||
console.log(' --test Test with homepage only')
|
||||
console.log(' --status Show current configuration')
|
||||
console.log(' [urls...] Ping specific URLs')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (args[0] === '--status') {
|
||||
console.log('✅ Configuration looks good!')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Ping provided URLs
|
||||
const success = await pinger.ping(args)
|
||||
process.exit(success ? 0 : 1)
|
||||
|
||||
} catch (error) {
|
||||
console.error('💥 Fatal error:', error instanceof Error ? error.message : error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle SIGINT gracefully
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n👋 Interrupted by user')
|
||||
process.exit(130)
|
||||
})
|
||||
|
||||
// Run main function
|
||||
if (require.main === module) {
|
||||
main()
|
||||
}
|
||||
|
||||
export { IndexNowPinger }
|
||||
Reference in New Issue
Block a user