DEV Community

wwx516
wwx516

Posted on

Building a Fantasy Football Team Name Generator with Next.js

As the 2025 NFL season heats up, millions of fantasy football players are looking for the perfect team name. I decided to build a comprehensive web application to solve this problem using modern web technologies. Here's how I built it and what I learned along the way.

๐ŸŽฏ The Problem

Every fantasy football season, the same struggle happens:

  • Players spend hours brainstorming team names
  • Generic names like "Team 1" or "John's Team" are boring
  • Coming up with clever puns requires creativity and time
  • Players want names based on current players and trends

I saw an opportunity to build a tool that could help fantasy football enthusiasts discover creative, funny, and memorable team names instantly.

๐Ÿ› ๏ธ Tech Stack

After evaluating different options, I chose a modern, performant stack:

Frontend Framework: Next.js 14+ (App Router)
Language: TypeScript
Styling: Tailwind CSS
Deployment: Cloudflare Pages
API Integration: OpenRouter (for AI-powered generation)
Enter fullscreen mode Exit fullscreen mode

Why Next.js?

  • Server-Side Rendering (SSR): Critical for SEO since users search for "fantasy football team names"
  • App Router: Modern routing with React Server Components
  • Built-in optimization: Image optimization, font loading, etc.
  • TypeScript support: Type safety out of the box

Why Tailwind CSS?

  • Utility-first approach: Rapid development without writing custom CSS
  • Responsive design: Mobile-first by default
  • Performance: Purges unused styles in production
  • Customization: Easy to create consistent design systems

๐Ÿ—๏ธ Project Architecture

Folder Structure

/app
  /generator          # Name generator page
  /categories         # Browse by category
  /players            # Player-specific names
  layout.tsx          # Root layout
  page.tsx            # Homepage
/components
  NameGenerator.tsx   # Main generator component
  NameCard.tsx        # Individual name display
  CategoryFilter.tsx  # Category selection
/lib
  types.ts           # TypeScript interfaces
  utils.ts           # Utility functions
  openrouter.ts      # API integration
/data
  team-names.json    # Static name database
  players.json       # Player information
Enter fullscreen mode Exit fullscreen mode

Key Design Decisions

1. Static Data + Dynamic Generation

I use a hybrid approach:

// Static names for instant results
const fallbackNames = [
  "Mahomes Alone",
  "Josh Allen My Eggs",
  "Hurts So Good",
  // ... 600+ more
];

// AI generation for personalized names
async function generateCustomName(preferences: UserPreferences) {
  const response = await openrouter.generate({
    prompt: buildPrompt(preferences),
    model: "gpt-3.5-turbo"
  });
  return response.choices[0].message.content;
}
Enter fullscreen mode Exit fullscreen mode

2. SEO-First Approach

Every page is optimized for search engines:

// app/categories/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
  const category = await getCategory(params.slug);

  return {
    title: `${category.count}+ ${category.name} Fantasy Football Team Names`,
    description: `Discover the best ${category.name.toLowerCase()} fantasy football team names for 2025. Creative, funny, and memorable options.`,
    keywords: [
      'fantasy football team names',
      `${category.name} team names`,
      'fantasy football 2025'
    ],
    openGraph: {
      title: `${category.name} Fantasy Football Names`,
      description: category.description,
      images: ['/og-image.jpg']
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

3. Performance Optimization

// Lazy loading for better initial load
const NameGenerator = dynamic(() => import('@/components/NameGenerator'), {
  loading: () => <LoadingSpinner />,
  ssr: false // Client-side only for interactive features
});

// Image optimization
import Image from 'next/image';

<Image
  src="/player-images/mahomes.jpg"
  alt="Patrick Mahomes"
  width={400}
  height={400}
  priority={false}
  loading="lazy"
/>
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ก Core Features Implementation

Feature 1: Name Generator

'use client'

import { useState } from 'react';

export default function NameGenerator() {
  const [generatedName, setGeneratedName] = useState<string>('');
  const [isLoading, setIsLoading] = useState(false);
  const [preferences, setPreferences] = useState({
    style: 'puns',
    includePlayerName: true,
    playerName: ''
  });

  const generateName = async () => {
    setIsLoading(true);
    try {
      const response = await fetch('/api/generate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(preferences)
      });

      const data = await response.json();
      setGeneratedName(data.name);
    } catch (error) {
      console.error('Generation failed:', error);
      // Fallback to static names
      setGeneratedName(getRandomFallbackName());
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="max-w-2xl mx-auto p-6">
      <h2 className="text-3xl font-bold mb-6">
        Generate Your Team Name
      </h2>

      {/* Preferences Selection */}
      <div className="space-y-4 mb-6">
        <select
          value={preferences.style}
          onChange={(e) => setPreferences({...preferences, style: e.target.value})}
          className="w-full p-3 border rounded-lg"
        >
          <option value="puns">Puns & Wordplay</option>
          <option value="player">Player Names</option>
          <option value="popculture">Pop Culture</option>
          <option value="trash-talk">Trash Talk</option>
        </select>

        {preferences.includePlayerName && (
          <input
            type="text"
            placeholder="Enter player name (optional)"
            value={preferences.playerName}
            onChange={(e) => setPreferences({...preferences, playerName: e.target.value})}
            className="w-full p-3 border rounded-lg"
          />
        )}
      </div>

      {/* Generate Button */}
      <button
        onClick={generateName}
        disabled={isLoading}
        className="w-full bg-blue-600 text-white py-3 px-6 rounded-lg 
                   hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed
                   transition-colors duration-200"
      >
        {isLoading ? 'Generating...' : 'Generate Team Name'}
      </button>

      {/* Result Display */}
      {generatedName && (
        <div className="mt-8 p-6 bg-gradient-to-r from-blue-50 to-purple-50 
                        rounded-lg border-2 border-blue-200">
          <p className="text-2xl font-bold text-center text-gray-800">
            {generatedName}
          </p>
          <div className="flex justify-center gap-4 mt-4">
            <button 
              onClick={() => navigator.clipboard.writeText(generatedName)}
              className="text-blue-600 hover:text-blue-800"
            >
              ๐Ÿ“‹ Copy
            </button>
            <button 
              onClick={generateName}
              className="text-blue-600 hover:text-blue-800"
            >
              ๐Ÿ”„ Generate Another
            </button>
          </div>
        </div>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Feature 2: Category Browsing

// app/categories/page.tsx
import Link from 'next/link';
import { categories } from '@/data/categories';

export default function CategoriesPage() {
  return (
    <div className="container mx-auto px-4 py-12">
      <h1 className="text-4xl font-bold mb-8">
        Browse by Category
      </h1>

      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {categories.map((category) => (
          <Link
            key={category.slug}
            href={`/categories/${category.slug}`}
            className="block p-6 bg-white rounded-lg shadow-md 
                       hover:shadow-xl transition-shadow duration-200
                       border-2 border-transparent hover:border-blue-500"
          >
            <div className="text-4xl mb-3">{category.icon}</div>
            <h3 className="text-xl font-bold mb-2">{category.name}</h3>
            <p className="text-gray-600 mb-3">{category.description}</p>
            <span className="text-blue-600 font-semibold">
              {category.count}+ names โ†’
            </span>
          </Link>
        ))}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Feature 3: API Route for Name Generation

// app/api/generate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { OpenRouter } from '@/lib/openrouter';

export async function POST(request: NextRequest) {
  try {
    const { style, playerName } = await request.json();

    // Build prompt based on preferences
    const prompt = buildPrompt(style, playerName);

    // Call AI API
    const openrouter = new OpenRouter(process.env.OPENROUTER_API_KEY);
    const response = await openrouter.generate({
      model: 'gpt-3.5-turbo',
      messages: [
        {
          role: 'system',
          content: 'You are a creative fantasy football team name generator.'
        },
        {
          role: 'user',
          content: prompt
        }
      ],
      temperature: 0.9, // Higher creativity
      max_tokens: 50
    });

    const generatedName = response.choices[0].message.content.trim();

    return NextResponse.json({ 
      name: generatedName,
      success: true 
    });

  } catch (error) {
    console.error('API Error:', error);

    // Fallback to static names on error
    return NextResponse.json({ 
      name: getRandomFallbackName(),
      success: true,
      fallback: true
    });
  }
}

function buildPrompt(style: string, playerName?: string): string {
  let prompt = `Generate 1 creative fantasy football team name. `;

  switch(style) {
    case 'puns':
      prompt += `It should be a clever pun or wordplay.`;
      break;
    case 'player':
      prompt += `It should reference NFL player ${playerName || 'a current NFL star'}.`;
      break;
    case 'popculture':
      prompt += `It should reference popular 2025 movies, TV shows, or memes.`;
      break;
    case 'trash-talk':
      prompt += `It should be funny trash talk (keep it PG-13).`;
      break;
  }

  prompt += ` Output only the team name, nothing else.`;
  return prompt;
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿš€ Deployment and Performance

Cloudflare Pages Configuration

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  images: {
    domains: ['localhost'],
    formats: ['image/avif', 'image/webp'],
  },
  // Optimize for edge deployment
  output: 'standalone',
}

module.exports = nextConfig
Enter fullscreen mode Exit fullscreen mode

Environment Variables

# .env.local (development)
OPENROUTER_API_KEY=sk-or-v1-xxxxxxxxxxxxx

# .env.production (Cloudflare Pages)
NEXT_PUBLIC_SITE_URL=https://www.ffteamnames.com
OPENROUTER_API_KEY=sk-or-v1-xxxxxxxxxxxxx
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
Enter fullscreen mode Exit fullscreen mode

Performance Results

After deployment:

  • Lighthouse Score: 95+
  • First Contentful Paint: < 1.2s
  • Time to Interactive: < 2.5s
  • Total Blocking Time: < 150ms

๐Ÿ“Š SEO Strategy

1. Dynamic Sitemap Generation

// app/sitemap.ts
import { MetadataRoute } from 'next';

export default function sitemap(): MetadataRoute.Sitemap {
  const baseUrl = 'https://www.ffteamnames.com';

  // Static pages
  const routes = ['', '/generator', '/categories'].map((route) => ({
    url: `${baseUrl}${route}`,
    lastModified: new Date(),
    changeFrequency: 'daily' as const,
    priority: route === '' ? 1 : 0.8,
  }));

  // Dynamic category pages
  const categoryRoutes = categories.map((category) => ({
    url: `${baseUrl}/categories/${category.slug}`,
    lastModified: new Date(),
    changeFrequency: 'weekly' as const,
    priority: 0.7,
  }));

  // Dynamic player pages
  const playerRoutes = players.map((player) => ({
    url: `${baseUrl}/players/${player.slug}`,
    lastModified: new Date(),
    changeFrequency: 'weekly' as const,
    priority: 0.6,
  }));

  return [...routes, ...categoryRoutes, ...playerRoutes];
}
Enter fullscreen mode Exit fullscreen mode

2. Structured Data

// Add JSON-LD structured data
export function generateStructuredData(name: string) {
  return {
    '@context': 'https://schema.org',
    '@type': 'WebApplication',
    'name': 'Fantasy Football Team Names Generator',
    'applicationCategory': 'SportsApplication',
    'offers': {
      '@type': 'Offer',
      'price': '0',
      'priceCurrency': 'USD'
    },
    'aggregateRating': {
      '@type': 'AggregateRating',
      'ratingValue': '4.8',
      'ratingCount': '1250'
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

๐ŸŽ“ Lessons Learned

1. AI Fallback is Essential

Always have static fallback content when using AI APIs:

try {
  result = await aiGenerate();
} catch {
  result = getStaticFallback(); // Never fail completely
}
Enter fullscreen mode Exit fullscreen mode

2. Mobile-First is Critical

Over 60% of my users are on mobile during game day. Responsive design isn't optional.

3. SEO Takes Time

  • Week 1: 0 organic traffic
  • Month 1: ~50 visits/day
  • Month 3: ~500 visits/day
  • Month 6: ~2,000+ visits/day (during season)

4. Content is King

Having 600+ pre-written team names provides immediate value while the AI generates custom options.

๐Ÿ”ฎ Future Improvements

  1. User Accounts: Save favorite names
  2. Social Sharing: Generate shareable images
  3. League Integration: Import roster to generate personalized names
  4. Weekly Trends: Auto-generate names based on that week's performances
  5. Name Voting: Community-driven ratings

๐Ÿ“š Resources

If you're interested in seeing the live project, check out FFTeamNames.com - it's currently serving thousands of fantasy football players during the 2025 season.

For the code architecture and implementation details, you can explore the project structure on GitHub.

๐Ÿค Contributing Ideas?

I'm always looking to improve the platform. Some areas where contributions would be welcome:

  • More player-specific name suggestions
  • Better AI prompts for generation quality
  • Performance optimizations
  • New category ideas
  • Accessibility improvements

๐Ÿ’ฌ Discussion

What would you build differently?

I'd love to hear your thoughts on:

  • Alternative tech stacks for this use case
  • Better approaches to AI integration
  • SEO strategies for niche content sites
  • Ways to monetize without ruining user experience

Drop your thoughts in the comments!


๐Ÿท๏ธ Tags

nextjs #typescript #webdev #react #tailwindcss #seo #fantasy football #sports #ai #openai


About Me: Full-stack developer and fantasy football addict. Currently building tools to make fantasy sports more fun. Always open to discussing tech, sports, or both!

Connect:


Published: October 2025 | Built with โค๏ธ and โ˜•

Top comments (0)