SEO chat gippity style

This commit is contained in:
Heidi
2026-05-30 01:00:25 +01:00
parent 1cd52d528f
commit b3eb9c2e19
3 changed files with 367 additions and 26 deletions
+18 -8
View File
@@ -1,6 +1,7 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<title>__SEO_TITLE__</title>
<link rel="preconnect" href="https://challenges.cloudflare.com" /> <link rel="preconnect" href="https://challenges.cloudflare.com" />
<script <script
src="https://challenges.cloudflare.com/turnstile/v0/api.js" src="https://challenges.cloudflare.com/turnstile/v0/api.js"
@@ -13,16 +14,23 @@
<meta name="theme-color" content="#e82517" /> <meta name="theme-color" content="#e82517" />
<meta <meta
name="description" name="description"
content="Live TSS team leaderboards, battle logs, team profiles, uptime, and consented viewer analytics for Toothless' TSS Bot." content="__SEO_DESCRIPTION__"
/> />
<meta name="robots" content="index, follow" /> <meta
name="keywords"
content="TSS Bot, Toothless TSS Bot, War Thunder TSS, TSS leaderboard, TSS battle logs, TSS teams"
/>
<meta name="author" content="Toothless' TSS Bot" />
<meta name="robots" content="__SEO_ROBOTS__" />
<link rel="canonical" href="__SEO_CANONICAL__" />
<meta property="og:site_name" content="Toothless' TSS Bot" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:url" content="__PUBLIC_ORIGIN__/" /> <meta property="og:url" content="__SEO_CANONICAL__" />
<meta property="og:title" content="Toothless' TSS Bot" /> <meta property="og:title" content="__SEO_TITLE__" />
<meta <meta
property="og:description" property="og:description"
content="Live TSS team leaderboards, battle logs, team profiles, uptime, and consented viewer analytics." content="__SEO_DESCRIPTION__"
/> />
<meta property="og:image" content="__PUBLIC_ORIGIN__/embed.svg" /> <meta property="og:image" content="__PUBLIC_ORIGIN__/embed.svg" />
<meta property="og:image:secure_url" content="__PUBLIC_ORIGIN__/embed.svg" /> <meta property="og:image:secure_url" content="__PUBLIC_ORIGIN__/embed.svg" />
@@ -32,16 +40,18 @@
<meta property="og:image:alt" content="Toothless' TSS Bot share card" /> <meta property="og:image:alt" content="Toothless' TSS Bot share card" />
<meta property="og:logo" content="__PUBLIC_ORIGIN__/embed-icon.svg" /> <meta property="og:logo" content="__PUBLIC_ORIGIN__/embed-icon.svg" />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Toothless' TSS Bot" /> <meta name="twitter:title" content="__SEO_TITLE__" />
<meta <meta
name="twitter:description" name="twitter:description"
content="Live TSS team leaderboards, battle logs, team profiles, uptime, and consented viewer analytics." content="__SEO_DESCRIPTION__"
/> />
<meta name="twitter:image" content="__PUBLIC_ORIGIN__/embed.svg" /> <meta name="twitter:image" content="__PUBLIC_ORIGIN__/embed.svg" />
<meta name="twitter:image:alt" content="Toothless' TSS Bot share card" /> <meta name="twitter:image:alt" content="Toothless' TSS Bot share card" />
<link rel="icon" type="image/svg+xml" href="/embed-icon.svg" /> <link rel="icon" type="image/svg+xml" href="/embed-icon.svg" />
<link rel="apple-touch-icon" href="/embed-icon.svg" /> <link rel="apple-touch-icon" href="/embed-icon.svg" />
<title>Toothless' TSS Bot</title> <script type="application/ld+json" id="site-structured-data">
__SEO_JSON_LD__
</script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+164 -17
View File
@@ -356,6 +356,163 @@ function routeLabel(route) {
return 'Home' return 'Home'
} }
function currentPublicOrigin() {
return window.location.origin.replace(/\/$/, '')
}
function canonicalPathForRoute(route) {
if (route.page === 'team' && route.teamName) return teamPath(route.teamName)
if (route.page === 'teams') return '/teams'
if (route.page === 'battle-logs') return '/battle-logs'
if (route.page === 'uptime') return '/uptime'
if (route.page === 'viewers') return '/viewers'
if (route.page === 'privacy') return '/privacy'
return '/'
}
function seoForRoute(route, profileDetail = null) {
const teamName =
route.page === 'team'
? canonicalTeamName(profileDetail, route.teamName).trim() || route.teamName
: ''
const teamSummary = profileDetail?.team_summary || profileDetail?.squadron_summary || null
const teamPoints = Number(teamSummary?.points?.total_points || teamSummary?.total_points || 0)
const teamBattles = Number(teamSummary?.total_battles || 0)
if (route.page === 'team' && teamName) {
const stats = [
teamPoints ? `${formatNumber(teamPoints)} points` : '',
teamBattles ? `${formatNumber(teamBattles)} battles` : '',
].filter(Boolean)
return {
title: `${teamName} TSS Team Profile | Toothless' TSS Bot`,
description: stats.length
? `${teamName} TSS team profile with ${stats.join(', ')}, roster details, battle history, and recent War Thunder squadron activity.`
: `${teamName} TSS team profile with roster details, battle history, and recent War Thunder squadron activity.`,
robots: 'index, follow',
path: canonicalPathForRoute({ ...route, teamName }),
}
}
const byPage = {
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',
},
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 byPage[route.page] || {
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: '/',
}
}
function upsertMeta(selector, createAttributes, valueAttribute, value) {
let element = document.head.querySelector(selector)
if (!element) {
element = document.createElement('meta')
Object.entries(createAttributes).forEach(([key, attributeValue]) => {
element.setAttribute(key, attributeValue)
})
document.head.appendChild(element)
}
element.setAttribute(valueAttribute, value)
}
function upsertLink(rel, href) {
let element = document.head.querySelector(`link[rel="${rel}"]`)
if (!element) {
element = document.createElement('link')
element.setAttribute('rel', rel)
document.head.appendChild(element)
}
element.setAttribute('href', href)
}
function structuredDataForSeo(seo, canonicalUrl) {
const base = currentPublicOrigin()
const items = [
{
'@context': 'https://schema.org',
'@type': 'WebSite',
name: "Toothless' TSS Bot",
url: `${base}/`,
description: "Live War Thunder TSS leaderboards, battle logs, and team profiles.",
potentialAction: {
'@type': 'SearchAction',
target: `${base}/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: `${base}/`,
},
},
]
return JSON.stringify(items)
}
function applySeo(route, profileDetail = null) {
const seo = seoForRoute(route, profileDetail)
const canonicalUrl = `${currentPublicOrigin()}${seo.path}`
document.title = seo.title
upsertMeta('meta[name="description"]', { name: 'description' }, 'content', seo.description)
upsertMeta('meta[name="robots"]', { name: 'robots' }, 'content', seo.robots)
upsertMeta('meta[property="og:title"]', { property: 'og:title' }, 'content', seo.title)
upsertMeta('meta[property="og:description"]', { property: 'og:description' }, 'content', seo.description)
upsertMeta('meta[property="og:url"]', { property: 'og:url' }, 'content', canonicalUrl)
upsertMeta('meta[name="twitter:title"]', { name: 'twitter:title' }, 'content', seo.title)
upsertMeta('meta[name="twitter:description"]', { name: 'twitter:description' }, 'content', seo.description)
upsertLink('canonical', canonicalUrl)
let structuredData = document.getElementById('site-structured-data')
if (!structuredData) {
structuredData = document.createElement('script')
structuredData.id = 'site-structured-data'
structuredData.type = 'application/ld+json'
document.head.appendChild(structuredData)
}
structuredData.textContent = structuredDataForSeo(seo, canonicalUrl)
}
async function fetchRecentTssGames(teams, signal) { async function fetchRecentTssGames(teams, signal) {
const teamNames = teams.map(bestTeamName).filter(Boolean).slice(0, 12) const teamNames = teams.map(bestTeamName).filter(Boolean).slice(0, 12)
@@ -748,23 +905,13 @@ function AppContent() {
}, [route.page]) }, [route.page])
useEffect(() => { useEffect(() => {
const title = applySeo(
route.page === 'team' && route.teamName route,
? `${route.teamName} | Toothless' TSS Bot` route.page === 'team' && profile.detail.status === 'ready'
: route.page === 'teams' ? profile.detail.data
? "Team leaderboard | Toothless' TSS Bot" : null,
: route.page === 'battle-logs' )
? "Battle Logs | Toothless' TSS Bot" }, [profile.detail.data, profile.detail.status, route])
: route.page === 'uptime'
? "Uptime | Toothless' TSS Bot"
: route.page === 'viewers'
? "Viewers | Toothless' TSS Bot"
: route.page === 'privacy'
? "Privacy notice | Toothless' TSS Bot"
: "Toothless' TSS Bot"
document.title = title
}, [route.page, route.teamName])
useEffect(() => { useEffect(() => {
if (!analyticsPreferences.analytics) return if (!analyticsPreferences.analytics) return
+185 -1
View File
@@ -1899,8 +1899,138 @@ function pagePublicOrigin(req) {
return `${String(proto).split(',')[0].trim()}://${String(host).split(',')[0].trim()}`.replace(/\/$/, '') return `${String(proto).split(',')[0].trim()}://${String(host).split(',')[0].trim()}`.replace(/\/$/, '')
} }
function escapeHtml(value) {
return String(value || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
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) { 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, { send(res, status, html, {
...securityHeaders(req, { html: true }), ...securityHeaders(req, { html: true }),
'content-type': mimeTypes['.html'], '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) { function serveStatic(req, res) {
let requestPath = '/' let requestPath = '/'
try { try {
@@ -1985,6 +2159,16 @@ function serveStatic(req, res) {
} }
const server = http.createServer((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') { if (req.url === '/health') {
sendJson(res, 200, { ok: true }) sendJson(res, 200, { ok: true })
return return