@akinon/pz-theme 2.0.121-rc.0 → 2.0.122-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -35
- package/package.json +4 -4
- package/src/blocks/group-block.tsx +6 -4
- package/src/blocks/oauth-buttons-block.tsx +4 -3
- package/src/components/__tests__/link-prefetch.test.tsx +121 -0
- package/src/components/__tests__/theme-link.test.tsx +53 -4
- package/src/components/theme-link.tsx +12 -1
- package/src/custom/README.md +6 -0
- package/src/hooks/use-localize-href.ts +5 -8
- package/src/utils/localize-href.ts +21 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,40 +1,21 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
- Updated dependencies [
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
- Updated dependencies [57d7eb305]
|
|
20
|
-
- Updated dependencies [d99a6a7d5]
|
|
21
|
-
- Updated dependencies [9db81a714]
|
|
22
|
-
- Updated dependencies [591e345e1]
|
|
23
|
-
- Updated dependencies [4de5303c5]
|
|
24
|
-
- Updated dependencies [95b139dc1]
|
|
25
|
-
- Updated dependencies [1d00f2d06]
|
|
26
|
-
- Updated dependencies [4ac7b2a1e]
|
|
27
|
-
- Updated dependencies [e9598c71d]
|
|
28
|
-
- Updated dependencies [4998a9631]
|
|
29
|
-
- Updated dependencies [804d2bd6c]
|
|
30
|
-
- Updated dependencies [3909d3224]
|
|
31
|
-
- Updated dependencies [6a3d8a631]
|
|
32
|
-
- Updated dependencies [e18836b20]
|
|
33
|
-
- Updated dependencies [b1111d7b1]
|
|
34
|
-
- Updated dependencies [1595d458]
|
|
35
|
-
- Updated dependencies [dca12811]
|
|
36
|
-
- @akinon/next@2.0.121-rc.0
|
|
37
|
-
- @akinon/theme-contract@2.0.121-rc.0
|
|
3
|
+
## 2.0.122-beta.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [cbbbfd757]
|
|
8
|
+
- @akinon/next@2.0.122-beta.0
|
|
9
|
+
- @akinon/theme-contract@2.0.122-beta.0
|
|
10
|
+
|
|
11
|
+
## 2.0.121
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 60fc43e: ZERO-5108: A theme link fetches nothing ahead of time unless its caller asks for it. Next otherwise fetches a link's page as soon as the link is on screen, and because the language lives in a cookie that any such request rewrites, one link that escaped the locale rules switched the whole session's language with nobody clicking anything. next/link now has a single importer in the package, held there by a test, so a block cannot bring its own link back in without the locale or this default.
|
|
16
|
+
- e64e783: ZERO-5107: Put the page locale on a link in the browser as well. A public env var is inlined when the app is built and ACC hands the container its environment afterwards, so the client bundle carried no site address: the server rendered the localized path, hydration put the merchant's full URL back, Next fetched that unprefixed path ahead of the shopper and the middleware answered it by switching the language cookie. The browser now falls back to the address it is served from, and every caller asks the same question through one helper.
|
|
17
|
+
- @akinon/next@2.0.121
|
|
18
|
+
- @akinon/theme-contract@2.0.121
|
|
38
19
|
|
|
39
20
|
## 2.0.120
|
|
40
21
|
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/pz-theme",
|
|
3
3
|
"description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.122-beta.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "jest"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"@akinon/next": "2.0.
|
|
11
|
+
"@akinon/next": "2.0.122-beta.0",
|
|
12
12
|
"react": "^18.0.0 || ^19.0.0",
|
|
13
13
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@akinon/theme-contract": "2.0.
|
|
16
|
+
"@akinon/theme-contract": "2.0.122-beta.0",
|
|
17
17
|
"clsx": "2.1.1",
|
|
18
18
|
"lottie-web": "^5.13.0",
|
|
19
19
|
"react-multi-carousel": "2.8.4",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"zod": "4.4.1"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@akinon/next": "2.0.
|
|
24
|
+
"@akinon/next": "2.0.122-beta.0",
|
|
25
25
|
"@testing-library/jest-dom": "5.16.4",
|
|
26
26
|
"@testing-library/react": "16.3.2",
|
|
27
27
|
"@testing-library/user-event": "14.1.1",
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { ThemeLink as NextLink } from '../components/theme-link';
|
|
3
3
|
import { getResponsiveValue } from '../utils';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
asSitePath,
|
|
6
|
+
isRootRelativeHref,
|
|
7
|
+
storefrontOrigin
|
|
8
|
+
} from '../utils/localize-href';
|
|
5
9
|
import {
|
|
6
10
|
buildCommerceItem,
|
|
7
11
|
emitCommerceEvent,
|
|
@@ -76,7 +80,7 @@ const GroupBlock = ({
|
|
|
76
80
|
const routerHref = isRootRelativeHref(href)
|
|
77
81
|
? href.trim()
|
|
78
82
|
: typeof href === 'string'
|
|
79
|
-
? asSitePath(href.trim(),
|
|
83
|
+
? asSitePath(href.trim(), storefrontOrigin())
|
|
80
84
|
: undefined;
|
|
81
85
|
const useRouterLink = tag === 'a' && !isDesigner && routerHref !== undefined;
|
|
82
86
|
if (tag === 'a' && href && !useRouterLink) {
|
|
@@ -119,7 +123,6 @@ const GroupBlock = ({
|
|
|
119
123
|
return (
|
|
120
124
|
<NextLink
|
|
121
125
|
href={routerHref as string}
|
|
122
|
-
prefetch={false}
|
|
123
126
|
className="contents"
|
|
124
127
|
onClick={handleAnchorAnalytics}
|
|
125
128
|
/>
|
|
@@ -168,7 +171,6 @@ const GroupBlock = ({
|
|
|
168
171
|
return (
|
|
169
172
|
<NextLink
|
|
170
173
|
href={routerHref as string}
|
|
171
|
-
prefetch={false}
|
|
172
174
|
className="contents"
|
|
173
175
|
onClick={handleAnchorAnalytics}
|
|
174
176
|
>
|
|
@@ -5,7 +5,7 @@ import React from 'react';
|
|
|
5
5
|
// `/{provider}/login/`, so there is no locale to add. `localizeHref` leaves that
|
|
6
6
|
// route alone too (`OAUTH_ROUTE`), which keeps a row template that renders
|
|
7
7
|
// `item.url` through a button or group working on an English page.
|
|
8
|
-
import NextLink from '
|
|
8
|
+
import { ThemeLink as NextLink } from '../components/theme-link';
|
|
9
9
|
import { getResponsiveValue, partAttrs } from '../utils';
|
|
10
10
|
import { DesignerPlaceholder, readToggle } from './product-block-utils';
|
|
11
11
|
import { hasItemTemplate, ItemTemplateList } from './item-template';
|
|
@@ -149,8 +149,9 @@ const OauthButtonsBlock = ({
|
|
|
149
149
|
data-oauth-button={provider}
|
|
150
150
|
{...partAttrs(`button-${provider}`)}
|
|
151
151
|
href={isDesigner ? '#' : providerHref(provider)}
|
|
152
|
-
// The handshake leaves the Next.js app entirely, so the client
|
|
153
|
-
// must not
|
|
152
|
+
// The handshake leaves the Next.js app entirely, so the client
|
|
153
|
+
// router must not soft-navigate it. `ThemeLink` leaves an OAuth
|
|
154
|
+
// route's href exactly as it is and prefetches nothing by default.
|
|
154
155
|
prefetch={false}
|
|
155
156
|
onClick={(event) => {
|
|
156
157
|
if (isDesigner) event.preventDefault();
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { render } from '@testing-library/react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ZERO-5108: a theme link prefetches nothing unless its caller asks.
|
|
8
|
+
*
|
|
9
|
+
* Next fetches a link's page as soon as the link is on screen, and the locale
|
|
10
|
+
* lives in a cookie that any request to an unprefixed path rewrites. One link
|
|
11
|
+
* that escapes the locale rules therefore stops being a wrong destination and
|
|
12
|
+
* becomes a silent language switch for the whole session, with nobody clicking
|
|
13
|
+
* anything — which is exactly how zsa-zsa-zsu's English storefront started
|
|
14
|
+
* serving Turkish menus. The rules still apply; this removes the mechanism
|
|
15
|
+
* that made a single miss cost the session.
|
|
16
|
+
*
|
|
17
|
+
* Asserted on the props `next/link` receives, because a boolean that React
|
|
18
|
+
* never puts on the anchor cannot be read back off the DOM.
|
|
19
|
+
*/
|
|
20
|
+
const received: Array<Record<string, unknown>> = [];
|
|
21
|
+
|
|
22
|
+
jest.mock('next/link', () => ({
|
|
23
|
+
__esModule: true,
|
|
24
|
+
default: (props: Record<string, unknown>) => {
|
|
25
|
+
received.push(props);
|
|
26
|
+
return React.createElement('a', {
|
|
27
|
+
href: typeof props.href === 'string' ? props.href : '#'
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
jest.mock('@akinon/next/hooks', () => ({
|
|
33
|
+
__esModule: true,
|
|
34
|
+
useLocalization: () => ({
|
|
35
|
+
locale: 'tr',
|
|
36
|
+
defaultLocaleValue: 'tr',
|
|
37
|
+
localeUrlStrategy: 'hide-default-locale'
|
|
38
|
+
})
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
import { ThemeLink } from '../theme-link';
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
received.length = 0;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('ThemeLink prefetching', () => {
|
|
48
|
+
it('asks for no prefetch by default', () => {
|
|
49
|
+
render(<ThemeLink href="/yatak-odasi/">Yatak odası</ThemeLink>);
|
|
50
|
+
|
|
51
|
+
expect(received).toHaveLength(1);
|
|
52
|
+
expect(received[0].prefetch).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('lets a caller turn it back on for its own case', () => {
|
|
56
|
+
render(
|
|
57
|
+
<ThemeLink href="/yatak-odasi/" prefetch>
|
|
58
|
+
Yatak odası
|
|
59
|
+
</ThemeLink>
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(received[0].prefetch).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* `oauth-buttons-block` renders its handshake through this component now.
|
|
67
|
+
* The handshake leaves the app, and the core middleware matches only the
|
|
68
|
+
* unprefixed path, so `/en/google/login/` would reach a page that does not
|
|
69
|
+
* exist.
|
|
70
|
+
*/
|
|
71
|
+
it('leaves an OAuth handshake unprefixed and unprefetched', () => {
|
|
72
|
+
render(<ThemeLink href="/google/login/">Continue with Google</ThemeLink>);
|
|
73
|
+
|
|
74
|
+
expect(received[0]).toMatchObject({
|
|
75
|
+
href: '/google/login/',
|
|
76
|
+
prefetch: false
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('keeps passing everything else through', () => {
|
|
81
|
+
render(
|
|
82
|
+
<ThemeLink href="/yatak-odasi/" className="row" data-part="link">
|
|
83
|
+
Yatak odası
|
|
84
|
+
</ThemeLink>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(received[0]).toMatchObject({
|
|
88
|
+
href: '/yatak-odasi/',
|
|
89
|
+
className: 'row',
|
|
90
|
+
'data-part': 'link'
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The guard that keeps the rule true for code written later: `next/link` is
|
|
97
|
+
* imported in exactly one file, so no block can bring its own link back in —
|
|
98
|
+
* with neither the locale nor this default on it.
|
|
99
|
+
*/
|
|
100
|
+
describe('next/link has one importer', () => {
|
|
101
|
+
const SRC = path.resolve(__dirname, '../..');
|
|
102
|
+
|
|
103
|
+
const sourceFiles = (dir: string): string[] =>
|
|
104
|
+
fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
105
|
+
const full = path.join(dir, entry.name);
|
|
106
|
+
if (entry.isDirectory()) {
|
|
107
|
+
return entry.name === '__tests__' || entry.name === 'node_modules'
|
|
108
|
+
? []
|
|
109
|
+
: sourceFiles(full);
|
|
110
|
+
}
|
|
111
|
+
return /\.tsx?$/.test(entry.name) ? [full] : [];
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('is components/theme-link.tsx and nothing else', () => {
|
|
115
|
+
const importers = sourceFiles(SRC)
|
|
116
|
+
.filter((file) => /from\s+'next\/link'/.test(fs.readFileSync(file, 'utf8')))
|
|
117
|
+
.map((file) => path.relative(SRC, file).split(path.sep).join('/'));
|
|
118
|
+
|
|
119
|
+
expect(importers).toEqual(['components/theme-link.tsx']);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -85,10 +85,9 @@ describe('ThemeLink', () => {
|
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
|
-
* The storefront's own address reaches the rules as `NEXT_PUBLIC_URL`,
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* the shopper sees is the same one the server sent.
|
|
88
|
+
* The storefront's own address reaches the rules as `NEXT_PUBLIC_URL`, so a
|
|
89
|
+
* merchant's pasted full URL reads as a path and the anchor the shopper sees
|
|
90
|
+
* is the same one the server sent.
|
|
92
91
|
*/
|
|
93
92
|
describe('with the site address configured', () => {
|
|
94
93
|
const configured = process.env.NEXT_PUBLIC_URL;
|
|
@@ -134,6 +133,56 @@ describe('ThemeLink', () => {
|
|
|
134
133
|
});
|
|
135
134
|
});
|
|
136
135
|
|
|
136
|
+
/**
|
|
137
|
+
* ZERO-5107: a public env var is inlined at BUILD time and ACC hands the
|
|
138
|
+
* container its environment afterwards, so no client bundle built there
|
|
139
|
+
* carries `NEXT_PUBLIC_URL` at all. Without a fallback the server rendered
|
|
140
|
+
* `/en/yeni-urunler/`, hydration put the full URL back, Next prefetched that
|
|
141
|
+
* unprefixed path and the middleware answered it with `pz-locale=tr`.
|
|
142
|
+
*/
|
|
143
|
+
describe('with the site address missing from the bundle', () => {
|
|
144
|
+
const configured = process.env.NEXT_PUBLIC_URL;
|
|
145
|
+
|
|
146
|
+
beforeEach(() => {
|
|
147
|
+
delete process.env.NEXT_PUBLIC_URL;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
afterAll(() => {
|
|
151
|
+
if (configured === undefined) delete process.env.NEXT_PUBLIC_URL;
|
|
152
|
+
else process.env.NEXT_PUBLIC_URL = configured;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('falls back to the address the page is served from', () => {
|
|
156
|
+
mockLocalization = { ...TURKISH, locale: 'en' };
|
|
157
|
+
|
|
158
|
+
expect(
|
|
159
|
+
hrefOf(
|
|
160
|
+
<ThemeLink href={`${window.location.origin}/yeni-urunler/`}>
|
|
161
|
+
New in
|
|
162
|
+
</ThemeLink>
|
|
163
|
+
)
|
|
164
|
+
).toBe('/en/yeni-urunler/');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('keeps that address as it was written on the Turkish page', () => {
|
|
168
|
+
const href = `${window.location.origin}/yeni-urunler/`;
|
|
169
|
+
|
|
170
|
+
expect(hrefOf(<ThemeLink href={href}>Yeni ürünler</ThemeLink>)).toBe(
|
|
171
|
+
href
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('still leaves another site alone', () => {
|
|
176
|
+
mockLocalization = { ...TURKISH, locale: 'en' };
|
|
177
|
+
|
|
178
|
+
expect(
|
|
179
|
+
hrefOf(
|
|
180
|
+
<ThemeLink href="https://lookbook.example.com/x">Lookbook</ThemeLink>
|
|
181
|
+
)
|
|
182
|
+
).toBe('https://lookbook.example.com/x');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
137
186
|
it('keeps every other prop and forwards a ref to the anchor', () => {
|
|
138
187
|
mockLocalization = { ...TURKISH, locale: 'en' };
|
|
139
188
|
const ref = React.createRef<HTMLAnchorElement>();
|
|
@@ -18,15 +18,26 @@ type ThemeLinkProps = React.ComponentPropsWithoutRef<typeof NextLink>;
|
|
|
18
18
|
* Rendered through `next/link` rather than `@akinon/next`'s `Link` so a ref
|
|
19
19
|
* still reaches the anchor, and so links and imperative navigations share one
|
|
20
20
|
* rule set.
|
|
21
|
+
*
|
|
22
|
+
* NOTHING IS PREFETCHED UNLESS THE CALLER ASKS (ZERO-5108). Next fetches a
|
|
23
|
+
* link's page as soon as the link scrolls into view, and the locale lives in a
|
|
24
|
+
* cookie that any request to an unprefixed path rewrites — so one link that
|
|
25
|
+
* escapes the rules above stops being a wrong destination and becomes a silent
|
|
26
|
+
* language switch for the whole session, with nobody clicking anything. A
|
|
27
|
+
* composition is a wall of links the merchant arranged, and the cost of not
|
|
28
|
+
* prefetching them is paid once per click by the shopper who actually
|
|
29
|
+
* navigates. `prefetch` is still a prop: a caller that measured its own case
|
|
30
|
+
* can pass `true`.
|
|
21
31
|
*/
|
|
22
32
|
export const ThemeLink = forwardRef<HTMLAnchorElement, ThemeLinkProps>(
|
|
23
|
-
function ThemeLink({ href, ...rest }, ref) {
|
|
33
|
+
function ThemeLink({ href, prefetch = false, ...rest }, ref) {
|
|
24
34
|
const localize = useLocalizeHref();
|
|
25
35
|
|
|
26
36
|
return (
|
|
27
37
|
<NextLink
|
|
28
38
|
ref={ref}
|
|
29
39
|
href={typeof href === 'string' ? localize(href) : href}
|
|
40
|
+
prefetch={prefetch}
|
|
30
41
|
{...rest}
|
|
31
42
|
/>
|
|
32
43
|
);
|
package/src/custom/README.md
CHANGED
|
@@ -175,6 +175,12 @@ write the locale yourself; there are three places it goes on for you:
|
|
|
175
175
|
| Any other link | `ThemeLink` in place of `next/link`, with the same props |
|
|
176
176
|
| A bare string: a plain `<a>`, `router.push`, a url read off commerce data | `const localize = useLocalizeHref();` then `localize(url)` |
|
|
177
177
|
|
|
178
|
+
`ThemeLink` prefetches nothing unless you pass `prefetch`. Next otherwise
|
|
179
|
+
fetches a link's page the moment it scrolls into view, and because the locale
|
|
180
|
+
lives in a cookie that any request to an unprefixed path rewrites, one link
|
|
181
|
+
that slips past these rules would switch the whole session's language without
|
|
182
|
+
anyone clicking it.
|
|
183
|
+
|
|
178
184
|
All three come from `@akinon/pz-theme/src/custom`. An `<a>` nested deeper
|
|
179
185
|
inside a `ThemeEditable` is not touched — that one is a link of yours, so it
|
|
180
186
|
takes the row it belongs to above.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useCallback } from 'react';
|
|
4
4
|
import { useLocalization } from '@akinon/next/hooks';
|
|
5
5
|
import { urlLocaleMatcherRegex } from '@akinon/next/utils';
|
|
6
|
-
import { localizeHref } from '../utils/localize-href';
|
|
6
|
+
import { localizeHref, storefrontOrigin } from '../utils/localize-href';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* `localizeHref` bound to the active locale, for anything that navigates
|
|
@@ -11,14 +11,11 @@ import { localizeHref } from '../utils/localize-href';
|
|
|
11
11
|
*
|
|
12
12
|
* Everything that localizes goes through this hook rather than calling
|
|
13
13
|
* `localizeHref` itself, because this is the only place that knows the
|
|
14
|
-
* storefront's own address
|
|
15
|
-
* site would not be recognized as a path. `NEXT_PUBLIC_URL` is a public env
|
|
16
|
-
* var, so Next inlines the same literal into the server render and the client
|
|
17
|
-
* bundle, and the two cannot disagree about which links are internal.
|
|
14
|
+
* storefront's own address.
|
|
18
15
|
*/
|
|
19
16
|
export const useLocalizeHref = (): ((href: string) => string) => {
|
|
20
17
|
const { locale, defaultLocaleValue, localeUrlStrategy } = useLocalization();
|
|
21
|
-
const
|
|
18
|
+
const site = storefrontOrigin();
|
|
22
19
|
|
|
23
20
|
return useCallback(
|
|
24
21
|
(href: string) =>
|
|
@@ -27,8 +24,8 @@ export const useLocalizeHref = (): ((href: string) => string) => {
|
|
|
27
24
|
defaultLocaleValue,
|
|
28
25
|
localeUrlStrategy,
|
|
29
26
|
localeMatcher: urlLocaleMatcherRegex,
|
|
30
|
-
siteUrl
|
|
27
|
+
siteUrl: site
|
|
31
28
|
}),
|
|
32
|
-
[locale, defaultLocaleValue, localeUrlStrategy,
|
|
29
|
+
[locale, defaultLocaleValue, localeUrlStrategy, site]
|
|
33
30
|
);
|
|
34
31
|
};
|
|
@@ -67,6 +67,27 @@ const hasNoLocaleVariant = (path: string): boolean => {
|
|
|
67
67
|
return /\.[^.]+$/.test(lastSegment);
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* The storefront's own address, which is what makes a merchant's full URL to
|
|
72
|
+
* this same site recognizable as one of its paths.
|
|
73
|
+
*
|
|
74
|
+
* `NEXT_PUBLIC_URL` is read first and is the only value the server has. The
|
|
75
|
+
* browser falls back to the address it is actually being served from, because
|
|
76
|
+
* a public env var is inlined at BUILD time and ACC hands the container its
|
|
77
|
+
* environment afterwards: no client bundle built there carries the value at
|
|
78
|
+
* all. Without the fallback the server rendered `/en/yeni-urunler/` and
|
|
79
|
+
* hydration put the full URL back, Next prefetched that unprefixed path, and
|
|
80
|
+
* the middleware answered it with `pz-locale=tr` — every client request on an
|
|
81
|
+
* English page came back Turkish (ZERO-5107, live on 2026-09-23).
|
|
82
|
+
*
|
|
83
|
+
* Exported because a block that decides whether an href is internal has to ask
|
|
84
|
+
* the same question the same way; `group-block` read the env var directly and
|
|
85
|
+
* answered "not this site" in every browser.
|
|
86
|
+
*/
|
|
87
|
+
export const storefrontOrigin = (): string | undefined =>
|
|
88
|
+
process.env.NEXT_PUBLIC_URL ||
|
|
89
|
+
(typeof window === 'undefined' ? undefined : window.location.origin);
|
|
90
|
+
|
|
70
91
|
/**
|
|
71
92
|
* `www.` is not a different site. A merchant pastes whichever host their
|
|
72
93
|
* browser showed them, and the storefront answers on both.
|