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 12 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
156 changes: 156 additions & 0 deletions packages/core/src/data/strategies/SearchNativeFetchStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
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;

/**
* 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 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 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,
};
}
}
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
58 changes: 58 additions & 0 deletions packages/core/src/data/types/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,64 @@ export interface SearchEntity extends Entity {
subtype: string;
}

/**
* Interface for search object entities from the /wp/v2/search endpoint.
*
* Interfaces that extends from this one are:
* - {@link PostSearchEntity}.
* - {@link TermSearchEntity}.
*/
export interface SearchObjectEntity extends Entity {
/**
* Unique identifier for the object.
*/
id: number;

/**
* URL to the object.
*/
url: string;

/**
* The title for the object.
*/
title: string;

/**
* Type of Search for the object.
*/
type: 'post' | 'term' | 'post-format';
}

/**
* Interface for posts entities from the /wp/v2/search endpoint.
*/
export interface PostSearchEntity extends SearchObjectEntity {
/**
* Subtype of Search for the object.
*/
subtype: string;

author?: AuthorEntity[];

terms?: Record<string, TermEntity[]>;

_embedded: {
author: AuthorEntity[];
nicholasio marked this conversation as resolved.
Show resolved Hide resolved
'wp:term': Array<TermEntity[]>;
self: Array<PostEntity[]>;
lucymtc marked this conversation as resolved.
Show resolved Hide resolved
};
}

/**
* Interface for terms entities from the /wp/v2/search endpoint.
*/
export interface TermSearchEntity extends SearchObjectEntity {
_embedded: {
self: Array<TermEntity[]>;
lucymtc marked this conversation as resolved.
Show resolved Hide resolved
};
}

export type Redirect = {
ID: number;
post_status: string;
Expand Down
10 changes: 7 additions & 3 deletions packages/core/src/data/utils/postHandling.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AttachmentEntity, AuthorEntity, PostEntity, TermEntity } from '../types';
import { AttachmentEntity, AuthorEntity, PostEntity, PostSearchEntity, TermEntity } from '../types';
import { removeFields } from './dataFilter';

/**
Expand All @@ -8,7 +8,7 @@ import { removeFields } from './dataFilter';
*
* @category Data Handling
*/
export function getPostAuthor(post: PostEntity) {
export function getPostAuthor(post: PostEntity | PostSearchEntity) {
return post?._embedded?.author;
}

Expand All @@ -19,7 +19,7 @@ export function getPostAuthor(post: PostEntity) {
*
* @category Data Handling
*/
export function getPostTerms(post: PostEntity): Record<string, TermEntity[]> {
export function getPostTerms(post: PostEntity | PostSearchEntity): Record<string, TermEntity[]> {
const terms: PostEntity['terms'] = {};

if (
Expand All @@ -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
86 changes: 86 additions & 0 deletions packages/core/src/react/hooks/useFetchSearchNative.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { useFetch } from './useFetch';

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

export interface useSearchNativeResponse<
T extends PostSearchEntity | TermSearchEntity = PostSearchEntity | TermSearchEntity,
> extends HookResponse {
data?: { searchResults: T[]; pageInfo: PageInfo; queriedObject: QueriedObject };
}

/**
* 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 PostSearchEntity | TermSearchEntity = PostSearchEntity | TermSearchEntity,
P extends SearchParams = SearchParams,
>(
params: P | {} = {},
options: FetchHookOptions<FetchResponse<T[]>> = {},
path = '',
): useSearchNativeResponse<T> {
const { data, error, isMainQuery } = useFetch<T[], P>(
params,
useFetchSearchNative.fetcher<T, P>(),
options,
path,
);

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

const { result, pageInfo, queriedObject } = data;

const searchResults = result.map((post) => {
if ('subtype' in post) {
const postSearchEntity = post as PostSearchEntity;
post.author = getPostAuthor(postSearchEntity);
post.terms = getPostTerms(postSearchEntity);
}

return post;
});

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

/**
* @internal
*/
// eslint-disable-next-line no-redeclare
export namespace useFetchSearchNative {
export const fetcher = <
T extends PostSearchEntity | TermSearchEntity = PostSearchEntity | TermSearchEntity,
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
Loading
Loading