Plugin Embed Guide (historical)

Lifecycle: HISTORICAL SUPPORTING
Recovered from legacy wiki path content/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:

  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