Skip to content

Commit 7bb46f3

Browse files
arthur791004claude
andauthored
Domain Search: Show free subdomain option for .blog subdomain queries (#109353)
* Domain Search: Show free subdomain option for .blog subdomain queries When users search for free .blog subdomains (e.g. test.tech.blog), getTld() returns "blog" which incorrectly treats the query as an FQDN and hides the free subdomain option. This adds detection for the 29 free .blog subdomains so they bypass the FQDN gate, and enables includeDotBlogSubdomain for all skippable flows (not just newsletter). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Domain Search: Consolidate free subdomain helpers into single file Combine is-wpcom-subdomain-query.ts and is-blog-subdomain-query.ts into is-free-subdomain-query.ts and extract isFreeSubdomainQuery helper to simplify consumers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Domain Search: Only include .blog suggestions when query is a .blog subdomain Send include_dotblogsubdomain=true only when both the config enables it (skippable flows) and the search query is a free .blog subdomain. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 424fd2a commit 7bb46f3

File tree

10 files changed

+171
-35
lines changed

10 files changed

+171
-35
lines changed

client/landing/stepper/declarative-flow/internals/steps-repository/domain-search/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,16 @@ const DomainSearchStep: StepType< {
118118
hidePrice: isHundredYearPlanFlow( flow ),
119119
oneTimePrice: isHundredYearDomainFlow( flow ),
120120
},
121-
includeDotBlogSubdomain: isNewsletterFlow( flow ),
122121
skippable:
123122
! isHundredYearPlanFlow( flow ) &&
124123
! isHundredYearDomainFlow( flow ) &&
125124
! isDomainFlow( flow ) &&
126125
! isDomainAndPlanFlow( flow ),
126+
includeDotBlogSubdomain:
127+
! isHundredYearPlanFlow( flow ) &&
128+
! isHundredYearDomainFlow( flow ) &&
129+
! isDomainFlow( flow ) &&
130+
! isDomainAndPlanFlow( flow ),
127131
allowedTlds:
128132
isHundredYearPlanFlow( flow ) || isHundredYearDomainFlow( flow )
129133
? HUNDRED_YEAR_DOMAIN_TLDS

packages/domain-search/src/components/skip-suggestion/index.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { useIsMutating, useQuery } from '@tanstack/react-query';
2-
import { isWpcomSubdomainQuery, stripWpcomSubdomainSuffix } from '../../helpers';
2+
import {
3+
isFreeSubdomainQuery,
4+
isWpcomSubdomainQuery,
5+
stripWpcomSubdomainSuffix,
6+
} from '../../helpers';
37
import { useDomainSearch } from '../../page/context';
48
import { DomainSearchSkipSuggestion } from '../../ui';
59

@@ -8,8 +12,10 @@ const SkipSuggestion = () => {
812

913
const isMutating = useIsMutating();
1014

11-
const isWpcomSubdomain = isWpcomSubdomainQuery( query );
12-
const normalizedQuery = isWpcomSubdomain ? stripWpcomSubdomainSuffix( query ) : query;
15+
const isFreeSubdomain = isFreeSubdomainQuery( query );
16+
const normalizedQuery = isWpcomSubdomainQuery( query )
17+
? stripWpcomSubdomainSuffix( query )
18+
: query;
1319

1420
const { data: suggestion } = useQuery( queries.freeSuggestion( normalizedQuery ) );
1521

@@ -24,7 +30,7 @@ const SkipSuggestion = () => {
2430
}
2531

2632
if ( suggestion ) {
27-
const isUnavailable = isWpcomSubdomain && suggestion.domain_name !== query;
33+
const isUnavailable = isFreeSubdomain && suggestion.domain_name !== query;
2834

2935
return (
3036
<DomainSearchSkipSuggestion

packages/domain-search/src/components/skip-suggestion/test/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,17 +104,17 @@ describe( 'SkipSuggestion', () => {
104104
expect( onSkip ).toHaveBeenCalledWith( freeSuggestion );
105105
} );
106106

107-
it( 'includes .blog suggestions when the config allows it', async () => {
108-
const freeSuggestion = buildFreeSuggestion( { domain_name: 'site.blog' } );
107+
it( 'includes .blog suggestions when the config allows it and query is a .blog subdomain', async () => {
108+
const freeSuggestion = buildFreeSuggestion( { domain_name: 'site.tech.blog' } );
109109

110110
const freeSuggestionQuery = mockGetFreeSuggestionQuery( {
111-
params: { query: 'site', include_dotblogsubdomain: true },
111+
params: { query: 'site.tech.blog', include_dotblogsubdomain: true },
112112
freeSuggestion,
113113
} );
114114

115115
render(
116116
<TestDomainSearchWithSuggestions
117-
query="site"
117+
query="site.tech.blog"
118118
config={ { skippable: true, includeDotBlogSubdomain: true } }
119119
>
120120
<SkipSuggestion />
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
export { getRootDomain } from './get-root-domain';
22
export { getTld } from './get-tld';
33
export { isSubdomain } from './is-subdomain';
4-
export { isWpcomSubdomainQuery, stripWpcomSubdomainSuffix } from './is-wpcom-subdomain-query';
4+
export {
5+
isBlogSubdomainQuery,
6+
isFreeSubdomainQuery,
7+
isWpcomSubdomainQuery,
8+
stripWpcomSubdomainSuffix,
9+
} from './is-free-subdomain-query';
510
export { parseDomainAgainstTldList } from './parse-domain-against-tld-list';
611
export { parseMatchReasons } from './parse-match-reasons';
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
const WPCOM_SUBDOMAIN_SUFFIX = '.wordpress.com';
2+
3+
/**
4+
* List of free .blog subdomains offered by WordPress.com.
5+
* Duplicated from packages/domains-table/src/utils/is-free-url-domain-name.ts
6+
* to avoid a heavy dependency. Keep both lists in sync when adding new subdomains.
7+
*/
8+
const FREE_DOT_BLOG_SUBDOMAINS = [
9+
'art',
10+
'business',
11+
'car',
12+
'code',
13+
'data',
14+
'design',
15+
'family',
16+
'fashion',
17+
'finance',
18+
'fitness',
19+
'food',
20+
'game',
21+
'health',
22+
'home',
23+
'law',
24+
'movie',
25+
'music',
26+
'news',
27+
'p2',
28+
'photo',
29+
'poetry',
30+
'politics',
31+
'school',
32+
'science',
33+
'sport',
34+
'tech',
35+
'travel',
36+
'video',
37+
'water',
38+
];
39+
40+
const BLOG_SUBDOMAIN_SUFFIXES = FREE_DOT_BLOG_SUBDOMAINS.map(
41+
( subdomain ) => `.${ subdomain }.blog`
42+
);
43+
44+
/**
45+
* Detects whether a search query is for a WordPress.com subdomain
46+
* (e.g. "mysite.wordpress.com").
47+
*/
48+
export function isWpcomSubdomainQuery( query: string ): boolean {
49+
return query.endsWith( WPCOM_SUBDOMAIN_SUFFIX );
50+
}
51+
52+
/**
53+
* Strips the WordPress.com subdomain suffix from a query, returning just the
54+
* subdomain label (e.g. "mysite.wordpress.com" → "mysite").
55+
*/
56+
export function stripWpcomSubdomainSuffix( query: string ): string {
57+
if ( query.endsWith( WPCOM_SUBDOMAIN_SUFFIX ) ) {
58+
return query.slice( 0, -WPCOM_SUBDOMAIN_SUFFIX.length );
59+
}
60+
return query;
61+
}
62+
63+
/**
64+
* Detects whether a search query is for a free .blog subdomain
65+
* (e.g. "test.tech.blog", "mysite.photo.blog").
66+
*/
67+
export function isBlogSubdomainQuery( query: string ): boolean {
68+
return BLOG_SUBDOMAIN_SUFFIXES.some( ( suffix ) => query.endsWith( suffix ) );
69+
}
70+
71+
/**
72+
* Detects whether a search query is for any free subdomain
73+
* (.wordpress.com or free .blog subdomains).
74+
*/
75+
export function isFreeSubdomainQuery( query: string ): boolean {
76+
return isWpcomSubdomainQuery( query ) || isBlogSubdomainQuery( query );
77+
}

packages/domain-search/src/helpers/is-wpcom-subdomain-query.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.

packages/domain-search/src/hooks/use-suggestions-list.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { type DomainAvailability, DomainAvailabilityStatus } from '@automattic/api-core';
22
import { DefinedUseQueryResult, useQueries, useQuery, UseQueryResult } from '@tanstack/react-query';
33
import { useMemo } from 'react';
4-
import { getTld, isWpcomSubdomainQuery, stripWpcomSubdomainSuffix } from '../helpers';
4+
import {
5+
getTld,
6+
isFreeSubdomainQuery,
7+
isWpcomSubdomainQuery,
8+
stripWpcomSubdomainSuffix,
9+
} from '../helpers';
510
import { addAvailabilityAsSuggestion } from '../helpers/add-availability-as-suggestion';
611
import { isSupportedPremiumDomain } from '../helpers/is-supported-premium-domain';
712
import { partitionSuggestions } from '../helpers/partition-suggestions';
@@ -27,15 +32,17 @@ const availablePremiumDomainsCombinator = (
2732
export const useSuggestionsList = () => {
2833
const { query, queries, config } = useDomainSearch();
2934

30-
const isWpcomSubdomain = isWpcomSubdomainQuery( query );
31-
const freeSuggestionQuery = isWpcomSubdomain ? stripWpcomSubdomainSuffix( query ) : query;
35+
const isFreeSubdomain = isFreeSubdomainQuery( query );
36+
const freeSuggestionQuery = isWpcomSubdomainQuery( query )
37+
? stripWpcomSubdomainSuffix( query )
38+
: query;
3239

3340
const { data: suggestions = [], isLoading: isLoadingSuggestions } = useQuery( {
3441
...queries.domainSuggestions( query ),
3542
enabled: true,
3643
} );
3744

38-
const isFqdnQuery = ! isWpcomSubdomain && !! getTld( query );
45+
const isFqdnQuery = ! isFreeSubdomain && !! getTld( query );
3946

4047
const { isLoading: isLoadingFreeSuggestion } = useQuery( {
4148
...queries.freeSuggestion( freeSuggestionQuery ),

packages/domain-search/src/page/context.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
freeSuggestionQuery,
66
} from '@automattic/api-queries';
77
import { createContext, useCallback, useContext, useMemo, useState } from 'react';
8+
import { isBlogSubdomainQuery } from '../helpers';
89
import { DEFAULT_FILTER } from './constants';
910
import { type DomainSearchProps, type DomainSearchContextType } from './types';
1011

@@ -155,7 +156,8 @@ export const useDomainSearchContextValue = ( {
155156
} ),
156157
freeSuggestion: ( query ) => ( {
157158
...freeSuggestionQuery( query, {
158-
include_dotblogsubdomain: normalizedConfig.includeDotBlogSubdomain,
159+
include_dotblogsubdomain:
160+
normalizedConfig.includeDotBlogSubdomain && isBlogSubdomainQuery( query ),
159161
} ),
160162
enabled: false,
161163
staleTime: Infinity,

packages/domain-search/src/page/test/results.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,59 @@ describe( 'ResultsPage', () => {
769769
} );
770770
} );
771771

772+
describe( 'free .blog subdomain suggestion', () => {
773+
it( 'renders the skip suggestion when searching for an available .blog subdomain', async () => {
774+
mockGetSuggestionsQuery( {
775+
params: { query: 'mysite.tech.blog' },
776+
suggestions: [ buildSuggestion( { domain_name: 'mysite.com' } ) ],
777+
} );
778+
779+
mockGetFreeSuggestionQuery( {
780+
params: { query: 'mysite.tech.blog', include_dotblogsubdomain: true },
781+
freeSuggestion: buildFreeSuggestion( { domain_name: 'mysite.tech.blog' } ),
782+
} );
783+
784+
render(
785+
<TestDomainSearch
786+
config={ { skippable: true, includeDotBlogSubdomain: true } }
787+
query="mysite.tech.blog"
788+
>
789+
<ResultsPage />
790+
</TestDomainSearch>
791+
);
792+
793+
expect( await screen.findByText( 'Start free with mysite.tech.blog' ) ).toBeInTheDocument();
794+
expect(
795+
screen.getByLabelText( 'Skip purchase and continue with mysite.tech.blog' )
796+
).toBeInTheDocument();
797+
} );
798+
799+
it( 'renders unavailable notice when the searched .blog subdomain is taken', async () => {
800+
mockGetSuggestionsQuery( {
801+
params: { query: 'taken.photo.blog' },
802+
suggestions: [ buildSuggestion( { domain_name: 'taken.com' } ) ],
803+
} );
804+
805+
mockGetFreeSuggestionQuery( {
806+
params: { query: 'taken.photo.blog', include_dotblogsubdomain: true },
807+
freeSuggestion: buildFreeSuggestion( { domain_name: 'taken123.photo.blog' } ),
808+
} );
809+
810+
render(
811+
<TestDomainSearch
812+
config={ { skippable: true, includeDotBlogSubdomain: true } }
813+
query="taken.photo.blog"
814+
>
815+
<ResultsPage />
816+
</TestDomainSearch>
817+
);
818+
819+
expect( await screen.findByText( 'taken.photo.blog is not available' ) ).toBeInTheDocument();
820+
expect( screen.getByRole( 'button', { name: 'taken123.photo.blog' } ) ).toBeInTheDocument();
821+
expect( screen.queryByLabelText( /Skip purchase/ ) ).not.toBeInTheDocument();
822+
} );
823+
} );
824+
772825
describe( 'tracking', () => {
773826
it( 'fires the onSuggestionsReceive event when the suggestions are received', async () => {
774827
const onSuggestionsReceive = jest.fn();

packages/domains-table/src/utils/is-free-url-domain-name.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
* mylinkinbio.w.link.
44
*
55
* We need to update this function every time a new free managed subdomain is added to WPCOM.
6+
* The .blog subdomain list is also duplicated in
7+
* packages/domain-search/src/helpers/is-blog-subdomain-query.ts — keep both in sync.
68
*/
79
export function isFreeUrlDomainName( domainName: string ): boolean {
810
const freeDotBlogSubdomains = [

0 commit comments

Comments
 (0)