SEO chat gippity style
This commit is contained in:
+185
-1
@@ -1899,8 +1899,138 @@ function pagePublicOrigin(req) {
|
||||
return `${String(proto).split(',')[0].trim()}://${String(host).split(',')[0].trim()}`.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function decodeRouteSegment(value) {
|
||||
try {
|
||||
return decodeURIComponent(value || '').replace(/\+/g, ' ').trim()
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function routeSeo(pathname) {
|
||||
const cleanPath = pathname.replace(/\/+$/, '') || '/'
|
||||
|
||||
if (cleanPath.startsWith('/teams/')) {
|
||||
const teamName = decodeRouteSegment(cleanPath.slice('/teams/'.length))
|
||||
if (teamName) {
|
||||
return {
|
||||
title: `${teamName} TSS Team Profile | Toothless' TSS Bot`,
|
||||
description: `${teamName} TSS team profile with roster details, battle history, and recent War Thunder squadron activity.`,
|
||||
robots: 'index, follow',
|
||||
path: `/teams/${encodeURIComponent(teamName)}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const byPath = {
|
||||
'/': {
|
||||
title: "Toothless' TSS Bot | Live TSS Leaderboards and Battle Logs",
|
||||
description: 'Live War Thunder TSS team leaderboards, battle logs, team profiles, uptime, and privacy-safe viewer analytics from Toothless TSS Bot.',
|
||||
robots: 'index, follow',
|
||||
path: '/',
|
||||
},
|
||||
'/teams': {
|
||||
title: "TSS Team Leaderboard | Toothless' TSS Bot",
|
||||
description: 'Browse the live TSS team leaderboard, compare War Thunder squadron rankings, points, players, and recent team activity.',
|
||||
robots: 'index, follow',
|
||||
path: '/teams',
|
||||
},
|
||||
'/battle-logs': {
|
||||
title: "TSS Battle Logs | Toothless' TSS Bot",
|
||||
description: 'Read recent TSS battle logs with team names, map history, player counts, battle times, and War Thunder match context.',
|
||||
robots: 'index, follow',
|
||||
path: '/battle-logs',
|
||||
},
|
||||
'/live': {
|
||||
title: "TSS Battle Logs | Toothless' TSS Bot",
|
||||
description: 'Read recent TSS battle logs with team names, map history, player counts, battle times, and War Thunder match context.',
|
||||
robots: 'index, follow',
|
||||
path: '/battle-logs',
|
||||
},
|
||||
'/uptime': {
|
||||
title: "TSS Bot Uptime Status | Toothless' TSS Bot",
|
||||
description: 'Check Toothless TSS Bot uptime, API health, TSS data proxy status, and recent availability history.',
|
||||
robots: 'index, follow',
|
||||
path: '/uptime',
|
||||
},
|
||||
'/viewers': {
|
||||
title: "Viewer Analytics | Toothless' TSS Bot",
|
||||
description: 'Opt-in viewer analytics for Toothless TSS Bot, including active pages, broad device signals, and privacy-safe activity trends.',
|
||||
robots: 'noindex, follow',
|
||||
path: '/viewers',
|
||||
},
|
||||
'/privacy': {
|
||||
title: "Privacy Notice | Toothless' TSS Bot",
|
||||
description: 'How Toothless TSS Bot handles cookies, opt-in analytics, viewer data, retention, deletion, and privacy rights.',
|
||||
robots: 'index, follow',
|
||||
path: '/privacy',
|
||||
},
|
||||
}
|
||||
|
||||
return byPath[cleanPath] || byPath['/']
|
||||
}
|
||||
|
||||
function routeStructuredData(origin, seo, canonicalUrl) {
|
||||
return JSON.stringify([
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
name: "Toothless' TSS Bot",
|
||||
url: `${origin}/`,
|
||||
description: 'Live War Thunder TSS leaderboards, battle logs, and team profiles.',
|
||||
potentialAction: {
|
||||
'@type': 'SearchAction',
|
||||
target: `${origin}/teams/{search_term_string}`,
|
||||
'query-input': 'required name=search_term_string',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
name: seo.title,
|
||||
url: canonicalUrl,
|
||||
description: seo.description,
|
||||
isPartOf: {
|
||||
'@type': 'WebSite',
|
||||
name: "Toothless' TSS Bot",
|
||||
url: `${origin}/`,
|
||||
},
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
function htmlWithSeo(req, data) {
|
||||
const origin = pagePublicOrigin(req)
|
||||
let pathname = '/'
|
||||
try {
|
||||
pathname = new URL(req.url, origin).pathname
|
||||
} catch {
|
||||
pathname = '/'
|
||||
}
|
||||
|
||||
const seo = routeSeo(pathname)
|
||||
const canonicalUrl = `${origin}${seo.path}`
|
||||
|
||||
return data.toString('utf8')
|
||||
.replaceAll('__PUBLIC_ORIGIN__', origin)
|
||||
.replaceAll('__SEO_TITLE__', escapeHtml(seo.title))
|
||||
.replaceAll('__SEO_DESCRIPTION__', escapeHtml(seo.description))
|
||||
.replaceAll('__SEO_ROBOTS__', escapeHtml(seo.robots))
|
||||
.replaceAll('__SEO_CANONICAL__', escapeHtml(canonicalUrl))
|
||||
.replaceAll('__SEO_JSON_LD__', routeStructuredData(origin, seo, canonicalUrl).replace(/</g, '\\u003c'))
|
||||
}
|
||||
|
||||
function sendHtml(req, res, data, status = 200) {
|
||||
const html = data.toString('utf8').replaceAll('__PUBLIC_ORIGIN__', pagePublicOrigin(req))
|
||||
const html = htmlWithSeo(req, data)
|
||||
send(res, status, html, {
|
||||
...securityHeaders(req, { html: true }),
|
||||
'content-type': mimeTypes['.html'],
|
||||
@@ -1938,6 +2068,50 @@ function sendComingSoonPage(req, res) {
|
||||
})
|
||||
}
|
||||
|
||||
function sendRobotsTxt(req, res) {
|
||||
const origin = pagePublicOrigin(req)
|
||||
const body = [
|
||||
'User-agent: *',
|
||||
'Allow: /',
|
||||
'Disallow: /api/',
|
||||
'Disallow: /viewers',
|
||||
`Sitemap: ${origin}/sitemap.xml`,
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
send(res, 200, body, {
|
||||
'content-type': 'text/plain; charset=utf-8',
|
||||
'cache-control': 'public, max-age=3600',
|
||||
})
|
||||
}
|
||||
|
||||
function sendSitemapXml(req, res) {
|
||||
const origin = pagePublicOrigin(req)
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
const urls = [
|
||||
{ path: '/', priority: '1.0', changefreq: 'hourly' },
|
||||
{ path: '/teams', priority: '0.9', changefreq: 'hourly' },
|
||||
{ path: '/battle-logs', priority: '0.9', changefreq: 'hourly' },
|
||||
{ path: '/uptime', priority: '0.5', changefreq: 'daily' },
|
||||
{ path: '/privacy', priority: '0.3', changefreq: 'monthly' },
|
||||
]
|
||||
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${urls.map((url) => ` <url>
|
||||
<loc>${escapeHtml(`${origin}${url.path}`)}</loc>
|
||||
<lastmod>${today}</lastmod>
|
||||
<changefreq>${url.changefreq}</changefreq>
|
||||
<priority>${url.priority}</priority>
|
||||
</url>`).join('\n')}
|
||||
</urlset>
|
||||
`
|
||||
|
||||
send(res, 200, body, {
|
||||
'content-type': 'application/xml; charset=utf-8',
|
||||
'cache-control': 'public, max-age=3600',
|
||||
})
|
||||
}
|
||||
|
||||
function serveStatic(req, res) {
|
||||
let requestPath = '/'
|
||||
try {
|
||||
@@ -1985,6 +2159,16 @@ function serveStatic(req, res) {
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.method === 'GET' && req.url === '/robots.txt') {
|
||||
sendRobotsTxt(req, res)
|
||||
return
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && req.url === '/sitemap.xml') {
|
||||
sendSitemapXml(req, res)
|
||||
return
|
||||
}
|
||||
|
||||
if (req.url === '/health') {
|
||||
sendJson(res, 200, { ok: true })
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user