From 90cf13954991590d55f1d94cb9853522e90b0a3b Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 21 Apr 2026 22:37:50 +0400 Subject: [PATCH 1/8] Combine two URL effects --- packages/block-library/src/embed/edit.js | 72 ++++++++++-------------- 1 file changed, 31 insertions(+), 41 deletions(-) diff --git a/packages/block-library/src/embed/edit.js b/packages/block-library/src/embed/edit.js index ef3eedc014633f..816799368a3c01 100644 --- a/packages/block-library/src/embed/edit.js +++ b/packages/block-library/src/embed/edit.js @@ -1,20 +1,3 @@ -/** - * Internal dependencies - */ -import { - createUpgradedEmbedBlock, - getClassNames, - removeAspectRatioClasses, - fallback, - getEmbedInfoByProvider, - getMergedAttributesWithPreview, -} from './util'; -import EmbedControls from './embed-controls'; -import { embedContentIcon } from './icons'; -import EmbedLoading from './embed-loading'; -import EmbedPlaceholder from './embed-placeholder'; -import EmbedPreview from './embed-preview'; - /** * External dependencies */ @@ -32,6 +15,23 @@ import { View } from '@wordpress/primitives'; import { getAuthority } from '@wordpress/url'; import { Caption } from '../utils/caption'; +/** + * Internal dependencies + */ +import { + createUpgradedEmbedBlock, + getClassNames, + removeAspectRatioClasses, + fallback, + getEmbedInfoByProvider, + getMergedAttributesWithPreview, +} from './util'; +import EmbedControls from './embed-controls'; +import { embedContentIcon } from './icons'; +import EmbedLoading from './embed-loading'; +import EmbedPlaceholder from './embed-placeholder'; +import EmbedPreview from './embed-preview'; + const EmbedEdit = ( props ) => { const { attributes: { @@ -131,38 +131,28 @@ const EmbedEdit = ( props ) => { } ); } + // When the preview can't be embedded, try rewriting the URL and resubmit. useEffect( () => { - if ( preview?.html || ! cannotEmbed || ! hasResolved ) { - return; - } - - // At this stage, we're not fetching the preview and know it can't be embedded, - // so try removing any trailing slash, and resubmit. - const newURL = attributesUrl.replace( /\/$/, '' ); - setURL( newURL ); - setIsEditingURL( false ); - setAttributes( { url: newURL } ); - }, [ - preview?.html, - attributesUrl, - cannotEmbed, - hasResolved, - setAttributes, - ] ); - - // Try a different provider in case the embed url is not supported. - useEffect( () => { - if ( ! cannotEmbed || fetching || ! url ) { + if ( ! cannotEmbed || ! hasResolved || ! attributesUrl ) { return; } // Until X provider is supported in WordPress, as a workaround we use Twitter provider. - if ( getAuthority( url ) === 'x.com' ) { - const newURL = new URL( url ); + if ( getAuthority( attributesUrl ) === 'x.com' ) { + const newURL = new URL( attributesUrl ); newURL.host = 'twitter.com'; setAttributes( { url: newURL.toString() } ); + return; + } + + // Otherwise, try removing any trailing slash. + const newURL = attributesUrl.replace( /\/$/, '' ); + if ( newURL !== attributesUrl ) { + setURL( newURL ); + setIsEditingURL( false ); + setAttributes( { url: newURL } ); } - }, [ url, cannotEmbed, fetching, setAttributes ] ); + }, [ attributesUrl, cannotEmbed, hasResolved, setAttributes ] ); // Handle incoming preview. useEffect( () => { From 951b55266bd8f93e40b598b68ccf36a95aaf0d4a Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 21 Apr 2026 23:30:18 +0400 Subject: [PATCH 2/8] Fix undo trap --- packages/block-library/src/embed/edit.js | 64 +++++++++++++------ .../block-library/src/embed/transforms.js | 6 +- .../specs/editor/various/embedding.spec.js | 30 +++++++++ 3 files changed, 77 insertions(+), 23 deletions(-) diff --git a/packages/block-library/src/embed/edit.js b/packages/block-library/src/embed/edit.js index 816799368a3c01..a28832629d9ecb 100644 --- a/packages/block-library/src/embed/edit.js +++ b/packages/block-library/src/embed/edit.js @@ -9,7 +9,10 @@ import clsx from 'clsx'; import { __, _x, sprintf } from '@wordpress/i18n'; import { useState, useEffect } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; -import { useBlockProps } from '@wordpress/block-editor'; +import { + useBlockProps, + store as blockEditorStore, +} from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; import { View } from '@wordpress/primitives'; import { getAuthority } from '@wordpress/url'; @@ -20,6 +23,7 @@ import { Caption } from '../utils/caption'; */ import { createUpgradedEmbedBlock, + findMoreSuitableBlock, getClassNames, removeAspectRatioClasses, fallback, @@ -58,6 +62,8 @@ const EmbedEdit = ( props ) => { const [ url, setURL ] = useState( attributesUrl ); const [ isEditingURL, setIsEditingURL ] = useState( false ); const { invalidateResolution } = useDispatch( coreStore ); + const { __unstableMarkNextChangeAsNotPersistent } = + useDispatch( blockEditorStore ); const { preview, @@ -141,7 +147,11 @@ const EmbedEdit = ( props ) => { if ( getAuthority( attributesUrl ) === 'x.com' ) { const newURL = new URL( attributesUrl ); newURL.host = 'twitter.com'; - setAttributes( { url: newURL.toString() } ); + const newURLString = newURL.toString(); + setAttributes( { + url: newURLString, + ...findMoreSuitableBlock( newURLString )?.attributes, + } ); return; } @@ -154,30 +164,38 @@ const EmbedEdit = ( props ) => { } }, [ attributesUrl, cannotEmbed, hasResolved, setAttributes ] ); - // Handle incoming preview. + // Apply preview-derived attributes once the preview resolves. useEffect( () => { - if ( preview && ! isEditingURL ) { - // When obtaining an incoming preview, - // we set the attributes derived from the preview data. - const mergedAttributes = getMergedAttributes(); - const hasChanges = Object.keys( mergedAttributes ).some( - ( key ) => mergedAttributes[ key ] !== attributes[ key ] + if ( ! preview || isEditingURL ) { + return; + } + + const mergedAttributes = getMergedAttributes(); + + if ( onReplace ) { + const upgradedBlock = createUpgradedEmbedBlock( + props, + mergedAttributes ); - if ( hasChanges ) { - setAttributes( mergedAttributes ); + if ( upgradedBlock ) { + // Mutate via setAttributes; onReplace would remount the + // block and clear the URL textbox on undo. + __unstableMarkNextChangeAsNotPersistent(); + setAttributes( upgradedBlock.attributes ); + return; } + } - if ( onReplace ) { - const upgradedBlock = createUpgradedEmbedBlock( - props, - mergedAttributes - ); + const hasChanges = Object.keys( mergedAttributes ).some( + ( key ) => mergedAttributes[ key ] !== attributes[ key ] + ); - if ( upgradedBlock ) { - onReplace( upgradedBlock ); - } - } + if ( hasChanges ) { + // Merge into the URL-submit undo level so a single undo + // reverts both the submit and the preview-driven attributes. + __unstableMarkNextChangeAsNotPersistent(); + setAttributes( mergedAttributes ); } }, [ preview, isEditingURL ] ); @@ -216,7 +234,11 @@ const EmbedEdit = ( props ) => { ); setIsEditingURL( false ); - setAttributes( { url, className: blockClass } ); + setAttributes( { + url, + ...findMoreSuitableBlock( url )?.attributes, + className: blockClass, + } ); } } value={ url } cannotEmbed={ cannotEmbed } diff --git a/packages/block-library/src/embed/transforms.js b/packages/block-library/src/embed/transforms.js index 82c46da1fa7f94..94eeb14e8ac804 100644 --- a/packages/block-library/src/embed/transforms.js +++ b/packages/block-library/src/embed/transforms.js @@ -7,7 +7,7 @@ import { createBlock } from '@wordpress/blocks'; * Internal dependencies */ import metadata from './block.json'; -import { removeAspectRatioClasses } from './util'; +import { findMoreSuitableBlock, removeAspectRatioClasses } from './util'; const { name: EMBED_BLOCK } = metadata; @@ -23,8 +23,10 @@ const transforms = { /^\s*(https?:\/\/\S+)\s*$/i.test( node.textContent ) && node.textContent?.match( /https/gi )?.length === 1, transform: ( node ) => { + const url = node.textContent.trim(); return createBlock( EMBED_BLOCK, { - url: node.textContent.trim(), + url, + ...findMoreSuitableBlock( url )?.attributes, } ); }, }, diff --git a/test/e2e/specs/editor/various/embedding.spec.js b/test/e2e/specs/editor/various/embedding.spec.js index 158864ded78ee0..e61f8c1a17f235 100644 --- a/test/e2e/specs/editor/various/embedding.spec.js +++ b/test/e2e/specs/editor/various/embedding.spec.js @@ -256,6 +256,36 @@ test.describe( 'Embedding content', () => { editor.canvas.getByRole( 'document', { name: 'Block: Embed' } ) ).toBeVisible(); } ); + + test( 'should undo URL submit and preview-driven upgrade in a single step', async ( { + editor, + embedUtils, + pageUtils, + } ) => { + await embedUtils.interceptRequests( { + 'https://www.youtube.com/watch?v=lXMskKTw3Bc': + MOCK_EMBED_VIDEO_SUCCESS_RESPONSE, + } ); + + await embedUtils.insertEmbed( + 'https://www.youtube.com/watch?v=lXMskKTw3Bc' + ); + + // Wait for the block to upgrade to the YouTube variant. + await expect( + editor.canvas.getByRole( 'document', { + name: 'Block: YouTube Embed', + } ) + ).toBeVisible(); + + await pageUtils.pressKeys( 'primary+z' ); + + await expect( + editor.canvas + .getByRole( 'document', { name: 'Block: Embed' } ) + .getByRole( 'textbox', { name: 'Embed URL' } ) + ).toHaveValue( 'https://www.youtube.com/watch?v=lXMskKTw3Bc' ); + } ); } ); class EmbedUtils { From 97fd4484149f613820a7beed356048602ef63385 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Wed, 22 Apr 2026 10:11:45 +0400 Subject: [PATCH 3/8] Add failing e2e test for remaining bug --- .../specs/editor/various/embedding.spec.js | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/e2e/specs/editor/various/embedding.spec.js b/test/e2e/specs/editor/various/embedding.spec.js index e61f8c1a17f235..1a813a26fa0171 100644 --- a/test/e2e/specs/editor/various/embedding.spec.js +++ b/test/e2e/specs/editor/various/embedding.spec.js @@ -286,6 +286,38 @@ test.describe( 'Embedding content', () => { .getByRole( 'textbox', { name: 'Embed URL' } ) ).toHaveValue( 'https://www.youtube.com/watch?v=lXMskKTw3Bc' ); } ); + + test.fixme( + 'should restore the paragraph containing the pasted URL when undoing the embed', + async ( { editor, embedUtils, pageUtils } ) => { + await embedUtils.interceptRequests( { + 'https://www.youtube.com/watch?v=lXMskKTw3Bc': + MOCK_EMBED_VIDEO_SUCCESS_RESPONSE, + } ); + + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); + pageUtils.setClipboardData( { + plainText: 'https://www.youtube.com/watch?v=lXMskKTw3Bc', + html: 'https://www.youtube.com/watch?v=lXMskKTw3Bc', + } ); + await pageUtils.pressKeys( 'primary+v' ); + + await expect( + editor.canvas.getByRole( 'document', { + name: 'Block: YouTube Embed', + } ) + ).toBeVisible(); + + await pageUtils.pressKeys( 'primary+z' ); + await expect( + editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ) + ).toHaveValue( 'https://www.youtube.com/watch?v=lXMskKTw3Bc' ); + } + ); } ); class EmbedUtils { From a164edc5a29e11b30d8df64f06661d885c96c3a3 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Wed, 22 Apr 2026 10:26:51 +0400 Subject: [PATCH 4/8] Align logic in native files --- packages/block-library/src/embed/edit.js | 4 +- .../block-library/src/embed/edit.native.js | 126 ++++++++++-------- 2 files changed, 74 insertions(+), 56 deletions(-) diff --git a/packages/block-library/src/embed/edit.js b/packages/block-library/src/embed/edit.js index a28832629d9ecb..e9c376f54b2943 100644 --- a/packages/block-library/src/embed/edit.js +++ b/packages/block-library/src/embed/edit.js @@ -159,8 +159,8 @@ const EmbedEdit = ( props ) => { const newURL = attributesUrl.replace( /\/$/, '' ); if ( newURL !== attributesUrl ) { setURL( newURL ); - setIsEditingURL( false ); setAttributes( { url: newURL } ); + setIsEditingURL( false ); } }, [ attributesUrl, cannotEmbed, hasResolved, setAttributes ] ); @@ -233,12 +233,12 @@ const EmbedEdit = ( props ) => { attributes.className ); - setIsEditingURL( false ); setAttributes( { url, ...findMoreSuitableBlock( url )?.attributes, className: blockClass, } ); + setIsEditingURL( false ); } } value={ url } cannotEmbed={ cannotEmbed } diff --git a/packages/block-library/src/embed/edit.native.js b/packages/block-library/src/embed/edit.native.js index 2144d2f2eba729..86bcc11cfbc94d 100644 --- a/packages/block-library/src/embed/edit.native.js +++ b/packages/block-library/src/embed/edit.native.js @@ -1,21 +1,3 @@ -/** - * Internal dependencies - */ -import { - createUpgradedEmbedBlock, - getClassNames, - removeAspectRatioClasses, - fallback, - getEmbedInfoByProvider, - getMergedAttributesWithPreview, -} from './util'; -import EmbedControls from './embed-controls'; -import { embedContentIcon } from './icons'; -import EmbedLoading from './embed-loading'; -import EmbedPlaceholder from './embed-placeholder'; -import EmbedPreview from './embed-preview'; -import EmbedLinkSettings from './embed-link-settings'; - /** * External dependencies */ @@ -35,6 +17,25 @@ import { store as coreStore } from '@wordpress/core-data'; import { View } from '@wordpress/primitives'; import { getAuthority } from '@wordpress/url'; +/** + * Internal dependencies + */ +import { + createUpgradedEmbedBlock, + findMoreSuitableBlock, + getClassNames, + removeAspectRatioClasses, + fallback, + getEmbedInfoByProvider, + getMergedAttributesWithPreview, +} from './util'; +import EmbedControls from './embed-controls'; +import { embedContentIcon } from './icons'; +import EmbedLoading from './embed-loading'; +import EmbedPlaceholder from './embed-placeholder'; +import EmbedPreview from './embed-preview'; +import EmbedLinkSettings from './embed-link-settings'; + // The inline preview feature will be released progressible, for this reason // the embed will only be considered previewable for the following providers list. const PREVIEWABLE_PROVIDERS = [ 'youtube', 'twitter', 'instagram', 'vimeo' ]; @@ -77,6 +78,8 @@ const EmbedEdit = ( props ) => { const [ showEmbedBottomSheet, setShowEmbedBottomSheet ] = useState( isEditingURL ); const { invalidateResolution } = useDispatch( coreStore ); + const { __unstableMarkNextChangeAsNotPersistent } = + useDispatch( blockEditorStore ); const { preview, fetching, themeSupportsResponsive, cannotEmbed } = useSelect( @@ -150,18 +153,7 @@ const EmbedEdit = ( props ) => { } ); }; - useEffect( () => { - if ( ! preview?.html || ! cannotEmbed || fetching ) { - return; - } - // At this stage, we're not fetching the preview and know it can't be embedded, - // so try removing any trailing slash, and resubmit. - const newURL = url.replace( /\/$/, '' ); - setIsEditingURL( false ); - setAttributes( { url: newURL } ); - }, [ preview?.html, url, cannotEmbed, fetching ] ); - - // Try a different provider in case the embed url is not supported. + // When the preview can't be embedded, try rewriting the URL and resubmit. useEffect( () => { if ( ! cannotEmbed || fetching || ! url ) { return; @@ -171,29 +163,55 @@ const EmbedEdit = ( props ) => { if ( getAuthority( url ) === 'x.com' ) { const newURL = new URL( url ); newURL.host = 'twitter.com'; - setAttributes( { url: newURL.toString() } ); + const newURLString = newURL.toString(); + setAttributes( { + url: newURLString, + ...findMoreSuitableBlock( newURLString )?.attributes, + } ); + return; + } + + // Otherwise, try removing any trailing slash. + const newURL = url.replace( /\/$/, '' ); + if ( newURL !== url ) { + setIsEditingURL( false ); + setAttributes( { url: newURL } ); } }, [ url, cannotEmbed, fetching, setAttributes ] ); - // Handle incoming preview. + // Apply preview-derived attributes once the preview resolves. useEffect( () => { - if ( preview && ! isEditingURL ) { - // When obtaining an incoming preview, - // we set the attributes derived from the preview data. - const mergedAttributes = getMergedAttributes(); - setAttributes( mergedAttributes ); + if ( ! preview || isEditingURL ) { + return; + } - if ( onReplace ) { - const upgradedBlock = createUpgradedEmbedBlock( - props, - mergedAttributes - ); + const mergedAttributes = getMergedAttributes(); - if ( upgradedBlock ) { - onReplace( upgradedBlock ); - } + if ( onReplace ) { + const upgradedBlock = createUpgradedEmbedBlock( + props, + mergedAttributes + ); + + if ( upgradedBlock ) { + // Mutate via setAttributes; onReplace would remount the + // block and clear the URL textbox on undo. + __unstableMarkNextChangeAsNotPersistent(); + setAttributes( upgradedBlock.attributes ); + return; } } + + const hasChanges = Object.keys( mergedAttributes ).some( + ( key ) => mergedAttributes[ key ] !== attributes[ key ] + ); + + if ( hasChanges ) { + // Merge into the URL-submit undo level so a single undo + // reverts both the submit and the preview-driven attributes. + __unstableMarkNextChangeAsNotPersistent(); + setAttributes( mergedAttributes ); + } }, [ preview, isEditingURL ] ); useEffect( @@ -203,19 +221,19 @@ const EmbedEdit = ( props ) => { const onEditURL = useCallback( ( value ) => { - // If the embed URL was changed, we need to reset the aspect ratio class. - // To do this we have to remove the existing ratio class so it can be recalculated. - if ( attributes.url !== value ) { - const blockClass = removeAspectRatioClasses( - attributes.className - ); - setAttributes( { className: blockClass } ); - } + const urlChanged = attributes.url !== value; // The order of the following calls is important, we need to update the URL attribute before changing `isEditingURL`, // otherwise the side-effect that potentially replaces the block when updating the local state won't use the new URL // for creating the new block. - setAttributes( { url: value } ); + setAttributes( { + url: value, + ...findMoreSuitableBlock( value )?.attributes, + // If the embed URL was changed, reset the aspect ratio class so it can be recalculated. + ...( urlChanged && { + className: removeAspectRatioClasses( attributes.className ), + } ), + } ); setIsEditingURL( false ); }, [ attributes, setAttributes ] From 2946e6d425616faf367cb65cd8e63c41718554c5 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Wed, 22 Apr 2026 12:54:28 +0400 Subject: [PATCH 5/8] Remove e2e test for different PR --- .../specs/editor/various/embedding.spec.js | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/test/e2e/specs/editor/various/embedding.spec.js b/test/e2e/specs/editor/various/embedding.spec.js index 1a813a26fa0171..e61f8c1a17f235 100644 --- a/test/e2e/specs/editor/various/embedding.spec.js +++ b/test/e2e/specs/editor/various/embedding.spec.js @@ -286,38 +286,6 @@ test.describe( 'Embedding content', () => { .getByRole( 'textbox', { name: 'Embed URL' } ) ).toHaveValue( 'https://www.youtube.com/watch?v=lXMskKTw3Bc' ); } ); - - test.fixme( - 'should restore the paragraph containing the pasted URL when undoing the embed', - async ( { editor, embedUtils, pageUtils } ) => { - await embedUtils.interceptRequests( { - 'https://www.youtube.com/watch?v=lXMskKTw3Bc': - MOCK_EMBED_VIDEO_SUCCESS_RESPONSE, - } ); - - await editor.canvas - .locator( 'role=button[name="Add default block"i]' ) - .click(); - pageUtils.setClipboardData( { - plainText: 'https://www.youtube.com/watch?v=lXMskKTw3Bc', - html: 'https://www.youtube.com/watch?v=lXMskKTw3Bc', - } ); - await pageUtils.pressKeys( 'primary+v' ); - - await expect( - editor.canvas.getByRole( 'document', { - name: 'Block: YouTube Embed', - } ) - ).toBeVisible(); - - await pageUtils.pressKeys( 'primary+z' ); - await expect( - editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ) - ).toHaveValue( 'https://www.youtube.com/watch?v=lXMskKTw3Bc' ); - } - ); } ); class EmbedUtils { From 3e964a14f76edb2046513ed101c57ea0b85bc2e7 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Thu, 23 Apr 2026 08:39:12 +0400 Subject: [PATCH 6/8] Mark URL rewriting attribute updates and non-persistent --- packages/block-library/src/embed/edit.js | 10 +++++++++- packages/block-library/src/embed/edit.native.js | 12 ++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/block-library/src/embed/edit.js b/packages/block-library/src/embed/edit.js index e9c376f54b2943..0b789a2fff82b0 100644 --- a/packages/block-library/src/embed/edit.js +++ b/packages/block-library/src/embed/edit.js @@ -148,6 +148,7 @@ const EmbedEdit = ( props ) => { const newURL = new URL( attributesUrl ); newURL.host = 'twitter.com'; const newURLString = newURL.toString(); + __unstableMarkNextChangeAsNotPersistent(); setAttributes( { url: newURLString, ...findMoreSuitableBlock( newURLString )?.attributes, @@ -159,10 +160,17 @@ const EmbedEdit = ( props ) => { const newURL = attributesUrl.replace( /\/$/, '' ); if ( newURL !== attributesUrl ) { setURL( newURL ); + __unstableMarkNextChangeAsNotPersistent(); setAttributes( { url: newURL } ); setIsEditingURL( false ); } - }, [ attributesUrl, cannotEmbed, hasResolved, setAttributes ] ); + }, [ + attributesUrl, + cannotEmbed, + hasResolved, + setAttributes, + __unstableMarkNextChangeAsNotPersistent, + ] ); // Apply preview-derived attributes once the preview resolves. useEffect( () => { diff --git a/packages/block-library/src/embed/edit.native.js b/packages/block-library/src/embed/edit.native.js index 86bcc11cfbc94d..0c7d2cd0770122 100644 --- a/packages/block-library/src/embed/edit.native.js +++ b/packages/block-library/src/embed/edit.native.js @@ -164,6 +164,7 @@ const EmbedEdit = ( props ) => { const newURL = new URL( url ); newURL.host = 'twitter.com'; const newURLString = newURL.toString(); + __unstableMarkNextChangeAsNotPersistent(); setAttributes( { url: newURLString, ...findMoreSuitableBlock( newURLString )?.attributes, @@ -174,10 +175,17 @@ const EmbedEdit = ( props ) => { // Otherwise, try removing any trailing slash. const newURL = url.replace( /\/$/, '' ); if ( newURL !== url ) { - setIsEditingURL( false ); + __unstableMarkNextChangeAsNotPersistent(); setAttributes( { url: newURL } ); + setIsEditingURL( false ); } - }, [ url, cannotEmbed, fetching, setAttributes ] ); + }, [ + url, + cannotEmbed, + fetching, + setAttributes, + __unstableMarkNextChangeAsNotPersistent, + ] ); // Apply preview-derived attributes once the preview resolves. useEffect( () => { From 55758ec52172af6eb1715814d9cbe6e49f8fe2fc Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Thu, 23 Apr 2026 09:13:19 +0400 Subject: [PATCH 7/8] Improve URL rewriting logic --- packages/block-library/src/embed/edit.js | 34 ++++++++++--------- .../block-library/src/embed/edit.native.js | 32 +++++++++-------- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/packages/block-library/src/embed/edit.js b/packages/block-library/src/embed/edit.js index 0b789a2fff82b0..38c7c68cc5f142 100644 --- a/packages/block-library/src/embed/edit.js +++ b/packages/block-library/src/embed/edit.js @@ -143,27 +143,29 @@ const EmbedEdit = ( props ) => { return; } + let newURL = attributesUrl; + // Until X provider is supported in WordPress, as a workaround we use Twitter provider. if ( getAuthority( attributesUrl ) === 'x.com' ) { - const newURL = new URL( attributesUrl ); - newURL.host = 'twitter.com'; - const newURLString = newURL.toString(); - __unstableMarkNextChangeAsNotPersistent(); - setAttributes( { - url: newURLString, - ...findMoreSuitableBlock( newURLString )?.attributes, - } ); - return; + const rewritten = new URL( attributesUrl ); + rewritten.host = 'twitter.com'; + newURL = rewritten.toString(); + } else { + // Otherwise, try removing any trailing slash. + newURL = attributesUrl.replace( /\/$/, '' ); } - // Otherwise, try removing any trailing slash. - const newURL = attributesUrl.replace( /\/$/, '' ); - if ( newURL !== attributesUrl ) { - setURL( newURL ); - __unstableMarkNextChangeAsNotPersistent(); - setAttributes( { url: newURL } ); - setIsEditingURL( false ); + if ( newURL === attributesUrl ) { + return; } + + setURL( newURL ); + setIsEditingURL( false ); + __unstableMarkNextChangeAsNotPersistent(); + setAttributes( { + url: newURL, + ...findMoreSuitableBlock( newURL )?.attributes, + } ); }, [ attributesUrl, cannotEmbed, diff --git a/packages/block-library/src/embed/edit.native.js b/packages/block-library/src/embed/edit.native.js index 0c7d2cd0770122..0a9bfb629b9c88 100644 --- a/packages/block-library/src/embed/edit.native.js +++ b/packages/block-library/src/embed/edit.native.js @@ -159,26 +159,28 @@ const EmbedEdit = ( props ) => { return; } + let newURL = url; + // Until X provider is supported in WordPress, as a workaround we use Twitter provider. if ( getAuthority( url ) === 'x.com' ) { - const newURL = new URL( url ); - newURL.host = 'twitter.com'; - const newURLString = newURL.toString(); - __unstableMarkNextChangeAsNotPersistent(); - setAttributes( { - url: newURLString, - ...findMoreSuitableBlock( newURLString )?.attributes, - } ); - return; + const rewritten = new URL( url ); + rewritten.host = 'twitter.com'; + newURL = rewritten.toString(); + } else { + // Otherwise, try removing any trailing slash. + newURL = url.replace( /\/$/, '' ); } - // Otherwise, try removing any trailing slash. - const newURL = url.replace( /\/$/, '' ); - if ( newURL !== url ) { - __unstableMarkNextChangeAsNotPersistent(); - setAttributes( { url: newURL } ); - setIsEditingURL( false ); + if ( newURL === url ) { + return; } + + setIsEditingURL( false ); + __unstableMarkNextChangeAsNotPersistent(); + setAttributes( { + url: newURL, + ...findMoreSuitableBlock( newURL )?.attributes, + } ); }, [ url, cannotEmbed, From ba67196f0b61217d9919cb18f2ab6779f6388ca1 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Thu, 23 Apr 2026 09:40:46 +0400 Subject: [PATCH 8/8] Handle X url rewrite during submission --- packages/block-library/src/embed/edit.js | 31 ++++++------------- .../block-library/src/embed/edit.native.js | 30 +++++------------- .../block-library/src/embed/transforms.js | 8 +++-- packages/block-library/src/embed/util.js | 17 ++++++++++ 4 files changed, 40 insertions(+), 46 deletions(-) diff --git a/packages/block-library/src/embed/edit.js b/packages/block-library/src/embed/edit.js index 38c7c68cc5f142..bbbcfb026a2a8e 100644 --- a/packages/block-library/src/embed/edit.js +++ b/packages/block-library/src/embed/edit.js @@ -15,7 +15,6 @@ import { } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; import { View } from '@wordpress/primitives'; -import { getAuthority } from '@wordpress/url'; import { Caption } from '../utils/caption'; /** @@ -25,6 +24,7 @@ import { createUpgradedEmbedBlock, findMoreSuitableBlock, getClassNames, + rewriteXToTwitter, removeAspectRatioClasses, fallback, getEmbedInfoByProvider, @@ -137,24 +137,13 @@ const EmbedEdit = ( props ) => { } ); } - // When the preview can't be embedded, try rewriting the URL and resubmit. + // When the preview can't be embedded, retry without any trailing slash. useEffect( () => { if ( ! cannotEmbed || ! hasResolved || ! attributesUrl ) { return; } - let newURL = attributesUrl; - - // Until X provider is supported in WordPress, as a workaround we use Twitter provider. - if ( getAuthority( attributesUrl ) === 'x.com' ) { - const rewritten = new URL( attributesUrl ); - rewritten.host = 'twitter.com'; - newURL = rewritten.toString(); - } else { - // Otherwise, try removing any trailing slash. - newURL = attributesUrl.replace( /\/$/, '' ); - } - + const newURL = attributesUrl.replace( /\/$/, '' ); if ( newURL === attributesUrl ) { return; } @@ -162,10 +151,7 @@ const EmbedEdit = ( props ) => { setURL( newURL ); setIsEditingURL( false ); __unstableMarkNextChangeAsNotPersistent(); - setAttributes( { - url: newURL, - ...findMoreSuitableBlock( newURL )?.attributes, - } ); + setAttributes( { url: newURL } ); }, [ attributesUrl, cannotEmbed, @@ -237,15 +223,16 @@ const EmbedEdit = ( props ) => { event.preventDefault(); } - // If the embed URL was changed, we need to reset the aspect ratio class. - // To do this we have to remove the existing ratio class so it can be recalculated. + const rewrittenURL = rewriteXToTwitter( url ); const blockClass = removeAspectRatioClasses( attributes.className ); + setURL( rewrittenURL ); setAttributes( { - url, - ...findMoreSuitableBlock( url )?.attributes, + url: rewrittenURL, + ...findMoreSuitableBlock( rewrittenURL ) + ?.attributes, className: blockClass, } ); setIsEditingURL( false ); diff --git a/packages/block-library/src/embed/edit.native.js b/packages/block-library/src/embed/edit.native.js index 0a9bfb629b9c88..b9cb0ca45d5642 100644 --- a/packages/block-library/src/embed/edit.native.js +++ b/packages/block-library/src/embed/edit.native.js @@ -15,8 +15,6 @@ import { } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; import { View } from '@wordpress/primitives'; -import { getAuthority } from '@wordpress/url'; - /** * Internal dependencies */ @@ -24,6 +22,7 @@ import { createUpgradedEmbedBlock, findMoreSuitableBlock, getClassNames, + rewriteXToTwitter, removeAspectRatioClasses, fallback, getEmbedInfoByProvider, @@ -153,34 +152,20 @@ const EmbedEdit = ( props ) => { } ); }; - // When the preview can't be embedded, try rewriting the URL and resubmit. + // When the preview can't be embedded, retry without any trailing slash. useEffect( () => { if ( ! cannotEmbed || fetching || ! url ) { return; } - let newURL = url; - - // Until X provider is supported in WordPress, as a workaround we use Twitter provider. - if ( getAuthority( url ) === 'x.com' ) { - const rewritten = new URL( url ); - rewritten.host = 'twitter.com'; - newURL = rewritten.toString(); - } else { - // Otherwise, try removing any trailing slash. - newURL = url.replace( /\/$/, '' ); - } - + const newURL = url.replace( /\/$/, '' ); if ( newURL === url ) { return; } setIsEditingURL( false ); __unstableMarkNextChangeAsNotPersistent(); - setAttributes( { - url: newURL, - ...findMoreSuitableBlock( newURL )?.attributes, - } ); + setAttributes( { url: newURL } ); }, [ url, cannotEmbed, @@ -231,14 +216,15 @@ const EmbedEdit = ( props ) => { const onEditURL = useCallback( ( value ) => { - const urlChanged = attributes.url !== value; + const rewrittenURL = rewriteXToTwitter( value ); + const urlChanged = attributes.url !== rewrittenURL; // The order of the following calls is important, we need to update the URL attribute before changing `isEditingURL`, // otherwise the side-effect that potentially replaces the block when updating the local state won't use the new URL // for creating the new block. setAttributes( { - url: value, - ...findMoreSuitableBlock( value )?.attributes, + url: rewrittenURL, + ...findMoreSuitableBlock( rewrittenURL )?.attributes, // If the embed URL was changed, reset the aspect ratio class so it can be recalculated. ...( urlChanged && { className: removeAspectRatioClasses( attributes.className ), diff --git a/packages/block-library/src/embed/transforms.js b/packages/block-library/src/embed/transforms.js index 94eeb14e8ac804..c49657612914b2 100644 --- a/packages/block-library/src/embed/transforms.js +++ b/packages/block-library/src/embed/transforms.js @@ -7,7 +7,11 @@ import { createBlock } from '@wordpress/blocks'; * Internal dependencies */ import metadata from './block.json'; -import { findMoreSuitableBlock, removeAspectRatioClasses } from './util'; +import { + findMoreSuitableBlock, + rewriteXToTwitter, + removeAspectRatioClasses, +} from './util'; const { name: EMBED_BLOCK } = metadata; @@ -23,7 +27,7 @@ const transforms = { /^\s*(https?:\/\/\S+)\s*$/i.test( node.textContent ) && node.textContent?.match( /https/gi )?.length === 1, transform: ( node ) => { - const url = node.textContent.trim(); + const url = rewriteXToTwitter( node.textContent.trim() ); return createBlock( EMBED_BLOCK, { url, ...findMoreSuitableBlock( url )?.attributes, diff --git a/packages/block-library/src/embed/util.js b/packages/block-library/src/embed/util.js index 9491b7d689b1b1..bd83292cc404e7 100644 --- a/packages/block-library/src/embed/util.js +++ b/packages/block-library/src/embed/util.js @@ -14,6 +14,7 @@ import { getBlockType, getBlockVariations, } from '@wordpress/blocks'; +import { getAuthority } from '@wordpress/url'; /** * Internal dependencies @@ -60,6 +61,22 @@ export const findMoreSuitableBlock = ( url ) => matchesPatterns( url, patterns ) ); +/** + * Rewrites `x.com` URLs to `twitter.com` as a workaround while the + * WordPress oEmbed registry lacks an X provider. See: https://core.trac.wordpress.org/ticket/59142. + * + * @param {string} url The URL to rewrite. + * @return {string} The (possibly) rewritten URL. + */ +export function rewriteXToTwitter( url ) { + if ( ! url || getAuthority( url ) !== 'x.com' ) { + return url; + } + const rewritten = new URL( url ); + rewritten.host = 'twitter.com'; + return rewritten.toString(); +} + export const isFromWordPress = ( html ) => html && html.includes( 'class="wp-embedded-content"' );