From c8caa31c875a8955aed6530c9fcda7fff2f246c5 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 25 Feb 2026 13:14:45 -0600 Subject: [PATCH 1/5] Use 'Homepage' badge if using relative / link --- .../src/navigation-link/shared/use-link-preview.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/navigation-link/shared/use-link-preview.js b/packages/block-library/src/navigation-link/shared/use-link-preview.js index d64484fc63ea6f..38049285b93c2e 100644 --- a/packages/block-library/src/navigation-link/shared/use-link-preview.js +++ b/packages/block-library/src/navigation-link/shared/use-link-preview.js @@ -98,7 +98,6 @@ export function computeBadges( { isEntityAvailable, } ) { const badges = []; - // Kind badge if ( url ) { if ( isExternal ) { @@ -113,6 +112,11 @@ export function computeBadges( { label: __( 'Internal link' ), intent: 'default', } ); + } else if ( url === '/' ) { + badges.push( { + label: __( 'Homepage' ), + intent: 'default', + } ); } else if ( type && type !== 'custom' ) { // Show entity type badge (page, post, category, etc.) // but not 'custom' since that's just a manual link From 3bc48ebee0a03d664f1f53f866b09dcf77f683fd Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 25 Feb 2026 13:16:32 -0600 Subject: [PATCH 2/5] Add unit test --- .../shared/test/use-link-preview.test.js | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/block-library/src/navigation-link/shared/test/use-link-preview.test.js b/packages/block-library/src/navigation-link/shared/test/use-link-preview.test.js index 4ddd9623940cb0..4a80129f044821 100644 --- a/packages/block-library/src/navigation-link/shared/test/use-link-preview.test.js +++ b/packages/block-library/src/navigation-link/shared/test/use-link-preview.test.js @@ -174,6 +174,18 @@ describe( 'computeBadges', () => { } ); } ); + it( 'should show "Homepage" badge for root path', () => { + const badges = computeBadges( { + url: '/', + isExternal: false, + } ); + + expect( badges ).toContainEqual( { + label: 'Homepage', + intent: 'default', + } ); + } ); + it( 'should show page badge for relative paths', () => { const badges = computeBadges( { url: '/relative-path', @@ -260,6 +272,25 @@ it( 'should show "Internal link" badge for hash links even when type is present' } ); } ); +it( 'should show "Homepage" badge for root path even when type is present', () => { + const badges = computeBadges( { + url: '/', + type: 'page', + isExternal: false, + } ); + + // Should prioritize homepage detection over type + expect( badges ).toContainEqual( { + label: 'Homepage', + intent: 'default', + } ); + // Should NOT show Page badge + expect( badges ).not.toContainEqual( { + label: 'Page', + intent: 'default', + } ); +} ); + test.each( [ [ 'www.test.com', 'URLs without protocol' ], [ 'google.com', 'domain-only URLs without protocol' ], From c4eb948d850c37c06d7e21c86558b2c1c5a38b36 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 25 Feb 2026 14:27:36 -0600 Subject: [PATCH 3/5] Move to a fuller featured isHomepage with tests --- .../shared/test/use-link-preview.test.js | 118 ++++++++++++++++++ .../shared/use-link-preview.js | 40 +++++- 2 files changed, 157 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/navigation-link/shared/test/use-link-preview.test.js b/packages/block-library/src/navigation-link/shared/test/use-link-preview.test.js index 4a80129f044821..fdc033daa36897 100644 --- a/packages/block-library/src/navigation-link/shared/test/use-link-preview.test.js +++ b/packages/block-library/src/navigation-link/shared/test/use-link-preview.test.js @@ -36,6 +36,7 @@ jest.mock( '../../../lock-unlock', () => ( { import { computeDisplayUrl, computeBadges, + isHomepage, useLinkPreview, } from '../use-link-preview'; @@ -135,6 +136,74 @@ describe( 'computeDisplayUrl', () => { } ); } ); +describe( 'isHomepage', () => { + const host = 'homepage.com'; + const siteUrl = 'https://' + host; + + test.each( [ + [ '/', siteUrl ], + [ '/', undefined ], + ] )( 'should return true for root path "%s"', ( url, siteUrlParam ) => { + expect( isHomepage( url, siteUrlParam ) ).toBe( true ); + } ); + + // Check combinations of http/s and trailing slash + test.each( [ + [ `http://${ host }`, siteUrl ], + [ `https://${ host }`, siteUrl ], + [ `http://${ host }/`, siteUrl ], + [ `https://${ host }/`, siteUrl ], + ] )( 'should return true for site URL "%s"', ( url ) => { + expect( isHomepage( url, siteUrl ) ).toBe( true ); + } ); + + test.each( [ + [ '', siteUrl ], + [ `https://${ host }`, '' ], + [ `https://${ host }`, undefined ], + ] )( + 'should return false when url or siteUrl is empty and not a / path', + ( url, siteUrlParam ) => { + expect( isHomepage( url, siteUrlParam ) ).toBe( false ); + } + ); + + const subdomain = 'sub.' + host; + test.each( [ + [ false, `http://${ subdomain }/`, siteUrl ], + [ false, `https://${ subdomain }`, siteUrl ], + [ true, `http://${ subdomain }/`, `https://${ subdomain }` ], + [ true, `https://${ subdomain }`, `https://${ subdomain }` ], + ] )( + 'should return %s for subdomain (url=%s, siteUrl=%s)', + ( expected, url, siteUrlParam ) => { + expect( isHomepage( url, siteUrlParam ) ).toBe( expected ); + } + ); + + const path = '/wordpress'; + const subdirSiteUrl = 'https://' + host + path; + + test.each( [ + [ `https://${ host }${ path }`, subdirSiteUrl ], + [ `https://${ host }${ path }/`, subdirSiteUrl ], + [ `http://${ host }${ path }`, subdirSiteUrl ], + [ `http://${ host }${ path }/`, subdirSiteUrl ], + ] )( 'should return true for subdirectory homepage "%s"', ( url ) => { + expect( isHomepage( url, subdirSiteUrl ) ).toBe( true ); + } ); + + test.each( [ + [ `https://${ host }/page`, siteUrl ], + [ '/page', siteUrl ], + [ `https://${ host }`, subdirSiteUrl ], + [ `https://${ host }/`, subdirSiteUrl ], + [ `https://${ host }${ path }/page`, subdirSiteUrl ], + ] )( 'should return false for non-homepage "%s"', ( url, siteUrlParam ) => { + expect( isHomepage( url, siteUrlParam ) ).toBe( false ); + } ); +} ); + describe( 'computeBadges', () => { describe( 'kind badges', () => { it( 'should show "External link" badge for external links', () => { @@ -186,6 +255,55 @@ describe( 'computeBadges', () => { } ); } ); + test.each( [ + [ 'https://example.com' ], + [ 'https://example.com/' ], + [ 'http://example.com' ], + [ 'http://example.com/' ], + ] )( 'should show "Homepage" badge when url is "%s"', ( url ) => { + const badges = computeBadges( { + url, + siteUrl: 'https://example.com', + isExternal: false, + } ); + expect( badges ).toContainEqual( { + label: 'Homepage', + intent: 'default', + } ); + } ); + + test.each( [ [ 'https://sub.example.com/', 'https://example.com' ] ] )( + 'should not show Homepage badge when subdomain url "%s" does not match siteUrl "%s"', + ( url, siteUrl ) => { + const badges = computeBadges( { + url, + siteUrl, + isExternal: false, + } ); + expect( badges ).not.toContainEqual( { + label: 'Homepage', + intent: 'default', + } ); + } + ); + + test.each( [ + [ 'https://sub.example.com/', 'https://sub.example.com' ], + ] )( + 'should show Homepage badge when subdomain url "%s" matches siteUrl "%s"', + ( url, siteUrl ) => { + const badges = computeBadges( { + url, + siteUrl, + isExternal: false, + } ); + expect( badges ).toContainEqual( { + label: 'Homepage', + intent: 'default', + } ); + } + ); + it( 'should show page badge for relative paths', () => { const badges = computeBadges( { url: '/relative-path', diff --git a/packages/block-library/src/navigation-link/shared/use-link-preview.js b/packages/block-library/src/navigation-link/shared/use-link-preview.js index 38049285b93c2e..fa6969db0f4262 100644 --- a/packages/block-library/src/navigation-link/shared/use-link-preview.js +++ b/packages/block-library/src/navigation-link/shared/use-link-preview.js @@ -26,6 +26,41 @@ function capitalize( str ) { return str.charAt( 0 ).toUpperCase() + str.slice( 1 ); } +/** + * Check if a URL points to the site homepage. + * Handles protocol (http/https) and trailing slash variations. + * Does not match subdomains unless they are the site URL. + * + * @param {string} url - The URL to check + * @param {string} siteUrl - The WordPress site URL + * @return {boolean} True if url is the homepage + */ +export function isHomepage( url, siteUrl ) { + if ( url === '/' ) { + return true; + } + if ( ! url || ! siteUrl ) { + return false; + } + try { + const urlParsed = new URL( url, siteUrl ); + const siteParsed = new URL( siteUrl ); + + // Same host (sub.homepage.com !== homepage.com unless siteUrl is sub.homepage.com) + if ( urlParsed.hostname !== siteParsed.hostname ) { + return false; + } + + // Path must match site root (normalize trailing slash) + const urlPath = urlParsed.pathname.replace( /\/$/, '' ); + const sitePath = siteParsed.pathname.replace( /\/$/, '' ); + + return urlPath === sitePath; + } catch { + return false; + } +} + /** * Compute display URL - strips site URL if internal, shows full URL if external. * @@ -82,6 +117,7 @@ export function computeDisplayUrl( { linkUrl, homeUrl } = {} ) { * * @param {Object} options - Options object * @param {string} options.url - Link URL + * @param {string} options.siteUrl - WordPress site URL (for homepage detection) * @param {string} options.type - Entity type (page, post, etc.) * @param {boolean} options.isExternal - Whether link is external * @param {string} options.entityStatus - Entity status (publish, draft, etc.) @@ -91,6 +127,7 @@ export function computeDisplayUrl( { linkUrl, homeUrl } = {} ) { */ export function computeBadges( { url, + siteUrl, type, isExternal, entityStatus, @@ -112,7 +149,7 @@ export function computeBadges( { label: __( 'Internal link' ), intent: 'default', } ); - } else if ( url === '/' ) { + } else if ( isHomepage( url, siteUrl ) ) { badges.push( { label: __( 'Homepage' ), intent: 'default', @@ -233,6 +270,7 @@ export function useLinkPreview( { // Compute badges const badges = computeBadges( { url, + siteUrl, type, isExternal, entityStatus: entityRecord?.status, From 2628cc41497e3f4a87a27fb747c699c95ea198ee Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Fri, 27 Feb 2026 08:55:36 -0600 Subject: [PATCH 4/5] Rename siteUrl to homeUrl for clarity --- .../shared/test/use-link-preview.test.js | 76 +++++++++---------- .../shared/use-link-preview.js | 20 ++--- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/packages/block-library/src/navigation-link/shared/test/use-link-preview.test.js b/packages/block-library/src/navigation-link/shared/test/use-link-preview.test.js index fdc033daa36897..31a8b302deecea 100644 --- a/packages/block-library/src/navigation-link/shared/test/use-link-preview.test.js +++ b/packages/block-library/src/navigation-link/shared/test/use-link-preview.test.js @@ -138,69 +138,69 @@ describe( 'computeDisplayUrl', () => { describe( 'isHomepage', () => { const host = 'homepage.com'; - const siteUrl = 'https://' + host; + const homeUrl = 'https://' + host; test.each( [ - [ '/', siteUrl ], + [ '/', homeUrl ], [ '/', undefined ], - ] )( 'should return true for root path "%s"', ( url, siteUrlParam ) => { - expect( isHomepage( url, siteUrlParam ) ).toBe( true ); + ] )( 'should return true for root path "%s"', ( url, homeUrlParam ) => { + expect( isHomepage( url, homeUrlParam ) ).toBe( true ); } ); // Check combinations of http/s and trailing slash test.each( [ - [ `http://${ host }`, siteUrl ], - [ `https://${ host }`, siteUrl ], - [ `http://${ host }/`, siteUrl ], - [ `https://${ host }/`, siteUrl ], + [ `http://${ host }`, homeUrl ], + [ `https://${ host }`, homeUrl ], + [ `http://${ host }/`, homeUrl ], + [ `https://${ host }/`, homeUrl ], ] )( 'should return true for site URL "%s"', ( url ) => { - expect( isHomepage( url, siteUrl ) ).toBe( true ); + expect( isHomepage( url, homeUrl ) ).toBe( true ); } ); test.each( [ - [ '', siteUrl ], + [ '', homeUrl ], [ `https://${ host }`, '' ], [ `https://${ host }`, undefined ], ] )( - 'should return false when url or siteUrl is empty and not a / path', - ( url, siteUrlParam ) => { - expect( isHomepage( url, siteUrlParam ) ).toBe( false ); + 'should return false when url or homeUrl is empty and not a / path', + ( url, homeUrlParam ) => { + expect( isHomepage( url, homeUrlParam ) ).toBe( false ); } ); const subdomain = 'sub.' + host; test.each( [ - [ false, `http://${ subdomain }/`, siteUrl ], - [ false, `https://${ subdomain }`, siteUrl ], + [ false, `http://${ subdomain }/`, homeUrl ], + [ false, `https://${ subdomain }`, homeUrl ], [ true, `http://${ subdomain }/`, `https://${ subdomain }` ], [ true, `https://${ subdomain }`, `https://${ subdomain }` ], ] )( - 'should return %s for subdomain (url=%s, siteUrl=%s)', - ( expected, url, siteUrlParam ) => { - expect( isHomepage( url, siteUrlParam ) ).toBe( expected ); + 'should return %s for subdomain (url=%s, homeUrl=%s)', + ( expected, url, homeUrlParam ) => { + expect( isHomepage( url, homeUrlParam ) ).toBe( expected ); } ); const path = '/wordpress'; - const subdirSiteUrl = 'https://' + host + path; + const subdirHomeUrl = 'https://' + host + path; test.each( [ - [ `https://${ host }${ path }`, subdirSiteUrl ], - [ `https://${ host }${ path }/`, subdirSiteUrl ], - [ `http://${ host }${ path }`, subdirSiteUrl ], - [ `http://${ host }${ path }/`, subdirSiteUrl ], + [ `https://${ host }${ path }`, subdirHomeUrl ], + [ `https://${ host }${ path }/`, subdirHomeUrl ], + [ `http://${ host }${ path }`, subdirHomeUrl ], + [ `http://${ host }${ path }/`, subdirHomeUrl ], ] )( 'should return true for subdirectory homepage "%s"', ( url ) => { - expect( isHomepage( url, subdirSiteUrl ) ).toBe( true ); + expect( isHomepage( url, subdirHomeUrl ) ).toBe( true ); } ); test.each( [ - [ `https://${ host }/page`, siteUrl ], - [ '/page', siteUrl ], - [ `https://${ host }`, subdirSiteUrl ], - [ `https://${ host }/`, subdirSiteUrl ], - [ `https://${ host }${ path }/page`, subdirSiteUrl ], - ] )( 'should return false for non-homepage "%s"', ( url, siteUrlParam ) => { - expect( isHomepage( url, siteUrlParam ) ).toBe( false ); + [ `https://${ host }/page`, homeUrl ], + [ '/page', homeUrl ], + [ `https://${ host }`, subdirHomeUrl ], + [ `https://${ host }/`, subdirHomeUrl ], + [ `https://${ host }${ path }/page`, subdirHomeUrl ], + ] )( 'should return false for non-homepage "%s"', ( url, homeUrlParam ) => { + expect( isHomepage( url, homeUrlParam ) ).toBe( false ); } ); } ); @@ -263,7 +263,7 @@ describe( 'computeBadges', () => { ] )( 'should show "Homepage" badge when url is "%s"', ( url ) => { const badges = computeBadges( { url, - siteUrl: 'https://example.com', + homeUrl: 'https://example.com', isExternal: false, } ); expect( badges ).toContainEqual( { @@ -273,11 +273,11 @@ describe( 'computeBadges', () => { } ); test.each( [ [ 'https://sub.example.com/', 'https://example.com' ] ] )( - 'should not show Homepage badge when subdomain url "%s" does not match siteUrl "%s"', - ( url, siteUrl ) => { + 'should not show Homepage badge when subdomain url "%s" does not match homeUrl "%s"', + ( url, homeUrl ) => { const badges = computeBadges( { url, - siteUrl, + homeUrl, isExternal: false, } ); expect( badges ).not.toContainEqual( { @@ -290,11 +290,11 @@ describe( 'computeBadges', () => { test.each( [ [ 'https://sub.example.com/', 'https://sub.example.com' ], ] )( - 'should show Homepage badge when subdomain url "%s" matches siteUrl "%s"', - ( url, siteUrl ) => { + 'should show Homepage badge when subdomain url "%s" matches homeUrl "%s"', + ( url, homeUrl ) => { const badges = computeBadges( { url, - siteUrl, + homeUrl, isExternal: false, } ); expect( badges ).toContainEqual( { diff --git a/packages/block-library/src/navigation-link/shared/use-link-preview.js b/packages/block-library/src/navigation-link/shared/use-link-preview.js index fa6969db0f4262..fe29987c53feff 100644 --- a/packages/block-library/src/navigation-link/shared/use-link-preview.js +++ b/packages/block-library/src/navigation-link/shared/use-link-preview.js @@ -32,21 +32,21 @@ function capitalize( str ) { * Does not match subdomains unless they are the site URL. * * @param {string} url - The URL to check - * @param {string} siteUrl - The WordPress site URL + * @param {string} homeUrl - The WordPress site URL * @return {boolean} True if url is the homepage */ -export function isHomepage( url, siteUrl ) { +export function isHomepage( url, homeUrl ) { if ( url === '/' ) { return true; } - if ( ! url || ! siteUrl ) { + if ( ! url || ! homeUrl ) { return false; } try { - const urlParsed = new URL( url, siteUrl ); - const siteParsed = new URL( siteUrl ); + const urlParsed = new URL( url, homeUrl ); + const siteParsed = new URL( homeUrl ); - // Same host (sub.homepage.com !== homepage.com unless siteUrl is sub.homepage.com) + // Same host (sub.homepage.com !== homepage.com unless homeUrl is sub.homepage.com) if ( urlParsed.hostname !== siteParsed.hostname ) { return false; } @@ -117,7 +117,7 @@ export function computeDisplayUrl( { linkUrl, homeUrl } = {} ) { * * @param {Object} options - Options object * @param {string} options.url - Link URL - * @param {string} options.siteUrl - WordPress site URL (for homepage detection) + * @param {string} options.homeUrl - WordPress site URL (for homepage detection) * @param {string} options.type - Entity type (page, post, etc.) * @param {boolean} options.isExternal - Whether link is external * @param {string} options.entityStatus - Entity status (publish, draft, etc.) @@ -127,7 +127,7 @@ export function computeDisplayUrl( { linkUrl, homeUrl } = {} ) { */ export function computeBadges( { url, - siteUrl, + homeUrl, type, isExternal, entityStatus, @@ -149,7 +149,7 @@ export function computeBadges( { label: __( 'Internal link' ), intent: 'default', } ); - } else if ( isHomepage( url, siteUrl ) ) { + } else if ( isHomepage( url, homeUrl ) ) { badges.push( { label: __( 'Homepage' ), intent: 'default', @@ -270,7 +270,7 @@ export function useLinkPreview( { // Compute badges const badges = computeBadges( { url, - siteUrl, + homeUrl, type, isExternal, entityStatus: entityRecord?.status, From ed0d98118a4c9b6f84f4caed713d6f49017cb0d7 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Fri, 27 Feb 2026 09:20:32 -0600 Subject: [PATCH 5/5] Minor cleanup --- .../src/navigation-link/shared/use-link-preview.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/block-library/src/navigation-link/shared/use-link-preview.js b/packages/block-library/src/navigation-link/shared/use-link-preview.js index fe29987c53feff..01aa9245439095 100644 --- a/packages/block-library/src/navigation-link/shared/use-link-preview.js +++ b/packages/block-library/src/navigation-link/shared/use-link-preview.js @@ -44,18 +44,18 @@ export function isHomepage( url, homeUrl ) { } try { const urlParsed = new URL( url, homeUrl ); - const siteParsed = new URL( homeUrl ); + const homeParsed = new URL( homeUrl ); - // Same host (sub.homepage.com !== homepage.com unless homeUrl is sub.homepage.com) - if ( urlParsed.hostname !== siteParsed.hostname ) { + // Same host, i.e. sub.homepage.com or homepage.com + if ( urlParsed.hostname !== homeParsed.hostname ) { return false; } // Path must match site root (normalize trailing slash) const urlPath = urlParsed.pathname.replace( /\/$/, '' ); - const sitePath = siteParsed.pathname.replace( /\/$/, '' ); + const homePath = homeParsed.pathname.replace( /\/$/, '' ); - return urlPath === sitePath; + return urlPath === homePath; } catch { return false; }