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
|
||||
|
||||
Reference in New Issue
Block a user