Lifecycle: HISTORICAL (published KEEP). For the promoted reader route, see Plugin Embed Guide.

Plugin Embed Guide — Funday Gaming Platform

Overview

This guide explains how to adapt games for the Funday unified app shell using the embed=1 parameter to hide plugin chrome and integrate with FundayBridge v1.

Embed Mode Contract

URL Parameter

When your plugin is loaded in the Funday app shell, it receives:

/game-plugins/mygame/index.html?embed=1

Your plugin must:

  1. Detect embed=1 query parameter
  2. Hide all platform chrome (nav, footer, branding)
  3. Render only the game canvas/content
  4. Emit Bridge events for HUD integration

Detection Pattern

const urlParams = new URLSearchParams(window.location.search)
const isEmbedMode = urlParams.get("embed") === "1"
 
if (isEmbedMode) {
  // Hide chrome, show only game
  document.getElementById("navbar")?.remove()
  document.getElementById("footer")?.remove()
  document.body.classList.add("embed-mode")
}
/* CSS approach */
body.embed-mode #navbar,
body.embed-mode #footer,
body.embed-mode .platform-chrome {
  display: none !important;
}

Migration Checklist

Phase 1: Layoutless Mode

  • Detect ?embed=1 parameter
  • Hide navbar/footer/branding in embed mode
  • Ensure game viewport fills 100% of container
  • Test: No double scrollbars when embedded
  • Test: Responsive to container resize

Phase 2: Bridge Integration

  • Listen for funday:handshake message
  • Send funday:ack acknowledgment
  • Send game:ready when playable
  • Emit funday:nav:set for HUD updates
  • Emit funday:analytics-event for telemetry
  • Emit funday:score-submitted for leaderboards
  • Handle funday:theme-inject for theming
  • Handle funday:locale-inject for i18n

Phase 3: Polish

  • Add Exit button that sends game:close
  • Handle funday:pause/funday:resume messages
  • Support fullscreen via game:fullscreen event
  • Test: Theme switching without reload
  • Test: Locale switching without reload

Example Implementations

SvelteKit Plugin

File: src/routes/+layout.svelte

<script lang="ts">
  import { page } from '$app/stores';
  import { onMount } from 'svelte';
  import { FundayBridge } from '$lib/bridge';  // your bridge wrapper
 
  let isEmbedMode = $derived($page.url.searchParams.get('embed') === '1');
  let bridge: FundayBridge | null = null;
 
  onMount(() => {
    if (isEmbedMode) {
      bridge = new FundayBridge();
      bridge.init();
 
      // Signal ready after initial render
      setTimeout(() => bridge?.ready(), 100);
    }
 
    return () => {
      bridge?.destroy();
    };
  });
</script>
 
{#if !isEmbedMode}
  <nav class="navbar">
    <!-- Your navbar -->
  </nav>
{/if}
 
<main class:embed-mode={isEmbedMode}>
  <slot />
</main>
 
{#if !isEmbedMode}
  <footer>
    <!-- Your footer -->
  </footer>
{/if}
 
<style>
  main.embed-mode {
    width: 100vw;
    height: 100vh;
    overflow: hidden;
  }
</style>

React Plugin

File: src/App.tsx

import { useEffect, useState } from "react"
import { useFundayBridge } from "./hooks/useFundayBridge"
 
function App() {
  const [isEmbedMode] = useState(() => {
    const params = new URLSearchParams(window.location.search)
    return params.get("embed") === "1"
  })
 
  const bridge = useFundayBridge(isEmbedMode)
 
  useEffect(() => {
    if (isEmbedMode && bridge) {
      bridge.ready()
    }
  }, [isEmbedMode, bridge])
 
  const handleScore = (score: number) => {
    bridge?.submitScore("weekly", score)
  }
 
  return (
    <div className={isEmbedMode ? "embed-mode" : "standalone"}>
      {!isEmbedMode && <Navbar />}
 
      <GameCanvas onScoreChange={handleScore} embedded={isEmbedMode} />
 
      {!isEmbedMode && <Footer />}
    </div>
  )
}

Vanilla JS Plugin

File: index.html

<!DOCTYPE html>
<html>
  <head>
    <style>
      body.embed-mode #navbar,
      body.embed-mode #footer {
        display: none !important;
      }
 
      body.embed-mode #game-canvas {
        width: 100vw;
        height: 100vh;
      }
    </style>
  </head>
  <body>
    <nav id="navbar">My Game</nav>
    <canvas id="game-canvas"></canvas>
    <footer id="footer">© 2025</footer>
 
    <script>
      const urlParams = new URLSearchParams(window.location.search)
      const isEmbedMode = urlParams.get("embed") === "1"
 
      if (isEmbedMode) {
        document.body.classList.add("embed-mode")
 
        // Bridge setup
        const origin = window.location.ancestorOrigins?.[0] || window.parent.location.origin
 
        window.addEventListener("message", (event) => {
          if (event.origin !== origin) return
 
          if (event.data?.type === "funday:handshake") {
            event.source.postMessage({ type: "funday:ack", version: "1" }, origin)
 
            // Game initialization
            initGame().then(() => {
              event.source.postMessage({ type: "game:ready" }, origin)
            })
          }
        })
      } else {
        // Standalone mode
        initGame()
      }
 
      function initGame() {
        // Your game init code
        return Promise.resolve()
      }
    </script>
  </body>
</html>

HUD Integration Examples

Simple Status Updates

bridge.setNav({
  status: "Level 3 - Wave 5",
})

Rich HUD Display

bridge.setNav({
  title: "Space Defenders",
  subtitle: "Hardcore Mode",
  status: "Lives: 3 | Score: 45,200",
  actions: [
    { id: "restart", label: "Restart", icon: "↻" },
    { id: "pause", label: "Pause", icon: "⏸" },
  ],
})

Dynamic Updates

let score = 0
let lives = 3
 
function updateHUD() {
  bridge.setNav({
    status: `Lives: ${lives} | Score: ${score}`,
  })
}
 
// On score change
score += 100
updateHUD()
 
// On death
lives--
if (lives === 0) {
  bridge.setNav({
    status: "Game Over",
    actions: [{ id: "retry", label: "Try Again", icon: "↻" }],
  })
}

Analytics Integration

Event Naming Convention

Use snake_case for event names:

  • level_complete, boss_defeated, item_collected
  • levelComplete, BossDefeated, item-collected

Common Events

// Level progression
bridge.analytics("level_start", { level: 3, difficulty: "hard" })
bridge.analytics("level_complete", { level: 3, time: 120, stars: 3 })
 
// Engagement
bridge.analytics("game_start", { mode: "arcade" })
bridge.analytics("game_end", { duration: 300, reason: "completed" })
 
// Achievements
bridge.analytics("achievement_unlock", { id: "first_win", rarity: "common" })
 
// Economy
bridge.analytics("item_purchase", { item_id: "power_boost", cost: 100 })
 
// Errors
bridge.analytics("error_occurred", { code: "NETWORK_TIMEOUT", context: "save_game" })

Props Guidelines

  • Keep props JSON-serializable (no functions, circular refs)
  • Use consistent types (number for scores, not strings)
  • Include context (level, mode, timestamp)
  • Limit to ~10 props per event

Leaderboard Integration

Score Submission

// Simple submission
bridge.submitScore("weekly-leaderboard", 12500)
 
// With metadata
bridge.submitScore("weekly-leaderboard", 12500, {
  level: 5,
  time: 120,
  difficulty: "hard",
  multiplier: 2.5,
})

Error Handling

Platform handles errors automatically with toast notifications. Game can listen for failures:

bridge.on("score_submit_failed", (error) => {
  console.error("Score not submitted:", error)
  // Show in-game retry option
})

Theme Integration

let currentColors = {
  primary: "#7c3aed",
  background: "#111827",
}
 
window.addEventListener("message", (event) => {
  if (event.data?.type === "funday:theme-inject") {
    currentColors = event.data.colors
    applyTheme(currentColors)
  }
})
 
function applyTheme(colors) {
  document.documentElement.style.setProperty("--primary", colors.primary)
  document.documentElement.style.setProperty("--bg", colors.base1)
  // ... apply to canvas/game UI
}

Canvas Games

For canvas-based games, store theme colors and use in draw calls:

let themeColors = { primary: "#7c3aed" }
 
function drawUI(ctx) {
  ctx.fillStyle = themeColors.primary
  ctx.fillRect(10, 10, 100, 50)
}

Locale Integration

Simple i18n

const translations = {
  en: { start: "Start Game", quit: "Quit" },
  es: { start: "Iniciar Juego", quit: "Salir" },
  fr: { start: "Démarrer", quit: "Quitter" },
}
 
let currentLocale = "en"
 
window.addEventListener("message", (event) => {
  if (event.data?.type === "funday:locale-inject") {
    currentLocale = event.data.locale
    updateUIText()
  }
})
 
function t(key) {
  return translations[currentLocale]?.[key] || translations.en[key]
}

Testing Embed Mode

Manual Testing

  1. Standalone mode:

    http://localhost:5173/
    

    Should show navbar, footer, and full chrome.

  2. Embed mode:

    http://localhost:5173/?embed=1
    

    Should show only game canvas, no chrome.

  3. In platform:

    http://platform.localhost:5174/play/mygame
    

    Loads via iframe with embed=1 automatically.

Automated Tests

// Playwright test
test("plugin respects embed mode", async ({ page }) => {
  await page.goto("/mygame?embed=1")
 
  // Chrome should be hidden
  await expect(page.locator("#navbar")).toBeHidden()
  await expect(page.locator("#footer")).toBeHidden()
 
  // Game canvas should be visible
  await expect(page.locator("#game-canvas")).toBeVisible()
 
  // Should send bridge messages
  const messages = []
  await page.exposeFunction("captureMessage", (msg) => messages.push(msg))
  await page.evaluate(() => {
    window.addEventListener("message", (e) => {
      window.captureMessage(e.data)
    })
  })
 
  // Trigger handshake
  await page.evaluate(() => {
    window.postMessage({ type: "funday:handshake", version: "1" }, "*")
  })
 
  // Verify ack sent
  await page.waitForFunction(() => messages.some((m) => m.type === "funday:ack"))
})

Common Pitfalls

❌ Hardcoded Heights

/* BAD: Fixed height breaks in embedded viewport */
#game-container {
  height: 800px;
}

✅ Responsive Heights

/* GOOD: Fills parent iframe container */
#game-container {
  height: 100vh;
  height: 100dvh; /* dynamic viewport height */
}

❌ Absolute Positioning of Chrome

/* BAD: Still visible even with display:none */
#navbar {
  position: fixed;
  top: 0;
  z-index: 9999;
}

✅ Conditional Rendering

<!-- GOOD: DOM element removed entirely -->
{#if !isEmbedMode}
<Navbar />
{/if}

❌ Forgetting Origin Validation

// BAD: Accepts messages from any origin
window.addEventListener("message", (event) => {
  if (event.data?.type === "funday:handshake") {
    // ... process
  }
})

✅ Strict Origin Check

// GOOD: Validates origin
const PLATFORM_ORIGIN = "https://funday.gg"
window.addEventListener("message", (event) => {
  if (event.origin !== PLATFORM_ORIGIN) return
  // ... process
})

Migration Tracking

File: docs/PLUGIN_MIGRATION.md

Track per-plugin progress:

# Plugin Migration Status
 
## Completed
 
- [x] hexapipes - embed mode + full bridge (2025-01-15)
- [x] pong-multiplayer - embed mode + analytics (2025-01-16)
 
## In Progress
 
- [ ] tictactoe-multiplayer - embed mode done, bridge pending
- [ ] networked-snake - needs refactor for embed
 
## Pending
 
- [ ] game-template - reference implementation needed
- [ ] legacy-game-1 - assess effort

Resources

  • Bridge spec: docs/BRIDGE_V1.md
  • Security guide: docs/SANDBOX_SECURITY.md
  • App shell guide: docs/APP_SHELL.md
  • Example SDK: game-plugins/_sdk/

Support

For plugin integration help:

  • Review example plugins in game-plugins/
  • Check Bridge implementation in frontend/src/lib/games/bridge.ts
  • Test in standalone mode first, then embedded

Ask Docs

AI assistant to help answer questions about the documentation. Answers are read-only and cite docs/source.

Hi! How can I help you with the documentation today? Answers are read-only and cite docs/source.

Ctrl+Enter to send