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

Category Path Matching #639

Merged
merged 21 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
9 changes: 9 additions & 0 deletions .changeset/lucky-buttons-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@headstartwp/core": minor
"@headstartwp/next": minor
---

Add support for archive path matching `matchArchivePath`.
Add support for passing a function to `customPostTypes` and `customTaxonomies` option in `headstartwp.config.js`.
Rename `headless.config.js` to `headstartwp.config.js` but keep backward compatibility.
Automatically load `headstartwp.config.js` or `headless.config.js` in `next.config.js`.
78 changes: 70 additions & 8 deletions docs/documentation/01-Getting Started/headless-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,33 @@
slug: /getting-started/headless-config
sidebar_position: 3
---
# Headless Config
# Configuring the Framework

The `headless.config.js` file contains several config options for HeadstartWP. This file should export an object of type [HeadlessConfig](/api/modules/headstartwp_core/#headlessconfig).
The `headstartwp.config.js` (previously, `headless.config.js`) file contains several config options for HeadstartWP. This file should export an object of type [HeadlessConfig](/api/modules/headstartwp_core/#headlessconfig).

## Usage with Next.js

The file **must be named** either `headstartwp.config.js` or `headless.config.js`. When `injectConfig` param of `withHeadstartWPConfig` (previously `withHeadlessConfig`) is set to true, the framework will look for these two files to be injected and loaded in the runtime bundle of the Next.js App.

```js title=next.config.js
const { withHeadstartWPConfig } = require('@headstartwp/next/config');

/**
* Update whatever you need within the nextConfig object.
*
* @type {import('next').NextConfig}
*/
const nextConfig = {};

module.exports = withHeadstartWPConfig(nextConfig);
```
:::caution
Since `@headstartwp/[email protected]` you do not need to import `headstartwp.config.js` in `next.config.js` anymore, the framework will dynamically load the config.
tobeycodes marked this conversation as resolved.
Show resolved Hide resolved
:::caution

Here's a sample config file

```javascript title="headless.config.js"
```javascript title="headstartwp.config.js"
module.exports = {
sourceUrl: process.env.NEXT_PUBLIC_HEADLESS_WP_URL,
hostUrl: process.env.HOST_URL,
Expand Down Expand Up @@ -41,9 +61,9 @@ The `host` option is automatically inferred if `hostUrl` is set. You probably do

## customPostTypes

To add support for custom post types, add your custom post type to the `customPostTypes` setting in `headless.config.js`.
To add support for custom post types, add your custom post type to the `customPostTypes` setting in `headstartwp.config.js`.

```js title="headless.config.js"
```js title="headstartwp.config.js"
module.exports = {
sourceUrl: process.env.NEXT_PUBLIC_HEADLESS_WP_URL,
hostUrl: process.env.HOST_URL,
Expand All @@ -66,15 +86,28 @@ usePost({ postType: ['book'] });
usePosts({ postType:'book', perPage: 10 });
```

The `single` option is required for a number of things that includes:
The `single` option is required for several things including:
- properly previewing custom post types when the "single" route is at a different prefix. E.g: `/book/da-vince-code` instead of `/da-vice-code`; In this case, the framework will use the `single` path to redirect the previewed post to the right path/route.
- Matching post path permalinks with the current URL. E.g: when fetching a single custom post type the framework will filter the returned posts to the one that matches the existing URL. Therefore, the framework needs to know the single prefix url for custom post types. This is required to properly handle parent pages that share the same child slug. See [post path mapping](/learn/data-fetching/usepost/#post-path-matching) for more info.

It is also possible to pass a function, when doing so the default post types (post and pages) will be passed to the function. The code snipped below will disable [post path mapping](/learn/data-fetching/usepost/#post-path-matching) to the default post types.

```js title="headstartwp.config.js"
module.exports = {
sourceUrl: process.env.NEXT_PUBLIC_HEADLESS_WP_URL,
hostUrl: process.env.HOST_URL,
customPostTypes: (defaultPostTypes) => {
// disable post path mapping for default post types
return defaultPostTypes.map((postType) => ({...postType, matchSinglePath: false}));
}
}
```

## customTaxonomies

To add support for custom taxonomies, add your custom taxonomy to the `customTaxonomies` setting in `headless.config.js`.
To add support for custom taxonomies, add your custom taxonomy to the `customTaxonomies` setting in `headstartwp.config.js`.

```js title="headless.config.js"
```js title="headstartwp.config.js"
module.exports = {
customPostTypes: [
{
Expand All @@ -90,6 +123,8 @@ module.exports = {
slug: 'genre',
endpoint: '/wp-json/wp/v2/genre',
postType: ['book'],
rewrite: 'genre',
restParam: 'genre'
},
],
}
Expand Down Expand Up @@ -140,6 +175,33 @@ This route would automatically handle the following URLs:
The code snippet above does not implement pre-fetching, which you probably want to. Check out the [pre-fetching docs](/learn/data-fetching/prefetching) for instructions.
:::caution

It is also possible to specify a function for 'customTaxonomies', when doing so the default taxonomies will be passed to the function. This can be used for instance to enable [archive path matching](/learn/data-fetching/useposts#archive-path-matching).

```js title="headstartwp.config.js"
module.exports = {
customPostTypes: [
{
slug: 'book',
endpoint: '/wp-json/wp/v2/book',
// these should match your file-system routing
single: '/book',
archive: '/books',
},
],
customTaxonomies: (defaultTaxonomies) => {
return defaultTaxonomies.map((taxonomy) => ({ ...taxonomy, matchArchivePath: true })),
},
}
```
### restParam

This option shouldn't be necessary most of the time, but this is used to map a custom taxonomy to its REST API parameter. Most of the times the slug is equal to the restParam but in some cases it differs. For instance, the default post tag taxonomy has a slug of `post_tag` but a `restParam` of `tags` (i.e., to filter posts by `tags` in the REST API, we must use `tags=<tag-id>`).


### rewrite

This option controls the expected prefix the taxonomy must use in front-end urls. This generally should match `rewrite.slug` of the [register_taxonomy](https://developer.wordpress.org/reference/functions/register_taxonomy/) function.

## redirectStrategy

This option control how redirects are handled. There are 2 supported methods of handling redirects.
Expand Down
2 changes: 1 addition & 1 deletion docs/documentation/01-Getting Started/quick-tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ The `resolveBatch` function is just a utility function that lets you run multipl

Next, we have the `addHookData` function which expects an array of "hook data" (i.e pre-fetched data for the custom hooks returned by `fetchHookData`). The `addHookData` will simply put the results on the cache and hydrate the custom hook with pre-fetched data. The second param is an object that represents the Next.js props you can return from `getStaticProps` or `getServerSideProps`.

If anything fails, we call the `handleError` function which provides standard error handling such as rendering a 404 page if a page is not found and optionally handling redirects (if the redirect strategy is set to 404 in headless.config.js).
If anything fails, we call the `handleError` function which provides standard error handling such as rendering a 404 page if a page is not found and optionally handling redirects (if the redirect strategy is set to 404 in headstartwp.config.js).

Lastly, the `getStaticPaths` will return an array of paths that should be pre-rendered at build time. This should only be used in conjunction with getStaticProps. Note that the framework doesn’t force getStaticProps you can use getServerSideProps (especially if your hosting doesn’t provide good support for ISR).

Expand Down
8 changes: 4 additions & 4 deletions docs/documentation/01-Getting Started/setting-up-manually.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ and install the following packages
npm install --save @headstartwp/core @headstartwp/next
```

### headless.config.js
### headstartwp.config.js

Create a `headless.config.js` file at the root of your Next.js project.
Create a `headstartwp.config.js` file at the root of your Next.js project.

```js title="headless.config.js"
```js title="headstartwp.config.js"
/**
* Headless Config
*
Expand All @@ -47,7 +47,7 @@ Then create a `.env` (or `.env.local`) with the following contents:
NEXT_PUBLIC_HEADLESS_WP_URL=https://my-wordpress.test
```

You can call the env variable anything you want, just make sure to update `headless.config.js` accordingly.
You can call the env variable anything you want, just make sure to update `headstartwp.config.js` accordingly.

If you're developing locally and your WordPress instance uses https but does not have a valid cert, add `NODE_TLS_REJECT_UNAUTHORIZED=0` to your env variables.

Expand Down
4 changes: 2 additions & 2 deletions docs/documentation/02 - Data Fetching/useAuthorArchive.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ The route will automatically render the latest 10 posts from the current author.

## Author Archive for Custom Post Type

In order to fetch posts from a custom post type, first declare the custom post type in `headless.config.js` as explained in the [headless.config.js](/learn/getting-started/headless-config#custom-post-types) section.
In order to fetch posts from a custom post type, first declare the custom post type in `headstartwp.config.js` as explained in the [headstartwp.config.js](/learn/getting-started/headless-config#custom-post-types) section.
```js title="src/pages/author/[...path].js"
import { useAuthorArchive } from '@headstartwp/next';

const ArchivePage = () => {
// book must be declared in headless.config.js
// book must be declared in headstartwp.config.js
const { loading, error, data } = useAuthorArchive({ postType: ['book'] });

if (loading) {
Expand Down
4 changes: 2 additions & 2 deletions docs/documentation/02 - Data Fetching/usePost.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Example where path does not match but is redirected to the right one:
- User visits URL `/post-name`
- The post with the `post-name` slug contains a `http://backend.com/2022/10/30/post-name` url
- Since the URL and the path of `post.link` do not match, a NotFound error is thrown
- If prefetching is setup following [pre-fetching](/learn/data-fetching/prefetching) and `redirectStrategy` is set to "404" or "always" in `headless.config.js`, `handleError` will then look if there's a redirect available and since WordPress redirects `/post-name` to `/2022/10/30/post-name`, the framework will also perform the redirect.
- If prefetching is setup following [pre-fetching](/learn/data-fetching/prefetching) and `redirectStrategy` is set to "404" or "always" in `headstartwp.config.js`, `handleError` will then look if there's a redirect available and since WordPress redirects `/post-name` to `/2022/10/30/post-name`, the framework will also perform the redirect.


### Fetching from multiple post types
Expand Down Expand Up @@ -103,7 +103,7 @@ const PostOrPage = () => {

## Fetching from a custom post type

To fetch a single from a custom post type, first declare the custom post type in `headless.config.js` as explained in the [headless.config.js](/learn/getting-started/headless-config#custom-post-types) section.
To fetch a single from a custom post type, first declare the custom post type in `headstartwp.config.js` as explained in the [headstartwp.config.js](/learn/getting-started/headless-config#custom-post-types) section.

```js title="src/pages/book/[...path].js"
import { usePost } from '@headstartwp/next';
Expand Down
25 changes: 24 additions & 1 deletion docs/documentation/02 - Data Fetching/usePosts.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ The route will automatically render the latest 10 posts and you get pagination,

The `usePosts` hook exposes a `queriedObject`. It's similar to WordPress [get_queried_object()](https://developer.wordpress.org/reference/functions/get_queried_object/) function.

It essentially returned the what's being queried for, e.g: author or category. If the current page is querying posts within a certain author, then that author object will be populated in `data.queriedObject.author`. Similarly, if the current page is querying posts from a given category `data.queriedObject.term` will be populated with that category.
It essentially returns what's being queried for, e.g., author or category. If the current page is querying posts within a certain author, then that author object will be populated in `data.queriedObject.author`. Similarly, if the current page is querying posts from a given category `data.queriedObject.term` will be populated with that category.

Example:
```javascript
Expand Down Expand Up @@ -97,8 +97,31 @@ const CategoryPage = () => {
</>
);
};
```

## Archive Path Matching

When using `usePosts` to create archive pages (e.g. a category archive) you can optionally enable "archive path matching" to ensure that your archive routes match the expected permalink dictated by WordPress. Without "archive path matching", your archive routes would match as long as the last segment of the url is a valid term slug.

For instance, let's take the `/category/[...path].js` route above. It will match URLs like:
- /category/cat-name
- /category/parent-cat-name/cat-name

The framework, however, does not check if `parent-cat-name` is the de facto parent of cat-name, and even worse, it has no way to know (without additional rest api calls) if `parent-cat-name` is even a valid category.

To address this, you can pass `matchArchivePath` to `usePosts` to tell the framework to check the `link` property of the `queriedObject`, i.e if the `link` property of `cat-name` returned by the [WordPress REST API](https://developer.wordpress.org/rest-api/reference/categories/#schema) matches the front-end path.

This setting can also be enabled in `headstartwp.config.js` globally.

```js title="headstartwp.config.js"
module.exports = {
// enable archive path mapping for all default taxonomies
customTaxonomies: (defaultTaxonomies) => {
return defaultTaxonomies.map((taxonomy) => ({ ...taxonomy, matchArchivePath: true })),
},
}
```

## Known limitations

- It is not possible to fetch posts from more than one post type.
4 changes: 2 additions & 2 deletions docs/documentation/06-WordPress Integration/multisite.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ slug: /wordpress-integration/multisite

# Multisite

HeadstartWP has built-in support for WordPress multisite via the `sites` property in the `headless.config.js` file. This transforms the Next.js app into a multi-tenant app.
HeadstartWP has built-in support for WordPress multisite via the `sites` property in the `headstartwp.config.js` file. This transforms the Next.js app into a multi-tenant app.

The `sites` option allows specifying as many sites you want to connect to your app. Each site must have a `sourceUrl` and a `hostUrl`. The `hostUrl` will be used to match the current site and `sourceUrl` indicates where content should be sourced from.

Expand All @@ -17,7 +17,7 @@ Take a look at the [multisite demo project](https://github.com/10up/headstartwp/

### Config

The first step is to declare all of your sites in `headless.config.js`. In the example below we're declaring two sites.
The first step is to declare all of your sites in `headstartwp.config.js`. In the example below we're declaring two sites.

```javascript
/**
Expand Down
4 changes: 2 additions & 2 deletions docs/documentation/06-WordPress Integration/polylang.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ slug: /wordpress-integration/polylang
Polylang Pro is required since only Polylang Pro offers the [REST API integration](https://polylang.pro/doc/rest-api/).
:::caution

It is possible to integrate with Polylang by enabling the integration in `headless.config.js` and adding the supported locales to [Next.js config](https://nextjs.org/docs/advanced-features/i18n-routing).
It is possible to integrate with Polylang by enabling the integration in `headstartwp.config.js` and adding the supported locales to [Next.js config](https://nextjs.org/docs/advanced-features/i18n-routing).

```js title="headless.config.js"
```js title="headstartwp.config.js"
module.exports = {
// other settings
integrations: {
Expand Down
76 changes: 75 additions & 1 deletion packages/core/src/data/strategies/PostsArchiveFetchStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
NotFoundError,
addQueryArgs,
getCustomTaxonomy,
removeSourceUrl,
} from '../../utils';
import { endpoints, getPostAuthor, getPostTerms, removeFieldsFromPostRelatedData } from '../utils';
import { apiGet } from '../api';
Expand Down Expand Up @@ -180,6 +181,13 @@
* Limit result set to items that are sticky.
*/
sticky?: boolean;

/**
* Overrides the value set in {@link CustomTaxonomy#matchArchivePath}
*
* @default false
*/
matchArchivePath?: boolean;
}

/**
Expand All @@ -199,6 +207,10 @@
T extends PostEntity = PostEntity,
P extends PostsArchiveParams = PostsArchiveParams,
> extends AbstractFetchStrategy<T[], P> {
path: string = '';

locale: string = '';

getDefaultEndpoint(): string {
return endpoints.posts;
}
Expand All @@ -213,9 +225,15 @@
* It also takes into account the custom taxonomies specified in `headless.config.js`
*
* @param path The URL path to extract params from
* @param params

Check warning on line 228 in packages/core/src/data/strategies/PostsArchiveFetchStrategy.ts

View workflow job for this annotation

GitHub Actions / eslint (16.x)

Missing JSDoc @param "params" description
*/
getParamsFromURL(path: string, params: Partial<P> = {}): Partial<P> {
const config = getSiteBySourceUrl(this.baseURL);

// this is required for post path mapping
this.locale = config.integrations?.polylang?.enable && params.lang ? params.lang : '';
tobeycodes marked this conversation as resolved.
Show resolved Hide resolved
this.path = path;

const matchers = [...postsMatchers];

if (typeof params.taxonomy === 'string') {
Expand Down Expand Up @@ -306,6 +324,62 @@
return super.buildEndpointURL(endpointParams as P);
}

prepareResponse(response: FetchResponse<T[]>, params: Partial<P>): FetchResponse<T[]> {
const queriedObject = this.getQueriedObject(response, params);

const currentPath = decodeURIComponent(this.path).replace(/\/?$/, '/');
let queriedObjectPath = '';
let taxonomySlug = '';

if (queriedObject?.term?.link) {
// if term is set then it should match the url
queriedObjectPath = decodeURIComponent(
removeSourceUrl({
link: queriedObject.term.link,
backendUrl: this.baseURL,
}),
)?.replace(/\/?$/, '/');
taxonomySlug = queriedObject.term.taxonomy;
}

if (queriedObjectPath && taxonomySlug) {
const taxonomyObj = getCustomTaxonomy(taxonomySlug, this.baseURL);
const shouldMatchArchivePath =
taxonomyObj?.matchArchivePath || params.matchArchivePath || false;

const prefixes = [
'',
taxonomyObj?.rewrite ? `/${taxonomyObj?.rewrite}` : null,
taxonomyObj?.restParam ? `/${taxonomyObj?.restParam}` : null,
taxonomyObj?.slug ? `/${taxonomyObj?.slug}` : null,
tobeycodes marked this conversation as resolved.
Show resolved Hide resolved
].filter((p) => p !== null);

if (shouldMatchArchivePath) {
let matched = false;
for (const prefix of prefixes) {
if (
queriedObjectPath === `${prefix}${currentPath}` ||
queriedObjectPath === `/${this.locale}${prefix}${currentPath}`
) {
matched = true;
break;
}
}
if (!matched) {
throw new NotFoundError(
`Posts were found but did not match current path: "${this.path}"`,
);
}
}
}

return {
...response,
queriedObject,
result: response.result as unknown as T[],
};
}

/**
* Before fetching posts, we need handle taxonomy and authors.
*
Expand Down Expand Up @@ -417,7 +491,7 @@
}
}

const taxonomies = getCustomTaxonomies();
const taxonomies = getCustomTaxonomies(this.baseURL);

taxonomies.forEach((taxonomy) => {
const termSlug = taxonomy.slug;
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/dom/__tests__/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Element } from 'html-react-parser';
import { isAnchorTag, isImageTag, isTwitterEmbed, isYoutubeEmbed } from '..';

jest.mock('../../utils/getHeadlessConfig', () => {
jest.mock('../../utils/config', () => {
return {
getWPUrl: () => 'https://backendurl.com',
};
Expand Down
Loading
Loading