diff --git a/.changeset/assetupdater-reinit-bypass.md b/.changeset/assetupdater-reinit-bypass.md new file mode 100644 index 0000000..1c7f360 --- /dev/null +++ b/.changeset/assetupdater-reinit-bypass.md @@ -0,0 +1,5 @@ +--- +"@axistaylor/nextpress": patch +--- + +AssetUpdater now re-executes WordPress view scripts on client-side navigation, so scripts using the standard `document.readyState` / `DOMContentLoaded` ready-check re-initialize for the new page's content without any SPA-specific code. Adds a `reinitBypassHandles` prop to opt non-idempotent scripts (e.g. `wc-order-attribution`, whose `customElements.define()` throws on a second run) out of re-running — those load once and are skipped on subsequent navigations. diff --git a/docs/api/asset-updater.md b/docs/api/asset-updater.md index 4ebd4ec..0d52794 100644 --- a/docs/api/asset-updater.md +++ b/docs/api/asset-updater.md @@ -15,18 +15,19 @@ Because route-group `layout.tsx` files do **not** re-render on client-side navig ```tsx // components/AssetUpdater.tsx -'use client'; +"use client"; -import { usePathname } from 'next/navigation'; +import { usePathname } from "next/navigation"; import { AssetUpdater as BaseAssetUpdater, AssetData, -} from '@axistaylor/nextpress/client'; +} from "@axistaylor/nextpress/client"; export interface AssetUpdaterProps { fetchAssets: (uri: string) => Promise; instance?: string; bypassDomains?: string[]; + reinitBypassHandles?: string[]; } export function AssetUpdater(props: AssetUpdaterProps) { @@ -36,6 +37,7 @@ export function AssetUpdater(props: AssetUpdaterProps) { fetchAssets={props.fetchAssets} instance={props.instance} bypassDomains={props.bypassDomains} + reinitBypassHandles={props.reinitBypassHandles} pathname={pathname} /> ); @@ -46,13 +48,13 @@ Then use the wrapper in your WordPress layout: ```tsx // app/(wordpress-pages)/layout.tsx -import { WPHead, WPFooter } from '@axistaylor/nextpress'; -import { AssetUpdater } from '@/components/AssetUpdater'; -import { fetchAssets } from '@/actions/fetchAssets'; -import { headers } from 'next/headers'; +import { WPHead, WPFooter } from "@axistaylor/nextpress"; +import { AssetUpdater } from "@/components/AssetUpdater"; +import { fetchAssets } from "@/actions/fetchAssets"; +import { headers } from "next/headers"; export default async function WordPressLayout({ children }) { - const uri = (await headers()).get('x-uri') || '/'; + const uri = (await headers()).get("x-uri") || "/"; // …fetch stylesheets, scripts, importMap, globalStyles for initial SSR… return ( @@ -79,10 +81,10 @@ export default async function WordPressLayout({ children }) { ```ts // actions/fetchAssets.ts -'use server'; +"use server"; -import type { AssetData } from '@axistaylor/nextpress/client'; -import { fetchStylesAndScriptsByUri, fetchGlobalStyles } from '@/lib/wordpress'; +import type { AssetData } from "@axistaylor/nextpress/client"; +import { fetchStylesAndScriptsByUri, fetchGlobalStyles } from "@/lib/wordpress"; export async function fetchAssets(uri: string): Promise { const [{ stylesheets, scripts }, globalStyles] = await Promise.all([ @@ -99,12 +101,13 @@ export async function fetchAssets(uri: string): Promise { ## Props -| Prop | Type | Required | Description | -|------|------|----------|-------------| -| `pathname` | `string` | Yes | The current pathname being rendered (usually sourced from `headers().get('x-uri')`) | -| `fetchAssets` | `(uri: string) => Promise` | Yes | Server action that returns fresh stylesheets, scripts, and global styles for a URI | -| `instance` | `string` | No | WordPress instance slug used for proxy URL rewriting (default: `'default'`) | -| `bypassDomains` | `string[]` | No | Domains whose scripts/stylesheets load directly from their original URL instead of being proxied — e.g. `['js.stripe.com', 'fonts.googleapis.com']`. Matched by hostname; a root domain covers its subdomains (`stripe.com` covers `js.stripe.com`). Default: `[]` | +| Prop | Type | Required | Description | +| --------------------- | ------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `pathname` | `string` | Yes | The current pathname being rendered (usually sourced from `headers().get('x-uri')`) | +| `fetchAssets` | `(uri: string) => Promise` | Yes | Server action that returns fresh stylesheets, scripts, and global styles for a URI | +| `instance` | `string` | No | WordPress instance slug used for proxy URL rewriting (default: `'default'`) | +| `bypassDomains` | `string[]` | No | Domains whose scripts/stylesheets load directly from their original URL instead of being proxied — e.g. `['js.stripe.com', 'fonts.googleapis.com']`. Matched by hostname; a root domain covers its subdomains (`stripe.com` covers `js.stripe.com`). Default: `[]` | +| `reinitBypassHandles` | `string[]` | No | Script handles to load once and **skip re-running** on client-side navigation. See [Script re-execution on navigation](#script-re-execution-on-navigation). Default: `[]` | ### `AssetData` @@ -120,11 +123,11 @@ type AssetData = { `AssetUpdater` relies on marker tags emitted by the server components: -| Markers | Owner | -|---------|-------| -| `nextpress-stylesheets-start` / `nextpress-stylesheets-end` | `` | -| `nextpress-head-scripts-start` / `nextpress-head-scripts-end` | `` | -| `nextpress-body-scripts-start` / `nextpress-body-scripts-end` | `` | +| Markers | Owner | +| ------------------------------------------------------------- | --------------- | +| `nextpress-stylesheets-start` / `nextpress-stylesheets-end` | `` | +| `nextpress-head-scripts-start` / `nextpress-head-scripts-end` | `` | +| `nextpress-body-scripts-start` / `nextpress-body-scripts-end` | `` | On initial mount the effect is skipped (the server already rendered those assets). On every subsequent navigation the effect: @@ -143,13 +146,13 @@ Pass `bypassDomains` to keep those assets on their original URL: ```tsx ``` Matching is by **hostname** (scheme- and port-agnostic), and a root domain covers its subdomains — `stripe.com` matches both `stripe.com` and `js.stripe.com`. -Keep this list aligned with the external origins your server-side asset rendering already emits directly: the server marks non-WordPress origins as external and renders their raw URLs, and `bypassDomains` is how the client reproduces that decision on navigation. When they agree, the initial SSR markup and the navigation-time markup match — including the external-script dedupe key — so third-party scripts aren't re-run. +Keep this list aligned with the external origins your server-side asset rendering already emits directly: the server marks non-WordPress origins as external and renders their raw URLs, and `bypassDomains` is how the client reproduces that decision on navigation. When they agree, the initial SSR markup and the navigation-time markup match. `bypassDomains` only controls the asset _URL_; whether a script re-runs on navigation is governed separately by [`reinitBypassHandles`](#script-re-execution-on-navigation) — add a third-party handle there too if its IIFE is non-idempotent. ## Inline Script Synchronization @@ -159,21 +162,50 @@ The built-in follow-up is for WooCommerce: - After the `wc-settings-js-before` inline script runs (which defines `window.wcSettings` for the new page), `processWcSettings(instance)` is invoked to resolve the `__NEXTPRESS_PROXY__` / `__NEXTPRESS_ASSETS__` placeholders inside the freshly-loaded settings object. Without this, cart and checkout blocks can get stuck in their skeleton state on client-side navigation because their Store API URLs still point at raw `__NEXTPRESS_*__` placeholders. +## Script re-execution on navigation + +On each navigation `AssetUpdater` re-inserts the page's `