social-embeds 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +309 -0
- package/dist/index-CGSxjvGN.d.ts +27 -0
- package/dist/index.d.ts +155 -0
- package/dist/index.js +938 -0
- package/dist/parser-DVync2rz.js +747 -0
- package/dist/parser.d.ts +2 -0
- package/dist/parser.js +1 -0
- package/package.json +65 -4
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Atanas Mitrev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# social-embeds
|
|
2
|
+
|
|
3
|
+
Turn social media embed code or post URLs into working React components.
|
|
4
|
+
|
|
5
|
+
Pasting a platform's embed code into `dangerouslySetInnerHTML` does not work in React: the `<script>` tag never runs, every embed adds the same script again, and nothing initialises after client-side navigation. `social-embeds` recognises the platform, reads the post and its options from the embed code, drops the `<script>` tag, loads each platform script once per page and asks the platform to render the post. Iframe-based platforms get a responsive `<iframe>` instead.
|
|
6
|
+
|
|
7
|
+
- X, Instagram, Threads, Facebook, TikTok and YouTube
|
|
8
|
+
- React 19, Next.js App Router (Server Components, client navigation), or plain React
|
|
9
|
+
- Zero runtime dependencies, ESM only, typed
|
|
10
|
+
- No styling and no defaults beyond what the embed code or your props ask for
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install social-embeds
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`react` and `react-dom` 19 are peer dependencies.
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { SocialEmbed } from 'social-embeds';
|
|
24
|
+
|
|
25
|
+
// Embed code, exactly as the platform's "Embed" dialog gave it
|
|
26
|
+
<SocialEmbed html={post.embedCode} />
|
|
27
|
+
|
|
28
|
+
// Or a post URL
|
|
29
|
+
<SocialEmbed url="https://x.com/jack/status/20" />
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Inputs: `html` vs `url`
|
|
33
|
+
|
|
34
|
+
There are two inputs, and they lead to the same component:
|
|
35
|
+
|
|
36
|
+
- **`html`**: the embed code from the platform's Embed dialog, stored as-is (for example in your CMS). It carries the post's options (theme, caption, start time, and so on), and for X, Instagram, Threads and some Facebook code it also carries **fallback content**: the post text, author and link inside the `<blockquote>`. That content is rendered on the server, so crawlers and visitors without JavaScript see it, and the platform script upgrades it in place.
|
|
37
|
+
- **`url`**: the post's address. A URL has no fallback content, so until the post has loaded the component shows a placeholder. This is a property of the input, not a limitation of the package. A URL carries no options either, except a YouTube start time (`t` or `start`) and the privacy-enhanced host.
|
|
38
|
+
|
|
39
|
+
Embed code is matched strictly: it must be one platform's own snippet, as the platform produced it. Extra wrappers, other markup or unknown scripts make it unrecognised (see [`fallback`](#unrecognised-embed-code)).
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
### `<SocialEmbed>`
|
|
44
|
+
|
|
45
|
+
`<SocialEmbed>` takes `html` or `url` (never both) and renders the matching platform component.
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { SocialEmbed } from 'social-embeds';
|
|
49
|
+
|
|
50
|
+
export function Embed({ code }: { code: string }) {
|
|
51
|
+
return (
|
|
52
|
+
<SocialEmbed
|
|
53
|
+
html={code}
|
|
54
|
+
maxWidth={560}
|
|
55
|
+
onLoad={() => console.log('rendered')}
|
|
56
|
+
onError={(error) => console.warn(error.message)}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
It accepts the [shared props](#shared-props); `height` reaches only the iframe-based platforms. Platform options come from the embed code. To set them with props, use a platform component.
|
|
63
|
+
|
|
64
|
+
#### Unrecognised embed code
|
|
65
|
+
|
|
66
|
+
When `html` is not a recognised embed code, `<SocialEmbed>` calls `fallback` with the original string and renders what it returns. Without `fallback`, it renders nothing and calls `onError`.
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
<SocialEmbed
|
|
70
|
+
html={code}
|
|
71
|
+
fallback={(html) => (
|
|
72
|
+
// Only if your CMS sanitised this HTML upstream: social-embeds never does.
|
|
73
|
+
<div dangerouslySetInnerHTML={{ __html: html }} />
|
|
74
|
+
)}
|
|
75
|
+
/>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
An unsupported URL never reaches `fallback`: nothing is rendered and `onError` names the URL.
|
|
79
|
+
|
|
80
|
+
### Platform components
|
|
81
|
+
|
|
82
|
+
Each platform component takes a `url`, or an `embed` already parsed by [`social-embeds/parser`](#parser), plus the shared props and its own option props. Option props override options from the embed code.
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import {
|
|
86
|
+
FacebookEmbed,
|
|
87
|
+
InstagramEmbed,
|
|
88
|
+
ThreadsEmbed,
|
|
89
|
+
TikTokEmbed,
|
|
90
|
+
XEmbed,
|
|
91
|
+
YouTubeEmbed,
|
|
92
|
+
} from 'social-embeds';
|
|
93
|
+
|
|
94
|
+
<XEmbed url="https://x.com/jack/status/20" theme="dark" cards="hidden" />
|
|
95
|
+
|
|
96
|
+
<InstagramEmbed url="https://www.instagram.com/p/DfPQchkG1tg/" captioned />
|
|
97
|
+
|
|
98
|
+
<ThreadsEmbed url="https://www.threads.com/@instagram/post/DfQSavwEs5C" theme="auto" />
|
|
99
|
+
|
|
100
|
+
<FacebookEmbed
|
|
101
|
+
url="https://www.facebook.com/facebook/videos/10153231379946729/"
|
|
102
|
+
locale="bg_BG"
|
|
103
|
+
showCaptions
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
<TikTokEmbed
|
|
107
|
+
url="https://www.tiktok.com/@scout2015/video/6718335390845095173"
|
|
108
|
+
musicInfo={false}
|
|
109
|
+
description={false}
|
|
110
|
+
/>
|
|
111
|
+
|
|
112
|
+
<YouTubeEmbed url="https://youtu.be/jNQXAC9IVRw?t=90" nocookie controls={false} />
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
| Component | Option props |
|
|
116
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
117
|
+
| `XEmbed` | `theme` (`'light'` \| `'dark'`), `conversation` (`'none'`), `cards` (`'hidden'`), `align` (`'left'` \| `'center'` \| `'right'`), `lang`, `dnt` |
|
|
118
|
+
| `InstagramEmbed` | `captioned` |
|
|
119
|
+
| `ThreadsEmbed` | `theme` (`'light'` \| `'dark'` \| `'auto'`) |
|
|
120
|
+
| `FacebookEmbed` | `locale` (SDK locale, default `'en_US'`), `showText`; videos also `allowfullscreen`, `autoplay`, `showCaptions` |
|
|
121
|
+
| `TikTokEmbed` | the [Embed Player](https://developers.tiktok.com/doc/embed-player) parameters: `controls`, `progressBar`, `playButton`, `volumeControl`, `fullscreenButton`, `timestamp`, `loop`, `autoplay`, `musicInfo`, `description`, `rel`, `nativeContextMenu`, `closedCaption`, `forceMuted` |
|
|
122
|
+
| `YouTubeEmbed` | the [player parameters](https://developers.google.com/youtube/player_parameters): `autoplay`, `ccLangPref`, `ccLoadPolicy`, `color`, `controls`, `disablekb`, `enablejsapi`, `end`, `fs`, `hl`, `ivLoadPolicy`, `list`, `listType`, `loop`, `origin`, `playlist`, `playsinline`, `rel`, `start`, `widgetReferrer`; plus `nocookie` (play from youtube-nocookie.com) |
|
|
123
|
+
|
|
124
|
+
Boolean player parameters are written as `1`/`0`.
|
|
125
|
+
|
|
126
|
+
### Parser
|
|
127
|
+
|
|
128
|
+
`social-embeds/parser` exports `parseEmbed(html)` and `matchUrl(url)`. They are pure functions with no React and no DOM, so they run the same in a Server Component, in Node (for example when your CMS saves a post) and in the browser. Both return a plain, serialisable `ParsedEmbed`, or `null` for input they do not recognise. They never throw.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { matchUrl, parseEmbed, type ParsedEmbed } from 'social-embeds/parser';
|
|
132
|
+
|
|
133
|
+
const embed = parseEmbed(code);
|
|
134
|
+
// {
|
|
135
|
+
// platform: 'x',
|
|
136
|
+
// kind: 'post',
|
|
137
|
+
// url: 'https://x.com/jack/status/20',
|
|
138
|
+
// id: '20',
|
|
139
|
+
// variant: 'sdk',
|
|
140
|
+
// options: { theme: 'dark' },
|
|
141
|
+
// fallbackHtml: '<p lang="en" dir="ltr">just setting up my twttr</p>…',
|
|
142
|
+
// }
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
type ParsedEmbed = {
|
|
147
|
+
platform: 'x' | 'instagram' | 'facebook' | 'threads' | 'tiktok' | 'youtube';
|
|
148
|
+
kind: 'post' | 'video';
|
|
149
|
+
url: string; // canonical post URL
|
|
150
|
+
id: string;
|
|
151
|
+
variant: 'sdk' | 'iframe'; // which rendering path applies
|
|
152
|
+
options: Record<string, string | number | boolean>;
|
|
153
|
+
size?: { width?: number; height?: number };
|
|
154
|
+
fallbackHtml?: string; // rebuilt from a whitelist, only from embed code
|
|
155
|
+
};
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`fallbackHtml` is not the raw input: it is rebuilt from the elements and attributes that appear in the platform's own markup, and everything else is dropped. Pass the result to the platform component as `embed`:
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
import { YouTubeEmbed } from 'social-embeds';
|
|
162
|
+
|
|
163
|
+
{
|
|
164
|
+
embed?.platform === 'youtube' && <YouTubeEmbed embed={embed} />;
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
A component checks an `embed` before using it: its `url` must be one `matchUrl` recognises with the same platform and id, and `fallbackHtml` must be exactly what the parser would produce. Otherwise it calls `onError`, or drops the fallback content.
|
|
169
|
+
|
|
170
|
+
## Shared props
|
|
171
|
+
|
|
172
|
+
| Prop | Type | Default | Notes |
|
|
173
|
+
| ------------- | ------------------------ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
174
|
+
| `url` | `string` | | Post URL. Platform components take `url` or `embed`; `<SocialEmbed>` takes `url` or `html`. |
|
|
175
|
+
| `embed` | `ParsedEmbed` | | Platform components only. |
|
|
176
|
+
| `html` | `string` | | `<SocialEmbed>` only. |
|
|
177
|
+
| `width` | `number \| string` | `'100%'` | Wrapper width. |
|
|
178
|
+
| `maxWidth` | `number` | per platform | The embed fills its container up to this width, in pixels. See [Platforms](#platforms). |
|
|
179
|
+
| `height` | `number` | | Iframe-based embeds only (YouTube, TikTok, Facebook video embed code): a fixed height instead of the aspect ratio. The others size themselves. |
|
|
180
|
+
| `placeholder` | `ReactNode` | a skeleton | Shown until the post has loaded. The default skeleton has a per-platform minimum height to limit layout shift. |
|
|
181
|
+
| `lazy` | `boolean` | `true` | Load the embed only when it comes within 200 px of the viewport. |
|
|
182
|
+
| `onLoad` | `() => void` | | Called once the platform has rendered the post (iframe embeds: when the iframe has loaded). |
|
|
183
|
+
| `onError` | `(error: Error) => void` | | Called on invalid input, a script that fails to load, or a post that does not render. Components never throw. |
|
|
184
|
+
| `...rest` | `ComponentProps<'div'>` | | Passed to the wrapper `div` (`className`, `style`, `data-*`, …). |
|
|
185
|
+
|
|
186
|
+
Script-based embeds try to render three times, waiting 15 s each time, 1 s and then 2 s apart, before `onError` reports the failure.
|
|
187
|
+
|
|
188
|
+
## Platforms
|
|
189
|
+
|
|
190
|
+
| Platform | Mechanism | URL formats | Options read from embed code | Default `maxWidth` |
|
|
191
|
+
| --------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- |
|
|
192
|
+
| X | script (`widgets.js`), `twttr.widgets.load` | `x.com/{user}/status/{id}`, `twitter.com/…` (also `www.` and `mobile.`) | `data-theme`, `data-conversation`, `data-cards`, `data-align` or `align`, `data-lang`, `data-dnt`, `data-width` | 550 |
|
|
193
|
+
| Instagram | script (`embed.js`), `instgrm.Embeds.process` | `instagram.com/p/{code}/`, `/reel/`, `/reels/`, `/tv/`, `/{user}/p/{code}/`, `/{user}/reel/{code}/` | `data-instgrm-captioned`, `data-instgrm-version` | 540 |
|
|
194
|
+
| Threads | script (`embed.js`), `instgrm.Embeds.process` | `threads.com/@{user}/post/{code}`, `threads.com/t/{code}` (and `threads.net`) | `data-theme`, `data-text-post-version` | 658 |
|
|
195
|
+
| Facebook | posts and video URLs: SDK (`sdk.js`), `FB.XFBML.parse`; video embed code: iframe | see [Facebook](#facebook) | `data-width`, `data-show-text`, `data-lazy`, `data-allowfullscreen`, `data-autoplay`, `data-show-captions`; iframe query (`show_text`, `width`, …); SDK locale | posts 750, videos none |
|
|
196
|
+
| TikTok | iframe ([Embed Player](https://developers.tiktok.com/doc/embed-player)) | `tiktok.com/@{user}/video/{id}` | every player query parameter | 325 |
|
|
197
|
+
| YouTube | iframe | `youtube.com/watch?v={id}`, `youtu.be/{id}`, `/shorts/{id}`, `/embed/{id}`, `/live/{id}`, `youtube-nocookie.com/embed/{id}`; `t` and `start` are kept | every player query parameter; the youtube-nocookie.com host | none |
|
|
198
|
+
|
|
199
|
+
URLs must be absolute `http(s)` URLs. Hosts may have `www.`; YouTube also `m.`, Facebook also `m.`.
|
|
200
|
+
|
|
201
|
+
### X
|
|
202
|
+
|
|
203
|
+
- The post that renders is the one the blockquote links to.
|
|
204
|
+
- widgets.js loads from `platform.twitter.com`; a host page that already loaded it from `platform.x.com` or `platform.twitter.com` is reused.
|
|
205
|
+
|
|
206
|
+
### Instagram
|
|
207
|
+
|
|
208
|
+
- Embed code has no caption text: its fallback content is "View this post on Instagram" and the "A post shared by …" link.
|
|
209
|
+
- The canonical URL is `https://www.instagram.com/{p|reel|tv}/{code}/`; `reels` becomes `reel` and a username in the path is dropped, because Instagram's embed only works with those paths.
|
|
210
|
+
- A post that has been removed never reports back, so `onError` fires after the retries.
|
|
211
|
+
- Instagram's script renders at most three posts at a time and starts the next one when one finishes, so on a page with many posts some appear later.
|
|
212
|
+
|
|
213
|
+
### Threads
|
|
214
|
+
|
|
215
|
+
- Embed code's fallback content is only a "View on Threads" link: Threads does not include the post text.
|
|
216
|
+
- The oEmbed `maxwidth` parameter only changes an inline style, which is not read. Use `maxWidth`.
|
|
217
|
+
- Threads' script also renders at most three posts at a time, and retries waiting ones after 5 s and 10 s.
|
|
218
|
+
- Threads and Instagram share one global (`instgrm.Embeds`). Each platform's own registration is what counts as ready, so Instagram's script alone never makes Threads look loaded.
|
|
219
|
+
|
|
220
|
+
### Facebook
|
|
221
|
+
|
|
222
|
+
The rules:
|
|
223
|
+
|
|
224
|
+
- **Posts** always render through the SDK, including when the embed code is the `plugins/post.php` iframe. That iframe's height only fits the `width` it was made for, so it cannot be responsive; the SDK resizes the post itself. `show_text` and `width` from the iframe carry over; `height` is dropped.
|
|
225
|
+
- **Video embed code** (the `plugins/video.php` iframe) renders as that iframe, in a box with its aspect ratio. No script.
|
|
226
|
+
- **Video URLs** and `fb-video` SDK code render through the SDK, which knows the video's real size (videos are both 16:9 and vertical).
|
|
227
|
+
|
|
228
|
+
URL shapes, each tested against Facebook's plugins on `www.facebook.com`, `facebook.com` and `m.facebook.com`:
|
|
229
|
+
|
|
230
|
+
| Kind | Shapes |
|
|
231
|
+
| ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
232
|
+
| Post | `/{user}/posts/{id}`, `/{user}/posts/pfbid…`, `/permalink.php?story_fbid={id}&id={owner}`, `/story.php?story_fbid={id}&id={owner}`, `/photo.php?fbid={id}`, `/photo/?fbid={id}`, `/{user}/photos/{album}/{fbid}/` |
|
|
233
|
+
| Video | `/{page}/videos/{id}`, `/{page}/videos/{slug}/{id}/`, `/watch/?v={id}`, `/video.php?v={id}`, `/reel/{id}` |
|
|
234
|
+
|
|
235
|
+
- One SDK serves the whole page, so the first `locale` requested wins.
|
|
236
|
+
- The SDK loads with `xfbml=0` and parses only the embed's own element, so it never scans the rest of the page. An SDK the host page already loaded is reused.
|
|
237
|
+
- Instagram's and Threads' scripts do nothing on a page where the Facebook SDK has already loaded; the loader works around that. If your page loads both the SDK and Instagram's script itself, load Instagram's first.
|
|
238
|
+
- A removed post shows Facebook's "no longer available" notice.
|
|
239
|
+
|
|
240
|
+
### TikTok
|
|
241
|
+
|
|
242
|
+
- Every input renders TikTok's [Embed Player](https://developers.tiktok.com/doc/embed-player) iframe at 9:16, the replacement TikTok recommends for its deprecated blockquote embed. Old `blockquote.tiktok-embed` code is still recognised and renders the player; its caption is not shown.
|
|
243
|
+
- No script and no fallback content.
|
|
244
|
+
|
|
245
|
+
### YouTube
|
|
246
|
+
|
|
247
|
+
- Videos are 16:9 and Shorts URLs 9:16, unless embed code gives a `width` and `height`.
|
|
248
|
+
- Privacy-enhanced embed code (youtube-nocookie.com) stays on that host; `nocookie` sets it for other input.
|
|
249
|
+
|
|
250
|
+
### Not supported
|
|
251
|
+
|
|
252
|
+
- Redirecting short links: `fb.watch/…`, Facebook share links (`/share/p/…`, `/share/v/…`), `vm.tiktok.com/…` and `vt.tiktok.com/…`. A redirect cannot be followed without a network request, and the package makes none. `matchUrl` returns `null`.
|
|
253
|
+
- Facebook group posts.
|
|
254
|
+
- Fetching embed code from oEmbed endpoints, consent gating and HTML sanitising: these stay with the host.
|
|
255
|
+
|
|
256
|
+
## Next.js
|
|
257
|
+
|
|
258
|
+
The `social-embeds` entry starts with `'use client'`, so its components can be rendered straight from a Server Component, with props that can be serialised:
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
// app/posts/[slug]/page.tsx: a Server Component
|
|
262
|
+
import { SocialEmbed } from 'social-embeds';
|
|
263
|
+
|
|
264
|
+
export default async function Page({
|
|
265
|
+
params,
|
|
266
|
+
}: {
|
|
267
|
+
params: Promise<{ slug: string }>;
|
|
268
|
+
}) {
|
|
269
|
+
const post = await getPost((await params).slug);
|
|
270
|
+
return <SocialEmbed html={post.embedCode} />;
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Functions cannot cross the server/client boundary, so `onLoad`, `onError` and `fallback` need a Client Component of your own:
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
'use client';
|
|
278
|
+
import { SocialEmbed } from 'social-embeds';
|
|
279
|
+
|
|
280
|
+
export function Embed({ html }: { html: string }) {
|
|
281
|
+
return (
|
|
282
|
+
<SocialEmbed html={html} onError={(error) => console.warn(error.message)} />
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`social-embeds/parser` has no directive. Use it in Server Components, route handlers or when content is saved, and pass the `ParsedEmbed` to a platform component as `embed`.
|
|
288
|
+
|
|
289
|
+
Client navigation needs nothing extra: each platform script loads once per page, and an embed that mounts after navigation renders without loading it again. With `lazy` (the default), a page with five embeds loads no platform script until the first embed comes near the viewport, and each embed then renders only when it comes near.
|
|
290
|
+
|
|
291
|
+
## Server-side fallback content
|
|
292
|
+
|
|
293
|
+
- **Embed code with fallback content** (X, Instagram, Threads, Facebook SDK code with a blockquote): the server HTML contains the platform's blockquote with its whitelisted content, such as the post text, author and link. No placeholder is shown. On the client, the platform script turns it into the post.
|
|
294
|
+
- **URLs and embed code without fallback content**: the server HTML contains the sized wrapper and the placeholder.
|
|
295
|
+
- **Iframe embeds** (YouTube, TikTok, Facebook video embed code): the iframe is added on the client, so `onLoad` can never be missed. Without JavaScript only the placeholder shows.
|
|
296
|
+
- The server HTML never contains a platform `<script>`.
|
|
297
|
+
|
|
298
|
+
The platform's class (`twitter-tweet`, `instagram-media`, `text-post-media`) is added only when the embed renders, so the platform scripts, which render every element with that class on the page, cannot render embeds that have not come near the viewport yet. CSS that targets those classes does not apply to the fallback content before then.
|
|
299
|
+
|
|
300
|
+
## Responsiveness
|
|
301
|
+
|
|
302
|
+
The host does nothing to make embeds responsive:
|
|
303
|
+
|
|
304
|
+
- **Script-based embeds** (X, Instagram, Threads, Facebook SDK) get a wrapper of `width: 100%` (or `width`) and `max-width: {maxWidth}px`, and the platform widget fills it. Their height is the platform's own.
|
|
305
|
+
- **Iframe embeds** get a wrapper with `aspect-ratio` taken from the embed code's `width` and `height` (16:9 without them; 9:16 for TikTok and YouTube Shorts URLs), and the iframe fills it. `height` replaces the aspect ratio with a fixed height.
|
|
306
|
+
|
|
307
|
+
## License
|
|
308
|
+
|
|
309
|
+
[MIT](./LICENSE.md)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
type Platform = 'x' | 'instagram' | 'facebook' | 'threads' | 'tiktok' | 'youtube';
|
|
2
|
+
type ParsedEmbed = {
|
|
3
|
+
platform: Platform;
|
|
4
|
+
kind: 'post' | 'video';
|
|
5
|
+
url: string;
|
|
6
|
+
id: string;
|
|
7
|
+
variant: 'sdk' | 'iframe';
|
|
8
|
+
options: Record<string, string | number | boolean>;
|
|
9
|
+
size?: {
|
|
10
|
+
width?: number;
|
|
11
|
+
height?: number;
|
|
12
|
+
};
|
|
13
|
+
fallbackHtml?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Recognises a post URL. The result has no fallback content, and options only for data the
|
|
17
|
+
* URL itself carries (docs/decisions.md D13). Never throws.
|
|
18
|
+
*/
|
|
19
|
+
declare function matchUrl(url: string): ParsedEmbed | null;
|
|
20
|
+
/**
|
|
21
|
+
* Recognises a platform's own embed code. The input must consist of exactly one embed
|
|
22
|
+
* element plus, optionally, the platform's own `<script src>` tags, Facebook's empty
|
|
23
|
+
* `<div id="fb-root">`, whitespace and comments. Anything else returns `null`
|
|
24
|
+
* (docs/decisions.md D14). Never throws.
|
|
25
|
+
*/
|
|
26
|
+
declare function parseEmbed(html: string): ParsedEmbed | null;
|
|
27
|
+
export { Platform as i, matchUrl as n, ParsedEmbed as r, parseEmbed as t };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { r as ParsedEmbed } from "./index-CGSxjvGN.js";
|
|
2
|
+
import { ComponentProps, ReactNode } from "react";
|
|
3
|
+
/** A platform component takes a post URL or an already-parsed embed (brief §5.2). */
|
|
4
|
+
type EmbedSource = {
|
|
5
|
+
url: string;
|
|
6
|
+
embed?: never;
|
|
7
|
+
} | {
|
|
8
|
+
embed: ParsedEmbed;
|
|
9
|
+
url?: never;
|
|
10
|
+
};
|
|
11
|
+
/** Props every embed component shares (brief §6). */
|
|
12
|
+
type SharedProps = Omit<ComponentProps<'div'>, 'children' | 'onLoad' | 'onError'> & {
|
|
13
|
+
/** Wrapper width. Default `'100%'`. */
|
|
14
|
+
width?: number | string;
|
|
15
|
+
/** The embed fills its container up to this width, in pixels. */
|
|
16
|
+
maxWidth?: number;
|
|
17
|
+
/** Rendered until the embed has loaded. Default: a neutral skeleton. */
|
|
18
|
+
placeholder?: ReactNode;
|
|
19
|
+
/** Load the embed only when it comes near the viewport. Default `true`. */
|
|
20
|
+
lazy?: boolean;
|
|
21
|
+
/** Fires once the platform has rendered the post. */
|
|
22
|
+
onLoad?: () => void;
|
|
23
|
+
/** Fires on invalid input, script load failure or init timeout. */
|
|
24
|
+
onError?: (error: Error) => void;
|
|
25
|
+
};
|
|
26
|
+
/** Iframe-based embeds (Group B) may also be given a fixed height. */
|
|
27
|
+
type FrameProps = SharedProps & {
|
|
28
|
+
/** A fixed height in pixels, instead of the embed's aspect ratio. */
|
|
29
|
+
height?: number;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The plugin attributes Facebook documents, plus the SDK locale. `data-width` and
|
|
33
|
+
* `data-lazy` have no props, because `width` and `lazy` are the wrapper's
|
|
34
|
+
* (docs/decisions.md D33).
|
|
35
|
+
*/
|
|
36
|
+
type FacebookOptions = {
|
|
37
|
+
/** SDK locale, such as `bg_BG`. One SDK serves the page: the first locale wins. */
|
|
38
|
+
locale?: string;
|
|
39
|
+
showText?: boolean;
|
|
40
|
+
/** Video only. */
|
|
41
|
+
allowfullscreen?: boolean;
|
|
42
|
+
/** Video only. */
|
|
43
|
+
autoplay?: boolean;
|
|
44
|
+
/** Video only. */
|
|
45
|
+
showCaptions?: boolean;
|
|
46
|
+
};
|
|
47
|
+
/** `height` applies only to video embed code, which renders as an iframe. */
|
|
48
|
+
type FacebookEmbedProps = EmbedSource & Omit<FrameProps, keyof FacebookOptions> & FacebookOptions;
|
|
49
|
+
/**
|
|
50
|
+
* Facebook rendering rules (brief §5.4): posts always through the SDK; video embed code as
|
|
51
|
+
* the iframe that came in; video URLs through the SDK. The parser has set `variant`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function FacebookEmbed(props: FacebookEmbedProps): import("react").JSX.Element | null;
|
|
54
|
+
type InstagramOptions = {
|
|
55
|
+
/** Show the caption (`data-instgrm-captioned`). */
|
|
56
|
+
captioned?: boolean;
|
|
57
|
+
};
|
|
58
|
+
type InstagramEmbedProps = EmbedSource & Omit<SharedProps, keyof InstagramOptions> & InstagramOptions;
|
|
59
|
+
export declare function InstagramEmbed(props: InstagramEmbedProps): import("react").JSX.Element | null;
|
|
60
|
+
type ThreadsOptions = {
|
|
61
|
+
/** `data-theme`; embed.js also reads `dark` and `auto` (docs/decisions.md D32). */
|
|
62
|
+
theme?: 'light' | 'dark' | 'auto';
|
|
63
|
+
};
|
|
64
|
+
type ThreadsEmbedProps = EmbedSource & Omit<SharedProps, keyof ThreadsOptions> & ThreadsOptions;
|
|
65
|
+
export declare function ThreadsEmbed(props: ThreadsEmbedProps): import("react").JSX.Element | null;
|
|
66
|
+
/** The Embed Player parameters TikTok documents (developers.tiktok.com/doc/embed-player). */
|
|
67
|
+
type TikTokOptions = {
|
|
68
|
+
controls?: boolean;
|
|
69
|
+
progressBar?: boolean;
|
|
70
|
+
playButton?: boolean;
|
|
71
|
+
volumeControl?: boolean;
|
|
72
|
+
fullscreenButton?: boolean;
|
|
73
|
+
timestamp?: boolean;
|
|
74
|
+
loop?: boolean;
|
|
75
|
+
autoplay?: boolean;
|
|
76
|
+
musicInfo?: boolean;
|
|
77
|
+
description?: boolean;
|
|
78
|
+
rel?: boolean;
|
|
79
|
+
nativeContextMenu?: boolean;
|
|
80
|
+
closedCaption?: boolean;
|
|
81
|
+
forceMuted?: boolean;
|
|
82
|
+
};
|
|
83
|
+
type TikTokEmbedProps = EmbedSource & Omit<FrameProps, keyof TikTokOptions> & TikTokOptions;
|
|
84
|
+
/**
|
|
85
|
+
* Every TikTok input renders the Embed Player iframe (docs/decisions.md D11). It is at
|
|
86
|
+
* most 325px wide, the size of TikTok's own embed code (D40).
|
|
87
|
+
*/
|
|
88
|
+
export declare function TikTokEmbed(props: TikTokEmbedProps): import("react").JSX.Element | null;
|
|
89
|
+
/**
|
|
90
|
+
* The embedded post attributes X documents. `data-width` has no prop, because `width` is
|
|
91
|
+
* the wrapper's; the post fills the wrapper up to `maxWidth` (docs/decisions.md D33).
|
|
92
|
+
*/
|
|
93
|
+
type XOptions = {
|
|
94
|
+
theme?: 'light' | 'dark';
|
|
95
|
+
conversation?: 'none';
|
|
96
|
+
cards?: 'hidden';
|
|
97
|
+
align?: 'left' | 'center' | 'right';
|
|
98
|
+
lang?: string;
|
|
99
|
+
dnt?: boolean;
|
|
100
|
+
};
|
|
101
|
+
type XEmbedProps = EmbedSource & Omit<SharedProps, keyof XOptions> & XOptions;
|
|
102
|
+
export declare function XEmbed(props: XEmbedProps): import("react").JSX.Element | null;
|
|
103
|
+
/** The player parameters YouTube documents (developers.google.com/youtube/player_parameters). */
|
|
104
|
+
type YouTubeOptions = {
|
|
105
|
+
autoplay?: boolean;
|
|
106
|
+
ccLangPref?: string;
|
|
107
|
+
ccLoadPolicy?: boolean;
|
|
108
|
+
color?: 'red' | 'white';
|
|
109
|
+
controls?: boolean;
|
|
110
|
+
disablekb?: boolean;
|
|
111
|
+
enablejsapi?: boolean;
|
|
112
|
+
end?: number;
|
|
113
|
+
fs?: boolean;
|
|
114
|
+
hl?: string;
|
|
115
|
+
ivLoadPolicy?: 1 | 3;
|
|
116
|
+
list?: string;
|
|
117
|
+
listType?: 'playlist' | 'user_uploads';
|
|
118
|
+
loop?: boolean;
|
|
119
|
+
origin?: string;
|
|
120
|
+
playlist?: string;
|
|
121
|
+
playsinline?: boolean;
|
|
122
|
+
rel?: boolean;
|
|
123
|
+
start?: number;
|
|
124
|
+
widgetReferrer?: string;
|
|
125
|
+
/** Privacy-enhanced mode: play from youtube-nocookie.com. */
|
|
126
|
+
nocookie?: boolean;
|
|
127
|
+
};
|
|
128
|
+
type YouTubeEmbedProps = EmbedSource & Omit<FrameProps, keyof YouTubeOptions> & YouTubeOptions;
|
|
129
|
+
export declare function YouTubeEmbed(props: YouTubeEmbedProps): import("react").JSX.Element | null;
|
|
130
|
+
/** Embed code or a post URL; never both (brief §5.2). */
|
|
131
|
+
type SocialEmbedSource = {
|
|
132
|
+
html: string;
|
|
133
|
+
url?: never;
|
|
134
|
+
} | {
|
|
135
|
+
url: string;
|
|
136
|
+
html?: never;
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Platform options come from the embed code. Div attributes that share a name with one
|
|
140
|
+
* (`lang`, `color`, `rel`) are left out, because each platform reads that name its own way.
|
|
141
|
+
*/
|
|
142
|
+
type PlatformOption = keyof XOptions | keyof InstagramOptions | keyof ThreadsOptions | keyof FacebookOptions | keyof TikTokOptions | keyof YouTubeOptions;
|
|
143
|
+
type SocialEmbedProps = SocialEmbedSource & Omit<FrameProps, PlatformOption> & {
|
|
144
|
+
/**
|
|
145
|
+
* Called with the original string when `html` is not a recognised embed code
|
|
146
|
+
* (brief §6.4). Without it, nothing is rendered and `onError` is called.
|
|
147
|
+
*/
|
|
148
|
+
fallback?: (html: string) => ReactNode;
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* Recognises embed code or a post URL and renders the platform's component. `height`
|
|
152
|
+
* reaches only the iframe-based embeds; the others size themselves (brief §6.1).
|
|
153
|
+
*/
|
|
154
|
+
export declare function SocialEmbed(props: SocialEmbedProps): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
|
|
155
|
+
export type { EmbedSource, FacebookEmbedProps, FacebookOptions, FrameProps, InstagramEmbedProps, InstagramOptions, SharedProps, SocialEmbedProps, SocialEmbedSource, ThreadsEmbedProps, ThreadsOptions, TikTokEmbedProps, TikTokOptions, XEmbedProps, XOptions, YouTubeEmbedProps, YouTubeOptions };
|