Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement WordPress native search endpoint #671

Merged
merged 19 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/wise-ligers-juggle.md
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
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,
lucymtc marked this conversation as resolved.
Show resolved Hide resolved
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}` : '';
nicholasio marked this conversation as resolved.
Show resolved Hide resolved

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',
nicholasio marked this conversation as resolved.
Show resolved Hide resolved
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) => {
lucymtc marked this conversation as resolved.
Show resolved Hide resolved
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
3 changes: 3 additions & 0 deletions wp/headless-wp/includes/classes/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ public function register() {

$gutenberg = new Gutenberg();
$gutenberg->register();

$search = new Search\Search();
$search->register();
nicholasio marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down
Loading
Loading