add umami and seo tags

This commit is contained in:
2026-03-01 02:22:39 +01:00
parent e915ba4919
commit a82eca4ef9
13 changed files with 300 additions and 2 deletions

View File

@@ -42,5 +42,39 @@ services:
retries: 3
start_period: 10s
umami-db:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: ${UMAMI_DB_PASSWORD}
volumes:
- umamidata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U umami -d umami"]
interval: 10s
timeout: 5s
retries: 5
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
restart: unless-stopped
depends_on:
umami-db:
condition: service_healthy
ports:
- "127.0.0.1:3001:3000" # Proxy via nginx at e.g. analytics.vash.software
environment:
DATABASE_URL: postgresql://umami:${UMAMI_DB_PASSWORD}@umami-db:5432/umami
APP_SECRET: ${UMAMI_APP_SECRET}
healthcheck:
test: ["CMD-SHELL", "wget -q -O /dev/null http://localhost:3000/api/heartbeat || exit 1"]
interval: 15s
timeout: 5s
retries: 5
start_period: 30s
volumes:
pgdata:
umamidata:

View File

@@ -99,5 +99,9 @@
<span class="text-base">⚙</span>
Settings
</a>
<div class="mt-2 flex gap-3 px-3 pb-1">
<a href="/terms" class="text-xs text-text-secondary/50 hover:text-text-secondary transition-colors">Terms</a>
<a href="/privacy" class="text-xs text-text-secondary/50 hover:text-text-secondary transition-colors">Privacy</a>
</div>
</div>
</aside>

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import { page } from '$app/state';
let { data } = $props();
@@ -87,6 +88,18 @@
}
</script>
<svelte:head>
<title>Vash Esports — osu! Tournament Platform</title>
<meta name="description" content="Run osu! tournaments with automated lobbies, live scoring, mappools, and team management. Built for serious competition." />
<meta property="og:title" content="Vash Esports — osu! Tournament Platform" />
<meta property="og:description" content="Run osu! tournaments with automated lobbies, live scoring, mappools, and team management." />
<meta property="og:type" content="website" />
<meta property="og:url" content={page.url.href} />
<meta property="og:image" content="/og-image.png" />
<meta property="og:site_name" content="Vash Esports" />
<meta name="twitter:card" content="summary_large_image" />
</svelte:head>
<!-- ── LANDING PAGE (not logged in) ── -->
{#if !data.user}
<div class="flex min-h-[70vh] flex-col items-center justify-center text-center">

View File

@@ -33,6 +33,15 @@
};
</script>
<svelte:head>
<title>Leaderboard — Vash Esports</title>
<meta name="description" content="Top players and teams on the Vash Esports osu! tournament platform." />
<meta property="og:title" content="Leaderboard — Vash Esports" />
<meta property="og:description" content="Top players and teams on the Vash Esports osu! tournament platform." />
<meta property="og:site_name" content="Vash Esports" />
<meta name="twitter:card" content="summary" />
</svelte:head>
<div class="mx-auto max-w-4xl">
<div>
<h1 class="text-2xl font-700 tracking-tight">Leaderboard</h1>

View File

@@ -5,6 +5,15 @@
let creating = $state(false);
</script>
<svelte:head>
<title>Mappools — Vash Esports</title>
<meta name="description" content="Browse and manage osu! tournament mappools on Vash Esports." />
<meta property="og:title" content="Mappools — Vash Esports" />
<meta property="og:description" content="Browse and manage osu! tournament mappools on Vash Esports." />
<meta property="og:site_name" content="Vash Esports" />
<meta name="twitter:card" content="summary" />
</svelte:head>
<div class="mx-auto max-w-4xl">
<div class="flex items-center justify-between">
<div>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { invalidateAll } from '$app/navigation';
import { page } from '$app/state';
let { data } = $props();
@@ -48,6 +49,18 @@
};
</script>
<svelte:head>
<title>{data.pool.name} — Mappool | Vash Esports</title>
<meta name="description" content="View the {data.pool.name} mappool with {data.pool.slots.length} maps on Vash Esports." />
<meta property="og:title" content="{data.pool.name} — Mappool | Vash Esports" />
<meta property="og:description" content="{data.pool.slots.length} maps · View on Vash Esports" />
<meta property="og:type" content="website" />
<meta property="og:url" content={page.url.href} />
<meta property="og:image" content="/og-image.png" />
<meta property="og:site_name" content="Vash Esports" />
<meta name="twitter:card" content="summary_large_image" />
</svelte:head>
<div class="mx-auto max-w-4xl">
<!-- Header -->
<div class="flex items-center justify-between">

View File

@@ -52,6 +52,15 @@
}
</script>
<svelte:head>
<title>Matches — Vash Esports</title>
<meta name="description" content="Browse all osu! tournament matches on Vash Esports." />
<meta property="og:title" content="Matches — Vash Esports" />
<meta property="og:description" content="Browse all osu! tournament matches on Vash Esports." />
<meta property="og:site_name" content="Vash Esports" />
<meta name="twitter:card" content="summary" />
</svelte:head>
<div class="mx-auto max-w-5xl">
<!-- Header -->
<div class="flex items-center justify-between">

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { invalidateAll } from '$app/navigation';
import { page } from '$app/state';
let { data } = $props();
@@ -99,8 +100,24 @@
function stateBorder(s: string) {
return ({ ROLLING: 'border-yellow-500/30', PICKING: 'border-blue-500/30', PLAYING: 'border-green-500/30', FINISHED: 'border-accent/30' })[s] ?? 'border-border';
}
const stateLabels: Record<string, string> = { LOBBY: 'In Lobby', ROLLING: 'Rolling', PICKING: 'Picking', PLAYING: 'Live Now', FINISHED: 'Finished', CANCELLED: 'Cancelled' };
const ogTitle = $derived(`${p1?.team?.name ?? 'TBD'} vs ${p2?.team?.name ?? 'TBD'} — ${m.name} | Vash Esports`);
const ogDesc = $derived(`${stateLabels[m.state] ?? m.state} · Best of ${(m.config as any)?.bestOf ?? '?'} · Watch on Vash Esports`);
</script>
<svelte:head>
<title>{ogTitle}</title>
<meta name="description" content={ogDesc} />
<meta property="og:title" content={ogTitle} />
<meta property="og:description" content={ogDesc} />
<meta property="og:type" content="website" />
<meta property="og:url" content={page.url.href} />
<meta property="og:image" content="/og-image.png" />
<meta property="og:site_name" content="Vash Esports" />
<meta name="twitter:card" content="summary_large_image" />
</svelte:head>
<!-- Shared reinvite snippet -->
{#snippet reinviteButton()}
<form method="post" action="?/reinvite" use:enhance={() => {

View File

@@ -0,0 +1,71 @@
<svelte:head>
<title>Privacy Policy — Vash Esports</title>
<meta name="description" content="Privacy Policy for Vash Esports." />
</svelte:head>
<div class="prose prose-invert mx-auto max-w-3xl">
<h1>Privacy Policy</h1>
<p class="text-text-secondary text-sm">Last updated: {new Date().getFullYear()}</p>
<h2>1. Information We Collect</h2>
<p>When you use Vash Esports, we collect:</p>
<ul>
<li>
<strong>osu! account data</strong> — your osu! username, user ID, and profile information
provided via osu! OAuth
</li>
<li>
<strong>Tournament data</strong> — match results, scores, and team membership you create or
participate in
</li>
<li>
<strong>Usage analytics</strong> — anonymized page views and interactions via Umami, a
privacy-focused analytics tool. No cookies are used and no personal data is collected.
</li>
</ul>
<h2>2. How We Use Your Information</h2>
<ul>
<li>To provide and operate the Service (tournament management, matchmaking, scoring)</li>
<li>To display your username and profile in matches, leaderboards, and team pages</li>
<li>To communicate with you about your matches via osu! IRC</li>
<li>To improve the Service through aggregated analytics</li>
</ul>
<h2>3. Data Sharing</h2>
<p>
We do not sell or share your personal data with third parties, except as required by law.
osu! account data is obtained via osu!'s OAuth API and subject to
<a href="https://osu.ppy.sh/legal/privacy" target="_blank" rel="noopener">osu!'s Privacy Policy</a
>.
</p>
<h2>4. Data Retention</h2>
<p>
Your account and tournament data are retained for as long as your account is active or as needed
to provide the Service. You may request deletion of your data by contacting us.
</p>
<h2>5. Cookies</h2>
<p>
We use a single session cookie for authentication. Our analytics (Umami) are cookieless and do
not track you across sites.
</p>
<h2>6. Your Rights (GDPR)</h2>
<p>If you are in the EU/EEA, you have the right to:</p>
<ul>
<li>Access the personal data we hold about you</li>
<li>Request correction or deletion of your data</li>
<li>Object to or restrict processing of your data</li>
<li>Data portability</li>
</ul>
<p>To exercise these rights, contact us at the address below.</p>
<h2>7. Contact</h2>
<p>
For privacy questions or data requests, contact:
<!-- TODO: add contact email -->
<a href="mailto:contact@vash.software">contact@vash.software</a>
</p>
</div>

View File

@@ -0,0 +1,64 @@
<svelte:head>
<title>Terms of Service — Vash Esports</title>
<meta name="description" content="Terms of Service for Vash Esports." />
</svelte:head>
<div class="prose prose-invert mx-auto max-w-3xl">
<h1>Terms of Service</h1>
<p class="text-text-secondary text-sm">Last updated: {new Date().getFullYear()}</p>
<h2>1. Acceptance of Terms</h2>
<p>
By accessing or using Vash Esports ("the Service"), you agree to be bound by these Terms of
Service. If you do not agree, do not use the Service.
</p>
<h2>2. Use of the Service</h2>
<p>
Vash Esports is an osu! tournament management platform. You may use the Service only for lawful
purposes and in accordance with these Terms. You agree not to:
</p>
<ul>
<li>Use the Service in any way that violates applicable laws or regulations</li>
<li>Attempt to interfere with the proper functioning of the Service</li>
<li>Use the Service to harass, abuse, or harm other users</li>
<li>Attempt to gain unauthorized access to any part of the Service</li>
</ul>
<h2>3. Accounts</h2>
<p>
Access to the Service requires an osu! account. You are responsible for maintaining the
confidentiality of your account and for all activities that occur under it.
</p>
<h2>4. Intellectual Property</h2>
<p>
The Service and its original content are owned by Vash Esports. osu! and related assets are
property of ppy Pty Ltd. Beatmap content belongs to their respective creators.
</p>
<h2>5. Disclaimers</h2>
<p>
The Service is provided "as is" without warranties of any kind. We do not guarantee uptime or
uninterrupted access. Use the Service at your own risk.
</p>
<h2>6. Limitation of Liability</h2>
<p>
To the fullest extent permitted by law, Vash Esports shall not be liable for any indirect,
incidental, or consequential damages arising from your use of the Service.
</p>
<h2>7. Changes to Terms</h2>
<p>
We reserve the right to modify these Terms at any time. Continued use of the Service after
changes constitutes acceptance of the new Terms.
</p>
<h2>8. Contact</h2>
<p>
For questions about these Terms, contact us at
<!-- TODO: add contact email -->
<a href="mailto:contact@vash.software">contact@vash.software</a>.
</p>
</div>

17
src/routes/+layout.svelte Normal file
View File

@@ -0,0 +1,17 @@
<script lang="ts">
import { env } from '$env/dynamic/public';
let { children } = $props();
const umamiUrl = env.PUBLIC_UMAMI_URL;
const umamiId = env.PUBLIC_UMAMI_WEBSITE_ID;
</script>
<svelte:head>
{#if umamiUrl && umamiId}
<!-- svelte-ignore a11y_missing_attribute -->
<script async defer src={umamiUrl} data-website-id={umamiId}></script>
{/if}
</svelte:head>
{@render children()}

View File

@@ -0,0 +1,34 @@
import type { RequestHandler } from './$types';
const ORIGIN = 'https://esports.vash.software';
const pages = [
{ url: '/', priority: '1.0', changefreq: 'daily' },
{ url: '/matches', priority: '0.8', changefreq: 'hourly' },
{ url: '/mappools', priority: '0.7', changefreq: 'daily' },
{ url: '/leaderboard', priority: '0.7', changefreq: 'daily' },
{ url: '/terms', priority: '0.3', changefreq: 'monthly' },
{ url: '/privacy', priority: '0.3', changefreq: 'monthly' }
];
export const GET: RequestHandler = () => {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${pages
.map(
(p) => ` <url>
<loc>${ORIGIN}${p.url}</loc>
<changefreq>${p.changefreq}</changefreq>
<priority>${p.priority}</priority>
</url>`
)
.join('\n')}
</urlset>`;
return new Response(xml, {
headers: {
'Content-Type': 'application/xml',
'Cache-Control': 'max-age=3600'
}
});
};

View File

@@ -1,3 +1,7 @@
# allow crawling everything by default
User-agent: *
Disallow:
Disallow: /api/
Disallow: /admin
Disallow: /demo
Disallow: /settings
Sitemap: https://esports.vash.software/sitemap.xml