@tokenoftrust/storefront-runner 4.2.2 → 4.2.3
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/apps/storefront/src/components/content/storefront/chrome/SiteFooter.astro +1 -1
- package/apps/storefront/src/components/content/storefront/chrome/SiteHeader.astro +10 -9
- package/apps/storefront/src/components/content/storefront/chrome/UtilityNav.astro +1 -1
- package/apps/storefront/src/layouts/Layout.astro +9 -4
- package/apps/storefront/src/lib/content/shared/rawMarketingHtml.ts +45 -8
- package/package.json +1 -1
|
@@ -122,7 +122,7 @@ const collapsible = variant === "light-collapsible";
|
|
|
122
122
|
<div class="mt-4 flex items-center gap-3">
|
|
123
123
|
{social.map((s) => (
|
|
124
124
|
<a
|
|
125
|
-
href={s.href}
|
|
125
|
+
href={withBase(basePath, s.href)}
|
|
126
126
|
aria-label={s.label}
|
|
127
127
|
rel="noopener noreferrer"
|
|
128
128
|
class="text-muted transition-colors hover:text-primary"
|
|
@@ -90,6 +90,7 @@ const {
|
|
|
90
90
|
|
|
91
91
|
const { basePath } = Astro.locals;
|
|
92
92
|
const homeHref = withBase(basePath, brandHref ?? "/");
|
|
93
|
+
const searchFormAction = searchAction ? withBase(basePath, searchAction) : undefined;
|
|
93
94
|
const showLogo = !!logo && /^(https?:|\/)/.test(logo.src);
|
|
94
95
|
const dark = variant === "dark";
|
|
95
96
|
const minimal = layout === "minimal";
|
|
@@ -205,11 +206,11 @@ const currentPath = Astro.url.pathname;
|
|
|
205
206
|
|
|
206
207
|
<!-- Search (live typeahead island) -->
|
|
207
208
|
{
|
|
208
|
-
!minimal && !compact &&
|
|
209
|
+
!minimal && !compact && searchFormAction && (
|
|
209
210
|
<div class="hidden flex-1 md:flex">
|
|
210
211
|
<SearchTypeahead
|
|
211
212
|
client:idle
|
|
212
|
-
action={
|
|
213
|
+
action={searchFormAction}
|
|
213
214
|
placeholder={searchPlaceholder}
|
|
214
215
|
label={searchLabel}
|
|
215
216
|
/>
|
|
@@ -220,9 +221,9 @@ const currentPath = Astro.url.pathname;
|
|
|
220
221
|
<!-- Actions: saved items + marketing CTAs + auth + account + cart (link only) + mobile menu -->
|
|
221
222
|
<div class:list={["flex items-center gap-1", compact ? "col-start-3 justify-self-end" : "ml-auto"]}>
|
|
222
223
|
{
|
|
223
|
-
compact &&
|
|
224
|
+
compact && searchFormAction && (
|
|
224
225
|
<a
|
|
225
|
-
href={
|
|
226
|
+
href={searchFormAction}
|
|
226
227
|
aria-label={searchLabel ?? "Search"}
|
|
227
228
|
class="hidden h-9 w-9 place-items-center rounded-full text-text transition-colors hover:text-accent-ink md:grid"
|
|
228
229
|
>
|
|
@@ -281,7 +282,7 @@ const currentPath = Astro.url.pathname;
|
|
|
281
282
|
{
|
|
282
283
|
authLink && (
|
|
283
284
|
<a data-mobile-menu-action
|
|
284
|
-
href={authLink.href}
|
|
285
|
+
href={withBase(basePath, authLink.href)}
|
|
285
286
|
class="inline-flex shrink-0 items-center rounded-full border border-current px-4 py-2 font-display text-sm font-medium transition-opacity hover:opacity-80"
|
|
286
287
|
>
|
|
287
288
|
{authLink.label}
|
|
@@ -338,11 +339,11 @@ const currentPath = Astro.url.pathname;
|
|
|
338
339
|
</div>
|
|
339
340
|
|
|
340
341
|
{
|
|
341
|
-
compact &&
|
|
342
|
+
compact && searchFormAction && (
|
|
342
343
|
<div class="border-b border-border bg-surface px-4 py-2 md:hidden" data-header-phone-search>
|
|
343
344
|
<SearchTypeahead
|
|
344
345
|
client:idle
|
|
345
|
-
action={
|
|
346
|
+
action={searchFormAction}
|
|
346
347
|
placeholder={searchPlaceholder}
|
|
347
348
|
label={searchLabel}
|
|
348
349
|
/>
|
|
@@ -387,8 +388,8 @@ const currentPath = Astro.url.pathname;
|
|
|
387
388
|
compact && (
|
|
388
389
|
<nav class:list={["fixed inset-x-0 bottom-0 z-50 grid h-[62px] border-t border-border bg-surface shadow-[0_-3px_14px_color-mix(in_srgb,var(--color-text)_10%,transparent)] md:hidden", cartHref ? "grid-cols-4" : "grid-cols-3"]} aria-label="Quick links">
|
|
389
390
|
<a href={homeHref} class="flex flex-col items-center justify-center gap-0.5 text-[0.65rem] text-text">Home</a>
|
|
390
|
-
<a href={withBase(basePath,
|
|
391
|
-
<a href={
|
|
391
|
+
<a href={searchFormAction ?? withBase(basePath, "/search")} class="flex flex-col items-center justify-center gap-0.5 text-[0.65rem] text-text">Search</a>
|
|
392
|
+
<a href={withBase(basePath, accountHref ?? authLink?.href ?? "/auth/login")} class="flex flex-col items-center justify-center gap-0.5 text-[0.65rem] text-text">Account</a>
|
|
392
393
|
{cartHref && <a href={withBase(basePath, cartHref)} class="flex flex-col items-center justify-center gap-0.5 text-[0.65rem] text-text">Cart</a>}
|
|
393
394
|
</nav>
|
|
394
395
|
)
|
|
@@ -64,7 +64,7 @@ const linkStamp = (l: ChromeLink, index: number) =>
|
|
|
64
64
|
<span class="flex items-center gap-3">
|
|
65
65
|
{social.map((s) => (
|
|
66
66
|
<a
|
|
67
|
-
href={s.href}
|
|
67
|
+
href={withBase(basePath, s.href)}
|
|
68
68
|
aria-label={s.label}
|
|
69
69
|
rel="noopener noreferrer"
|
|
70
70
|
class="transition-colors hover:text-primary"
|
|
@@ -29,7 +29,8 @@ import { organizationLd, websiteLd } from "@/lib/content/shared/jsonld";
|
|
|
29
29
|
import { composeStructuredData, structuredDataProfile, type StructuredDataPage } from "@/lib/content/seo/structuredData";
|
|
30
30
|
import { canonicalUrl } from "@/lib/content/seo/urlForm";
|
|
31
31
|
import { documentTitle } from "@/lib/content/seo/documentTitle";
|
|
32
|
-
import { nonceInlineTags,
|
|
32
|
+
import { nonceInlineTags, prefixSharedChrome, CHROME_BODY_MARKER } from "@/lib/content/shared/rawMarketingHtml";
|
|
33
|
+
import { withBase } from "@/lib/publication/shared/basePath";
|
|
33
34
|
import { CHROME_BEHAVIOR_JS, CHROME_HEAD_JS, sanitizeSharedChromeFragment } from "@/lib/content/shared/rawChrome";
|
|
34
35
|
import { ASYNC_CSS_SWAP_JS } from "@/lib/content/shared/asyncCss";
|
|
35
36
|
import { reportTrackingChainHealth } from "@/lib/measurement/tracking/interactionHealth";
|
|
@@ -196,17 +197,19 @@ if (chromeHtml && chromeHtml.includes(CHROME_BODY_MARKER)) {
|
|
|
196
197
|
const bodyEnd = chromeHtml.lastIndexOf("</body>");
|
|
197
198
|
sharedChrome = {
|
|
198
199
|
header: nonceInlineTags(
|
|
199
|
-
|
|
200
|
+
prefixSharedChrome(
|
|
200
201
|
sanitizeSharedChromeFragment(chromeHtml.slice(bodyOpen, marker)),
|
|
202
|
+
basePath,
|
|
201
203
|
apiBasePath,
|
|
202
204
|
),
|
|
203
205
|
cspNonce,
|
|
204
206
|
),
|
|
205
207
|
footer: nonceInlineTags(
|
|
206
|
-
|
|
208
|
+
prefixSharedChrome(
|
|
207
209
|
sanitizeSharedChromeFragment(
|
|
208
210
|
chromeHtml.slice(marker + CHROME_BODY_MARKER.length, bodyEnd >= 0 ? bodyEnd : undefined),
|
|
209
211
|
),
|
|
212
|
+
basePath,
|
|
210
213
|
apiBasePath,
|
|
211
214
|
),
|
|
212
215
|
cspNonce,
|
|
@@ -394,7 +397,9 @@ const footerWidgets = await renderWidgetSlot(
|
|
|
394
397
|
layout={resolvedHeaderVariant}
|
|
395
398
|
announcement={chrome.header.announcement?.messages[0]}
|
|
396
399
|
searchAction={
|
|
397
|
-
features.productSearch
|
|
400
|
+
features.productSearch
|
|
401
|
+
? withBase(basePath, chrome.header.search?.action ?? "/search")
|
|
402
|
+
: undefined
|
|
398
403
|
}
|
|
399
404
|
searchPlaceholder={chrome.header.search?.placeholder}
|
|
400
405
|
searchLabel={chrome.header.search?.label}
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A marketing tenant can ship a self-contained document (`content/<tenant>/home.html`
|
|
5
5
|
* or `content/<tenant>/pages-html/<slug>.html`) that we serve VERBATIM, bypassing the
|
|
6
|
-
* block Layout.
|
|
6
|
+
* block Layout. Three transforms are applied at serve time:
|
|
7
7
|
* 1. nonce-stamp the document's inline <style>/<script> so a strict CSP admits them;
|
|
8
8
|
* 2. under path-prefix tenant routing (locals.basePath set), prefix same-origin
|
|
9
|
-
* root-absolute anchor hrefs with the base path so
|
|
10
|
-
* the raw doc bypasses the Layout's client
|
|
9
|
+
* root-absolute anchor hrefs and page form actions with the base path so
|
|
10
|
+
* navigation stays in-tenant — the raw doc bypasses the Layout's client
|
|
11
|
+
* base-path fixer, so we do it here;
|
|
12
|
+
* 3. prefix `/api` form actions with apiBasePath, never a preview pin.
|
|
11
13
|
*
|
|
12
14
|
* Single home for both routes (`index.astro`, `[...slug].astro`).
|
|
13
15
|
*
|
|
@@ -24,7 +26,7 @@ import {
|
|
|
24
26
|
} from "@tot/public-runtime";
|
|
25
27
|
import { reportTrackingChainHealth } from "../../measurement/tracking/interactionHealth.js";
|
|
26
28
|
import { ASYNC_CSS_SWAP_JS } from "./asyncCss.js";
|
|
27
|
-
import { ROOT_SERVED_HREF_NAMESPACES } from "../../publication/shared/basePath.js";
|
|
29
|
+
import { ROOT_SERVED_HREF_NAMESPACES, withBase } from "../../publication/shared/basePath.js";
|
|
28
30
|
|
|
29
31
|
export { rawHtmlPolicy } from "@tot/public-runtime";
|
|
30
32
|
|
|
@@ -205,6 +207,44 @@ export function prefixApiFormActions(html: string, apiBasePath: string): string
|
|
|
205
207
|
return html.replace(API_FORM_ACTION_RE, (_match, attr: string) => `${attr}="${apiBasePath}/api/`);
|
|
206
208
|
}
|
|
207
209
|
|
|
210
|
+
/** True when a form target is an `/api` route. Those use {@link prefixApiFormActions}. */
|
|
211
|
+
function isApiFormTarget(href: string): boolean {
|
|
212
|
+
if (!href.startsWith("/")) return false;
|
|
213
|
+
const path = href.slice(1).split(/[?#]/, 1)[0] ?? "";
|
|
214
|
+
return path === "api" || path.startsWith("api/");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const PAGE_FORM_ACTION_RE = /\b(action|formaction)=("([^"]*)"|'([^']*)')/g;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Prefix same-origin page form targets (`action`/`formaction`) with `basePath`
|
|
221
|
+
* via {@link withBase}. A preview pin's header search (`/search`) stays inside
|
|
222
|
+
* that pin. `/api` targets are left for {@link prefixApiFormActions}. No-op
|
|
223
|
+
* when `basePath` is empty.
|
|
224
|
+
*/
|
|
225
|
+
export function prefixPageFormActions(html: string, basePath: string): string {
|
|
226
|
+
if (!basePath) return html;
|
|
227
|
+
return html.replace(PAGE_FORM_ACTION_RE, (full, attr: string, _quoted: string, dq: string | undefined, sq: string | undefined) => {
|
|
228
|
+
const href = dq ?? sq ?? "";
|
|
229
|
+
if (isApiFormTarget(href)) return full;
|
|
230
|
+
const next = withBase(basePath, href);
|
|
231
|
+
if (next === href) return full;
|
|
232
|
+
const quote = dq !== undefined ? '"' : "'";
|
|
233
|
+
return `${attr}=${quote}${next}${quote}`;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Serve-time prefix for chrome HTML on both mounts (raw documents and Layout's
|
|
239
|
+
* shared-chrome splice). Anchors go through {@link prefixTenantLinks}. Page
|
|
240
|
+
* form actions go through {@link prefixPageFormActions} (`withBase`). `/api`
|
|
241
|
+
* form actions go through {@link prefixApiFormActions} and `apiBasePath`, never
|
|
242
|
+
* a preview pin.
|
|
243
|
+
*/
|
|
244
|
+
export function prefixSharedChrome(html: string, basePath: string, apiBasePath: string): string {
|
|
245
|
+
return prefixApiFormActions(prefixPageFormActions(prefixTenantLinks(html, basePath), basePath), apiBasePath);
|
|
246
|
+
}
|
|
247
|
+
|
|
208
248
|
/** Raw pages with a robots `noindex` directive remain routable but stay out of discovery. */
|
|
209
249
|
export function isIndexableHtml(html: string): boolean {
|
|
210
250
|
for (const match of html.matchAll(/<meta\b([^>]*)>/gi)) {
|
|
@@ -397,10 +437,7 @@ export function rawHtmlResponse(
|
|
|
397
437
|
composed = injectBodyEnd(composed, interactionTrackingScript(opts.nonce));
|
|
398
438
|
composed = injectBodyEnd(composed, asyncCssSwapScript(opts.nonce));
|
|
399
439
|
composed = resolveCanonicalHref(composed, opts.canonicalUrl);
|
|
400
|
-
const doc =
|
|
401
|
-
prefixTenantLinks(composed, opts.basePath),
|
|
402
|
-
opts.apiBasePath ?? "",
|
|
403
|
-
);
|
|
440
|
+
const doc = prefixSharedChrome(composed, opts.basePath, opts.apiBasePath ?? "");
|
|
404
441
|
return new Response(doc, {
|
|
405
442
|
headers: { "content-type": "text/html; charset=utf-8" },
|
|
406
443
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.3",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
|
|
6
6
|
"packageManager": "pnpm@11.9.0",
|