diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js
index 9ae7738510c726..f53825a6164b09 100644
--- a/packages/block-editor/src/private-apis.js
+++ b/packages/block-editor/src/private-apis.js
@@ -63,6 +63,10 @@ import {
} from './components/block-list/use-block-props/use-block-refs';
import { LinkPicker } from './components/link-picker';
import useRemoteUrlData from './components/link-control/use-rich-url-data';
+import {
+ isHashLink,
+ isRelativePath,
+} from './components/link-control/is-url-like';
/**
* Private @wordpress/block-editor APIs.
@@ -122,4 +126,6 @@ lock( privateApis, {
useBlockElementRef,
LinkPicker,
useRemoteUrlData,
+ isHashLink,
+ isRelativePath,
} );
diff --git a/packages/block-library/src/navigation-link/shared/controls.js b/packages/block-library/src/navigation-link/shared/controls.js
index ea5d40245be7f7..06976097dc4991 100644
--- a/packages/block-library/src/navigation-link/shared/controls.js
+++ b/packages/block-library/src/navigation-link/shared/controls.js
@@ -2,17 +2,23 @@
* WordPress dependencies
*/
import {
+ Button,
__experimentalToolsPanel as ToolsPanel,
__experimentalToolsPanelItem as ToolsPanelItem,
+ __experimentalHStack as HStack,
CheckboxControl,
TextControl,
TextareaControl,
} from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { __unstableStripHTML as stripHTML } from '@wordpress/dom';
-import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
+import {
+ privateApis as blockEditorPrivateApis,
+ store as blockEditorStore,
+} from '@wordpress/block-editor';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
+import { external } from '@wordpress/icons';
/**
* Internal dependencies
@@ -25,7 +31,9 @@ import { useLinkPreview } from './use-link-preview';
import { useIsInvalidLink } from './use-is-invalid-link';
import { unlock } from '../../lock-unlock';
-const { LinkPicker } = unlock( blockEditorPrivateApis );
+const { LinkPicker, isHashLink, isRelativePath } = unlock(
+ blockEditorPrivateApis
+);
/**
* Get a human-readable entity type name.
@@ -139,6 +147,17 @@ export function Controls( { attributes, setAttributes, clientId } ) {
[ entityRecord?.featured_media ]
);
+ const onNavigateToEntityRecord = useSelect(
+ ( select ) =>
+ select( blockEditorStore ).getSettings().onNavigateToEntityRecord,
+ []
+ );
+
+ const homeUrl = useSelect( ( select ) => {
+ return select( coreStore ).getEntityRecord( 'root', '__unstableBase' )
+ ?.home;
+ }, [] );
+
const preview = useLinkPreview( {
url,
title: linkTitle,
@@ -149,6 +168,21 @@ export function Controls( { attributes, setAttributes, clientId } ) {
isEntityAvailable: isBoundEntityAvailable,
} );
+ // Check if URL is viewable (not hash link or other relative path like ./ or ../)
+ const isViewableUrl =
+ url &&
+ ( ! isHashLink( url ) ||
+ ( isRelativePath( url ) && ! url.startsWith( '/' ) ) );
+
+ // Construct full URL for viewing (prepend home URL for absolute paths starting with /)
+ const viewUrl =
+ isViewableUrl && url.startsWith( '/' ) && homeUrl ? homeUrl + url : url;
+
+ const entityTypeName = getEntityTypeName(
+ attributes.type,
+ attributes.kind
+ );
+
return (
+ { url && (
+
+ { hasUrlBinding &&
+ isBoundEntityAvailable &&
+ entityRecord?.id &&
+ attributes.kind === 'post-type' &&
+ onNavigateToEntityRecord && (
+
+ ) }
+ { isViewableUrl && (
+
+ ) }
+
+ ) }
+
!! opensInNewTab }
label={ __( 'Open in new tab' ) }