Devlog 20: SEO for Viral Content - Making Matchups Discoverable

6 min read
seo marketing discoverability

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-dogs
  • tuggy.io/trump-vs-biden

Bad:

  • tuggy.io/matchup?id=123
  • tuggy.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:

  1. Custom OG images - Show live vote counts (see my detailed guide on generating dynamic OG images)
  2. Compelling titles - "{A} vs {B} - Who wins?"
  3. Clear CTAs - "Vote now" in description
  4. 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:

  1. Google Search Console - Impressions, clicks, positions
  2. Google Analytics - Organic traffic, bounce rate
  3. 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.