SEO chat gippity style
This commit is contained in:
+18
-8
@@ -1,6 +1,7 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>__SEO_TITLE__</title>
|
||||
<link rel="preconnect" href="https://challenges.cloudflare.com" />
|
||||
<script
|
||||
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
|
||||
@@ -13,16 +14,23 @@
|
||||
<meta name="theme-color" content="#e82517" />
|
||||
<meta
|
||||
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:url" content="__PUBLIC_ORIGIN__/" />
|
||||
<meta property="og:title" content="Toothless' TSS Bot" />
|
||||
<meta property="og:url" content="__SEO_CANONICAL__" />
|
||||
<meta property="og:title" content="__SEO_TITLE__" />
|
||||
<meta
|
||||
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: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:logo" content="__PUBLIC_ORIGIN__/embed-icon.svg" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="Toothless' TSS Bot" />
|
||||
<meta name="twitter:title" content="__SEO_TITLE__" />
|
||||
<meta
|
||||
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:alt" content="Toothless' TSS Bot share card" />
|
||||
<link rel="icon" type="image/svg+xml" 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>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
+164
-17
@@ -356,6 +356,163 @@ function routeLabel(route) {
|
||||
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) {
|
||||
const teamNames = teams.map(bestTeamName).filter(Boolean).slice(0, 12)
|
||||
|
||||
@@ -748,23 +905,13 @@ function AppContent() {
|
||||
}, [route.page])
|
||||
|
||||
useEffect(() => {
|
||||
const title =
|
||||
route.page === 'team' && route.teamName
|
||||
? `${route.teamName} | Toothless' TSS Bot`
|
||||
: route.page === 'teams'
|
||||
? "Team leaderboard | Toothless' TSS Bot"
|
||||
: route.page === 'battle-logs'
|
||||
? "Battle Logs | Toothless' TSS Bot"
|
||||
: 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])
|
||||
applySeo(
|
||||
route,
|
||||
route.page === 'team' && profile.detail.status === 'ready'
|
||||
? profile.detail.data
|
||||
: null,
|
||||
)
|
||||
}, [profile.detail.data, profile.detail.status, route])
|
||||
|
||||
useEffect(() => {
|
||||
if (!analyticsPreferences.analytics) return
|
||||
|
||||
+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