diff --git a/.changeset/config.json b/.changeset/config.json
index 538a20311..66c8227c7 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -8,5 +8,5 @@
"baseBranch": "trunk",
"updateInternalDependencies": "patch",
"privatePackages": { "version": true, "tag": false },
- "ignore": ["@10up/wp-nextjs", "@10up/wp-nextjs-app", "@10up/wp-multisite-nextjs-app", "@10up/wp-multisite-nextjs", "@10up/wp-multisite-i18n-nextjs"]
+ "ignore": ["@10up/wp-nextjs", "@10up/wp-nextjs-app", "@10up/wp-polylang-nextjs-app", "@10up/wp-multisite-nextjs-app", "@10up/wp-multisite-nextjs", "@10up/wp-multisite-i18n-nextjs"]
}
\ No newline at end of file
diff --git a/.changeset/happy-squids-admire.md b/.changeset/happy-squids-admire.md
new file mode 100644
index 000000000..c2447593e
--- /dev/null
+++ b/.changeset/happy-squids-admire.md
@@ -0,0 +1,6 @@
+---
+"@headstartwp/core": minor
+"@headstartwp/next": minor
+---
+
+Adding support for `i18n` routing in app router
diff --git a/package-lock.json b/package-lock.json
index 57a69ddee..b0e0c5a5a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -114,6 +114,10 @@
"resolved": "projects/wp-nextjs-app",
"link": true
},
+ "node_modules/@10up/wp-polylang-nextjs-app": {
+ "resolved": "projects/wp-polylang-nextjs-app",
+ "link": true
+ },
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"license": "Apache-2.0",
@@ -2791,6 +2795,14 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@formatjs/intl-localematcher": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz",
+ "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@headstartwp/core": {
"resolved": "packages/core",
"link": true
@@ -15387,7 +15399,6 @@
},
"node_modules/negotiator": {
"version": "0.6.3",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -20950,7 +20961,7 @@
"msw": "^0.35.0",
"ts-jest": "^29.0.3",
"tsc-esm-fix": "^2.20.27",
- "typescript": "^5.4.5"
+ "typescript": "^5.5.3"
},
"peerDependencies": {
"react": ">= 17.0.2"
@@ -20983,9 +20994,10 @@
}
},
"packages/core/node_modules/typescript": {
- "version": "5.4.5",
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
+ "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"dev": true,
- "license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -20999,10 +21011,12 @@
"version": "1.4.3",
"license": "MIT",
"dependencies": {
+ "@formatjs/intl-localematcher": "^0.5.4",
"@headstartwp/core": "^1.4.4",
"@isaacs/ttlcache": "^1.4.1",
"deepmerge": "^4.3.1",
"loader-utils": "^3.2.0",
+ "negotiator": "^0.6.3",
"schema-utils": "^4.0.0"
},
"devDependencies": {
@@ -21020,7 +21034,7 @@
"node-mocks-http": "^1.14.1",
"ts-jest": "^29.0.1",
"tsc-esm-fix": "^2.20.27",
- "typescript": "^5.4.5"
+ "typescript": "^5.5.3"
},
"peerDependencies": {
"next": ">= 12.0.0",
@@ -21041,7 +21055,7 @@
"ioredis-mock": "^8.9.0",
"jest": "^29.0.3",
"ts-jest": "^29.0.1",
- "typescript": "^5.4.5"
+ "typescript": "^5.5.3"
},
"peerDependencies": {
"next": ">= 13.2.0"
@@ -21053,9 +21067,10 @@
"license": "MIT"
},
"packages/next-redis-cache-provider/node_modules/typescript": {
- "version": "5.4.5",
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
+ "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"dev": true,
- "license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -21091,9 +21106,10 @@
}
},
"packages/next/node_modules/typescript": {
- "version": "5.4.5",
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
+ "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"dev": true,
- "license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -21312,7 +21328,7 @@
"@types/react-dom": "^18",
"@typescript-eslint/eslint-plugin": "^7.9.0",
"@typescript-eslint/parser": "^7.9.0",
- "typescript": "^5.4.5"
+ "typescript": "^5.5.3"
},
"engines": {
"node": ">=18.0.0",
@@ -21545,9 +21561,135 @@
}
},
"projects/wp-nextjs/node_modules/typescript": {
- "version": "5.4.5",
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
+ "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "projects/wp-polylang-nextjs-app": {
+ "version": "0.1.0",
+ "dependencies": {
+ "@headstartwp/core": "^1.4.3",
+ "@headstartwp/next": "^1.4.2",
+ "next": "14.2.3",
+ "react": "^18",
+ "react-dom": "^18"
+ },
+ "devDependencies": {
+ "@10up/eslint-config": "^4.0.0",
+ "@types/node": "^20",
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "eslint": "^8",
+ "eslint-config-next": "14.2.3",
+ "typescript": "^5"
+ }
+ },
+ "projects/wp-polylang-nextjs-app/node_modules/@next/env": {
+ "version": "14.2.3",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz",
+ "integrity": "sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA=="
+ },
+ "projects/wp-polylang-nextjs-app/node_modules/@next/swc-linux-x64-gnu": {
+ "version": "14.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz",
+ "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "projects/wp-polylang-nextjs-app/node_modules/@next/swc-linux-x64-musl": {
+ "version": "14.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz",
+ "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "projects/wp-polylang-nextjs-app/node_modules/@types/node": {
+ "version": "20.14.12",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz",
+ "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==",
+ "dev": true,
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
+ },
+ "projects/wp-polylang-nextjs-app/node_modules/next": {
+ "version": "14.2.3",
+ "resolved": "https://registry.npmjs.org/next/-/next-14.2.3.tgz",
+ "integrity": "sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==",
+ "dependencies": {
+ "@next/env": "14.2.3",
+ "@swc/helpers": "0.5.5",
+ "busboy": "1.6.0",
+ "caniuse-lite": "^1.0.30001579",
+ "graceful-fs": "^4.2.11",
+ "postcss": "8.4.31",
+ "styled-jsx": "5.1.1"
+ },
+ "bin": {
+ "next": "dist/bin/next"
+ },
+ "engines": {
+ "node": ">=18.17.0"
+ },
+ "optionalDependencies": {
+ "@next/swc-darwin-arm64": "14.2.3",
+ "@next/swc-darwin-x64": "14.2.3",
+ "@next/swc-linux-arm64-gnu": "14.2.3",
+ "@next/swc-linux-arm64-musl": "14.2.3",
+ "@next/swc-linux-x64-gnu": "14.2.3",
+ "@next/swc-linux-x64-musl": "14.2.3",
+ "@next/swc-win32-arm64-msvc": "14.2.3",
+ "@next/swc-win32-ia32-msvc": "14.2.3",
+ "@next/swc-win32-x64-msvc": "14.2.3"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.1.0",
+ "@playwright/test": "^1.41.2",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "sass": "^1.3.0"
+ },
+ "peerDependenciesMeta": {
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@playwright/test": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ }
+ }
+ },
+ "projects/wp-polylang-nextjs-app/node_modules/typescript": {
+ "version": "5.5.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
+ "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
"dev": true,
- "license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -21566,6 +21708,111 @@
"engines": {
"node": ">=16.0.0"
}
+ },
+ "projects/wp-polylang-nextjs-app/node_modules/@next/swc-darwin-arm64": {
+ "version": "14.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.3.tgz",
+ "integrity": "sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "projects/wp-polylang-nextjs-app/node_modules/@next/swc-darwin-x64": {
+ "version": "14.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz",
+ "integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "projects/wp-polylang-nextjs-app/node_modules/@next/swc-linux-arm64-gnu": {
+ "version": "14.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz",
+ "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "projects/wp-polylang-nextjs-app/node_modules/@next/swc-linux-arm64-musl": {
+ "version": "14.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz",
+ "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "projects/wp-polylang-nextjs-app/node_modules/@next/swc-win32-arm64-msvc": {
+ "version": "14.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz",
+ "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "projects/wp-polylang-nextjs-app/node_modules/@next/swc-win32-ia32-msvc": {
+ "version": "14.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz",
+ "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==",
+ "cpu": [
+ "ia32"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "projects/wp-polylang-nextjs-app/node_modules/@next/swc-win32-x64-msvc": {
+ "version": "14.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz",
+ "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
}
}
}
diff --git a/package.json b/package.json
index ecf8927b6..c297aab63 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,9 @@
"dev": "turbo run dev --parallel --filter=./projects/wp-nextjs --filter=./packages/core --filter=./packages/next",
"dev:app": "turbo run dev --parallel --filter=./projects/wp-nextjs-app --filter=./packages/core --filter=./packages/next",
"dev:app:multisite": "turbo run dev --parallel --filter=./projects/wp-multisite-nextjs-app --filter=./packages/core --filter=./packages/next",
+ "dev:app:polylang": "turbo run dev --parallel --filter=./projects/wp-polylang-nextjs-app --filter=./packages/core --filter=./packages/next",
"dev:multisite": "turbo run dev --parallel --filter=./projects/wp-multisite-nextjs --filter=./packages/core --filter=./packages/next",
+ "dev:multisite:i18n": "turbo run dev --parallel --filter=./projects/wp-multisite-i18n-nextjs --filter=./packages/core --filter=./packages/next",
"prepare": "husky install",
"typedoc": "typedoc",
"typedoc:watch": "typedoc --watch",
diff --git a/packages/core/package.json b/packages/core/package.json
index e18074e31..b0e6127c9 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -69,7 +69,7 @@
"jest": "^29.3.1",
"msw": "^0.35.0",
"ts-jest": "^29.0.3",
- "typescript": "^5.4.5",
+ "typescript": "^5.5.3",
"isomorphic-fetch": "^3.0.0",
"tsc-esm-fix": "^2.20.27",
"@types/react": "^18",
diff --git a/packages/core/src/data/fetchFn/fetchAppSettings.ts b/packages/core/src/data/fetchFn/fetchAppSettings.ts
index 25ba99275..10fa5c1f7 100644
--- a/packages/core/src/data/fetchFn/fetchAppSettings.ts
+++ b/packages/core/src/data/fetchFn/fetchAppSettings.ts
@@ -4,7 +4,7 @@ import { getHeadstartWPConfig, getObjectProperty, getWPUrl } from '../../utils';
import { AppEntity, MenuItemEntity } from '../types';
import { QueryProps } from './types';
-export type AppQueryProps
= QueryProps
& {
+export type AppQueryProps
= QueryProps
& {
menu?: string;
blockSetting?: {
blockName?: string;
diff --git a/packages/core/src/data/fetchFn/types.ts b/packages/core/src/data/fetchFn/types.ts
index 2dd6a51d7..bf9e4153e 100644
--- a/packages/core/src/data/fetchFn/types.ts
+++ b/packages/core/src/data/fetchFn/types.ts
@@ -1,6 +1,6 @@
-import { FetchOptions } from '../strategies';
+import { EndpointParams, FetchOptions } from '../strategies';
-export type QueryProps
= {
+export type QueryProps
= {
path?: string;
params?: Partial
;
options?: Partial;
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index c9e97baa8..fac4a511b 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -137,6 +137,18 @@ export type FetchStrategyCacheConfig = {
cacheHandler?: FetchStrategyCacheHandler;
};
+export type I18NConfig = {
+ locales: string[];
+ defaultLocale: string;
+
+ /**
+ * Whether HeadstartWP should try to detect the browser preferred locale
+ *
+ * @default true
+ */
+ localeDetection?: boolean;
+};
+
export type HeadlessConfig = {
host?: string;
locale?: string;
@@ -149,7 +161,7 @@ export type HeadlessConfig = {
redirectStrategy?: RedirectStrategy;
useWordPressPlugin?: boolean;
integrations?: Integrations;
- sites?: HeadlessConfig[];
+ i18n?: I18NConfig;
preview?: PreviewConfig;
debug?: {
requests?: boolean;
@@ -157,4 +169,5 @@ export type HeadlessConfig = {
devMode?: boolean;
};
cache?: FetchStrategyCacheConfig;
+ sites?: Array>;
};
diff --git a/packages/core/src/utils/config.ts b/packages/core/src/utils/config.ts
index 4846e9e62..0fa290091 100644
--- a/packages/core/src/utils/config.ts
+++ b/packages/core/src/utils/config.ts
@@ -32,6 +32,8 @@ export function getHeadstartWPConfig(): HeadlessConfig {
debug,
preview,
cache,
+ locale,
+ i18n,
} = __10up__HEADLESS_CONFIG;
const defaultTaxonomies: CustomTaxonomies = [
@@ -73,6 +75,7 @@ export function getHeadstartWPConfig(): HeadlessConfig {
: [...(customPostTypes || []), ...defaultPostTypes];
const headlessConfig = {
+ locale,
sourceUrl,
hostUrl: hostUrl || '',
customPostTypes: postTypes,
@@ -83,6 +86,7 @@ export function getHeadstartWPConfig(): HeadlessConfig {
debug,
preview,
cache,
+ i18n,
sites: (sites || []).map((site) => {
// if host is not defined but hostUrl is, infer host from hostUrl
if (typeof site.host === 'undefined' && typeof site.hostUrl !== 'undefined') {
diff --git a/packages/next-redis-cache-provider/package.json b/packages/next-redis-cache-provider/package.json
index 868494a5e..cbef6094d 100644
--- a/packages/next-redis-cache-provider/package.json
+++ b/packages/next-redis-cache-provider/package.json
@@ -32,7 +32,7 @@
"@types/node": "^18.15.11",
"jest": "^29.0.3",
"ts-jest": "^29.0.1",
- "typescript": "^5.4.5",
+ "typescript": "^5.5.3",
"ioredis-mock": "^8.9.0",
"@types/ioredis-mock": "^8.2.5"
},
diff --git a/packages/next/package.json b/packages/next/package.json
index 3828c0ed7..5f1f24196 100644
--- a/packages/next/package.json
+++ b/packages/next/package.json
@@ -54,6 +54,8 @@
"lint": "eslint src"
},
"dependencies": {
+ "negotiator": "^0.6.3",
+ "@formatjs/intl-localematcher": "^0.5.4",
"deepmerge": "^4.3.1",
"@headstartwp/core": "^1.4.4",
"loader-utils": "^3.2.0",
@@ -70,7 +72,7 @@
"next-router-mock": "^0.9.13",
"node-mocks-http": "^1.14.1",
"ts-jest": "^29.0.1",
- "typescript": "^5.4.5",
+ "typescript": "^5.5.3",
"isomorphic-fetch": "^3.0.0",
"jest-fetch-mock": "^3.0.3",
"tsc-esm-fix": "^2.20.27",
diff --git a/packages/next/src/config/withHeadstartWPConfig.ts b/packages/next/src/config/withHeadstartWPConfig.ts
index 8b4715462..23106075e 100644
--- a/packages/next/src/config/withHeadstartWPConfig.ts
+++ b/packages/next/src/config/withHeadstartWPConfig.ts
@@ -121,7 +121,7 @@ export function withHeadstartWPConfig(
}
});
- return {
+ const config: NextConfig = {
...nextConfig,
images: {
...nextConfig.images,
@@ -285,6 +285,17 @@ export function withHeadstartWPConfig(
return config;
},
};
+
+ // if i18n is sets
+ // but we are on pages router
+ // error it out!
+ if ((headlessConfig.i18n?.locales?.length ?? 0) > 0 && !isUsingAppRouter) {
+ throw new ConfigError(
+ 'The `i18n` option is not supported in the pages router. In the Pages router you must set the locales in the next config',
+ );
+ }
+
+ return config;
}
export function withHeadlessConfig(
diff --git a/packages/next/src/middlewares/__tests__/appMiddleware.ts b/packages/next/src/middlewares/__tests__/appMiddleware.ts
index 03c807296..670f8840d 100644
--- a/packages/next/src/middlewares/__tests__/appMiddleware.ts
+++ b/packages/next/src/middlewares/__tests__/appMiddleware.ts
@@ -1,6 +1,6 @@
import { NextRequest } from 'next/server';
import { setHeadstartWPConfig } from '@headstartwp/core/utils';
-import { AppMiddleware } from '../appMidleware';
+import { AppMiddleware, getAppRouterLocale } from '../appMidleware';
describe('appMiddleware', () => {
it('adds headers', async () => {
@@ -110,6 +110,7 @@ describe('appMiddleware', () => {
'http://test2.com/_sites/test.com/post-name',
);
expect(res.headers.get('x-headstartwp-site')).toBe('test.com');
+ expect(res.headers.get('x-headstartwp-locale')).toBe('en');
expect(res.headers.get('x-headstartwp-current-url')).toBe('/post-name');
});
@@ -156,7 +157,7 @@ describe('appMiddleware', () => {
method: 'GET',
nextConfig: {
i18n: {
- locales: ['zh', 'pr'],
+ locales: ['zh', 'pt-br'],
defaultLocale: 'pt-br',
},
},
@@ -167,7 +168,7 @@ describe('appMiddleware', () => {
await expect(() => AppMiddleware(req)).rejects.toThrow('Site not found.');
});
- it('supports multisite in with App Router', async () => {
+ it('supports multisite with App Router', async () => {
setHeadstartWPConfig({
sites: [
{
@@ -196,7 +197,7 @@ describe('appMiddleware', () => {
expect(res.headers.get('x-headstartwp-current-url')).toBe('/post-name');
});
- it('redirect /page/1 to page without 1', async () => {
+ it('redirect /page/1 to url without /page/1', async () => {
setHeadstartWPConfig({
sourceUrl: 'http://testwp.com',
});
@@ -248,4 +249,363 @@ describe('appMiddleware', () => {
res = await AppMiddleware(req);
expect(res.headers.get('x-headstartwp-current-url')).toBeNull();
});
+
+ it('[multisite] supports locales with App Router', async () => {
+ setHeadstartWPConfig({
+ i18n: {
+ defaultLocale: 'en',
+ locales: ['en', 'pt'],
+ },
+ sites: [
+ {
+ sourceUrl: 'http://testwp.com',
+ hostUrl: 'http://test.com',
+ locale: 'en',
+ },
+ {
+ sourceUrl: 'http://testwp2.com',
+ hostUrl: 'http://test2.com',
+ locale: 'pt',
+ },
+ {
+ sourceUrl: 'http://testwp2.com/en',
+ hostUrl: 'http://test2.com',
+ locale: 'en',
+ },
+ ],
+ });
+
+ let req = new NextRequest('http://test2.com/post-name', {
+ method: 'GET',
+ });
+
+ req.headers.set('host', 'test2.com');
+ req.headers.set('accept-language', 'pt-BR');
+
+ let res = await AppMiddleware(req, { appRouter: true });
+
+ expect(getAppRouterLocale(req)).toStrictEqual(['en', 'pt']);
+ expect(res.status).toBe(307);
+ expect(res.headers.get('Location')).toBe('http://test2.com/pt/post-name');
+
+ // follow the redirect
+ req = new NextRequest('http://test2.com/pt/post-name', {
+ method: 'GET',
+ });
+
+ req.headers.set('host', 'test2.com');
+ req.headers.set('accept-language', 'pt-BR');
+
+ res = await AppMiddleware(req, { appRouter: true });
+
+ expect(res.headers.get('x-middleware-rewrite')).toBe(
+ 'http://test2.com/pt/test2.com/post-name',
+ );
+ expect(res.headers.get('x-headstartwp-site')).toBe('test2.com');
+ expect(res.headers.get('x-headstartwp-locale')).toBe('pt');
+ });
+
+ it('[polylang] supports locales with app router', async () => {
+ setHeadstartWPConfig({
+ sourceUrl: 'http://testwp.com',
+ hostUrl: 'http://test.com',
+ integrations: {
+ polylang: {
+ enable: true,
+ },
+ },
+ i18n: {
+ locales: ['en', 'es'],
+ defaultLocale: 'en',
+ },
+ });
+
+ let req = new NextRequest('http://test.com/post-name', {
+ method: 'GET',
+ });
+
+ req.headers.set('accept-language', 'es');
+
+ expect(getAppRouterLocale(req)).toStrictEqual(['en', 'es']);
+
+ let res = await AppMiddleware(req, { appRouter: true });
+
+ expect(res.headers.get('x-headstartwp-locale')).toBe('es');
+ // it should redirect
+ expect(res.status).toBe(307);
+ expect(res.headers.get('Location')).toBe('http://test.com/es/post-name');
+
+ req = new NextRequest('http://test.com/post-name', {
+ method: 'GET',
+ });
+
+ req.headers.set('accept-language', 'en');
+
+ res = await AppMiddleware(req, { appRouter: true });
+ expect(res.headers.get('x-middleware-rewrite')).toBe('http://test.com/en/post-name');
+ });
+
+ it('[polylang no locale detection] supports locales with app router', async () => {
+ setHeadstartWPConfig({
+ sourceUrl: 'http://testwp.com',
+ hostUrl: 'http://test.com',
+ integrations: {
+ polylang: {
+ enable: true,
+ },
+ },
+ i18n: {
+ locales: ['en', 'es'],
+ defaultLocale: 'en',
+ localeDetection: false,
+ },
+ });
+
+ let req = new NextRequest('http://test.com/post-name', {
+ method: 'GET',
+ });
+
+ req.headers.set('accept-language', 'es');
+
+ expect(getAppRouterLocale(req)).toStrictEqual(['en', 'en']);
+
+ let res = await AppMiddleware(req, { appRouter: true });
+
+ expect(res.headers.get('x-headstartwp-locale')).toBe('en');
+ expect(res.status).toBe(200);
+
+ req = new NextRequest('http://test.com/post-name', {
+ method: 'GET',
+ });
+
+ req.headers.set('accept-language', 'en');
+
+ res = await AppMiddleware(req, { appRouter: true });
+ expect(res.headers.get('x-middleware-rewrite')).toBe('http://test.com/en/post-name');
+ });
+
+ it('[i18n] it does not cause redirect loops', async () => {
+ setHeadstartWPConfig({
+ sourceUrl: 'http://testwp.com',
+ hostUrl: 'http://test.com',
+ integrations: {
+ polylang: {
+ enable: true,
+ },
+ },
+ i18n: {
+ locales: ['en', 'es'],
+ defaultLocale: 'en',
+ },
+ });
+
+ let req = new NextRequest('http://test.com/post-name', {
+ method: 'GET',
+ });
+
+ req.headers.set('accept-language', 'es');
+
+ let res = await AppMiddleware(req, { appRouter: true });
+
+ expect(res.headers.get('x-headstartwp-locale')).toBe('es');
+ // it should redirect
+ expect(res.status).toBe(307);
+ expect(res.headers.get('Location')).toBe('http://test.com/es/post-name');
+
+ req = new NextRequest('http://test.com/es/post-name', {
+ method: 'GET',
+ });
+
+ req.headers.set('accept-language', 'es');
+
+ res = await AppMiddleware(req, { appRouter: true });
+ expect(res.status).toBe(200);
+
+ req = new NextRequest('http://test.com/en/post-name', {
+ method: 'GET',
+ });
+
+ req.headers.set('accept-language', 'en');
+
+ res = await AppMiddleware(req, { appRouter: true });
+ expect(res.headers.get('Location')).toBe('http://test.com/post-name');
+
+ req = new NextRequest('http://test.com/post-name', {
+ method: 'GET',
+ });
+
+ req.headers.set('accept-language', 'en');
+ res = await AppMiddleware(req, { appRouter: true });
+ expect(res.status).toBe(200);
+ });
+
+ it('[i18n] it skips locale detection if no locales array is set', async () => {
+ setHeadstartWPConfig({
+ sourceUrl: 'http://testwp.com',
+ hostUrl: 'http://test.com',
+ integrations: {
+ polylang: {
+ enable: true,
+ },
+ },
+ // @ts-expect-error
+ i18n: {
+ defaultLocale: 'en',
+ },
+ });
+
+ const req = new NextRequest('http://test.com/post-name', {
+ method: 'GET',
+ });
+
+ req.headers.set('accept-language', 'es');
+
+ expect(getAppRouterLocale(req)).toBeUndefined();
+ });
+
+ it('throws on polylang and multisite together', async () => {
+ setHeadstartWPConfig({
+ sourceUrl: 'http://testwp.com',
+ hostUrl: 'http://test.com',
+ integrations: {
+ polylang: {
+ enable: true,
+ },
+ },
+ sites: [
+ {
+ sourceUrl: 'http://testwp.com',
+ hostUrl: 'http://test.com',
+ locale: 'en',
+ },
+ {
+ sourceUrl: 'http://testwp2.com',
+ hostUrl: 'http://test2.com',
+ locale: 'pt',
+ },
+ {
+ sourceUrl: 'http://testwp2.com/en',
+ hostUrl: 'http://test2.com',
+ locale: 'en',
+ },
+ ],
+ });
+
+ const req = new NextRequest('http://test.com/post-name', {
+ method: 'GET',
+ });
+
+ req.headers.set('host', 'test.com');
+ req.headers.set('accept-language', 'es');
+
+ expect(getAppRouterLocale(req)).toBeUndefined();
+
+ await expect(() => AppMiddleware(req, { appRouter: true })).rejects.toThrow(
+ 'Polylang and multisite are not supported together',
+ );
+ });
+
+ it('[polylang] gets locale from url if set', async () => {
+ setHeadstartWPConfig({
+ sourceUrl: 'http://testwp.com',
+ hostUrl: 'http://test.com',
+ integrations: {
+ polylang: {
+ enable: true,
+ },
+ },
+ i18n: {
+ locales: ['en', 'es'],
+ defaultLocale: 'en',
+ },
+ });
+
+ let req = new NextRequest('http://test.com/en/post-name', {
+ method: 'GET',
+ });
+
+ req.headers.set('host', 'test.com');
+ req.headers.set('accept-language', 'es');
+
+ expect(getAppRouterLocale(req)).toStrictEqual(['en', 'en']);
+ let res = await AppMiddleware(req, { appRouter: true });
+ expect(res.headers.get('x-headstartwp-locale')).toBe('en');
+ expect(res.headers.get('x-middleware-rewrite')).toBeNull();
+ // should redirect from /en/post-name to /post-name
+ expect(res.status).toBe(307);
+ expect(res.headers.get('Location')).toBe('http://test.com/post-name');
+
+ // pt is a unsuported but valid
+ // the it should not redirect but just let it 404
+ req = new NextRequest('http://test.com/pt/post-name', {
+ method: 'GET',
+ });
+
+ req.headers.set('host', 'test.com');
+ req.headers.set('accept-language', 'es');
+
+ expect(getAppRouterLocale(req)).toStrictEqual(['en', 'pt']);
+ res = await AppMiddleware(req, { appRouter: true });
+ expect(res.headers.get('x-headstartwp-locale')).toBe('pt');
+ expect(res.headers.get('x-middleware-rewrite')).toBeNull();
+
+ expect(res.status).toBe(200);
+ });
+
+ it('[multisite with locale] gets locale from url if set', async () => {
+ setHeadstartWPConfig({
+ i18n: {
+ locales: ['en', 'es'],
+ defaultLocale: 'en',
+ },
+ sites: [
+ {
+ sourceUrl: 'http://testwp.com',
+ hostUrl: 'http://test.com',
+ locale: 'en',
+ },
+ {
+ sourceUrl: 'http://testwp2.com',
+ hostUrl: 'http://test2.com',
+ locale: 'pt',
+ },
+ {
+ sourceUrl: 'http://testwp2.com/en',
+ hostUrl: 'http://test2.com',
+ locale: 'en',
+ },
+ ],
+ });
+
+ // request for test2.com with en locale so should match `http://testwp2.com/en as sourceUrl`
+ let req = new NextRequest('http://test2.com/en/post-name', {
+ method: 'GET',
+ });
+
+ req.headers.set('host', 'test2.com');
+ // this locale should be skipped since it isn't supported and there's a locale in the URL
+ req.headers.set('accept-language', 'es');
+
+ expect(getAppRouterLocale(req)).toStrictEqual(['en', 'en']);
+ const res = await AppMiddleware(req, { appRouter: true });
+ expect(res.headers.get('x-headstartwp-locale')).toBe('en');
+ expect(res.headers.get('x-middleware-rewrite')).toBeNull();
+ // should redirect from /en/post-name to /post-name
+ expect(res.status).toBe(307);
+ expect(res.headers.get('Location')).toBe('http://test2.com/post-name');
+
+ // es is an unsuported but valid locale
+ req = new NextRequest('http://test2.com/es/post-name', {
+ method: 'GET',
+ });
+
+ req.headers.set('host', 'test2.com');
+ req.headers.set('accept-language', 'pt');
+
+ expect(getAppRouterLocale(req)).toStrictEqual(['en', 'es']);
+ // TODO: should we 404 instead?
+ await expect(() => AppMiddleware(req, { appRouter: true })).rejects.toThrow(
+ 'Site not found',
+ );
+ });
});
diff --git a/packages/next/src/middlewares/appMidleware.ts b/packages/next/src/middlewares/appMidleware.ts
index b8fdf1a75..df0b0e3d2 100644
--- a/packages/next/src/middlewares/appMidleware.ts
+++ b/packages/next/src/middlewares/appMidleware.ts
@@ -1,6 +1,8 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { fetchRedirect, getHeadstartWPConfig, getSiteByHost } from '@headstartwp/core/utils';
+import Negotiator from 'negotiator';
+import { match as matchLocale } from '@formatjs/intl-localematcher';
const ALLOWED_STATIC_PATHS = /^\/.*\.(ico|png|jpg|jpeg)$/g;
@@ -12,24 +14,113 @@ function isInternalRequest(req: NextRequest) {
return req.nextUrl.pathname.startsWith('/_next');
}
+function hasMultisiteConfig() {
+ const config = getHeadstartWPConfig();
+ return (config.sites?.length ?? 0) > 0;
+}
+
+function isPolylangIntegrationEnabled() {
+ const config = getHeadstartWPConfig();
+ return config.integrations?.polylang?.enable ?? false;
+}
+
+function isValidLocale(locale: string) {
+ try {
+ return Intl.getCanonicalLocales(locale).length > 0;
+ } catch (e) {
+ return false;
+ }
+}
+
+function getAppRouterSupportedLocales() {
+ const config = getHeadstartWPConfig();
+
+ const { defaultLocale, locales = [], localeDetection = true } = config.i18n ?? {};
+
+ return {
+ defaultLocale,
+ supportedLocales: [...new Set(locales)],
+ localeDetection,
+ };
+}
+
+/**
+ * On App Router it returns a tuple with defaultLocale and locale
+ *
+ * @param request Next Request
+ * @returns A tuple [defaultLocale, locale]
+ */
+export function getAppRouterLocale(request: NextRequest): [string, string] | undefined {
+ const { defaultLocale, supportedLocales, localeDetection } = getAppRouterSupportedLocales();
+
+ if (typeof defaultLocale === 'undefined') {
+ return undefined;
+ }
+
+ if (supportedLocales.length === 0) {
+ return undefined;
+ }
+
+ // if there's a locale in the URL and it's a supported or valid locale, use it
+ const urlLocale = request.nextUrl.pathname.split('/')[1];
+ if (supportedLocales.includes(urlLocale) || isValidLocale(urlLocale)) {
+ return [defaultLocale, urlLocale];
+ }
+
+ if (localeDetection) {
+ // Negotiator expects plain object so we need to transform headers
+ const negotiatorHeaders: Record = {};
+ request.headers.forEach((value, key) => {
+ negotiatorHeaders[key] = value;
+ });
+
+ const locales: readonly string[] = [defaultLocale, ...(supportedLocales ?? [])];
+
+ // Use negotiator and intl-localematcher to get best locale
+ const languages = new Negotiator({ headers: negotiatorHeaders }).languages(locales);
+
+ const locale = matchLocale(languages, locales, defaultLocale);
+
+ return [defaultLocale, locale];
+ }
+
+ return [defaultLocale, defaultLocale];
+}
+
type AppMidlewareOptions = {
appRouter: boolean;
};
export async function AppMiddleware(
req: NextRequest,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
options: AppMidlewareOptions = { appRouter: false },
) {
let response = NextResponse.next();
- const currentUrl = req.nextUrl.pathname;
+ const { pathname } = req.nextUrl;
if (isStaticAssetRequest(req) || isInternalRequest(req)) {
return response;
}
+ const isPotentiallyMultisite = hasMultisiteConfig();
+ const hasPolylangIntegration = isPolylangIntegrationEnabled();
+
+ if (hasPolylangIntegration && isPotentiallyMultisite && options.appRouter) {
+ // potentially conflicting set up
+ // will figure out later if we need to support this
+ throw new Error('Polylang and multisite are not supported together');
+ }
+
+ const [defaultAppRouterLocale, appRouterLocale] = options.appRouter
+ ? getAppRouterLocale(req) ?? []
+ : [];
+
+ const locale = options.appRouter && appRouterLocale ? appRouterLocale : req.nextUrl.locale;
+ const firstPathSlice = pathname.split('/')[1];
const hostname = req.headers.get('host') || '';
- const site = getSiteByHost(hostname, req.nextUrl.locale);
+
+ // if it's polylang integration, we should not be using locale to get site
+ const site = getSiteByHost(hostname, !hasPolylangIntegration ? locale : undefined);
const isMultisiteRequest = site !== null && typeof site.sourceUrl !== 'undefined';
const {
@@ -39,11 +130,10 @@ export async function AppMiddleware(
} = isMultisiteRequest ? site : getHeadstartWPConfig();
if (!sourceUrl) {
+ // todo: should we 404 instead?
throw new Error('Site not found.');
}
- const { pathname } = req.nextUrl;
-
if (redirectStrategy === 'always') {
const redirect = await fetchRedirect(pathname, sourceUrl || '');
@@ -55,25 +145,71 @@ export async function AppMiddleware(
}
}
- if (req.nextUrl.pathname.endsWith('/page/1') || req.nextUrl.pathname.endsWith('/page/1/')) {
+ if (pathname.endsWith('/page/1') || pathname.endsWith('/page/1/')) {
return NextResponse.redirect(req.url.replace('/page/1', ''));
}
- if (isMultisiteRequest) {
- const url = req.nextUrl;
+ let shouldRedirect = false;
+
+ if (locale && options.appRouter) {
+ const { supportedLocales } = getAppRouterSupportedLocales();
+
+ const pathnameIsMissingLocale = supportedLocales.every(
+ (loc) => !pathname.startsWith(`/${loc}/`) && pathname !== `/${loc}`,
+ );
+
+ // redirect default locale in the URL to a version without the locale
+ // e.g /en/about-us -> /about-us where en is the default locale
+ if (locale === defaultAppRouterLocale && firstPathSlice === locale) {
+ shouldRedirect = true;
+ const pathNameWithoutLocale = pathname.replace(`/${locale}`, '');
+ response = NextResponse.redirect(
+ new URL(pathNameWithoutLocale, req.url.replace(`/${locale}`, '')),
+ );
+ }
+ // if we detected a non-default locale, there isn't a supported locale in the URL already
+ // but the first part of pathname (what is assumed to be a locale) is not a valid locale
+ // then we should redirect to add the locale
+ // e.g /about-us -> /en/about-us
+ else if (
+ locale !== defaultAppRouterLocale &&
+ pathnameIsMissingLocale &&
+ !isValidLocale(firstPathSlice)
+ ) {
+ shouldRedirect = true;
+ response = NextResponse.redirect(
+ new URL(`/${locale}${pathname.startsWith('/') ? '' : '/'}${pathname}`, req.url),
+ );
+ }
+ // nothing else and there's not a locale in path then rewrite to add default locale
+ else if (pathnameIsMissingLocale && !isValidLocale(firstPathSlice)) {
+ response = NextResponse.rewrite(
+ new URL(
+ `/${defaultAppRouterLocale}${pathname.startsWith('/') ? '' : '/'}${pathname}`,
+ req.url,
+ ),
+ );
+ }
+ }
+
+ if (isMultisiteRequest && !shouldRedirect) {
+ const pagesRouterRewrite = `/_sites/${hostname}${pathname}`;
+ const appRouterRewrite = locale
+ ? `/${locale}/${hostname}${pathname.replace(`/${locale}`, '')}`
+ : `/${hostname}${pathname}`;
response = NextResponse.rewrite(
- new URL(
- options.appRouter
- ? `/${hostname}${url.pathname}`
- : `/_sites/${hostname}${url.pathname}`,
- url,
- ),
+ new URL(options.appRouter ? appRouterRewrite : pagesRouterRewrite, req.nextUrl),
);
+
response.headers.set('x-headstartwp-site', hostname);
}
- response.headers.set('x-headstartwp-current-url', currentUrl);
+ if (locale) {
+ response.headers.set('x-headstartwp-locale', locale);
+ }
+
+ response.headers.set('x-headstartwp-current-url', pathname);
return response;
}
diff --git a/packages/next/src/rsc/data/queries/__tests__/prepareQuery.ts b/packages/next/src/rsc/data/queries/__tests__/prepareQuery.ts
index cbebfce97..5e1707cf9 100644
--- a/packages/next/src/rsc/data/queries/__tests__/prepareQuery.ts
+++ b/packages/next/src/rsc/data/queries/__tests__/prepareQuery.ts
@@ -1,4 +1,4 @@
-import { setHeadstartWPConfig } from '@headstartwp/core';
+import { getHeadstartWPConfig, setHeadstartWPConfig } from '@headstartwp/core';
import { prepareQuery } from '../prepareQuery';
describe('prepareQuery', () => {
@@ -82,3 +82,96 @@ describe('prepareQuery', () => {
).toThrow('Sub site not found, make sure to add site3.com to headstartwp.config.js');
});
});
+
+describe('prepareQuery with lang and multisite', () => {
+ beforeAll(() => {
+ setHeadstartWPConfig({
+ sites: [
+ {
+ sourceUrl: 'https://backend1.com',
+ hostUrl: 'https://site1.com',
+ locale: 'en',
+ },
+ {
+ sourceUrl: 'https://backend2.com',
+ hostUrl: 'https://site1.com',
+ locale: 'pt',
+ },
+ ],
+ });
+ });
+
+ it('gets site correctly based on lang and host', () => {
+ expect(
+ prepareQuery({
+ routeParams: { site: 'site1.com', lang: 'pt' },
+ }),
+ ).toMatchObject({
+ config: {
+ sourceUrl: 'https://backend2.com',
+ hostUrl: 'https://site1.com',
+ },
+ });
+
+ expect(
+ prepareQuery({
+ routeParams: { site: 'site1.com', lang: 'en' },
+ }),
+ ).toMatchObject({
+ config: {
+ sourceUrl: 'https://backend1.com',
+ hostUrl: 'https://site1.com',
+ },
+ });
+ });
+});
+
+describe('prepareQuery with lang and polylang', () => {
+ beforeAll(() => {
+ setHeadstartWPConfig({
+ sourceUrl: 'https://backend1.com',
+ hostUrl: 'https://site1.com',
+ integrations: {
+ polylang: {
+ enable: true,
+ },
+ },
+ i18n: {
+ locales: ['en', 'pt'],
+ defaultLocale: 'en',
+ },
+ });
+ });
+
+ it('prepares params correctly', () => {
+ expect(
+ prepareQuery(
+ {
+ routeParams: { lang: 'pt' },
+ },
+ getHeadstartWPConfig(),
+ ),
+ ).toMatchObject({
+ config: {
+ sourceUrl: 'https://backend1.com',
+ hostUrl: 'https://site1.com',
+ },
+ params: {
+ lang: 'pt',
+ },
+ });
+ });
+
+ it('throws for unsuported locales', () => {
+ expect(() =>
+ prepareQuery(
+ {
+ routeParams: { lang: 'br' },
+ },
+ getHeadstartWPConfig(),
+ ),
+ ).toThrow(
+ 'Unsuported lang, make sure you add all desired locales to `config.i18n.locales`',
+ );
+ });
+});
diff --git a/packages/next/src/rsc/data/queries/prepareQuery.ts b/packages/next/src/rsc/data/queries/prepareQuery.ts
index cd6a68b48..9e1092ccb 100644
--- a/packages/next/src/rsc/data/queries/prepareQuery.ts
+++ b/packages/next/src/rsc/data/queries/prepareQuery.ts
@@ -1,4 +1,5 @@
import {
+ EndpointParams,
FrameworkError,
HeadlessConfig,
getHeadstartWPConfig,
@@ -11,15 +12,23 @@ import type { NextQueryProps } from './types';
const { all: merge } = deepmerge;
-export function prepareQuery(
+export function prepareQuery
(
query: NextQueryProps
,
_config: HeadlessConfig | undefined = undefined,
) {
- const { routeParams, handleError = true, ...rest } = query;
+ const { routeParams, handleError = true, params: originalParams, ...rest } = query;
const path = routeParams?.path ?? '';
+ const site = decodeURIComponent(routeParams?.site ?? '');
+
+ const rootConfig = getHeadstartWPConfig();
+ const isPolylangEnabled = rootConfig.integrations?.polylang?.enable;
+
+ // eslint-disable-next-line no-nested-ternary
const siteConfig = routeParams?.site
- ? getSiteByHost(decodeURIComponent(routeParams?.site))
+ ? routeParams.lang && !isPolylangEnabled
+ ? getSiteByHost(site, routeParams.lang)
+ : getSiteByHost(site)
: null;
if (routeParams?.site && !siteConfig) {
@@ -37,11 +46,25 @@ export function prepareQuery
(
rest.options ?? {},
]);
- const config = siteConfig ?? _config;
+ const config = siteConfig ?? (_config || rootConfig);
const pathname = Array.isArray(path) ? convertToPath(path) : path;
+ const params: typeof originalParams =
+ typeof originalParams !== 'undefined' ? { ...originalParams } : {};
+
+ if (routeParams?.lang && isPolylangEnabled) {
+ const supportedLocales = rootConfig.i18n?.locales ?? [];
+ if (!supportedLocales.includes(routeParams.lang)) {
+ throw new FrameworkError(
+ 'Unsuported lang, make sure you add all desired locales to `config.i18n.locales`',
+ );
+ }
+ params.lang = routeParams.lang;
+ }
+
return {
...rest,
+ params,
options,
path: pathname,
config: config ?? getHeadstartWPConfig(),
diff --git a/packages/next/src/rsc/data/queries/types.ts b/packages/next/src/rsc/data/queries/types.ts
index 41452dff8..120c75362 100644
--- a/packages/next/src/rsc/data/queries/types.ts
+++ b/packages/next/src/rsc/data/queries/types.ts
@@ -1,9 +1,10 @@
-import type { QueryProps } from '@headstartwp/core';
+import type { EndpointParams, QueryProps } from '@headstartwp/core';
-export type NextQueryProps
= {
+export type NextQueryProps
= {
routeParams?: {
path?: string | string[];
site?: string;
+ lang?: string;
[k: string]: unknown;
};
handleError?: boolean;
diff --git a/packages/next/src/rsc/types.ts b/packages/next/src/rsc/types.ts
index 7b4ebcbe0..e18dce76f 100644
--- a/packages/next/src/rsc/types.ts
+++ b/packages/next/src/rsc/types.ts
@@ -1,8 +1,8 @@
export type HeadstartWPRoute = {
- params: { path: string[]; site?: string };
+ params: { path: string[]; site?: string; lang?: string };
} & Params;
export type HeadstartWPLayout = {
- params: { site?: string };
+ params: { site?: string; lang?: string };
children: React.ReactNode;
} & Params;
diff --git a/projects/wp-multisite-i18n-nextjs/.env.trunk b/projects/wp-multisite-i18n-nextjs/.env.trunk
deleted file mode 100644
index e69de29bb..000000000
diff --git a/projects/wp-multisite-i18n-nextjs/babel.config.js b/projects/wp-multisite-i18n-nextjs/babel.config.js
deleted file mode 100644
index 2675b8c62..000000000
--- a/projects/wp-multisite-i18n-nextjs/babel.config.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
- presets: ['next/babel'],
-};
diff --git a/projects/wp-multisite-i18n-nextjs/headless.config.js b/projects/wp-multisite-i18n-nextjs/headstartwp.config.js
similarity index 100%
rename from projects/wp-multisite-i18n-nextjs/headless.config.js
rename to projects/wp-multisite-i18n-nextjs/headstartwp.config.js
diff --git a/projects/wp-multisite-i18n-nextjs/next.config.js b/projects/wp-multisite-i18n-nextjs/next.config.js
index e30be1210..e481b81e0 100644
--- a/projects/wp-multisite-i18n-nextjs/next.config.js
+++ b/projects/wp-multisite-i18n-nextjs/next.config.js
@@ -1,11 +1,9 @@
-const { withHeadlessConfig } = require('@headstartwp/next/config');
+const { withHeadstartWPConfig } = require('@headstartwp/next/config');
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
-const headlessConfig = require('./headless.config');
-
/**
* Update whatever you need within the nextConfig object.
*
@@ -22,4 +20,4 @@ const nextConfig = {
},
};
-module.exports = withBundleAnalyzer(withHeadlessConfig(nextConfig, headlessConfig));
+module.exports = withBundleAnalyzer(withHeadstartWPConfig(nextConfig));
diff --git a/projects/wp-multisite-i18n-nextjs/src/components/Header/Nav.js b/projects/wp-multisite-i18n-nextjs/src/components/Header/Nav.js
index fcf19d9a7..7b281f805 100644
--- a/projects/wp-multisite-i18n-nextjs/src/components/Header/Nav.js
+++ b/projects/wp-multisite-i18n-nextjs/src/components/Header/Nav.js
@@ -33,12 +33,7 @@ const navStyles = css`
`;
export const Nav = () => {
- const { data, loading, error } = useMenu('primary', {
- // these settings will re-render menu client side to ensure
- // it always have the latest items
- revalidateOnMount: true,
- revalidateOnFocus: true,
- });
+ const { data, loading, error } = useMenu('primary');
if (loading || error) {
return null;
diff --git a/projects/wp-nextjs/package.json b/projects/wp-nextjs/package.json
index 6c2ed47c2..466d88b7b 100644
--- a/projects/wp-nextjs/package.json
+++ b/projects/wp-nextjs/package.json
@@ -33,7 +33,7 @@
"@next/bundle-analyzer": "^12.1.0",
"@typescript-eslint/eslint-plugin": "^7.9.0",
"@typescript-eslint/parser": "^7.9.0",
- "typescript": "^5.4.5",
+ "typescript": "^5.5.3",
"@types/nprogress": "^0.2.3",
"@types/react": "^18",
"@types/react-dom": "^18"
diff --git a/projects/wp-polylang-nextjs-app/.env b/projects/wp-polylang-nextjs-app/.env
new file mode 100644
index 000000000..b013281ea
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/.env
@@ -0,0 +1,2 @@
+NEXT_PUBLIC_HEADLESS_WP_URL=https://js1.10up.com
+NEXT_PUBLIC_HOST_URL=https://js1.10up.com
\ No newline at end of file
diff --git a/projects/wp-polylang-nextjs-app/.eslintrc.js b/projects/wp-polylang-nextjs-app/.eslintrc.js
new file mode 100644
index 000000000..16b1cc13e
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/.eslintrc.js
@@ -0,0 +1,14 @@
+module.exports = {
+ extends: ['@10up/eslint-config/react'],
+ parser: '@typescript-eslint/parser',
+ plugins: ['@typescript-eslint'],
+ settings: {
+ jsdoc: {
+ mode: 'typescript',
+ },
+ },
+ rules: {
+ 'react/require-default-props': ['error', { functions: 'defaultArguments' }],
+ 'jsdoc/require-returns-type': 'off',
+ },
+};
diff --git a/projects/wp-polylang-nextjs-app/.gitignore b/projects/wp-polylang-nextjs-app/.gitignore
new file mode 100644
index 000000000..fd3dbb571
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/.gitignore
@@ -0,0 +1,36 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+.yarn/install-state.gz
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/projects/wp-polylang-nextjs-app/README.md b/projects/wp-polylang-nextjs-app/README.md
new file mode 100644
index 000000000..c4033664f
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/README.md
@@ -0,0 +1,36 @@
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
diff --git a/projects/wp-polylang-nextjs-app/headstartwp.config.js b/projects/wp-polylang-nextjs-app/headstartwp.config.js
new file mode 100644
index 000000000..3a23b5906
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/headstartwp.config.js
@@ -0,0 +1,22 @@
+/**
+ * Headless Config
+ *
+ * @type {import('@headstartwp/core').HeadlessConfig}
+ */
+module.exports = {
+ useWordPressPlugin: true,
+ sourceUrl: process.env.NEXT_PUBLIC_HEADLESS_WP_URL,
+ hostUrl: process.env.NEXT_PUBLIC_HOST_URL,
+ preview: {
+ usePostLinkForRedirect: true,
+ },
+ i18n: {
+ locales: ['en', 'pt', 'es'],
+ defaultLocale: 'en',
+ },
+ integrations: {
+ polylang: {
+ enable: true,
+ },
+ },
+};
diff --git a/projects/wp-polylang-nextjs-app/next.config.js b/projects/wp-polylang-nextjs-app/next.config.js
new file mode 100644
index 000000000..bf74d4ecd
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/next.config.js
@@ -0,0 +1,22 @@
+const { withHeadstartWPConfig } = require('@headstartwp/next/config');
+
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ webpack: (config) => {
+ // TODO: figure out why this is needed
+ config.resolve = {
+ ...config.resolve,
+ conditionNames: ['import'],
+ };
+
+ return config;
+ },
+
+ logging: {
+ fetches: {
+ fullUrl: true,
+ },
+ },
+};
+
+module.exports = withHeadstartWPConfig(nextConfig);
diff --git a/projects/wp-polylang-nextjs-app/package.json b/projects/wp-polylang-nextjs-app/package.json
new file mode 100644
index 000000000..9e01db510
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "@10up/wp-polylang-nextjs-app",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "react": "^18",
+ "react-dom": "^18",
+ "next": "14.2.3",
+ "@headstartwp/core": "^1.4.3",
+ "@headstartwp/next": "^1.4.2"
+ },
+ "devDependencies": {
+ "@10up/eslint-config": "^4.0.0",
+ "typescript": "^5",
+ "@types/node": "^20",
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "eslint": "^8",
+ "eslint-config-next": "14.2.3"
+ }
+}
diff --git a/projects/wp-polylang-nextjs-app/src/app/[lang]/(single)/[...path]/page.tsx b/projects/wp-polylang-nextjs-app/src/app/[lang]/(single)/[...path]/page.tsx
new file mode 100644
index 000000000..4fd359fe4
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/src/app/[lang]/(single)/[...path]/page.tsx
@@ -0,0 +1,28 @@
+import { BlocksRenderer, HtmlDecoder } from '@headstartwp/core/react';
+import { HeadstartWPRoute, queryPost } from '@headstartwp/next/app';
+
+const Single = async ({ params }: HeadstartWPRoute) => {
+ const { data } = await queryPost({
+ routeParams: params,
+ params: {
+ postType: ['post', 'page'],
+ },
+ options: {
+ headers: {
+ cache: 'force-cache',
+ },
+ },
+ });
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default Single;
diff --git a/projects/wp-polylang-nextjs-app/src/app/[lang]/author/[...path]/page.tsx b/projects/wp-polylang-nextjs-app/src/app/[lang]/author/[...path]/page.tsx
new file mode 100644
index 000000000..08eec52ea
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/src/app/[lang]/author/[...path]/page.tsx
@@ -0,0 +1,24 @@
+import { HeadstartWPRoute, queryAuthorArchive } from '@headstartwp/next/app';
+import Link from 'next/link';
+
+const AuthorArchive = async ({ params }: HeadstartWPRoute) => {
+ const { data } = await queryAuthorArchive({
+ routeParams: params,
+ });
+
+ return (
+
+ {data.queriedObject.author?.name}
+
+
+ {data.posts.map((post) => (
+ -
+ {post.title.rendered}
+
+ ))}
+
+
+ );
+};
+
+export default AuthorArchive;
diff --git a/projects/wp-polylang-nextjs-app/src/app/[lang]/blog/[[...path]]/page.tsx b/projects/wp-polylang-nextjs-app/src/app/[lang]/blog/[[...path]]/page.tsx
new file mode 100644
index 000000000..f4aa233e0
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/src/app/[lang]/blog/[[...path]]/page.tsx
@@ -0,0 +1,61 @@
+import { PostEntity, QueriedObject } from '@headstartwp/core';
+import { HeadstartWPRoute, queryPostOrPosts } from '@headstartwp/next/app';
+import Link from 'next/link';
+import { notFound } from 'next/navigation';
+
+type ArchiveProps = {
+ posts: PostEntity[];
+ queriedObject: QueriedObject;
+};
+
+const Archive = ({ posts, queriedObject }: ArchiveProps) => {
+ return (
+
+ {queriedObject.term?.name}
+
+
+ {posts.map((post) => (
+ -
+ {post.title.rendered}
+
+ ))}
+
+
+ );
+};
+
+const BlogPage = async ({ params }: HeadstartWPRoute) => {
+ const { isArchive, isSingle, data } = await queryPostOrPosts({
+ routeParams: params,
+ params: {
+ single: {
+ postType: 'post',
+ },
+ archive: {
+ postType: 'post',
+ /**
+ * Specifying the _fields param reduces the amount of data queried and returned by the API.
+ */
+ _fields: ['id', 'title', 'link'],
+ },
+ priority: 'single',
+ routeMatchStrategy: 'single',
+ },
+ });
+
+ if (isArchive && typeof data.posts !== 'undefined') {
+ return ;
+ }
+
+ if (isSingle && typeof data.post !== 'undefined') {
+ return (
+
+ {data.post.title.rendered}
+
+ );
+ }
+
+ return notFound();
+};
+
+export default BlogPage;
diff --git a/projects/wp-polylang-nextjs-app/src/app/[lang]/category/[...path]/page.tsx b/projects/wp-polylang-nextjs-app/src/app/[lang]/category/[...path]/page.tsx
new file mode 100644
index 000000000..8538d57f1
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/src/app/[lang]/category/[...path]/page.tsx
@@ -0,0 +1,27 @@
+import { HeadstartWPRoute, queryPosts } from '@headstartwp/next/app';
+import Link from 'next/link';
+
+const CategoryArchive = async ({ params }: HeadstartWPRoute) => {
+ const { data } = await queryPosts({
+ routeParams: params,
+ params: {
+ taxonomy: 'category',
+ },
+ });
+
+ return (
+
+ {data.queriedObject.term?.name}
+
+
+ {data.posts.map((post) => (
+ -
+ {post.title.rendered}
+
+ ))}
+
+
+ );
+};
+
+export default CategoryArchive;
diff --git a/projects/wp-polylang-nextjs-app/src/app/[lang]/favicon.ico b/projects/wp-polylang-nextjs-app/src/app/[lang]/favicon.ico
new file mode 100644
index 000000000..718d6fea4
Binary files /dev/null and b/projects/wp-polylang-nextjs-app/src/app/[lang]/favicon.ico differ
diff --git a/projects/wp-polylang-nextjs-app/src/app/[lang]/globals.css b/projects/wp-polylang-nextjs-app/src/app/[lang]/globals.css
new file mode 100644
index 000000000..687196d61
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/src/app/[lang]/globals.css
@@ -0,0 +1,34 @@
+.form-container {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ background-color: #f1f1f1;
+ padding: 20px;
+ border-radius: 5px;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
+}
+
+.form-container form {
+ display: flex;
+ flex-direction: column;
+}
+
+.form-container input {
+ margin-bottom: 10px;
+ padding: 10px;
+ border: 1px solid #ccc;
+ border-radius: 3px;
+}
+
+.form-container button {
+ padding: 10px;
+ background-color: #4CAF50;
+ color: white;
+ border: none;
+ border-radius: 3px;
+ cursor: pointer;
+}
+
+.form-container button:hover {
+ background-color: #45a049;
+}
diff --git a/projects/wp-polylang-nextjs-app/src/app/[lang]/layout.tsx b/projects/wp-polylang-nextjs-app/src/app/[lang]/layout.tsx
new file mode 100644
index 000000000..0501c9c14
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/src/app/[lang]/layout.tsx
@@ -0,0 +1,19 @@
+import { HeadstartWPLayout, PreviewIndicator, queryAppSettings } from '@headstartwp/next/app';
+import { Menu, SettingsProvider, ThemeSettingsProvider } from '@headstartwp/core/react';
+import { getHeadstartWPConfig } from '@headstartwp/core';
+
+const RootLayout = async ({ children, params }: Readonly) => {
+ const { menu, data } = await queryAppSettings({ menu: 'primary', routeParams: params });
+
+ return (
+
+
+ {menu ? : null}
+ {children}
+
+
+
+ );
+};
+
+export default RootLayout;
diff --git a/projects/wp-polylang-nextjs-app/src/app/[lang]/not-found.tsx b/projects/wp-polylang-nextjs-app/src/app/[lang]/not-found.tsx
new file mode 100644
index 000000000..0b6d8855c
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/src/app/[lang]/not-found.tsx
@@ -0,0 +1,13 @@
+import Link from 'next/link';
+
+const NotFound = () => {
+ return (
+
+
Not Found
+
Could not find requested resource
+
Return Home
+
+ );
+};
+
+export default NotFound;
diff --git a/projects/wp-polylang-nextjs-app/src/app/[lang]/page.tsx b/projects/wp-polylang-nextjs-app/src/app/[lang]/page.tsx
new file mode 100644
index 000000000..712e3efc7
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/src/app/[lang]/page.tsx
@@ -0,0 +1,28 @@
+import { BlocksRenderer } from '@headstartwp/core/react';
+import { HeadstartWPRoute, queryAppSettings, queryPost } from '@headstartwp/next/app';
+
+const Home = async ({ params }: HeadstartWPRoute) => {
+ const {
+ data: { home },
+ } = await queryAppSettings({
+ routeParams: params,
+ });
+
+ const { data } = await queryPost({
+ routeParams: params,
+ params: {
+ slug: home.slug ?? 'front-page',
+ postType: 'page',
+ },
+ });
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default Home;
diff --git a/projects/wp-polylang-nextjs-app/src/app/[lang]/search/[[...path]]/page.tsx b/projects/wp-polylang-nextjs-app/src/app/[lang]/search/[[...path]]/page.tsx
new file mode 100644
index 000000000..aa29ec914
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/src/app/[lang]/search/[[...path]]/page.tsx
@@ -0,0 +1,29 @@
+import { HeadstartWPRoute, querySearch } from '@headstartwp/next/app';
+import Link from 'next/link';
+
+const Search = async ({ params }: HeadstartWPRoute) => {
+ const { data } = await querySearch({
+ routeParams: params,
+ });
+
+ if (data.pageInfo.totalItems === 0) {
+ return 'Nothing found';
+ }
+
+ return (
+ <>
+ Search Results
+
+ {data.searchResults.map((item) => (
+ -
+
+ {item.id} - {item.title}
+
+
+ ))}
+
+ >
+ );
+};
+
+export default Search;
diff --git a/projects/wp-polylang-nextjs-app/src/app/[lang]/tag/[...path]/page.tsx b/projects/wp-polylang-nextjs-app/src/app/[lang]/tag/[...path]/page.tsx
new file mode 100644
index 000000000..283091063
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/src/app/[lang]/tag/[...path]/page.tsx
@@ -0,0 +1,27 @@
+import { HeadstartWPRoute, queryPosts } from '@headstartwp/next/app';
+import Link from 'next/link';
+
+const TagArchive = async ({ params }: HeadstartWPRoute) => {
+ const { data } = await queryPosts({
+ routeParams: params,
+ params: {
+ taxonomy: 'post_tag',
+ },
+ });
+
+ return (
+
+ {data.queriedObject.term?.name}
+
+
+ {data.posts.map((post) => (
+ -
+ {post.title.rendered}
+
+ ))}
+
+
+ );
+};
+
+export default TagArchive;
diff --git a/projects/wp-polylang-nextjs-app/src/app/api/preview/route.ts b/projects/wp-polylang-nextjs-app/src/app/api/preview/route.ts
new file mode 100644
index 000000000..ce26916d5
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/src/app/api/preview/route.ts
@@ -0,0 +1,7 @@
+import { previewRouteHandler } from '@headstartwp/next/app';
+import type { NextRequest } from 'next/server';
+
+export async function GET(request: NextRequest) {
+ // @ts-expect-error
+ return previewRouteHandler(request);
+}
diff --git a/projects/wp-polylang-nextjs-app/src/app/api/revalidate/route.ts b/projects/wp-polylang-nextjs-app/src/app/api/revalidate/route.ts
new file mode 100644
index 000000000..dc1dc3548
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/src/app/api/revalidate/route.ts
@@ -0,0 +1,7 @@
+import { revalidateRouteHandler } from '@headstartwp/next/app';
+import type { NextRequest } from 'next/server';
+
+export async function GET(request: NextRequest) {
+ // @ts-expect-error
+ return revalidateRouteHandler(request);
+}
diff --git a/projects/wp-polylang-nextjs-app/src/app/layout.tsx b/projects/wp-polylang-nextjs-app/src/app/layout.tsx
new file mode 100644
index 000000000..36d26353e
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/src/app/layout.tsx
@@ -0,0 +1,23 @@
+import type { Metadata } from 'next';
+import { Inter } from 'next/font/google';
+
+const inter = Inter({ subsets: ['latin'] });
+
+export const metadata: Metadata = {
+ title: 'Create Next App',
+ description: 'Generated by create next app',
+};
+
+const RootLayout = async ({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export default RootLayout;
diff --git a/projects/wp-polylang-nextjs-app/src/app/not-found.tsx b/projects/wp-polylang-nextjs-app/src/app/not-found.tsx
new file mode 100644
index 000000000..0b6d8855c
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/src/app/not-found.tsx
@@ -0,0 +1,13 @@
+import Link from 'next/link';
+
+const NotFound = () => {
+ return (
+
+
Not Found
+
Could not find requested resource
+
Return Home
+
+ );
+};
+
+export default NotFound;
diff --git a/projects/wp-polylang-nextjs-app/src/middleware.ts b/projects/wp-polylang-nextjs-app/src/middleware.ts
new file mode 100644
index 000000000..f73062bbd
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/src/middleware.ts
@@ -0,0 +1,20 @@
+import { AppMiddleware } from '@headstartwp/next/middlewares';
+import { NextRequest } from 'next/server';
+
+export const config = {
+ matcher: [
+ /*
+ * Match all paths except for:
+ * 1. /api routes
+ * 2. /_next (Next.js internals)
+ * 3. /fonts (inside /public)
+ * 4. all root files inside /public (e.g. /favicon.ico)
+ */
+ '/((?!api|cache-healthcheck|_next|fonts[\\w-]+\\.\\w+).*)',
+ ],
+};
+
+export async function middleware(req: NextRequest) {
+ // @ts-expect-error
+ return AppMiddleware(req, { appRouter: true });
+}
diff --git a/projects/wp-polylang-nextjs-app/tsconfig.json b/projects/wp-polylang-nextjs-app/tsconfig.json
new file mode 100644
index 000000000..0471d5fc7
--- /dev/null
+++ b/projects/wp-polylang-nextjs-app/tsconfig.json
@@ -0,0 +1,35 @@
+{
+ "compilerOptions": {
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": false,
+ "noEmit": true,
+ "incremental": true,
+ "module": "esnext",
+ "esModuleInterop": true,
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": [
+ "next-env.d.ts",
+ ".next/types/**/*.ts",
+ "**/*.ts",
+ "**/*.tsx"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}