SEO chat gippity style
This commit is contained in:
+18
-8
@@ -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
@@ -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
@@ -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, '&')
|
||||||
|
.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) {
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user