DEV Community

Cover image for Game Site SEO in Practice: URLs, JSON-LD, SSG, and iframe Layout Shift
no momo
no momo

Posted on AI-assisted

Game Site SEO in Practice: URLs, JSON-LD, SSG, and iframe Layout Shift

The biggest SEO trap for a game site isn't technical — it's thin content. My game pages had only 2–3 sentences of description, and Google Search Console showed a large number of pages as "Crawled — currently not indexed."

After digging in, I found five separate problems, ordered by impact:

  1. Game pages had too little content and were judged as low-quality.
  2. JSON-LD used only the VideoGame type, so Google didn't show rich results.
  3. The iframe had no reserved dimensions, causing CLS of 0.32.
  4. Metadata was generated client-side, so crawlers couldn't see it.

Here's the debugging process and fix for each one.


Environment

  • Next.js 14.2.3 (App Router + output: 'export' static export)
  • Deployment: Vercel static hosting
  • Game source: third-party iframe embeds on a separate domain -- playfiddlebops.com
  • Tools: Google Search Console, Lighthouse 12, Chrome DevTools

Problem 1: Pages Marked "Crawled — Currently Not Indexed"

Symptoms

Two months after launch, Google Search Console showed:

  • Only 1 of the 6 submitted game detail pages was indexed
  • The other 5 were "Crawled — currently not indexed"

Drilling into the details, Google's explanation was "low content quality."

How I Tracked It Down

I compared the one indexed game page with the ones that weren't. The difference was obvious:

Indexed page: ~200 words of description, with controls, tips, and 3 FAQs.
Non-indexed pages: One sentence of description, like "Type words quickly to score points."

The problem wasn't technical. It was content. A game page is essentially an iframe container plus a short description, and Google decided that didn't provide enough informational value.

The Fix

I added three types of content to each game page:

1. Controls (~80 words): keyboard/touch controls, game objective, scoring.

2. Tips (~100 words): 2–3 practical tips so users know how to play better.

3. FAQ (3 Q&As): covering questions users actually ask, like "Does this game need internet?", "Where are scores saved?", "Can I play on mobile?"

That's about 300 words total, written manually per game, not bulk-generated. A developer shared a similar experience: adding controls, how-to-play, tips, FAQ, or similar game recommendations below the game is what separates a game page from "thin content."

One trade-off: content maintenance cost goes up. Each game page takes an extra 15–20 minutes to write and proofread. My strategy is to do full content only for the top 20% of games by traffic, and keep long-tail games with a short description plus auto-generated FAQ.


Problem 2: JSON-LD Didn't Trigger Rich Results

Symptoms

The game detail page used @type: "VideoGame" in JSON-LD, but no rich results appeared in Google search (ratings, price, platform info, etc.).

How I Tracked It Down

Google's official documentation clearly states: for software applications, Google won't show richer search results for structured data that only contains the VideoGame type. To get rich results, you need to combine VideoGame with other available node types.

The Fix

Change the JSON-LD type to a combined array:

{
  "@context": "https://schema.org",
  "@type": ["VideoGame", "MobileApplication"],
  "name": "Typing Hero",
  "description": "Type words quickly to score points.",
  "applicationCategory": "Game",
  "operatingSystem": "Web",
  "offers": {
    "@type": "Offer",
    "price": "0",
    "priceCurrency": "USD"
  },
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "4.2",
    "ratingCount": "128"
  },
  "gamePlatform": ["Web Browser", "Mobile"]
}
Enter fullscreen mode Exit fullscreen mode

The key is that the @type array must have a second usable type. MobileApplication is the most sensible choice, since most HTML5 games also run in mobile browsers.

Verification: Use Google's Rich Results Test to confirm both VideoGame and MobileApplication parse correctly.


Problem 3: CLS of 0.32 Caused by the iframe

Symptoms

Lighthouse mobile testing showed CLS of 0.32, far above the "good" threshold (0.1). The Performance panel showed layout shift happening when the game iframe loaded.

How I Tracked It Down

iframes have no intrinsic dimensions by default. Before src finishes loading, the iframe's placeholder height is 0. When the game HTML loads, the browser has to recalculate layout and push the content below down. That's the source of the CLS.

This is essentially the same problem as YouTube embed CLS: layout shift on the parent page usually comes from a zero-height or delayed-size embed box.

The Fix

Use aspect-ratio to reserve space for the iframe container. The position is occupied before the container loads, and the iframe fills that space after loading, without causing reflow.

/* Game container fixed at 16:9, space reserved before load */
.game-embed-wrapper {
  aspect-ratio: 16 / 9;
  width: 100%;
  overflow: hidden;
  border-radius: 0.5rem;
  background-color: #18181b;
}

.game-embed-wrapper iframe {
  width: 100%;
  height: 100%;
  border: 0;
}
Enter fullscreen mode Exit fullscreen mode

aspect-ratio has over 94% global browser support and is the standard solution for responsive embed CLS. If you need to support older browsers, the padding-bottom hack works as a fallback:

.game-embed-wrapper {
  position: relative;
  padding-bottom: 56.25%; /* 16:9 */
  height: 0;
}
.game-embed-wrapper iframe {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
Enter fullscreen mode Exit fullscreen mode

Verification data: CLS dropped from 0.32 to 0.04 after the fix.


Problem 4: Metadata Rendered Client-Side, Crawlers Couldn't See It

Symptoms

The game detail page's <title> and <meta description> were visible in the browser, but View Source showed them as empty.

Cause

Metadata was set dynamically on the client after Vue/React's onMounted via document.title. Crawlers have limited JavaScript execution ability, and many see only the initial HTML, which lacks these tags.

The Fix

Use Next.js generateMetadata to generate metadata on the server. The generateMetadata function export is only supported in Server Components, and it injects metadata into <head> before HTML rendering.

// app/games/[category]/[slug]/page.tsx
import type { Metadata } from 'next'

export async function generateMetadata({ params }) {
  const { slug } = await params
  const game = await getGameBySlug(slug)

  return {
    title: `${game.title} - Play Free Online`,
    description: game.description,
    openGraph: {
      title: game.title,
      description: game.description,
      images: [game.thumbnail],
    },
    alternates: {
      canonical: `yourdomain/games/${game.category}/${game.slug}`,
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

When used with generateStaticParams, metadata is pre-generated at build time and output into static HTML.

One trap to note: Next.js 15.2 introduced streaming metadata, where async generateMetadata may output after the body. The official docs say this doesn't affect SEO, because search engine crawlers receive the HTML only after metadata is fully generated. But if your crawler simulation tool sees incomplete HTML, don't worry.


Complete SEO Configuration Checklist

In Next.js App Router, these files go in the app/ directory:

app/robots.ts:

import type { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: '*',
      allow: '/',
      disallow: ['/play/'],  // Don't index game runtime pages
    },
    sitemap: 'yourdomain/sitemap.xml',
  }
}
Enter fullscreen mode Exit fullscreen mode

Next.js's Metadata API automatically generates a robots.txt file compliant with the Robots Exclusion Standard.

app/sitemap.ts:

import type { MetadataRoute } from 'next'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const games = await getAllGames()

  const gameUrls = games.map((game) => ({
    url: `yourdomain/games/${game.category}/${game.slug}`,
    lastModified: new Date(),
    changeFrequency: 'weekly' as const,
    priority: 0.8,
  }))

  return [
    {
      url: 'https://www.playfiddlebops.com/',
      lastModified: new Date(),
      changeFrequency: 'daily',
      priority: 1,
    },
    ...gameUrls,
  ]
}
Enter fullscreen mode Exit fullscreen mode

URL structure: Use /games/{category}/{slug} for a semantic structure that puts both category and game identifier in the URL.


Verification Data

Before and after optimization (Lighthouse mobile, Moto G Power, 4G throttling):

Metric Before After
CLS 0.32 0.04
LCP 2.4s 2.1s
Game detail pages indexed (GSC) 1/6 5/6
Rich results shown No Yes (rating, price)

Test timing: 4 weeks after optimization, giving Google enough time to recrawl and re-evaluate. Test conditions: Vercel deployment, no extra CDN configuration.


Trade-offs and Limitations

  • Content maintenance cost goes up: each game page takes an extra 15–20 minutes to write controls, tips, and FAQ. Strategy is to do full content only for the top 20% of games by traffic.
  • JSON-LD rating data is manually maintained: aggregateRating is currently hardcoded because there's no backend storing user ratings yet. This risks structured data not matching reality and could trigger a manual penalty from Google. Needs a rating API or temporary removal of this field.
  • Full sitemap generation becomes a performance issue as game count grows: generateStaticParams pre-generates all game pages at build time. If the game count reaches 200+, build time increases significantly.

Unresolved

  • FAQ schema controversy: Google announced in May 2026 that it would gradually retire FAQ rich results, meaning FAQ schema may no longer provide visual advantages in search results. But structured data still helps Google understand page content, so it's kept for now, to be observed later.
  • Game runtime page indexing strategy: /play/:id is currently blocked by robots.txt because it's an iframe container with no independent content value. But if game pages later carry ads or community comments, whether to allow indexing needs reconsideration.

References

  • Google Search Central documentation on SoftwareApplication structured data, explaining that VideoGame type needs to be combined to get rich results
  • web.dev guide on CLS optimization, recommending aspect-ratio to reserve space for embedded content
  • Next.js official docs on generateMetadata, robots.ts, and sitemap.ts configuration

Top comments (0)