@emdash-cms/plugin-embeds 0.0.1

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/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@emdash-cms/plugin-embeds",
3
+ "version": "0.0.1",
4
+ "description": "Embed blocks for EmDash CMS - YouTube, Vimeo, Twitter, Bluesky, Mastodon, and more",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./astro": "./src/astro/index.ts"
10
+ },
11
+ "files": [
12
+ "src"
13
+ ],
14
+ "keywords": [
15
+ "emdash",
16
+ "cms",
17
+ "plugin",
18
+ "embed",
19
+ "youtube",
20
+ "vimeo",
21
+ "twitter",
22
+ "bluesky"
23
+ ],
24
+ "author": "Matt Kane",
25
+ "license": "MIT",
26
+ "peerDependencies": {
27
+ "astro": ">=6.0.0-beta.0",
28
+ "emdash": "0.0.1"
29
+ },
30
+ "dependencies": {
31
+ "astro-embed": "^0.12.0",
32
+ "@emdash-cms/blocks": "0.0.1"
33
+ },
34
+ "scripts": {
35
+ "typecheck": "tsgo --noEmit"
36
+ }
37
+ }
@@ -0,0 +1,22 @@
1
+ ---
2
+ /**
3
+ * Bluesky post embed component for Portable Text
4
+ *
5
+ * Wraps astro-embed's BlueskyPost component, extracting props from the PT block node.
6
+ * astro-portabletext passes `node` (not `value`) for custom type components.
7
+ *
8
+ * Accepts either `id` or `url` field for compatibility with different content sources.
9
+ */
10
+ import { BlueskyPost } from "astro-embed";
11
+ import type { BlueskyBlock } from "../schemas.js";
12
+
13
+ interface Props {
14
+ node: BlueskyBlock & { url?: string };
15
+ }
16
+
17
+ const { node } = Astro.props;
18
+ // Support both 'id' (schema) and 'url' (admin editor) field names
19
+ const postId = node.id || node.url;
20
+ ---
21
+
22
+ {postId && <BlueskyPost id={postId} />}
@@ -0,0 +1,19 @@
1
+ ---
2
+ /**
3
+ * GitHub Gist embed component for Portable Text
4
+ *
5
+ * Wraps astro-embed's Gist component, extracting props from the PT block node.
6
+ * astro-portabletext passes `node` (not `value`) for custom type components.
7
+ */
8
+ import { Gist as AstroGist } from "astro-embed";
9
+ import type { GistBlock } from "../schemas.js";
10
+
11
+ interface Props {
12
+ node: GistBlock;
13
+ }
14
+
15
+ const { node } = Astro.props;
16
+ const { id, file } = node;
17
+ ---
18
+
19
+ <AstroGist id={id} file={file} />
@@ -0,0 +1,19 @@
1
+ ---
2
+ /**
3
+ * Link preview (Open Graph) embed component for Portable Text
4
+ *
5
+ * Wraps astro-embed's LinkPreview component, extracting props from the PT block node.
6
+ * astro-portabletext passes `node` (not `value`) for custom type components.
7
+ */
8
+ import { LinkPreview as AstroLinkPreview } from "astro-embed";
9
+ import type { LinkPreviewBlock } from "../schemas.js";
10
+
11
+ interface Props {
12
+ node: LinkPreviewBlock;
13
+ }
14
+
15
+ const { node } = Astro.props;
16
+ const { id, hideMedia } = node;
17
+ ---
18
+
19
+ <AstroLinkPreview id={id} hideMedia={hideMedia} />
@@ -0,0 +1,19 @@
1
+ ---
2
+ /**
3
+ * Mastodon post embed component for Portable Text
4
+ *
5
+ * Wraps astro-embed's MastodonPost component, extracting props from the PT block node.
6
+ * astro-portabletext passes `node` (not `value`) for custom type components.
7
+ */
8
+ import { MastodonPost } from "astro-embed";
9
+ import type { MastodonBlock } from "../schemas.js";
10
+
11
+ interface Props {
12
+ node: MastodonBlock;
13
+ }
14
+
15
+ const { node } = Astro.props;
16
+ const { id } = node;
17
+ ---
18
+
19
+ <MastodonPost id={id} />
@@ -0,0 +1,19 @@
1
+ ---
2
+ /**
3
+ * Tweet embed component for Portable Text
4
+ *
5
+ * Wraps astro-embed's Tweet component, extracting props from the PT block node.
6
+ * astro-portabletext passes `node` (not `value`) for custom type components.
7
+ */
8
+ import { Tweet as AstroTweet } from "astro-embed";
9
+ import type { TweetBlock } from "../schemas.js";
10
+
11
+ interface Props {
12
+ node: TweetBlock;
13
+ }
14
+
15
+ const { node } = Astro.props;
16
+ const { id, theme } = node;
17
+ ---
18
+
19
+ <AstroTweet id={id} theme={theme} />
@@ -0,0 +1,25 @@
1
+ ---
2
+ /**
3
+ * Vimeo embed component for Portable Text
4
+ *
5
+ * Wraps astro-embed's Vimeo component, extracting props from the PT block node.
6
+ * astro-portabletext passes `node` (not `value`) for custom type components.
7
+ */
8
+ import { Vimeo as AstroVimeo } from "astro-embed";
9
+ import type { VimeoBlock } from "../schemas.js";
10
+
11
+ interface Props {
12
+ node: VimeoBlock;
13
+ }
14
+
15
+ const { node } = Astro.props;
16
+ const { id, poster, posterQuality, params, playlabel } = node;
17
+ ---
18
+
19
+ <AstroVimeo
20
+ id={id}
21
+ poster={poster}
22
+ posterQuality={posterQuality}
23
+ params={params}
24
+ playlabel={playlabel}
25
+ />
@@ -0,0 +1,26 @@
1
+ ---
2
+ /**
3
+ * YouTube embed component for Portable Text
4
+ *
5
+ * Wraps astro-embed's YouTube component, extracting props from the PT block node.
6
+ * astro-portabletext passes `node` (not `value`) for custom type components.
7
+ */
8
+ import { YouTube as AstroYouTube } from "astro-embed";
9
+ import type { YouTubeBlock } from "../schemas.js";
10
+
11
+ interface Props {
12
+ node: YouTubeBlock;
13
+ }
14
+
15
+ const { node } = Astro.props;
16
+ const { id, poster, posterQuality, params, playlabel, title } = node;
17
+ ---
18
+
19
+ <AstroYouTube
20
+ id={id}
21
+ poster={poster}
22
+ posterQuality={posterQuality}
23
+ params={params}
24
+ playlabel={playlabel}
25
+ title={title}
26
+ />
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Astro components for rendering embed blocks in Portable Text
3
+ *
4
+ * These components are automatically registered with PortableText when
5
+ * the embeds plugin is enabled. Manual wiring is no longer needed!
6
+ *
7
+ * The components are exported with lowercase names matching their block types
8
+ * for auto-registration, plus PascalCase aliases for direct usage.
9
+ *
10
+ * @example Direct usage (if you need to customize)
11
+ * ```astro
12
+ * ---
13
+ * import { YouTube } from "@emdash-cms/plugin-embeds/astro";
14
+ * ---
15
+ * <YouTube value={{ id: "dQw4w9WgXcQ", _type: "youtube", _key: "1" }} />
16
+ * ```
17
+ */
18
+
19
+ import BlueskyComponent from "./Bluesky.astro";
20
+ import GistComponent from "./Gist.astro";
21
+ import LinkPreviewComponent from "./LinkPreview.astro";
22
+ import MastodonComponent from "./Mastodon.astro";
23
+ import TweetComponent from "./Tweet.astro";
24
+ import VimeoComponent from "./Vimeo.astro";
25
+ // Import all components
26
+ import YouTubeComponent from "./YouTube.astro";
27
+
28
+ // Export with lowercase names (for auto-registration via virtual module)
29
+ // These names MUST match the block type names in EMBED_BLOCK_TYPES
30
+ export {
31
+ YouTubeComponent as youtube,
32
+ VimeoComponent as vimeo,
33
+ TweetComponent as tweet,
34
+ BlueskyComponent as bluesky,
35
+ MastodonComponent as mastodon,
36
+ LinkPreviewComponent as linkPreview,
37
+ GistComponent as gist,
38
+ };
39
+
40
+ // Also export with PascalCase for direct usage
41
+ export {
42
+ YouTubeComponent as YouTube,
43
+ VimeoComponent as Vimeo,
44
+ TweetComponent as Tweet,
45
+ BlueskyComponent as Bluesky,
46
+ MastodonComponent as Mastodon,
47
+ LinkPreviewComponent as LinkPreview,
48
+ GistComponent as Gist,
49
+ };
50
+
51
+ /**
52
+ * All embed components keyed by their Portable Text block type.
53
+ * Exported as `blockComponents` for auto-registration via the virtual module,
54
+ * and as `embedComponents` for direct usage.
55
+ */
56
+ export const blockComponents = {
57
+ youtube: YouTubeComponent,
58
+ vimeo: VimeoComponent,
59
+ tweet: TweetComponent,
60
+ bluesky: BlueskyComponent,
61
+ mastodon: MastodonComponent,
62
+ linkPreview: LinkPreviewComponent,
63
+ gist: GistComponent,
64
+ } as const;
65
+
66
+ export { blockComponents as embedComponents };
package/src/index.ts ADDED
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Embeds Plugin for EmDash CMS
3
+ *
4
+ * Provides Portable Text block types for embedding external content:
5
+ * - YouTube videos
6
+ * - Vimeo videos
7
+ * - Twitter/X tweets
8
+ * - Bluesky posts
9
+ * - Mastodon posts
10
+ * - Link previews (Open Graph)
11
+ * - GitHub Gists
12
+ *
13
+ * Uses astro-embed components for high-performance, privacy-respecting embeds.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * // live.config.ts
18
+ * import { embedsPlugin } from "@emdash-cms/plugin-embeds";
19
+ *
20
+ * export default defineConfig({
21
+ * plugins: [embedsPlugin()],
22
+ * });
23
+ * ```
24
+ *
25
+ * Embed components are automatically registered with PortableText when
26
+ * the plugin is enabled. No manual component wiring needed!
27
+ *
28
+ * If you need to customize rendering, you can still override specific types:
29
+ *
30
+ * @example
31
+ * ```astro
32
+ * <PortableText
33
+ * value={content}
34
+ * components={{
35
+ * types: {
36
+ * youtube: MyCustomYouTube, // Override just this one
37
+ * },
38
+ * }}
39
+ * />
40
+ * ```
41
+ */
42
+
43
+ import type { Element } from "@emdash-cms/blocks";
44
+ import type { PluginDescriptor, ResolvedPlugin } from "emdash";
45
+ import { definePlugin } from "emdash";
46
+
47
+ import { EMBED_BLOCK_TYPES } from "./schemas.js";
48
+
49
+ /** Rich metadata for each embed block type */
50
+ const EMBED_BLOCK_META: Record<
51
+ string,
52
+ {
53
+ label: string;
54
+ icon?: string;
55
+ description?: string;
56
+ placeholder?: string;
57
+ fields?: Element[];
58
+ }
59
+ > = {
60
+ youtube: {
61
+ label: "YouTube Video",
62
+ icon: "video",
63
+ placeholder: "Paste YouTube URL...",
64
+ fields: [
65
+ {
66
+ type: "text_input",
67
+ action_id: "id",
68
+ label: "YouTube URL",
69
+ placeholder: "https://youtube.com/watch?v=...",
70
+ },
71
+ { type: "text_input", action_id: "title", label: "Title" },
72
+ { type: "text_input", action_id: "poster", label: "Poster Image URL" },
73
+ {
74
+ type: "text_input",
75
+ action_id: "params",
76
+ label: "Player Parameters",
77
+ placeholder: "start=57&end=75",
78
+ },
79
+ ],
80
+ },
81
+ vimeo: {
82
+ label: "Vimeo Video",
83
+ icon: "video",
84
+ placeholder: "Paste Vimeo URL...",
85
+ fields: [
86
+ {
87
+ type: "text_input",
88
+ action_id: "id",
89
+ label: "Vimeo URL",
90
+ placeholder: "https://vimeo.com/...",
91
+ },
92
+ { type: "text_input", action_id: "poster", label: "Poster Image URL" },
93
+ { type: "text_input", action_id: "params", label: "Player Parameters" },
94
+ ],
95
+ },
96
+ tweet: { label: "Tweet (X)", icon: "link", placeholder: "Paste tweet URL..." },
97
+ bluesky: { label: "Bluesky Post", icon: "link", placeholder: "Paste Bluesky post URL..." },
98
+ mastodon: { label: "Mastodon Post", icon: "link", placeholder: "Paste Mastodon post URL..." },
99
+ linkPreview: {
100
+ label: "Link Preview",
101
+ icon: "link-external",
102
+ placeholder: "Paste any URL...",
103
+ },
104
+ gist: {
105
+ label: "GitHub Gist",
106
+ icon: "code",
107
+ placeholder: "Paste Gist URL...",
108
+ fields: [
109
+ {
110
+ type: "text_input",
111
+ action_id: "id",
112
+ label: "Gist URL",
113
+ placeholder: "https://gist.github.com/.../...",
114
+ },
115
+ {
116
+ type: "text_input",
117
+ action_id: "file",
118
+ label: "Specific File",
119
+ placeholder: "Optional: filename to show",
120
+ },
121
+ ],
122
+ },
123
+ };
124
+
125
+ export interface EmbedsPluginOptions {
126
+ /**
127
+ * Which embed types to enable.
128
+ * Defaults to all types.
129
+ */
130
+ types?: Array<(typeof EMBED_BLOCK_TYPES)[number]>;
131
+ }
132
+
133
+ /**
134
+ * Create the embeds plugin descriptor
135
+ */
136
+ export function embedsPlugin(
137
+ options: EmbedsPluginOptions = {},
138
+ ): PluginDescriptor<EmbedsPluginOptions> {
139
+ return {
140
+ id: "embeds",
141
+ version: "0.0.1",
142
+ entrypoint: "@emdash-cms/plugin-embeds",
143
+ componentsEntry: "@emdash-cms/plugin-embeds/astro",
144
+ options,
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Create the embeds plugin
150
+ */
151
+ export function createPlugin(options: EmbedsPluginOptions = {}): ResolvedPlugin {
152
+ const _enabledTypes = options.types ?? [...EMBED_BLOCK_TYPES];
153
+
154
+ return definePlugin({
155
+ id: "embeds",
156
+ version: "0.0.1",
157
+
158
+ // This plugin only provides block types - no server-side capabilities needed
159
+ capabilities: [],
160
+
161
+ admin: {
162
+ portableTextBlocks: _enabledTypes.map((type) => {
163
+ const meta = EMBED_BLOCK_META[type];
164
+ return {
165
+ type,
166
+ label: meta?.label ?? type,
167
+ icon: meta?.icon,
168
+ description: meta?.description,
169
+ placeholder: meta?.placeholder,
170
+ fields: meta?.fields,
171
+ };
172
+ }),
173
+ },
174
+ });
175
+ }
176
+
177
+ // Re-export schemas for consumers who need them
178
+ export * from "./schemas.js";
179
+
180
+ export default createPlugin;
181
+
182
+ // Re-export the enabled types for the plugin to use
183
+ export { EMBED_BLOCK_TYPES };
package/src/schemas.ts ADDED
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Block schemas for embed types
3
+ *
4
+ * These define the Portable Text block structure for each embed type.
5
+ * The schemas match the props expected by astro-embed components.
6
+ */
7
+
8
+ import { z } from "astro/zod";
9
+
10
+ /** Matches http(s) scheme at start of URL */
11
+ const HTTP_SCHEME_RE = /^https?:\/\//i;
12
+
13
+ /** Validates that a URL string uses http or https scheme. Rejects javascript:/data: URI XSS vectors. */
14
+ const httpUrl = z
15
+ .string()
16
+ .url()
17
+ .refine((url) => HTTP_SCHEME_RE.test(url), "URL must use http or https");
18
+
19
+ /**
20
+ * YouTube embed block
21
+ * @see https://astro-embed.netlify.app/components/youtube/
22
+ */
23
+ export const youtubeBlockSchema = z.object({
24
+ _type: z.literal("youtube"),
25
+ _key: z.string(),
26
+ /** YouTube video ID or URL */
27
+ id: z.string(),
28
+ /** Custom poster image URL */
29
+ poster: httpUrl.optional(),
30
+ /** Poster quality when using default YouTube thumbnail */
31
+ posterQuality: z.enum(["max", "high", "default", "low"]).optional(),
32
+ /** YouTube player parameters (e.g., "start=57&end=75") */
33
+ params: z.string().optional(),
34
+ /** Accessible label for the play button */
35
+ playlabel: z.string().optional(),
36
+ /** Visible title overlay */
37
+ title: z.string().optional(),
38
+ });
39
+
40
+ export type YouTubeBlock = z.infer<typeof youtubeBlockSchema>;
41
+
42
+ /**
43
+ * Vimeo embed block
44
+ * @see https://astro-embed.netlify.app/components/vimeo/
45
+ */
46
+ export const vimeoBlockSchema = z.object({
47
+ _type: z.literal("vimeo"),
48
+ _key: z.string(),
49
+ /** Vimeo video ID or URL */
50
+ id: z.string(),
51
+ /** Custom poster image URL */
52
+ poster: httpUrl.optional(),
53
+ /** Poster quality */
54
+ posterQuality: z.enum(["max", "high", "default", "low"]).optional(),
55
+ /** Vimeo player parameters */
56
+ params: z.string().optional(),
57
+ /** Accessible label for the play button */
58
+ playlabel: z.string().optional(),
59
+ });
60
+
61
+ export type VimeoBlock = z.infer<typeof vimeoBlockSchema>;
62
+
63
+ /**
64
+ * Twitter/X tweet embed block
65
+ * @see https://astro-embed.netlify.app/components/twitter/
66
+ */
67
+ export const tweetBlockSchema = z.object({
68
+ _type: z.literal("tweet"),
69
+ _key: z.string(),
70
+ /** Tweet URL or ID */
71
+ id: z.string(),
72
+ /** Color theme */
73
+ theme: z.enum(["light", "dark"]).optional(),
74
+ });
75
+
76
+ export type TweetBlock = z.infer<typeof tweetBlockSchema>;
77
+
78
+ /**
79
+ * Bluesky post embed block
80
+ * @see https://astro-embed.netlify.app/components/bluesky/
81
+ */
82
+ export const blueskyBlockSchema = z.object({
83
+ _type: z.literal("bluesky"),
84
+ _key: z.string(),
85
+ /** Bluesky post URL or AT URI */
86
+ id: z.string(),
87
+ });
88
+
89
+ export type BlueskyBlock = z.infer<typeof blueskyBlockSchema>;
90
+
91
+ /**
92
+ * Mastodon post embed block
93
+ * @see https://astro-embed.netlify.app/components/mastodon/
94
+ */
95
+ export const mastodonBlockSchema = z.object({
96
+ _type: z.literal("mastodon"),
97
+ _key: z.string(),
98
+ /** Mastodon post URL */
99
+ id: z.string(),
100
+ });
101
+
102
+ export type MastodonBlock = z.infer<typeof mastodonBlockSchema>;
103
+
104
+ /**
105
+ * Link preview / Open Graph embed block
106
+ * @see https://astro-embed.netlify.app/components/link-preview/
107
+ */
108
+ export const linkPreviewBlockSchema = z.object({
109
+ _type: z.literal("linkPreview"),
110
+ _key: z.string(),
111
+ /** URL to fetch Open Graph data from */
112
+ id: httpUrl,
113
+ /** Hide media (image/video) even if present in OG data */
114
+ hideMedia: z.boolean().optional(),
115
+ });
116
+
117
+ export type LinkPreviewBlock = z.infer<typeof linkPreviewBlockSchema>;
118
+
119
+ /**
120
+ * GitHub Gist embed block
121
+ * @see https://astro-embed.netlify.app/components/gist/
122
+ */
123
+ export const gistBlockSchema = z.object({
124
+ _type: z.literal("gist"),
125
+ _key: z.string(),
126
+ /** Gist URL */
127
+ id: httpUrl,
128
+ /** Specific file to show (case-sensitive) */
129
+ file: z.string().optional(),
130
+ });
131
+
132
+ export type GistBlock = z.infer<typeof gistBlockSchema>;
133
+
134
+ /**
135
+ * Union of all embed block types
136
+ */
137
+ export const embedBlockSchema = z.discriminatedUnion("_type", [
138
+ youtubeBlockSchema,
139
+ vimeoBlockSchema,
140
+ tweetBlockSchema,
141
+ blueskyBlockSchema,
142
+ mastodonBlockSchema,
143
+ linkPreviewBlockSchema,
144
+ gistBlockSchema,
145
+ ]);
146
+
147
+ export type EmbedBlock = z.infer<typeof embedBlockSchema>;
148
+
149
+ /**
150
+ * Block type names for use in plugin registration
151
+ */
152
+ export const EMBED_BLOCK_TYPES = [
153
+ "youtube",
154
+ "vimeo",
155
+ "tweet",
156
+ "bluesky",
157
+ "mastodon",
158
+ "linkPreview",
159
+ "gist",
160
+ ] as const;
161
+
162
+ export type EmbedBlockType = (typeof EMBED_BLOCK_TYPES)[number];