Skip to content

Commit

Permalink
feat: add WP native search fetch strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
lucymtc committed Jan 26, 2024
1 parent a593a75 commit 9d2d097
Show file tree
Hide file tree
Showing 11 changed files with 316 additions and 21 deletions.
155 changes: 155 additions & 0 deletions packages/core/src/data/strategies/SearchNativeFetchStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { getSiteBySourceUrl, addQueryArgs, getWPUrl } from '../../utils';
import { endpoints } from '../utils';
import { apiGet } from '../api';
import { PostEntity, 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;

/**
* 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 PostEntity = PostEntity,
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 taxonomy filters, date filters and pagination params from the URL
*
* It also takes into account the custom taxonomies specified in `headless.config.js`
*
* @param path The URL path to extract params from
* @param params
*/
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>;
}

/**
* 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 result = await apiGet(
addQueryArgs(`${wpUrl}${endpoints.yoast}`, {
url: `${wpUrl}/?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: 'post',
subtype: (params.postType as string) ?? 'post',
yoast_head: seo,
yoast_head_json: {
...seo_json,
},
},
};

const response = await super.fetcher(url, params, { ...options, throwIfNotFound: false });

return {
...response,
queriedObject,
};
}
}
1 change: 1 addition & 0 deletions packages/core/src/data/strategies/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './AbstractFetchStrategy';
export * from './SinglePostFetchStrategy';
export * from './PostsArchiveFetchStrategy';
export * from './SearchFetchStrategy';
export * from './SearchNativeFetchStrategy';
export * from './AppSettingsStrategy';
export * from './TaxonomyTermsStrategy';
export * from './AuthorArchiveFetchStrategy';
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/data/utils/postHandling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export function getPostTerms(post: PostEntity): Record<string, TermEntity[]> {
}

post._embedded['wp:term'].forEach((taxonomy) => {
if (!Array.isArray(taxonomy)) {
return;
}

taxonomy.forEach((term) => {
const taxonomySlug = term.taxonomy;

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/react/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './useFetch';
export * from './useFetchPost';
export * from './useFetchPosts';
export * from './useFetchSearch';
export * from './useFetchSearchNative';
export * from './types';
export * from './useFetchTerms';
export * from './useFetchAuthorArchive';
Expand Down
77 changes: 77 additions & 0 deletions packages/core/src/react/hooks/useFetchSearchNative.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { useFetch } from './useFetch';

import type { FetchHookOptions } from './types';
import {
FetchResponse,
getPostAuthor,
getPostTerms,
PageInfo,
PostEntity,
SearchParams,
QueriedObject,
SearchNativeFetchStrategy,
} from '../../data';
import { getWPUrl } from '../../utils';
import { makeErrorCatchProxy } from './util';
import { useSearchResponse } from './useFetchSearch';

/**
* The useFetchSearchNative hook. Returns a collection of search entities retrieved through the WP native search endpoint
*
* See {@link useSearchNative} for usage instructions.
*
* @param params The list of params to pass to the fetch strategy. It overrides the ones in the URL.
* @param options The options to pass to the swr hook.
* @param path The path of the url to get url params from.
*
* @category Data Fetching Hooks
*/
export function useFetchSearchNative<
T extends PostEntity = PostEntity,
P extends SearchParams = SearchParams,
>(
params: P | {} = {},
options: FetchHookOptions<FetchResponse<T[]>> = {},
path = '',
): useSearchResponse<T> {
const { data, error, isMainQuery } = useFetch<T[], P>(
params,
useFetchSearchNative.fetcher<T, P>(),
options,
path,
);

if (error || !data) {
const fakeData = {
posts: makeErrorCatchProxy<T[]>('posts'),
pageInfo: makeErrorCatchProxy<PageInfo>('pageInfo'),
queriedObject: makeErrorCatchProxy<QueriedObject>('queriedObject'),
};
return { error, loading: !data, data: fakeData, isMainQuery };
}

const { result, pageInfo, queriedObject } = data;

const posts = result.map((post) => {
post.author = getPostAuthor(post);
post.terms = getPostTerms(post);

return post;
});

return { data: { posts, pageInfo, queriedObject }, loading: false, isMainQuery };
}

/**
* @internal
*/
// eslint-disable-next-line no-redeclare
export namespace useFetchSearchNative {
export const fetcher = <
T extends PostEntity = PostEntity,
P extends SearchParams = SearchParams,
>(
sourceUrl?: string,
defaultParams?: P,
) => new SearchNativeFetchStrategy<T, P>(sourceUrl ?? getWPUrl(), defaultParams);
}
1 change: 1 addition & 0 deletions packages/core/src/utils/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const endpoints = {
appSettings: '/wp-json/headless-wp/v1/app',
category: '/wp-json/wp/v2/categories',
tags: '/wp-json/wp/v2/tags',
search: '/wp-json/wp/v2/search',
tokenVerify: '/wp-json/headless-wp/v1/token',
yoast: '/wp-json/yoast/v1/get_head',
};
1 change: 1 addition & 0 deletions packages/next/src/data/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './usePost';
export * from './usePosts';
export * from './useSearch';
export * from './useSearchNative';
export * from './useAppSettings';
export * from './useMenu';
export * from './useTerms';
Expand Down
37 changes: 37 additions & 0 deletions packages/next/src/data/hooks/useSearchNative.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { PostEntity, SearchParams, FetchResponse } from '@headstartwp/core';
import { FetchHookOptions, useFetchSearchNative } from '@headstartwp/core/react';
import { usePrepareFetch } from './usePrepareFetch';

/**
* The useSearchNative hook. Returns a collection of search entities retrieved through the WP native search endpoint.
*
* In order to automatically map URL params create a catch-all route named `[...path].js`.
* You can create the catch-all at any level e.g: `pages/search/[[...path]].js`
*
* The `pages/search/[[...path]].js` route for instance would yield a URL like this: `/search/[term]/page/[number]`, `/search/[term]` etc
*
* @param params The parameters accepted by the hook
* @param options Options for the SWR configuration
*
* @category Data Fetching Hooks
*/
export function useSearchNative<
T extends PostEntity = PostEntity,
P extends SearchParams = SearchParams,
>(params: Partial<P> = {}, options: FetchHookOptions<FetchResponse<T[]>> = {}) {
const useFetchArguments = usePrepareFetch(params, options);

return useFetchSearchNative(
useFetchArguments.params,
useFetchArguments.options,
useFetchArguments.path,
);
}

/**
* @internal
*/
// eslint-disable-next-line no-redeclare
export namespace useSearchNative {
export const { fetcher } = useFetchSearchNative;
}
10 changes: 5 additions & 5 deletions projects/wp-nextjs/src/pages/search/[[...path]].js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
useSearch,
useSearchNative,
fetchHookData,
addHookData,
handleError,
Expand All @@ -10,7 +10,7 @@ import { searchParams } from '../../params';
import { resolveBatch } from '../../utils/promises';

const SearchPage = () => {
const { data } = useSearch(searchParams);
const { data } = useSearchNative(searchParams);

if (data.pageInfo.totalItems === 0) {
return 'Nothing found';
Expand All @@ -22,8 +22,8 @@ const SearchPage = () => {
<ul>
{data.posts.map((item) => (
<li key={item.id}>
<Link href={item.link}>
{item.id} - {item.title.rendered}
<Link href={item.url}>
{item.id} - {item.title}
</Link>
</li>
))}
Expand All @@ -38,7 +38,7 @@ export async function getServerSideProps(context) {
try {
const settledPromises = await resolveBatch([
{
func: fetchHookData(useSearch.fetcher(), context, { params: searchParams }),
func: fetchHookData(useSearchNative.fetcher(), context, { params: searchParams }),
},
{
func: fetchHookData(useAppSettings.fetcher(), context),
Expand Down
5 changes: 4 additions & 1 deletion projects/wp-nextjs/src/params.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ export const indexParams = { postType: ['page'] };
/**
* @type {import('@headstartwp/core').PostsArchiveParams}
*/
export const searchParams = { postType: 'post' };
export const searchParams = {
type: 'post',
subtype: 'page, post',
};

/**
* @type {import('@headstartwp/core').PostOrPostsParams}
Expand Down
Loading

0 comments on commit 9d2d097

Please sign in to comment.