Plugin Embed Guide (historical)
Lifecycle: HISTORICAL SUPPORTING
Recovered from legacy wiki pathcontent/archive/architecture/PLUGIN_EMBED_GUIDE.md(attic copy retained; not deleted).
Prefer current SSOT pages for production decisions. Last promoted: 2026-07-20.
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