Skip to content

Embed Plugins ​

What it does ​

An embed plugin responds to slash commands. A user types /weather (or any command registered by the plugin), and the plugin returns an HTML embed that renders inline in the message feed. The embed is ephemeral — it's not stored in message history.

When to use this context ​

Use embed plugins for:

  • Slash command results: /weather London, /rank player-name, /stock AAPL
  • Lookups: emoji picker, giphy, meme generator
  • Live data: score check, sports stats, cryptocurrency prices
  • Transformations: markdown to PDF, JSON validator output

Embeds are great for utility commands that users invoke on demand.

Slash commands do not reach plugins yet

Dissent.onCommand() exists in the SDK, but the current app's chat composer does not route a typed /command to a plugin, so the handler is never called. The rest of this page describes the intended flow. See onCommand in the SDK reference.

Tier requirement ​

Nothing enforces a tier for embed plugins: registering a command and rendering its embed need no permission.

Minimal example ​

html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <script src="https://app.dissent.chat/sdk/v1/dissent-plugin-sdk.iife.js"></script>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: system-ui, sans-serif;
      background: transparent;
      color: rgba(255,255,255,0.85);
      padding: 12px;
    }
    .embed {
      background: rgba(255,255,255,0.04);
      border-left: 4px solid #3b82f6;
      border-radius: 8px;
      padding: 12px;
      max-width: 400px;
    }
    .title {
      font-size: 14px;
      font-weight: 700;
      margin-bottom: 8px;
    }
    .content {
      font-size: 13px;
      color: rgba(255,255,255,0.7);
      line-height: 1.5;
    }
  </style>
</head>
<body>
  <div class="embed">
    <div class="title" id="location">London, UK</div>
    <div class="content">
      <div id="weather-main">Clear sky</div>
      <div id="weather-temp">15°C</div>
      <div id="weather-wind">Wind: 12 km/h</div>
    </div>
  </div>
  <script>
    async function main() {
      const ctx = await Dissent.init({
        onFallback: () => {
          document.body.textContent = 'Running outside Dissent';
        },
      });

      // Apply theme
      for (const [k, v] of Object.entries(ctx.theme)) {
        document.documentElement.style.setProperty(k, v);
      }

      // Parse command arguments from the embed payload
      const { args } = ctx.embed ?? {};
      const location = args?.[0] ?? 'London';

      try {
        // Fetch weather from Open-Meteo (free, no API key needed)
        const geoRes = await Dissent.fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${location}&count=1`);
        const geoData = JSON.parse(geoRes.body);

        if (!geoData.results?.[0]) {
          document.body.textContent = `Location not found: ${location}`;
          return;
        }

        const { latitude, longitude, name, country } = geoData.results[0];
        document.getElementById('location').textContent = `${name}, ${country}`;

        // Fetch weather
        const weatherRes = await Dissent.fetch(
          `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m,weather_code,wind_speed_10m`
        );
        const weatherData = JSON.parse(weatherRes.body);
        const current = weatherData.current;

        const weatherCode = {
          0: 'Clear sky',
          1: 'Mainly clear',
          2: 'Partly cloudy',
          3: 'Overcast',
          45: 'Foggy',
          48: 'Depositing rime fog',
          51: 'Light drizzle',
          61: 'Slight rain',
          71: 'Slight snow',
          80: 'Moderate rain',
          85: 'Moderate snow showers',
          95: 'Thunderstorm',
        }[current.weather_code] ?? 'Unknown';

        document.getElementById('weather-main').textContent = weatherCode;
        document.getElementById('weather-temp').textContent = `${Math.round(current.temperature_2m)}°C`;
        document.getElementById('weather-wind').textContent = `Wind: ${Math.round(current.wind_speed_10m)} km/h`;
      } catch (err) {
        document.body.textContent = `Error: ${err.message}`;
        console.error(err);
      }

      // Theme updates
      Dissent.on('theme:change', (theme) => {
        for (const [k, v] of Object.entries(theme)) {
          document.documentElement.style.setProperty(k, v);
        }
      });
    }

    main().catch(err => console.error(err));
  </script>
</body>
</html>

The EmbedPayload shape ​

When your embed plugin is rendered, it receives an EmbedPayload via ctx.embed:

typescript
interface EmbedPayload {
  html: string;          // The full HTML you just returned
  height?: number;       // Optional: height in pixels (defaults to 200px)
  args?: string[];       // Arguments from the slash command
}

The embed is inserted into the message feed at the size you specify. Use args to read the slash command parameters the user provided.

Gotchas ​

  • Ephemeral by design. Embeds are not stored in message history. Reload the channel and they're gone.
  • Visible to all connected members. Everyone in the channel sees the embed in real-time.
  • No tier is required. Registering a command and rendering an embed need no permission.
  • ctx.channel is always present. The embed is rendered inside a channel context.
  • Keep embed HTML self-contained. Don't rely on external stylesheets or scripts (except the SDK). Inline all CSS. No relative paths.