add umami and seo tags
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
71
src/routes/(app)/privacy/+page.svelte
Normal file
71
src/routes/(app)/privacy/+page.svelte
Normal 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>
|
||||
64
src/routes/(app)/terms/+page.svelte
Normal file
64
src/routes/(app)/terms/+page.svelte
Normal 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
17
src/routes/+layout.svelte
Normal 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()}
|
||||
34
src/routes/sitemap.xml/+server.ts
Normal file
34
src/routes/sitemap.xml/+server.ts
Normal 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'
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user