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:
- Detect
embed=1query parameter - Hide all platform chrome (nav, footer, branding)
- Render only the game canvas/content
- 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=1parameter - 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:handshakemessage - Send
funday:ackacknowledgment - Send
game:readywhen playable - Emit
funday:nav:setfor HUD updates - Emit
funday:analytics-eventfor telemetry - Emit
funday:score-submittedfor leaderboards - Handle
funday:theme-injectfor theming - Handle
funday:locale-injectfor i18n
Phase 3: Polish
- Add Exit button that sends
game:close - Handle
funday:pause/funday:resumemessages - Support fullscreen via
game:fullscreenevent - 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
Recommended Pattern
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
-
Standalone mode:
http://localhost:5173/Should show navbar, footer, and full chrome.
-
Embed mode:
http://localhost:5173/?embed=1Should show only game canvas, no chrome.
-
In platform:
http://platform.localhost:5174/play/mygameLoads 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 effortResources
- 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