@@ -389,6 +419,10 @@ input[type='checkbox'] + label {
padding: var(--spacing-s);
overflow-x: auto;
}
+
+ iframe {
+ aspect-ratio: 16/9 auto;
+ }
}
.spacer {
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nMarkdown/youtube.test.ts b/packages/frontend/@n8n/design-system/src/components/N8nMarkdown/youtube.test.ts
new file mode 100644
index 0000000000..b3fc04bbcc
--- /dev/null
+++ b/packages/frontend/@n8n/design-system/src/components/N8nMarkdown/youtube.test.ts
@@ -0,0 +1,58 @@
+import Markdown from 'markdown-it';
+
+import { markdownYoutubeEmbed, type YoutubeEmbedConfig } from './youtube';
+
+describe('markdownYoutubeEmbed', () => {
+ it('should render YouTube embed iframe with default options', () => {
+ const options: YoutubeEmbedConfig = {};
+ const md = new Markdown().use(markdownYoutubeEmbed, options);
+
+ const result = md.render('@[youtube](ZCuL2e4zC_4)');
+ expect(result).toContain(
+ '
',
+ );
+ });
+
+ it('should render YouTube embed iframe with custom options', () => {
+ const options: YoutubeEmbedConfig = {
+ width: 640,
+ height: 360,
+ title: 'Test Title',
+ };
+ const md = new Markdown().use(markdownYoutubeEmbed, options);
+
+ const result = md.render('@[youtube](ZCuL2e4zC_4)');
+ expect(result).toContain(
+ '
',
+ );
+ });
+
+ it('should render YouTube embed iframe with cookies', () => {
+ const options: YoutubeEmbedConfig = {
+ width: 640,
+ height: 360,
+ title: 'Test Title',
+ nocookie: false,
+ };
+ const md = new Markdown().use(markdownYoutubeEmbed, options);
+
+ const result = md.render('@[youtube](ZCuL2e4zC_4)');
+ expect(result).toContain(
+ '
',
+ );
+ });
+
+ it('should render sticky HTML content with YouTube embed', () => {
+ const options: YoutubeEmbedConfig = {};
+ const md = new Markdown().use(markdownYoutubeEmbed, options);
+
+ const result = md.render(
+ "## I'm a note \n**Double click** to edit me. [Guide](https://docs.n8n.io/workflows/sticky-notes/)\n@[youtube](ZCuL2e4zC_4)",
+ );
+ expect(result).toContain(
+ "
I'm a note
\n" +
+ '
Double click to edit me. Guide\n' +
+ '
',
+ );
+ });
+});
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nMarkdown/youtube.ts b/packages/frontend/@n8n/design-system/src/components/N8nMarkdown/youtube.ts
new file mode 100644
index 0000000000..16c4c1d879
--- /dev/null
+++ b/packages/frontend/@n8n/design-system/src/components/N8nMarkdown/youtube.ts
@@ -0,0 +1,65 @@
+import type MarkdownIt from 'markdown-it';
+import type { Token } from 'markdown-it';
+import type StateInline from 'markdown-it/lib/rules_inline/state_inline';
+
+// Detects custom markdown tags in format "@[youtube](
)"
+const YOUTUBE_TAG_REGEX = /@\[youtube]\(([\w-]{11}(?:\?.*)?)\)/im;
+
+export const YOUTUBE_EMBED_SRC_REGEX =
+ /^https:\/\/(?:www\.)?(youtube\.com|youtube-nocookie\.com)\/embed\/[\w-]{11}(?:\?.*)?$/i;
+
+export interface YoutubeEmbedConfig {
+ width?: number | string;
+ height?: number | string;
+ title?: string;
+ nocookie?: boolean;
+}
+
+export const markdownYoutubeEmbed = (md: MarkdownIt, options: YoutubeEmbedConfig) => {
+ const opts = {
+ width: '100%',
+ title: 'YouTube video player',
+ nocookie: true,
+ ...options,
+ };
+
+ const parser = (state: StateInline, silent: boolean): boolean => {
+ const { pos, src } = state;
+
+ // Must start with @
+ if (src.charCodeAt(pos) !== 0x40 /* @ */) return false;
+
+ const match = YOUTUBE_TAG_REGEX.exec(src.slice(pos));
+ if (!match) return false;
+
+ if (!silent) {
+ const token = state.push('youtube_embed', '', 0);
+ token.meta = { videoId: match[1] };
+ }
+
+ state.pos += match[0].length;
+ return true;
+ };
+
+ const youtubeUrl = opts.nocookie
+ ? 'https://www.youtube-nocookie.com/embed/'
+ : 'https://www.youtube.com/embed/';
+
+ md.inline.ruler.before('link', 'youtube_embed', parser);
+ md.renderer.rules.youtube_embed = (tokens: Token[], idx: number): string => {
+ const { videoId } = tokens[idx].meta as { videoId: string };
+
+ const parameters = [
+ `width="${opts.width}"`,
+ ...(opts.height ? [`height="${opts.height}"`] : []),
+ `src="${youtubeUrl}${videoId}"`,
+ `title="${md.utils.escapeHtml(opts.title)}"`,
+ 'frameborder="0"',
+ 'allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"',
+ 'referrerpolicy="strict-origin-when-cross-origin"',
+ 'allowfullscreen',
+ ];
+
+ return ``;
+ };
+};
diff --git a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/__snapshots__/CanvasNodeStickyNote.test.ts.snap b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/__snapshots__/CanvasNodeStickyNote.test.ts.snap
index 658e31dc1e..a5dc0219ad 100644
--- a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/__snapshots__/CanvasNodeStickyNote.test.ts.snap
+++ b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/__snapshots__/CanvasNodeStickyNote.test.ts.snap
@@ -12,6 +12,8 @@ exports[`CanvasNodeStickyNote > should render node correctly 1`] = `