# Link previews

> **Info:** This is release candidate documentation for **Stream Chat React SDK v15**. For the latest stable version, see the [latest version (v14)](https://getstream.io/chat/docs/sdk/react/).

A link preview is the card with a title, a description and an image that shows for a link in a message. This page covers how the composer loads a preview while the user types, what's sent with the message, and how the message list shows it.

## What users get

The user types "Look at `https://example.com/launch`". When they stop typing, a card appears above the input with the page's image, title and description, and the URL. Hovering the card shows the full URL. They can dismiss it with its ✕. When they send, the message in the list shows a card for the link above the text.

## How it works across the layers

- **State.** The composer's `linkPreviewsManager` finds the links in the text, loads a preview for each, and keeps them in its store. Sending adds the loaded previews to the message as attachments.
- **UI.** `LinkPreviewList` shows the previews above the input, as `LinkPreviewCard`s. In the message list, `Attachment` renders each preview with `Card`.
- **Layout.** Not involved.

### From typing to sending

1. Each text change starts a wait of `linkPreviews.debounceURLEnrichmentMs`, 1500 ms by default. When the user stops typing for that long, `linkPreviews.findURLFn` finds the URLs in the text.
2. Each new URL gets a preview with status `loading`, and the manager asks the server for the page's data with `client.getOG({ url })`. The preview becomes `loaded`, or `failed` when the request fails. When the text has no URL left, all previews are cleared.
3. Dismissing a preview calls `LinkPreviewsManager.dismissPreview(url)`. Its status becomes `dismissed`, and `linkPreviews.onLinkPreviewDismissed` is called.
4. On send, the composition middleware adds every loaded preview to the message's attachments, and tells the server not to add previews of its own (`skip_enrich_url`). It also sets `skip_enrich_url` when the user dismissed a preview, but only when the message carries attachments. A text-only message whose preview was dismissed goes out without the flag, and the server adds the card.
5. If a preview is still loading when the user sends, no previews are sent, and the server adds them while it saves the message.

[Message composer](https://getstream.io/chat/docs/sdk/react/v15/state/writing-messages/message-composer/#link-previews) lists the statuses.

### In the composer and the message list

`LinkPreviewList` shows the previews that are loading or loaded. It shows only the first one by default; the others are still sent. `LinkPreviewCard` shows the image, title, description and URL, and its ✕ dismisses the preview.

In the message list, an attachment with `og_scrape_url` or `title_link` is a link preview, and `Attachment` renders it with `Card`.

## Turning it on or off

Link previews are on by default. Two sides decide:

- **The client:** `messageComposer.linkPreviews.enabled`, `true` by default. It decides whether the composer loads and shows previews.
- **The server:** the channel type's `url_enrichment` setting. It decides whether the server adds previews to messages. With it off, the composer doesn't load previews either.

To stop the composer from loading previews:

```ts
client.config.set({
  messageComposer: { linkPreviews: { enabled: false } },
});
```

The server still adds previews to sent messages while `url_enrichment` is on, so messages keep their cards; only the composer stops showing them. To send messages without cards, turn `url_enrichment` off for the channel type. [Configuration](https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/configuration/#who-decides-what) lists every setting both sides decide.

## Customizing

### Change how links are found and loaded

```ts
import type { LinkPreview } from "stream-chat";

client.config.set({
  messageComposer: {
    linkPreviews: {
      // Only links to your own domain get a preview.
      findURLFn: (text: string) =>
        Array.from(
          text.matchAll(/https:\/\/example\.com\/\S*/g),
          ([url]) => url,
        ),
      onLinkPreviewDismissed: (linkPreview: LinkPreview) => {
        console.log(`Dismissed ${linkPreview.og_scrape_url}`);
      },
    },
  },
});
```

`debounceURLEnrichmentMs` is read only when a composer is built, so set it before the client creates composers, for example in the `config` you pass to `useCreateChatClient`.

### Show more than one preview

Wrap `LinkPreviewList` and pass `displayLinkCount`:

```tsx
import {
  Channel,
  LinkPreviewList,
  MessageComposer,
  MessageList,
  WithComponents,
} from "stream-chat-react";

const ThreeLinkPreviews = () => <LinkPreviewList displayLinkCount={3} />;

const overrides = { LinkPreviewList: ThreeLinkPreviews };

export const ChannelWithThreePreviews = () => (
  <WithComponents overrides={overrides}>
    <Channel channel={channel}>
      <MessageList />
      <MessageComposer />
    </Channel>
  </WithComponents>
);
```

### Replace the preview list

A list of your own reads `linkPreviewsManager` from the composer and renders the previews it wants. `LinkPreviewsManager.previewIsLoaded()` and `LinkPreviewsManager.previewIsLoading()` tell the statuses apart:

```tsx
import type { LinkPreviewsManagerState } from "stream-chat";
import { LinkPreviewsManager } from "stream-chat";
import {
  LinkPreviewCard,
  useMessageComposerController,
  useStateStore,
} from "stream-chat-react";

const loadedPreviewsSelector = ({ previews }: LinkPreviewsManagerState) => ({
  previews: Array.from(previews.values()).filter(
    LinkPreviewsManager.previewIsLoaded,
  ),
});

export const LoadedLinkPreviews = () => {
  const { linkPreviewsManager } = useMessageComposerController();
  const { previews } = useStateStore(
    linkPreviewsManager.state,
    loadedPreviewsSelector,
  );

  if (!previews.length) return null;

  return (
    <div className="loaded-link-previews">
      {previews.map((preview) => (
        <LinkPreviewCard key={preview.og_scrape_url} linkPreview={preview} />
      ))}
    </div>
  );
};
```

Register it with `<WithComponents overrides={{ LinkPreviewList: LoadedLinkPreviews }}>`. It works in channel, thread and edit composers alike, because `useMessageComposerController()` returns the composer it's rendered in.

### Change the card in the message list

Pass a `Card` of your own to `Attachment`, and register the result as the `Attachment` override:

```tsx
import type { AttachmentProps, CardProps } from "stream-chat-react";
import { Attachment } from "stream-chat-react";

const CompactCard = ({ og_scrape_url, title, title_link }: CardProps) => (
  <a className="compact-card" href={title_link ?? og_scrape_url}>
    {title ?? og_scrape_url}
  </a>
);

export const AttachmentWithCompactCards = (props: AttachmentProps) => (
  <Attachment {...props} Card={CompactCard} />
);
```

## Reference

- [Attachments](https://getstream.io/chat/docs/sdk/react/v15/component-reference/message/attachments/): the `Attachment` props, including `Card`.
- [Composer UI components](https://getstream.io/chat/docs/sdk/react/v15/component-reference/message-composer/composer-ui-components/): `LinkPreviewList` and the composer's other parts.
- [MessageComposer API](https://getstream.io/chat/docs/sdk/react/v15/reference/state/messagecomposer-api/): `linkPreviewsManager`.
- [Configuration tree](https://getstream.io/chat/docs/sdk/react/v15/reference/state/configuration-tree/): every `messageComposer.linkPreviews` setting.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v15/features/rich-content/link-previews/](https://getstream.io/chat/docs/sdk/react/v15/features/rich-content/link-previews/).