Devlog 20: SEO for Viral Content - Making Matchups Discoverable
Tuggy is only valuable if people find it. I need matchups showing up in Google searches and looking good when shared on social media.
Here's how I optimized for discoverability.
Dynamic Meta Tags
Each matchup needs unique meta tags:
<!-- src/routes/[slug]/+page.svelte -->
<svelte:head>
<title>{matchup.title} - Who will win? | Tuggy</title>
<meta name="description" content="Vote now: {matchup.contestant_a} vs {matchup.contestant_b}. Join {matchup.total_votes.toLocaleString()} other voters!" />
<!-- Open Graph -->
<meta property="og:title" content="{matchup.title}" />
<meta property="og:description" content="{matchup.contestant_a} vs {matchup.contestant_b} - Cast your vote!" />
<meta property="og:image" content="{ogImageUrl}" />
<meta property="og:url" content="https://tuggy.io/{matchup.slug}" />
<meta property="og:type" content="website" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="{matchup.title}" />
<meta name="twitter:description" content="Vote: {matchup.contestant_a} vs {matchup.contestant_b}" />
<meta name="twitter:image" content="{ogImageUrl}" />
<!-- Canonical URL -->
<link rel="canonical" href="https://tuggy.io/{matchup.slug}" />
</svelte:head>
Server-side rendering ensures crawlers see these.
Structured Data (JSON-LD)
Help search engines understand the content:
<svelte:head>
{@html `
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "${matchup.title}",
"description": "Vote in this battle: ${matchup.contestant_a} vs ${matchup.contestant_b}",
"url": "https://tuggy.io/${matchup.slug}",
"image": "${ogImageUrl}",
"author": {
"@type": "Organization",
"name": "Tuggy"
},
"interactionStatistic": {
"@type": "InteractionCounter",
"interactionType": "https://schema.org/VoteAction",
"userInteractionCount": ${matchup.total_votes}
}
}
</script>
`}
</svelte:head>
Google can show rich results with vote counts.
Sitemap Generation
Generate sitemap for all matchups:
// src/routes/sitemap.xml/+server.ts
export async function GET() {
const { data: matchups } = await supabase
.from('matchups')
.select('slug, updated_at')
.order('updated_at', { ascending: false });
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://tuggy.io</loc>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
${matchups.map(m => `
<url>
<loc>https://tuggy.io/${m.slug}</loc>
<lastmod>${m.updated_at}</lastmod>
<changefreq>hourly</changefreq>
<priority>0.8</priority>
</url>
`).join('')}
</urlset>
`;
return new Response(xml, {
headers: {
'Content-Type': 'application/xml',
'Cache-Control': 'public, max-age=3600'
}
});
}
Submit to Google Search Console.
robots.txt
Allow crawling:
# static/robots.txt
User-agent: *
Allow: /
Sitemap: https://tuggy.io/sitemap.xml
# Don't index API routes
Disallow: /api/
URL Structure
SEO-friendly URLs:
Good:
tuggy.io/cats-vs-dogstuggy.io/trump-vs-biden
Bad:
tuggy.io/matchup?id=123tuggy.io/m/abc123def
Slugs are readable and keyword-rich.
Page Speed
Google ranks faster sites higher. My optimizations:
Image optimization:
- WebP format
- Lazy loading
- Responsive sizes
Code splitting:
const VotingArea = () => import('./VotingArea.svelte');
Caching:
- Static assets: 1 year
- API responses: 1 hour
- HTML: No cache (dynamic)
Result: 95+ Lighthouse score.
Social Sharing Optimization
Make shares look good:
- Custom OG images - Show live vote counts (see my detailed guide on generating dynamic OG images)
- Compelling titles - "{A} vs {B} - Who wins?"
- Clear CTAs - "Vote now" in description
- Vote counts - "{X} people have voted"
Creates FOMO and curiosity.
Internal Linking
Link related matchups:
<div class="related-matchups">
<h3>More battles you might like:</h3>
{#each relatedMatchups as related}
<a href="/{related.slug}">
{related.title}
</a>
{/each}
</div>
Helps crawlers discover content and keeps users engaged.
Trending Page
Dedicated page for trending matchups:
/trending
Updated hourly, shows top 50 by recent activity. Great for search traffic on "trending votes" etc.
User-Generated Content
Matchup titles and descriptions are UGC. This creates long-tail SEO:
- "Who would win: Superman vs Goku"
- "Best programming language: Python vs JavaScript"
- "Cats vs Dogs 2024"
Each matchup targets niche keywords.
Mobile-First Indexing
Google indexes mobile version first. My site is mobile-first by default, so no issues.
Verify in Search Console that mobile version renders correctly.
Monitoring & Analytics
Track search performance:
- Google Search Console - Impressions, clicks, positions
- Google Analytics - Organic traffic, bounce rate
- Ahrefs/SEMrush - Keyword rankings
Check weekly for trends.
Takeaway
SEO isn't one thing. It's:
- Fast loading
- Mobile-friendly
- Proper meta tags
- Structured data
- Good URLs
- Internal linking
- Fresh content
User-generated content is gold for SEO. Every matchup is a new landing page targeting different keywords.
Dynamic OG images make social shares look professional, driving more clicks. Combined with smart viral mechanics, SEO helps build sustainable organic growth.
Server-side rendering is essential. SPAs without SSR struggle with SEO.
Next up: mobile-first CSS architecture and responsive design patterns.