add umami and seo tags
This commit is contained in:
@@ -42,5 +42,39 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
start_period: 10s
|
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:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
umamidata:
|
||||||
|
|||||||
@@ -99,5 +99,9 @@
|
|||||||
<span class="text-base">⚙</span>
|
<span class="text-base">⚙</span>
|
||||||
Settings
|
Settings
|
||||||
</a>
|
</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>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { invalidateAll } from '$app/navigation';
|
import { invalidateAll } from '$app/navigation';
|
||||||
|
import { page } from '$app/state';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
@@ -87,6 +88,18 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</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) ── -->
|
<!-- ── LANDING PAGE (not logged in) ── -->
|
||||||
{#if !data.user}
|
{#if !data.user}
|
||||||
<div class="flex min-h-[70vh] flex-col items-center justify-center text-center">
|
<div class="flex min-h-[70vh] flex-col items-center justify-center text-center">
|
||||||
|
|||||||
@@ -33,6 +33,15 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</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 class="mx-auto max-w-4xl">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-700 tracking-tight">Leaderboard</h1>
|
<h1 class="text-2xl font-700 tracking-tight">Leaderboard</h1>
|
||||||
|
|||||||
@@ -5,6 +5,15 @@
|
|||||||
let creating = $state(false);
|
let creating = $state(false);
|
||||||
</script>
|
</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="mx-auto max-w-4xl">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import { invalidateAll } from '$app/navigation';
|
import { invalidateAll } from '$app/navigation';
|
||||||
|
import { page } from '$app/state';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
@@ -48,6 +49,18 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</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">
|
<div class="mx-auto max-w-4xl">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
|
|||||||
@@ -52,6 +52,15 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</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">
|
<div class="mx-auto max-w-5xl">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import { invalidateAll } from '$app/navigation';
|
import { invalidateAll } from '$app/navigation';
|
||||||
|
import { page } from '$app/state';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
@@ -99,8 +100,24 @@
|
|||||||
function stateBorder(s: string) {
|
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';
|
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>
|
</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 -->
|
<!-- Shared reinvite snippet -->
|
||||||
{#snippet reinviteButton()}
|
{#snippet reinviteButton()}
|
||||||
<form method="post" action="?/reinvite" use:enhance={() => {
|
<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: *
|
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