-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement WordPress native search endpoint (#671)
Co-authored-by: Nícholas André <[email protected]> Co-authored-by: Nícholas André <[email protected]>
- Loading branch information
1 parent
e0e7c82
commit 8452279
Showing
24 changed files
with
3,491 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
"@headstartwp/headstartwp": minor | ||
"@headstartwp/core": minor | ||
"@headstartwp/next": minor | ||
--- | ||
|
||
Implement WordPress native search endpoint |
163 changes: 163 additions & 0 deletions
163
docs/documentation/02 - Data Fetching/useSearchNative.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
--- | ||
slug: /data-fetching/usesnative-earch | ||
sidebar_position: 5 | ||
--- | ||
|
||
# The useSearchNative hook | ||
|
||
> The [useSearchNative](/api/modules/headstartwp_next#usesearchnative) hook is the Next.js binding for the [useFetchSearchNative](/api/namespaces/headstartwp_core.react#usefetchsearchnative). | ||
The `useSearchNative` hook is the implementation of core [Search Results](https://developer.wordpress.org/rest-api/reference/search-results/) endpoint. | ||
|
||
:::caution | ||
This hook was introduced in `@headstartwp/[email protected]`, `@headstartwp/[email protected]` and requires the the HeadstartWP WordPress plugin >= 1.1.0 | ||
:::caution | ||
|
||
The headstartwp WordPress plugin does additional customizations to ensure the Search Results endpoints return all the embeddable data associated with search results. | ||
|
||
## Basic Usage | ||
|
||
Assuming a `src/pages/search/[[...path]].js` route with the following content. | ||
|
||
:::info | ||
This example is using the optional catch-all route `[[..path]].js` because we want the `/search` route to be handled by the same file and fetch the latest posts. | ||
:::info | ||
|
||
```js title="src/pages/search/[[...path]].js" | ||
import { useSearchNative } from '@headstartwp/next'; | ||
|
||
const ArchivePage = () => { | ||
const { loading, error, data } = useSearchNative({ per_page: 10 }); | ||
|
||
if (loading) { | ||
return 'Loading...'; | ||
} | ||
|
||
if (error) { | ||
return 'error...'; | ||
} | ||
|
||
if (data.pageInfo.totalItems === 0) { | ||
return 'Nothing found'; | ||
} | ||
|
||
return ( | ||
<> | ||
<h1>Search Results</h1> | ||
<ul> | ||
{data.searchResults.map((item) => ( | ||
<li key={item.id}> | ||
<Link href={item.url}> | ||
{item.id} - {item.title} | ||
</Link> | ||
</li> | ||
))} | ||
</ul> | ||
</> | ||
); | ||
}; | ||
``` | ||
|
||
The route will automatically render the latest 10 results if no search term is provided. The following paths are automatically handled: | ||
|
||
- /search/search-term | ||
- /search/search-term/page/2 | ||
- /search | ||
|
||
## Searching from multiple post types | ||
|
||
You can specify any of the supported parameters described in the [Search Results](https://developer.wordpress.org/rest-api/reference/search-results/#arguments) endpoint documentation. | ||
|
||
```js title="src/pages/search/[[...path]].js" | ||
import { useSearchNative } from '@headstartwp/next'; | ||
|
||
const ArchivePage = () => { | ||
const { loading, error, data } = useSearchNative({ | ||
per_page: 10, | ||
type: 'post', | ||
subtype: ['post', 'page'] | ||
}); | ||
|
||
if (loading) { | ||
return 'Loading...'; | ||
} | ||
|
||
if (error) { | ||
return 'error...'; | ||
} | ||
|
||
if (data.pageInfo.totalItems === 0) { | ||
return 'Nothing found'; | ||
} | ||
|
||
return ( | ||
<> | ||
<h1>Search Results</h1> | ||
<ul> | ||
{data.searchResults.map((item) => ( | ||
<li key={item.id}> | ||
<Link href={item.url}> | ||
{item.id} - {item.title} | ||
</Link> | ||
</li> | ||
))} | ||
</ul> | ||
</> | ||
); | ||
}; | ||
``` | ||
|
||
## Searching for terms | ||
|
||
You can also search for terms: | ||
|
||
```js title="src/pages/terms/search/[[...path]].js" | ||
import { useSearch } from '@headstartwp/next'; | ||
|
||
const ArchivePage = () => { | ||
const { loading, error, data } = useSearchNative({ | ||
per_page: 10, | ||
type: 'term', | ||
subtype: ['category', 'category'] | ||
}); | ||
|
||
if (loading) { | ||
return 'Loading...'; | ||
} | ||
|
||
if (error) { | ||
return 'error...'; | ||
} | ||
|
||
if (data.pageInfo.totalItems === 0) { | ||
return 'Nothing found'; | ||
} | ||
|
||
return ( | ||
<> | ||
<h1>Search Results</h1> | ||
<ul> | ||
{data.searchResults.map((item) => ( | ||
<li key={item.id}> | ||
<Link href={item.url}> | ||
{item.id} - {item.title} | ||
</Link> | ||
</li> | ||
))} | ||
</ul> | ||
</> | ||
); | ||
}; | ||
``` | ||
|
||
## Accessing embeddable data | ||
By default, the Search Results endpoints only return the object of the associated search results but do not return embeddable data of the search results entities themselves. For instance, when searching for posts, even if you pass the `_embed` parameter, WordPress won't return the associated term objects, author objects etc. | ||
|
||
HeadstartWP plugin extends the core endpoint so that it returns these embedded objects to avoid the need for additional queries. Check the [PostSearchEntity](/api/interfaces/headstartwp_core.PostSearchEntity/) and [TermSearcheEntity](api/interfaces/headstartwp_core.TermSearchEntity/). | ||
|
||
## QueriedObject | ||
|
||
The `useNativeSearch` hook also exposes a `queriedObject`. | ||
|
||
The queried object for this hook is an object of type [SearchEntity](/api/interfaces/headstartwp_core.SearchEntity/). | ||
|
172 changes: 172 additions & 0 deletions
172
packages/core/src/data/strategies/SearchNativeFetchStrategy.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
import { getSiteBySourceUrl, addQueryArgs, getWPUrl } from '../../utils'; | ||
import { endpoints } from '../utils'; | ||
import { apiGet } from '../api'; | ||
import { PostSearchEntity, TermSearchEntity, QueriedObject } from '../types'; | ||
import { searchMatchers } from '../utils/matchers'; | ||
import { parsePath } from '../utils/parsePath'; | ||
import { FetchOptions, AbstractFetchStrategy, EndpointParams } from './AbstractFetchStrategy'; | ||
|
||
/** | ||
* The EndpointParams supported by the {@link SearchNativeFetchStrategy} | ||
*/ | ||
export interface SearchParams extends EndpointParams { | ||
/** | ||
* Current page of the collection. | ||
* | ||
* @default 1 | ||
*/ | ||
page?: number; | ||
|
||
/** | ||
* Maximum number of items to be returned in result set. | ||
* | ||
* @default 10 | ||
*/ | ||
per_page?: number; | ||
|
||
/** | ||
* Limit results to those matching a string. | ||
*/ | ||
search?: string; | ||
|
||
/** | ||
* Limit results to items of an object type. | ||
* | ||
* @default 'post' | ||
*/ | ||
type?: 'post' | 'term' | 'post-format'; | ||
|
||
/** | ||
* Limit results to items of one or more object subtypes. | ||
*/ | ||
subtype?: string | string[]; | ||
|
||
/** | ||
* Ensure result set excludes specific IDs. | ||
*/ | ||
exclude?: number[]; | ||
|
||
/** | ||
* Limit result set to specific IDs. | ||
*/ | ||
include?: number[]; | ||
} | ||
|
||
/** | ||
* The SearchNativeFetchStrategy is used to fetch search results for a given search query | ||
* Uses the native WordPress search endpoint. | ||
* | ||
* Note that custom post types and custom taxonomies should be defined in `headless.config.js` | ||
* | ||
* This strategy supports extracting endpoint params from url E.g: | ||
* - `/page/2/` maps to `{ page: 2 }` | ||
* - `/searched-term/page/2` maps to `{ search: 'searched-term', page: 2 }` | ||
* | ||
* @see {@link getParamsFromURL} to learn about url param mapping | ||
* | ||
* @category Data Fetching | ||
*/ | ||
export class SearchNativeFetchStrategy< | ||
T extends PostSearchEntity | TermSearchEntity = PostSearchEntity | TermSearchEntity, | ||
P extends SearchParams = SearchParams, | ||
> extends AbstractFetchStrategy<T[], P> { | ||
path: string = ''; | ||
|
||
locale: string = ''; | ||
|
||
getDefaultEndpoint() { | ||
return endpoints.search; | ||
} | ||
|
||
getDefaultParams(): Partial<P> { | ||
return { _embed: true, ...super.getDefaultParams() } as P; | ||
} | ||
|
||
/** | ||
* This strategy automatically extracts the search term and and pagination params from the URL | ||
* | ||
* @param path The URL path to extract params from | ||
* @param params The params passed to the strategy | ||
*/ | ||
getParamsFromURL(path: string, params: Partial<P> = {}): Partial<P> { | ||
const config = getSiteBySourceUrl(this.baseURL); | ||
|
||
// Required for search lang url. | ||
this.locale = config.integrations?.polylang?.enable && params.lang ? params.lang : ''; | ||
|
||
return parsePath(searchMatchers, path) as Partial<P>; | ||
} | ||
|
||
/** | ||
* Builds the endpoint url for the search endpoint | ||
* | ||
* @param params The params for the request | ||
* @returns | ||
*/ | ||
buildEndpointURL(params: Partial<P>): string { | ||
const normalizedParams = { ...params }; | ||
if (Array.isArray(normalizedParams.subtype)) { | ||
normalizedParams.subtype = normalizedParams.subtype.join(','); | ||
} | ||
|
||
return super.buildEndpointURL(normalizedParams); | ||
} | ||
|
||
/** | ||
* The fetcher function is overridden to disable throwing if not found | ||
* | ||
* If a search request returns not found we do not want to redirect to a 404 page, | ||
* instead the user should be informed that no posts were found | ||
* | ||
* @param url The url to parse | ||
* @param params The params to build the endpoint with | ||
* @param options FetchOptions | ||
*/ | ||
async fetcher(url: string, params: Partial<P>, options: Partial<FetchOptions> = {}) { | ||
const { burstCache = false } = options; | ||
let seo_json: Record<string, any> = {}; | ||
let seo: string = ''; | ||
|
||
// Request SEO data. | ||
try { | ||
const wpUrl = getWPUrl().replace(/\/$/, ''); // Ensure no double slash in url param | ||
const localeParam = this.locale ? `&lang=${this.locale}` : ''; | ||
const pageParam = params.page ? `/page/${params.page}` : ''; | ||
|
||
const result = await apiGet( | ||
addQueryArgs(`${wpUrl}${endpoints.yoast}`, { | ||
url: `${wpUrl}${pageParam}/?s=${params.search ?? ''}${localeParam}`, | ||
}), | ||
{}, | ||
burstCache, | ||
); | ||
|
||
seo = result.json.html ?? null; | ||
seo_json = { ...result.json.json }; | ||
} catch (e) { | ||
// do nothing | ||
} | ||
|
||
const queriedObject: QueriedObject = { | ||
search: { | ||
searchedValue: params.search ?? '', | ||
type: params.type ?? 'post', | ||
subtype: params.subtype ?? 'post', | ||
yoast_head: seo, | ||
yoast_head_json: { | ||
...seo_json, | ||
}, | ||
}, | ||
}; | ||
|
||
const response = await super.fetcher(url, params, { | ||
...options, | ||
throwIfNotFound: false, | ||
}); | ||
|
||
return { | ||
...response, | ||
queriedObject, | ||
}; | ||
} | ||
} |
39 changes: 39 additions & 0 deletions
39
packages/core/src/data/strategies/__tests__/SearchNativeFetchStrategy.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { SearchNativeFetchStrategy } from '../SearchNativeFetchStrategy'; | ||
import { setHeadstartWPConfig } from '../../../utils'; | ||
|
||
describe('SearchNativeFetchStrategy', () => { | ||
let fetchStrategy: SearchNativeFetchStrategy; | ||
|
||
beforeEach(() => { | ||
fetchStrategy = new SearchNativeFetchStrategy(); | ||
|
||
setHeadstartWPConfig({}); | ||
}); | ||
|
||
it('parse url properly', async () => { | ||
expect(fetchStrategy.getParamsFromURL('/')).toEqual({}); | ||
|
||
expect(fetchStrategy.getParamsFromURL('/searched-term')).toEqual({ | ||
search: 'searched-term', | ||
}); | ||
|
||
expect(fetchStrategy.getParamsFromURL('/searched-term/page/2')).toEqual({ | ||
search: 'searched-term', | ||
page: '2', | ||
}); | ||
expect(fetchStrategy.getParamsFromURL('/searched-term/page/2/')).toEqual({ | ||
search: 'searched-term', | ||
page: '2', | ||
}); | ||
}); | ||
|
||
it('builds the endpoint url properly', () => { | ||
expect(fetchStrategy.buildEndpointURL({})).toBe('/wp-json/wp/v2/search'); | ||
}); | ||
|
||
it('allows overriding default params', () => { | ||
const defaultParams = { taxonomy: 'genre' }; | ||
const fetcher = new SearchNativeFetchStrategy('http://sourceurl.com', defaultParams); | ||
expect(fetcher.getDefaultParams()).toMatchObject(defaultParams); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.