dataconnect/demo

EX-01 · Components · Link previews

Discord component embeds

Since September 2026 a website can replace its Discord link preview with a layout built from the same Components V2 parts that bots use in messages: containers, text, galleries, sections, and link buttons. It needs one JSON tag in the page head, and no bot or app.

Announced
21 Sep 2026
Mechanism
discord:component-embed
Budget
40 components · 10 s fetch
Source
discord-api-docs #8606

How it works

  1. Someone posts a link. Discord's crawler fetches the page as Discordbot/2.0 and runs no JavaScript.
  2. It finds the tag. Either an inline <script id="discord:component-embed" type="application/json"> or a <link rel="discord:component-embed"> pointing at same-site JSON.
  3. It checks the payload. { "component": … } holding one Container, allowed component types only, and link buttons only. Then it fetches every image.
  4. It renders the card in place of the Open Graph preview. If anything fails, the Open Graph tags are the fallback.
<head>
  <meta property="og:title" content="…">            <!-- fallback -->
  <script id="discord:component-embed" type="application/json">
    { "component": { "type": 17, "components": [ … ] } }
  </script>
</head>

Designs

Five cards, one per page on this site. Each share URL serves its own payload, so pasting it into Discord shows that design. The previews below come from the same payloads, built for your request.

D1

Board

The home page card. Same shape as the Bellum server board: hero image, linked heading, a stat line, a timestamp, a divider, and a row of link buttons.

  • Media Gallery (1 item)
  • Markdown heading link
  • Relative timestamp
  • Divider
  • Action Row × 4 buttons
dataconnectToday at 5:55 PM
1 exhibit · 5 embed designs · 0 dependencies
A bench for trying web tech in public. Exhibit 01 is Discord component embeds, and this card is one of them.
Rendered at the CMH edge · as of
Approximation of Discord's dark theme. Times shown in UTC until your browser localises them.
Payload · inline in the page head Valid · 12 components · 1,139 bytes
{
  "component": {
    "type": 17,
    "accent_color": 15759941,
    "spoiler": false,
    "components": [
      {
        "type": 12,
        "items": [
          {
            "media": {
              "url": "https://demo-dataconnect-dev.pages.dev/og/home.png?v=e27972da6b"
            },
            "description": null,
            "spoiler": false
          }
        ]
      },
      {
        "type": 10,
        "content": "### [dataconnect demo · 1 exhibit live · 5 embed designs](https://demo-dataconnect-dev.pages.dev/)"
      },
      {
        "type": 10,
        "content": "**1** exhibit · **5** embed designs · **0** dependencies"
      },
      {
        "type": 10,
        "content": "A bench for trying web tech in public. Exhibit 01 is Discord component embeds, and this card is one of them."
      },
      {
        "type": 10,
        "content": "-# Rendered at the CMH edge · as of <t:1790186149:R>"
      },
      {
        "type": 14,
        "spacing": 1,
        "divider": true
      },
      {
        "type": 1,
        "components": [
          {
            "type": 2,
            "style": 5,
            "label": "Open the bench",
            "url": "https://demo-dataconnect-dev.pages.dev/"
          },
          {
            "type": 2,
            "style": 5,
            "label": "Component embeds",
            "url": "https://demo-dataconnect-dev.pages.dev/discord-embeds"
          },
          {
            "type": 2,
            "style": 5,
            "label": "Playground",
            "url": "https://demo-dataconnect-dev.pages.dev/discord-embeds#playground"
          },
          {
            "type": 2,
            "style": 5,
            "label": "Embed designs",
            "url": "https://demo-dataconnect-dev.pages.dev/discord-embeds#designs"
          }
        ]
      }
    ]
  }
}
D2

Spec sheet

The exhibit page card. A section with a thumbnail accessory for the heading, a checklist, and a footer line.

  • Section + Thumbnail accessory
  • Markdown list with inline code
  • Spacing-only separator
  • Subtext
dataconnectToday at 5:55 PM
Exhibit 01 · Link previews built from Components V2 layouts. No bot, no app, just a tag in the page head.
Exhibit 01 mark
  • One <script id="discord:component-embed"> holding { "component": … }
  • Up to 40 components; buttons are link-style only
  • Open Graph tags stay as the fallback card
Exhibit 01 · demo-dataconnect-dev.pages.dev ·
Approximation of Discord's dark theme. Times shown in UTC until your browser localises them.
Payload · inline in the page head Valid · 13 components · 1,227 bytes
{
  "component": {
    "type": 17,
    "accent_color": 15759941,
    "spoiler": false,
    "components": [
      {
        "type": 9,
        "components": [
          {
            "type": 10,
            "content": "## [Discord component embeds](https://demo-dataconnect-dev.pages.dev/discord-embeds)"
          },
          {
            "type": 10,
            "content": "Exhibit 01 · Link previews built from Components V2 layouts. No bot, no app, just a tag in the page head."
          }
        ],
        "accessory": {
          "type": 11,
          "media": {
            "url": "https://demo-dataconnect-dev.pages.dev/og/mark.png?v=f082f5cea0"
          },
          "description": "Exhibit 01 mark"
        }
      },
      {
        "type": 14,
        "spacing": 1,
        "divider": false
      },
      {
        "type": 10,
        "content": "- One `<script id=\"discord:component-embed\">` holding `{ \"component\": … }`\n- Up to **40** components; buttons are link-style only\n- Open Graph tags stay as the fallback card"
      },
      {
        "type": 14,
        "spacing": 1,
        "divider": true
      },
      {
        "type": 1,
        "components": [
          {
            "type": 2,
            "style": 5,
            "label": "Read the exhibit",
            "url": "https://demo-dataconnect-dev.pages.dev/discord-embeds"
          },
          {
            "type": 2,
            "style": 5,
            "label": "Try the playground",
            "url": "https://demo-dataconnect-dev.pages.dev/discord-embeds#playground"
          },
          {
            "type": 2,
            "style": 5,
            "label": "Docs PR #8606",
            "url": "https://github.com/discord/discord-api-docs/pull/8606"
          }
        ]
      },
      {
        "type": 10,
        "content": "-# Exhibit 01 · demo-dataconnect-dev.pages.dev · <t:1790186149:R>"
      }
    ]
  }
}
D3

Request receipt

Built per request from Cloudflare's request.cf data, so the card in Discord describes Discord's own crawler fetch: edge, network, protocol, and time.

  • Server-rendered per request
  • Absolute + relative timestamps
  • Inline code
  • Trailing subtext
dataconnectToday at 5:55 PM
What the edge saw when this link was fetched.
Edge CMH · Country US
Network Anthropic, PBC
Protocol HTTP/2 · TLS TLSv1.3
Client Mozilla/5.0 AppleWebKit/537.36 (KHTML, l
Fetched ()
Discord caches a preview for about 30 minutes. Share it with ?v= plus anything to force a fresh fetch.
Approximation of Discord's dark theme. Times shown in UTC until your browser localises them.
Payload · inline in the page head Valid · 9 components · 944 bytes
{
  "component": {
    "type": 17,
    "accent_color": 4165498,
    "spoiler": false,
    "components": [
      {
        "type": 10,
        "content": "### [Request receipt](https://demo-dataconnect-dev.pages.dev/discord-embeds/receipt)\nWhat the edge saw when this link was fetched."
      },
      {
        "type": 14,
        "spacing": 1,
        "divider": true
      },
      {
        "type": 10,
        "content": "**Edge** `CMH` · **Country** `US`\n**Network** `Anthropic, PBC`\n**Protocol** `HTTP/2` · **TLS** `TLSv1.3`\n**Client** `Mozilla/5.0 AppleWebKit/537.36 (KHTML, l`\n**Fetched** <t:1790186149:F> (<t:1790186149:R>)"
      },
      {
        "type": 14,
        "spacing": 1,
        "divider": true
      },
      {
        "type": 1,
        "components": [
          {
            "type": 2,
            "style": 5,
            "label": "Get your own receipt",
            "url": "https://demo-dataconnect-dev.pages.dev/discord-embeds/receipt"
          },
          {
            "type": 2,
            "style": 5,
            "label": "How it works",
            "url": "https://demo-dataconnect-dev.pages.dev/discord-embeds#designs"
          }
        ]
      },
      {
        "type": 10,
        "content": "-# Discord caches a preview for about 30 minutes. Share it with `?v=` plus anything to force a fresh fetch."
      }
    ]
  }
}
D5

Linked JSON

Nothing inline: the page head has a <link rel="discord:component-embed"> pointing at a JSON endpoint. The card reports its own size against the 3,000-byte cap.

  • <link rel> discovery
  • Same-site JSON endpoint
  • Self-measuring payload
dataconnectToday at 5:55 PM
This card is not in the page. The page links to /api/embed/linked, and this payload is 703 bytes of the 3,000 allowed.
Same-site href · absolute https · 3,000-byte cap ·
Approximation of Discord's dark theme. Times shown in UTC until your browser localises them.
Payload · served from /api/embed/linked Valid · 7 components · 703 bytes
{
  "component": {
    "type": 17,
    "accent_color": 11569455,
    "spoiler": false,
    "components": [
      {
        "type": 10,
        "content": "### [Linked JSON](https://demo-dataconnect-dev.pages.dev/discord-embeds/linked)\nThis card is not in the page. The page links to `/api/embed/linked`, and this payload is **703 bytes** of the 3,000 allowed."
      },
      {
        "type": 14,
        "spacing": 1,
        "divider": true
      },
      {
        "type": 1,
        "components": [
          {
            "type": 2,
            "style": 5,
            "label": "Open the JSON",
            "url": "https://demo-dataconnect-dev.pages.dev/api/embed/linked"
          },
          {
            "type": 2,
            "style": 5,
            "label": "Back to the exhibit",
            "url": "https://demo-dataconnect-dev.pages.dev/discord-embeds#designs"
          }
        ]
      },
      {
        "type": 10,
        "content": "-# Same-site href · absolute https · 3,000-byte cap · <t:1790186149:R>"
      }
    ]
  }
}

Playground

Edit a payload and check it against the published rules. It runs entirely in your browser with the same validator and renderer this site uses on the server.

Field notes

  1. Input is not output

    The message API shows a component embed as embeds[].type = "components", with component ids, proxy_url, placeholders, and sizes. That is what Discord built from the page. Pasting it back as input fails: an id on a button invalidates the whole payload. Load Discord's unfurl output in the playground to see the errors.

  2. Render on the server

    Discordbot runs no JavaScript, so a tag injected by a client framework never gets read. Here, Pages Functions write the tag into the HTML on each request, which is also how the timestamps and edge codes stay live.

  3. Let the crawler in

    Bot challenges block previews silently. Discordbot needs a plain 200 for the page and for every image. On Cloudflare, check Bot Fight Mode and any WAF rules for user agents containing Discordbot.

  4. Version your images

    Every image is fetched within the 10-second budget and then proxied. The images here carry a content hash (?v=…) so a new image gets a new URL instead of a stale proxy copy.

  5. Caching is about 30 minutes

    Resharing the same URL shows the cached card. Add a query string such as ?v=2 to force a fresh fetch. Changing only the #fragment doesn't work.

  6. Linked JSON has tighter rules

    The href must be absolute https on the same site (a parent or subdomain also counts), and the response is capped at 3,000 raw bytes. Inline scripts have no byte cap, just the 40-component limit.

  7. Keep the Open Graph tags

    They are the fallback whenever the component embed can't be used, and they are what every other platform reads. Every page here ships both.

  8. Timestamps localise for free

    <t:unix:R> in a text display renders in each reader's own time zone and language, and relative times keep updating.