Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .changeset/itchy-areas-enter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@headstartwp/core": patch
---

Fix: post path matching logic when params.fullPath is set
13 changes: 11 additions & 2 deletions packages/core/src/data/strategies/SinglePostFetchStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ export class SinglePostFetchStrategy<
* @returns
*/
getPostThatMatchesCurrentPath(result: T[], params: Partial<P>): T | undefined {
const currentPath = decodeURIComponent(this.path).replace(/\/?$/, '/');

return result.find((post) => {
const postPath = decodeURIComponent(
removeSourceUrl({
Expand All @@ -172,8 +174,6 @@ export class SinglePostFetchStrategy<
}),
)?.replace(/\/?$/, '/');

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

if (params.postType && params.postType.length > 0) {
const expectedPostTypes = Array.isArray(params.postType)
? params.postType
Expand All @@ -182,6 +182,15 @@ export class SinglePostFetchStrategy<

if (expectedPostTypes.includes(postType)) {
const postTypeObject = getCustomPostType(postType, this.baseURL);

if (params.fullPath) {
const normalizedFullPath = params.fullPath.replace(/\/?$/, '/');
return (
postPath === normalizedFullPath ||
postPath === `/${this.locale}${normalizedFullPath}`
);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is failing because it doesn't check for the current path with locale like further down
postPath === /${this.locale}${singlePrefix}${currentPath}``

The patch I had:

const singlePrefix = params?.fullPath
	? ''
	: (postTypeObject?.single?.replace(/\/?$/, '') ?? '');

return (
    postPath === `${singlePrefix}${currentPath}` ||
    postPath === `/${this.locale}${singlePrefix}${currentPath}`
);

As it only needs to have the singlePrefix empty

Copy link
Member Author

@nicholasio nicholasio Jul 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated the logic to also account for locale

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks! approved @nicholasio

const singlePrefix = postTypeObject?.single?.replace(/\/?$/, '') ?? '';

return (
Expand Down
220 changes: 220 additions & 0 deletions packages/core/src/data/strategies/__tests__/SinglePostFetchStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -857,4 +857,224 @@ describe('SinglePostFetchStrategy', () => {
false,
);
});

it('handles fullPath parameter for exact path matching with custom post types', async () => {
// Setup multiple book posts that could potentially match based on slug
const book1 = {
title: 'Test Book 1',
id: 1,
slug: 'test-book',
link: 'http://sourceurl.com/book/test-book',
type: 'book',
};

const book2 = {
title: 'Test Book 2',
id: 2,
slug: 'test-book',
link: 'http://sourceurl.com/custom-path/test-book',
type: 'book',
};

const book3 = {
title: 'Test Book 3',
id: 3,
slug: 'test-book',
link: 'http://sourceurl.com/library/test-book',
type: 'book',
};

// Configure custom post type
setHeadlessConfig({
sourceUrl: 'http://sourceurl.com',
customPostTypes: [
{
slug: 'book',
single: '/book',
endpoint: '/wp-json/wp/v2/book',
},
],
});

// Mock API to return multiple books that could match
apiGetMock.mockResolvedValue({
headers: {
'x-wp-totalpages': 1,
'x-wp-total': 3,
},
json: [book1, book2, book3],
});

fetchStrategy.setBaseURL('http://sourceurl.com');

const postTypeParam = { postType: 'book' };

// Test 1: Without fullPath, should match based on prefix matching (book1 matches /book/ prefix)
let params = {
...fetchStrategy.getParamsFromURL('/test-book'),
...postTypeParam,
};
let results = await fetchStrategy.fetcher(fetchStrategy.buildEndpointURL(params), params);

expect(results).toMatchObject({
result: book1, // Should match book1 with correct /book/ prefix
});

// Test 2: With fullPath, should match exact path regardless of prefix logic
params = {
...fetchStrategy.getParamsFromURL('/test-book'),
...postTypeParam,
fullPath: '/book/test-book',
};
results = await fetchStrategy.fetcher(fetchStrategy.buildEndpointURL(params), params);

expect(results).toMatchObject({
result: book1,
});

// Test 3: With fullPath that doesn't match any book should throw NotFoundError
params = {
...fetchStrategy.getParamsFromURL('/test-book'),
...postTypeParam,
fullPath: '/non-existent-path/test-book',
};

await expect(
fetchStrategy.fetcher(fetchStrategy.buildEndpointURL(params), params),
).rejects.toThrow('was found but did not match current path');
});

it('handles fullPath parameter with locale for exact path matching', async () => {
// Setup multiple book posts with different locales and paths
const englishBook = {
title: 'English Book',
id: 1,
slug: 'test-book',
link: 'http://sourceurl.com/en/book/test-book',
type: 'book',
};

const arabicBook = {
title: 'Arabic Book',
id: 2,
slug: 'test-book',
link: 'http://sourceurl.com/ar/book/test-book',
type: 'book',
};

const frenchBook = {
title: 'French Book',
id: 3,
slug: 'test-book',
link: 'http://sourceurl.com/fr/book/test-book',
type: 'book',
};

// Configure custom post type and enable polylang
setHeadstartWPConfig({
sourceUrl: 'http://sourceurl.com',
integrations: {
polylang: {
enable: true,
},
},
customPostTypes: [
{
slug: 'book',
single: '/book',
endpoint: '/wp-json/wp/v2/book',
},
],
});

// Mock API to return multiple books with different locales
apiGetMock.mockResolvedValue({
headers: {
'x-wp-totalpages': 1,
'x-wp-total': 3,
},
json: [englishBook, arabicBook, frenchBook],
});

fetchStrategy.setBaseURL('http://sourceurl.com');

const postTypeParam = { postType: 'book' };

// Test 1: With fullPath and English locale, should match English book
let params = {
...postTypeParam,
lang: 'en',
fullPath: '/book/test-book',
};
let results = await fetchStrategy.fetcher(
fetchStrategy.buildEndpointURL(fetchStrategy.getParamsFromURL('/test-book', params)),
params,
);

expect(results).toMatchObject({
result: englishBook,
});

// Test 2: With fullPath and Arabic locale, should match Arabic book
params = {
...postTypeParam,
lang: 'ar',
fullPath: '/book/test-book',
};
results = await fetchStrategy.fetcher(
fetchStrategy.buildEndpointURL(fetchStrategy.getParamsFromURL('/test-book', params)),
params,
);

expect(results).toMatchObject({
result: arabicBook,
});

// Test 3: With fullPath and French locale, should match French book
params = {
...postTypeParam,
lang: 'fr',
fullPath: '/book/test-book',
};
results = await fetchStrategy.fetcher(
fetchStrategy.buildEndpointURL(fetchStrategy.getParamsFromURL('/test-book', params)),
params,
);

expect(results).toMatchObject({
result: frenchBook,
});

// Test 4: With fullPath that doesn't match any locale should throw NotFoundError
params = {
...postTypeParam,
lang: 'en',
fullPath: '/non-existent-path/test-book',
};

await expect(
fetchStrategy.fetcher(
fetchStrategy.buildEndpointURL(
fetchStrategy.getParamsFromURL('/test-book', params),
),
params,
),
).rejects.toThrow('was found but did not match current path');

// Test 5: With fullPath that doesn't match the expected post type path should throw NotFoundError
params = {
...postTypeParam,
lang: 'en',
fullPath: '/wrong-prefix/test-book',
};

await expect(
fetchStrategy.fetcher(
fetchStrategy.buildEndpointURL(
fetchStrategy.getParamsFromURL('/test-book', params),
),
params,
),
).rejects.toThrow('was found but did not match current path');
});
});
Loading