@brandlab-digital/site-next 3.2.0 → 3.3.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/dist/analytics.d.ts +2 -0
- package/dist/analytics.d.ts.map +1 -1
- package/dist/analytics.js +5 -1
- package/dist/analytics.js.map +1 -1
- package/dist/contact/form-client.d.ts +1 -1
- package/dist/contact/form-client.d.ts.map +1 -1
- package/dist/contact/form-client.js +1 -1
- package/dist/contact/form-client.js.map +1 -1
- package/dist/contact/resend.d.ts +25 -3
- package/dist/contact/resend.d.ts.map +1 -1
- package/dist/contact/resend.js +54 -8
- package/dist/contact/resend.js.map +1 -1
- package/dist/contact/server.d.ts +11 -1
- package/dist/contact/server.d.ts.map +1 -1
- package/dist/contact/server.js +50 -2
- package/dist/contact/server.js.map +1 -1
- package/dist/contact/testing.js +1 -1
- package/dist/contact/testing.js.map +1 -1
- package/dist/google-reviews-client.d.ts +9 -4
- package/dist/google-reviews-client.d.ts.map +1 -1
- package/dist/google-reviews-client.js +60 -13
- package/dist/google-reviews-client.js.map +1 -1
- package/dist/google-reviews-server.d.ts +11 -2
- package/dist/google-reviews-server.d.ts.map +1 -1
- package/dist/google-reviews-server.js +52 -18
- package/dist/google-reviews-server.js.map +1 -1
- package/dist/google-tag-plan.d.ts +5 -0
- package/dist/google-tag-plan.d.ts.map +1 -1
- package/dist/google-tag-plan.js +16 -1
- package/dist/google-tag-plan.js.map +1 -1
- package/dist/headers.d.ts +22 -1
- package/dist/headers.d.ts.map +1 -1
- package/dist/headers.js +34 -1
- package/dist/headers.js.map +1 -1
- package/dist/lead-registry.d.ts +2 -0
- package/dist/lead-registry.d.ts.map +1 -1
- package/dist/lead-registry.js.map +1 -1
- package/dist/lead-reporting.d.ts +7 -3
- package/dist/lead-reporting.d.ts.map +1 -1
- package/dist/lead-reporting.js +5 -2
- package/dist/lead-reporting.js.map +1 -1
- package/dist/next-config.d.ts +15 -0
- package/dist/next-config.d.ts.map +1 -1
- package/dist/next-config.js +29 -4
- package/dist/next-config.js.map +1 -1
- package/dist/robots.d.ts +4 -2
- package/dist/robots.d.ts.map +1 -1
- package/dist/robots.js +12 -2
- package/dist/robots.js.map +1 -1
- package/dist/user-interaction.d.ts +14 -0
- package/dist/user-interaction.d.ts.map +1 -0
- package/dist/user-interaction.js +53 -0
- package/dist/user-interaction.js.map +1 -0
- package/package.json +2 -2
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
import { createContext, useContext, useEffect, useMemo, useState, useSyncExternalStore } from 'react';
|
|
4
4
|
import { normalizeGooglePlaceReviewsSelection, parseGooglePlaceReviewsSnapshot, selectGooglePlaceReviewsSnapshot, } from '@brandlab-digital/site-core/google-reviews';
|
|
5
5
|
import { GooglePlaceReviews, GooglePlaceReviewsPolicyAttribution, GoogleReviewsLink, } from './google-reviews-ui.js';
|
|
6
|
+
import { afterUserInteraction } from './user-interaction.js';
|
|
6
7
|
const MAXIMUM_ROUTE_RESPONSE_BYTES = 64 * 1_024;
|
|
7
8
|
function normalizeSameOriginEndpoint(value) {
|
|
8
9
|
if (!value || !value.startsWith('/') || value.startsWith('//'))
|
|
@@ -82,31 +83,74 @@ function createPageReviewStore(endpoint, mode) {
|
|
|
82
83
|
const PageReviewsContext = createContext(null);
|
|
83
84
|
const emptySnapshot = () => null;
|
|
84
85
|
const emptySubscribe = () => () => { };
|
|
86
|
+
/** A browser under automation (`navigator.webdriver`): it keeps the fallback and never requests live reviews. */
|
|
87
|
+
function isAutomatedBrowser() {
|
|
88
|
+
return typeof navigator !== 'undefined' && navigator.webdriver === true;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Unit tests (NODE_ENV=test, as Vitest sets it) see a placement activate as before,
|
|
92
|
+
* when it nears the viewport: jsdom sends no trusted events, and a project's
|
|
93
|
+
* component tests stub fetch. Every `next build` (Production and Preview) inlines
|
|
94
|
+
* NODE_ENV=production and `next dev` development, so no build is affected.
|
|
95
|
+
*/
|
|
96
|
+
function unitTestEnvironment() {
|
|
97
|
+
return process.env.NODE_ENV === 'test';
|
|
98
|
+
}
|
|
85
99
|
function useReviewSnapshot(store) {
|
|
86
100
|
const [element, ref] = useState(null);
|
|
87
101
|
const snapshot = useSyncExternalStore(store?.subscribe ?? emptySubscribe, store?.getSnapshot ?? emptySnapshot, emptySnapshot);
|
|
88
102
|
useEffect(() => {
|
|
89
103
|
if (!element || !store?.endpoint)
|
|
90
104
|
return;
|
|
91
|
-
|
|
92
|
-
|
|
105
|
+
const unitTest = unitTestEnvironment();
|
|
106
|
+
if (!unitTest && isAutomatedBrowser())
|
|
93
107
|
return;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
108
|
+
// The paid lookup starts only once a person has interacted with the page and the
|
|
109
|
+
// placement is near the viewport (as before); until then the fallback shows.
|
|
110
|
+
let inView = typeof IntersectionObserver !== 'function';
|
|
111
|
+
let interacted = unitTest;
|
|
112
|
+
let done = false;
|
|
113
|
+
let observer;
|
|
114
|
+
let cancelInteraction = () => { };
|
|
115
|
+
const stop = () => {
|
|
116
|
+
done = true;
|
|
117
|
+
observer?.disconnect();
|
|
118
|
+
cancelInteraction();
|
|
119
|
+
};
|
|
120
|
+
const activateWhenReady = () => {
|
|
121
|
+
if (done || !inView || !interacted)
|
|
97
122
|
return;
|
|
123
|
+
stop();
|
|
98
124
|
store.activate();
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
125
|
+
};
|
|
126
|
+
if (!inView) {
|
|
127
|
+
observer = new IntersectionObserver((entries) => {
|
|
128
|
+
if (!entries.some((entry) => entry.isIntersecting))
|
|
129
|
+
return;
|
|
130
|
+
inView = true;
|
|
131
|
+
observer?.disconnect();
|
|
132
|
+
activateWhenReady();
|
|
133
|
+
}, { rootMargin: '320px 0px' });
|
|
134
|
+
observer.observe(element);
|
|
135
|
+
}
|
|
136
|
+
if (interacted)
|
|
137
|
+
activateWhenReady();
|
|
138
|
+
else {
|
|
139
|
+
cancelInteraction = afterUserInteraction(() => {
|
|
140
|
+
interacted = true;
|
|
141
|
+
activateWhenReady();
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return stop;
|
|
103
145
|
}, [element, store]);
|
|
104
146
|
return { ref, snapshot };
|
|
105
147
|
}
|
|
106
148
|
/**
|
|
107
149
|
* Inline project-owned presentation sharing its enclosing page provider.
|
|
108
|
-
* Attach ref to the real placement for lazy activation
|
|
109
|
-
*
|
|
150
|
+
* Attach ref to the real placement for lazy activation: the live lookup starts once
|
|
151
|
+
* a person has interacted with the page and the placement nears the viewport, never
|
|
152
|
+
* in an automated browser. The caller owns fallback content and must display
|
|
153
|
+
* GooglePlaceReviewsAttribution beside live data.
|
|
110
154
|
*/
|
|
111
155
|
export function useGooglePlaceReviewsSnapshot() {
|
|
112
156
|
const store = useContext(PageReviewsContext);
|
|
@@ -125,8 +169,11 @@ function ReviewFallback(props) {
|
|
|
125
169
|
return (_jsxs(_Fragment, { children: [props.fallbackContent, _jsx(GoogleReviewsLink, { className: props.fallbackClassName, googleMapsUri: props.fallbackGoogleMapsUri, label: props.fallbackLabel })] }));
|
|
126
170
|
}
|
|
127
171
|
/**
|
|
128
|
-
* Cost-aware display controller. It requests live Places data only
|
|
129
|
-
*
|
|
172
|
+
* Cost-aware display controller. It requests live Places data only after a
|
|
173
|
+
* person's first interaction with the page (a trusted pointer, touch, key or wheel
|
|
174
|
+
* event) and once the approved placement approaches the viewport, never in an
|
|
175
|
+
* automated browser (`navigator.webdriver`), and always retains a stable link. The
|
|
176
|
+
* fallback shows until then.
|
|
130
177
|
*/
|
|
131
178
|
export function GooglePlaceReviewsClient({ mode, endpoint, fallbackGoogleMapsUri, fallbackLabel, fallbackClassName, fallbackContent, renderSnapshot, wrapperProps, selection = 'relevance', ...reviewProps }) {
|
|
132
179
|
const safeEndpoint = normalizeSameOriginEndpoint(endpoint);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-reviews-client.js","sourceRoot":"","sources":["../src/google-reviews-client.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport { createContext, useContext, useEffect, useMemo, useState, useSyncExternalStore, type HTMLAttributes, type ReactNode } from 'react'\nimport {\n normalizeGooglePlaceReviewsSelection,\n parseGooglePlaceReviewsSnapshot,\n selectGooglePlaceReviewsSnapshot,\n type GooglePlaceReviewsAvailableSnapshot,\n type GooglePlaceReviewsMode,\n} from '@brandlab-digital/site-core/google-reviews'\nimport {\n GooglePlaceReviews,\n GooglePlaceReviewsPolicyAttribution,\n GoogleReviewsLink,\n type GooglePlaceReviewsProps,\n} from './google-reviews-ui.js'\n\nconst MAXIMUM_ROUTE_RESPONSE_BYTES = 64 * 1_024\n\nexport type BrandlabGoogleReviewsDisplayMode = 'link-only' | GooglePlaceReviewsMode\n\nexport interface GooglePlaceReviewsClientProps\n extends Omit<GooglePlaceReviewsProps, 'snapshot'> {\n mode: BrandlabGoogleReviewsDisplayMode\n /** Same-origin Brandlab route. Ignored in link-only mode. */\n endpoint?: string\n fallbackGoogleMapsUri: string\n fallbackLabel?: string\n fallbackClassName?: string\n /** Existing project-owned proof shown while live data is unavailable. The Google link is retained separately. */\n fallbackContent?: ReactNode\n /** Lets the project own all visual presentation while retaining shared data safety. */\n renderSnapshot?: (snapshot: GooglePlaceReviewsAvailableSnapshot) => ReactNode\n /** Wrapper used only for lazy activation; projects own its visual treatment. */\n wrapperProps?: Omit<HTMLAttributes<HTMLDivElement>, 'children'>\n}\n\nfunction normalizeSameOriginEndpoint(value: string | undefined) {\n if (!value || !value.startsWith('/') || value.startsWith('//')) return undefined\n try {\n const parsed = new URL(value, 'https://brandlab.invalid')\n if (parsed.origin !== 'https://brandlab.invalid' || parsed.hash) return undefined\n return `${parsed.pathname}${parsed.search}`\n } catch {\n return undefined\n }\n}\n\nasync function requestSnapshot(endpoint: string, mode: GooglePlaceReviewsMode, signal: AbortSignal) {\n try {\n const response = await fetch(endpoint, {\n cache: 'no-store',\n credentials: 'same-origin',\n headers: { accept: 'application/json' },\n signal,\n })\n if (!response.ok) return null\n const raw = await response.text()\n if (new TextEncoder().encode(raw).byteLength > MAXIMUM_ROUTE_RESPONSE_BYTES) return null\n const parsed = parseGooglePlaceReviewsSnapshot(JSON.parse(raw), mode)\n return parsed?.status === 'available' ? parsed : null\n } catch {\n return null\n }\n}\n\nfunction createPageReviewStore(endpoint: string | undefined, mode: GooglePlaceReviewsMode) {\n let snapshot: GooglePlaceReviewsAvailableSnapshot | null = null\n let attempted = false\n let disposed = false\n let lifecycle = 0\n let controller: AbortController | undefined\n const listeners = new Set<() => void>()\n return {\n endpoint,\n mode,\n getSnapshot: () => snapshot,\n subscribe(listener: () => void) {\n listeners.add(listener)\n return () => { listeners.delete(listener) }\n },\n activate() {\n if (attempted || disposed || !endpoint) return\n attempted = true\n controller = new AbortController()\n void requestSnapshot(endpoint, mode, controller.signal).then((result) => {\n if (disposed || !result) return\n snapshot = result\n listeners.forEach((listener) => listener())\n })\n },\n mount() {\n const current = ++lifecycle\n return () => {\n // React's development effect replay must not cancel the page's only request.\n queueMicrotask(() => {\n if (lifecycle !== current) return\n disposed = true\n controller?.abort()\n listeners.clear()\n })\n }\n },\n }\n}\n\nconst PageReviewsContext = createContext<ReturnType<typeof createPageReviewStore> | null>(null)\nconst emptySnapshot = () => null\nconst emptySubscribe = () => () => {}\n\nfunction useReviewSnapshot<Element extends HTMLElement>(store: ReturnType<typeof createPageReviewStore> | null) {\n const [element, ref] = useState<Element | null>(null)\n const snapshot = useSyncExternalStore(store?.subscribe ?? emptySubscribe, store?.getSnapshot ?? emptySnapshot, emptySnapshot)\n\n useEffect(() => {\n if (!element || !store?.endpoint) return\n if (typeof IntersectionObserver !== 'function') {\n store.activate()\n return\n }\n const observer = new IntersectionObserver(\n (entries) => {\n if (!entries.some((entry) => entry.isIntersecting)) return\n store.activate()\n observer.disconnect()\n },\n { rootMargin: '320px 0px' },\n )\n observer.observe(element)\n return () => observer.disconnect()\n }, [element, store])\n\n return { ref, snapshot }\n}\n\n/**\n * Inline project-owned presentation sharing its enclosing page provider.\n * Attach ref to the real placement for lazy activation. The caller owns fallback\n * content and must display GooglePlaceReviewsAttribution beside live data.\n */\nexport function useGooglePlaceReviewsSnapshot<Element extends HTMLElement = HTMLSpanElement>() {\n const store = useContext(PageReviewsContext)\n if (!store) throw new Error('useGooglePlaceReviewsSnapshot requires GooglePlaceReviewsProvider.')\n return useReviewSnapshot<Element>(store)\n}\n\nexport interface GooglePlaceReviewsProviderProps {\n endpoint: string\n mode: GooglePlaceReviewsMode\n /** Refreshes only review data on navigation; preserves state in child components. */\n pageKey?: string\n children: ReactNode\n}\n\n/** Shares one in-memory request among matching placements for this page only. */\nexport function GooglePlaceReviewsProvider({ endpoint, mode, pageKey, children }: GooglePlaceReviewsProviderProps) {\n const safeEndpoint = normalizeSameOriginEndpoint(endpoint)\n const store = useMemo(() => createPageReviewStore(safeEndpoint, mode), [safeEndpoint, mode, pageKey])\n useEffect(() => store.mount(), [store])\n return <PageReviewsContext.Provider value={store}>{children}</PageReviewsContext.Provider>\n}\n\nfunction ReviewFallback(props: Pick<\n GooglePlaceReviewsClientProps,\n 'fallbackGoogleMapsUri' | 'fallbackLabel' | 'fallbackClassName' | 'fallbackContent'\n>) {\n return (\n <>\n {props.fallbackContent}\n <GoogleReviewsLink\n className={props.fallbackClassName}\n googleMapsUri={props.fallbackGoogleMapsUri}\n label={props.fallbackLabel}\n />\n </>\n )\n}\n\n/**\n * Cost-aware display controller. It requests live Places data only when the\n * approved placement approaches the viewport and always retains a stable link.\n */\nexport function GooglePlaceReviewsClient({\n mode,\n endpoint,\n fallbackGoogleMapsUri,\n fallbackLabel,\n fallbackClassName,\n fallbackContent,\n renderSnapshot,\n wrapperProps,\n selection = 'relevance',\n ...reviewProps\n}: GooglePlaceReviewsClientProps) {\n const safeEndpoint = normalizeSameOriginEndpoint(endpoint)\n const provider = useContext(PageReviewsContext)\n const shared = provider?.endpoint === safeEndpoint && provider?.mode === mode ? provider : null\n const localStore = useMemo(\n () => !shared && mode !== 'link-only' ? createPageReviewStore(safeEndpoint, mode) : null,\n [shared, safeEndpoint, mode],\n )\n useEffect(() => localStore?.mount(), [localStore])\n const { ref: wrapperRef, snapshot } = useReviewSnapshot<HTMLDivElement>(shared ?? localStore)\n const normalizedSelection = normalizeGooglePlaceReviewsSelection(selection)\n if (normalizedSelection === 'five-star-featured' && mode !== 'full') {\n throw new Error('Five-star featured reviews require full mode.')\n }\n\n if (mode === 'link-only') {\n return (\n <ReviewFallback\n fallbackClassName={fallbackClassName}\n fallbackContent={fallbackContent}\n fallbackGoogleMapsUri={fallbackGoogleMapsUri}\n fallbackLabel={fallbackLabel}\n />\n )\n }\n\n return (\n <div {...wrapperProps} ref={wrapperRef}>\n {snapshot?.status === 'available'\n ? renderSnapshot\n ? (\n <>\n {renderSnapshot(selectGooglePlaceReviewsSnapshot(snapshot, normalizedSelection))}\n <GooglePlaceReviewsPolicyAttribution\n attributionMode={reviewProps.attributionMode}\n attributionTone={reviewProps.attributionTone}\n className={reviewProps.attributionClassName}\n policyAttributionClassName={reviewProps.policyAttributionClassName}\n policyClassName={reviewProps.policyClassName}\n selection={normalizedSelection}\n snapshot={selectGooglePlaceReviewsSnapshot(snapshot, normalizedSelection)}\n />\n </>\n )\n : (\n <GooglePlaceReviews\n {...reviewProps}\n selection={normalizedSelection}\n snapshot={snapshot}\n />\n )\n : (\n <ReviewFallback\n fallbackClassName={fallbackClassName}\n fallbackContent={fallbackContent}\n fallbackGoogleMapsUri={fallbackGoogleMapsUri}\n fallbackLabel={fallbackLabel}\n />\n )}\n </div>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"google-reviews-client.js","sourceRoot":"","sources":["../src/google-reviews-client.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport { createContext, useContext, useEffect, useMemo, useState, useSyncExternalStore, type HTMLAttributes, type ReactNode } from 'react'\nimport {\n normalizeGooglePlaceReviewsSelection,\n parseGooglePlaceReviewsSnapshot,\n selectGooglePlaceReviewsSnapshot,\n type GooglePlaceReviewsAvailableSnapshot,\n type GooglePlaceReviewsMode,\n} from '@brandlab-digital/site-core/google-reviews'\nimport {\n GooglePlaceReviews,\n GooglePlaceReviewsPolicyAttribution,\n GoogleReviewsLink,\n type GooglePlaceReviewsProps,\n} from './google-reviews-ui.js'\nimport { afterUserInteraction } from './user-interaction.js'\n\nconst MAXIMUM_ROUTE_RESPONSE_BYTES = 64 * 1_024\n\nexport type BrandlabGoogleReviewsDisplayMode = 'link-only' | GooglePlaceReviewsMode\n\nexport interface GooglePlaceReviewsClientProps\n extends Omit<GooglePlaceReviewsProps, 'snapshot'> {\n mode: BrandlabGoogleReviewsDisplayMode\n /** Same-origin Brandlab route. Ignored in link-only mode. */\n endpoint?: string\n fallbackGoogleMapsUri: string\n fallbackLabel?: string\n fallbackClassName?: string\n /** Existing project-owned proof shown while live data is unavailable. The Google link is retained separately. */\n fallbackContent?: ReactNode\n /** Lets the project own all visual presentation while retaining shared data safety. */\n renderSnapshot?: (snapshot: GooglePlaceReviewsAvailableSnapshot) => ReactNode\n /** Wrapper used only for lazy activation; projects own its visual treatment. */\n wrapperProps?: Omit<HTMLAttributes<HTMLDivElement>, 'children'>\n}\n\nfunction normalizeSameOriginEndpoint(value: string | undefined) {\n if (!value || !value.startsWith('/') || value.startsWith('//')) return undefined\n try {\n const parsed = new URL(value, 'https://brandlab.invalid')\n if (parsed.origin !== 'https://brandlab.invalid' || parsed.hash) return undefined\n return `${parsed.pathname}${parsed.search}`\n } catch {\n return undefined\n }\n}\n\nasync function requestSnapshot(endpoint: string, mode: GooglePlaceReviewsMode, signal: AbortSignal) {\n try {\n const response = await fetch(endpoint, {\n cache: 'no-store',\n credentials: 'same-origin',\n headers: { accept: 'application/json' },\n signal,\n })\n if (!response.ok) return null\n const raw = await response.text()\n if (new TextEncoder().encode(raw).byteLength > MAXIMUM_ROUTE_RESPONSE_BYTES) return null\n const parsed = parseGooglePlaceReviewsSnapshot(JSON.parse(raw), mode)\n return parsed?.status === 'available' ? parsed : null\n } catch {\n return null\n }\n}\n\nfunction createPageReviewStore(endpoint: string | undefined, mode: GooglePlaceReviewsMode) {\n let snapshot: GooglePlaceReviewsAvailableSnapshot | null = null\n let attempted = false\n let disposed = false\n let lifecycle = 0\n let controller: AbortController | undefined\n const listeners = new Set<() => void>()\n return {\n endpoint,\n mode,\n getSnapshot: () => snapshot,\n subscribe(listener: () => void) {\n listeners.add(listener)\n return () => { listeners.delete(listener) }\n },\n activate() {\n if (attempted || disposed || !endpoint) return\n attempted = true\n controller = new AbortController()\n void requestSnapshot(endpoint, mode, controller.signal).then((result) => {\n if (disposed || !result) return\n snapshot = result\n listeners.forEach((listener) => listener())\n })\n },\n mount() {\n const current = ++lifecycle\n return () => {\n // React's development effect replay must not cancel the page's only request.\n queueMicrotask(() => {\n if (lifecycle !== current) return\n disposed = true\n controller?.abort()\n listeners.clear()\n })\n }\n },\n }\n}\n\nconst PageReviewsContext = createContext<ReturnType<typeof createPageReviewStore> | null>(null)\nconst emptySnapshot = () => null\nconst emptySubscribe = () => () => {}\n\n/** A browser under automation (`navigator.webdriver`): it keeps the fallback and never requests live reviews. */\nfunction isAutomatedBrowser() {\n return typeof navigator !== 'undefined' && navigator.webdriver === true\n}\n\n/**\n * Unit tests (NODE_ENV=test, as Vitest sets it) see a placement activate as before,\n * when it nears the viewport: jsdom sends no trusted events, and a project's\n * component tests stub fetch. Every `next build` (Production and Preview) inlines\n * NODE_ENV=production and `next dev` development, so no build is affected.\n */\nfunction unitTestEnvironment() {\n return process.env.NODE_ENV === 'test'\n}\n\nfunction useReviewSnapshot<Element extends HTMLElement>(store: ReturnType<typeof createPageReviewStore> | null) {\n const [element, ref] = useState<Element | null>(null)\n const snapshot = useSyncExternalStore(store?.subscribe ?? emptySubscribe, store?.getSnapshot ?? emptySnapshot, emptySnapshot)\n\n useEffect(() => {\n if (!element || !store?.endpoint) return\n const unitTest = unitTestEnvironment()\n if (!unitTest && isAutomatedBrowser()) return\n // The paid lookup starts only once a person has interacted with the page and the\n // placement is near the viewport (as before); until then the fallback shows.\n let inView = typeof IntersectionObserver !== 'function'\n let interacted = unitTest\n let done = false\n let observer: IntersectionObserver | undefined\n let cancelInteraction = () => {}\n const stop = () => {\n done = true\n observer?.disconnect()\n cancelInteraction()\n }\n const activateWhenReady = () => {\n if (done || !inView || !interacted) return\n stop()\n store.activate()\n }\n if (!inView) {\n observer = new IntersectionObserver(\n (entries) => {\n if (!entries.some((entry) => entry.isIntersecting)) return\n inView = true\n observer?.disconnect()\n activateWhenReady()\n },\n { rootMargin: '320px 0px' },\n )\n observer.observe(element)\n }\n if (interacted) activateWhenReady()\n else {\n cancelInteraction = afterUserInteraction(() => {\n interacted = true\n activateWhenReady()\n })\n }\n return stop\n }, [element, store])\n\n return { ref, snapshot }\n}\n\n/**\n * Inline project-owned presentation sharing its enclosing page provider.\n * Attach ref to the real placement for lazy activation: the live lookup starts once\n * a person has interacted with the page and the placement nears the viewport, never\n * in an automated browser. The caller owns fallback content and must display\n * GooglePlaceReviewsAttribution beside live data.\n */\nexport function useGooglePlaceReviewsSnapshot<Element extends HTMLElement = HTMLSpanElement>() {\n const store = useContext(PageReviewsContext)\n if (!store) throw new Error('useGooglePlaceReviewsSnapshot requires GooglePlaceReviewsProvider.')\n return useReviewSnapshot<Element>(store)\n}\n\nexport interface GooglePlaceReviewsProviderProps {\n endpoint: string\n mode: GooglePlaceReviewsMode\n /** Refreshes only review data on navigation; preserves state in child components. */\n pageKey?: string\n children: ReactNode\n}\n\n/** Shares one in-memory request among matching placements for this page only. */\nexport function GooglePlaceReviewsProvider({ endpoint, mode, pageKey, children }: GooglePlaceReviewsProviderProps) {\n const safeEndpoint = normalizeSameOriginEndpoint(endpoint)\n const store = useMemo(() => createPageReviewStore(safeEndpoint, mode), [safeEndpoint, mode, pageKey])\n useEffect(() => store.mount(), [store])\n return <PageReviewsContext.Provider value={store}>{children}</PageReviewsContext.Provider>\n}\n\nfunction ReviewFallback(props: Pick<\n GooglePlaceReviewsClientProps,\n 'fallbackGoogleMapsUri' | 'fallbackLabel' | 'fallbackClassName' | 'fallbackContent'\n>) {\n return (\n <>\n {props.fallbackContent}\n <GoogleReviewsLink\n className={props.fallbackClassName}\n googleMapsUri={props.fallbackGoogleMapsUri}\n label={props.fallbackLabel}\n />\n </>\n )\n}\n\n/**\n * Cost-aware display controller. It requests live Places data only after a\n * person's first interaction with the page (a trusted pointer, touch, key or wheel\n * event) and once the approved placement approaches the viewport, never in an\n * automated browser (`navigator.webdriver`), and always retains a stable link. The\n * fallback shows until then.\n */\nexport function GooglePlaceReviewsClient({\n mode,\n endpoint,\n fallbackGoogleMapsUri,\n fallbackLabel,\n fallbackClassName,\n fallbackContent,\n renderSnapshot,\n wrapperProps,\n selection = 'relevance',\n ...reviewProps\n}: GooglePlaceReviewsClientProps) {\n const safeEndpoint = normalizeSameOriginEndpoint(endpoint)\n const provider = useContext(PageReviewsContext)\n const shared = provider?.endpoint === safeEndpoint && provider?.mode === mode ? provider : null\n const localStore = useMemo(\n () => !shared && mode !== 'link-only' ? createPageReviewStore(safeEndpoint, mode) : null,\n [shared, safeEndpoint, mode],\n )\n useEffect(() => localStore?.mount(), [localStore])\n const { ref: wrapperRef, snapshot } = useReviewSnapshot<HTMLDivElement>(shared ?? localStore)\n const normalizedSelection = normalizeGooglePlaceReviewsSelection(selection)\n if (normalizedSelection === 'five-star-featured' && mode !== 'full') {\n throw new Error('Five-star featured reviews require full mode.')\n }\n\n if (mode === 'link-only') {\n return (\n <ReviewFallback\n fallbackClassName={fallbackClassName}\n fallbackContent={fallbackContent}\n fallbackGoogleMapsUri={fallbackGoogleMapsUri}\n fallbackLabel={fallbackLabel}\n />\n )\n }\n\n return (\n <div {...wrapperProps} ref={wrapperRef}>\n {snapshot?.status === 'available'\n ? renderSnapshot\n ? (\n <>\n {renderSnapshot(selectGooglePlaceReviewsSnapshot(snapshot, normalizedSelection))}\n <GooglePlaceReviewsPolicyAttribution\n attributionMode={reviewProps.attributionMode}\n attributionTone={reviewProps.attributionTone}\n className={reviewProps.attributionClassName}\n policyAttributionClassName={reviewProps.policyAttributionClassName}\n policyClassName={reviewProps.policyClassName}\n selection={normalizedSelection}\n snapshot={selectGooglePlaceReviewsSnapshot(snapshot, normalizedSelection)}\n />\n </>\n )\n : (\n <GooglePlaceReviews\n {...reviewProps}\n selection={normalizedSelection}\n snapshot={snapshot}\n />\n )\n : (\n <ReviewFallback\n fallbackClassName={fallbackClassName}\n fallbackContent={fallbackContent}\n fallbackGoogleMapsUri={fallbackGoogleMapsUri}\n fallbackLabel={fallbackLabel}\n />\n )}\n </div>\n )\n}\n"]}
|
|
@@ -34,12 +34,21 @@ export interface BrandlabGooglePlaceReviewsProductionRuntimeOptions {
|
|
|
34
34
|
export interface BrandlabGooglePlaceReviewsProductionRuntime {
|
|
35
35
|
runtimeKind: typeof BRANDLAB_GOOGLE_REVIEWS_PRODUCTION_RUNTIME;
|
|
36
36
|
mode: GooglePlaceReviewsMode;
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* The route's GET. A paid Places lookup runs only for a request from the site's
|
|
39
|
+
* own page (`Sec-Fetch-Site: same-origin`); anything else gets the unavailable
|
|
40
|
+
* snapshot. It reads the request it is given (`export const GET =
|
|
41
|
+
* runtime.routeHandler`, which Next calls with the request), and otherwise Next's
|
|
42
|
+
* request scope (`export function GET() { return runtime.routeHandler() }`).
|
|
43
|
+
*/
|
|
44
|
+
routeHandler: (request?: Request) => Promise<Response>;
|
|
38
45
|
}
|
|
39
46
|
/**
|
|
40
47
|
* Creates the approved production route. Neither the public response nor the
|
|
41
48
|
* server-only Places response is stored. Concurrent identical requests are
|
|
42
|
-
* coalesced only until the active request completes.
|
|
49
|
+
* coalesced only until the active request completes. With a Places key set, a
|
|
50
|
+
* request that is not from the site's own page answers the unavailable snapshot
|
|
51
|
+
* (`not-same-origin`) without calling Places.
|
|
43
52
|
*/
|
|
44
53
|
export declare function createBrandlabGooglePlaceReviewsProductionRuntime(options: BrandlabGooglePlaceReviewsProductionRuntimeOptions): BrandlabGooglePlaceReviewsProductionRuntime;
|
|
45
54
|
//# sourceMappingURL=google-reviews-server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-reviews-server.d.ts","sourceRoot":"","sources":["../src/google-reviews-server.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAEpB,OAAO,
|
|
1
|
+
{"version":3,"file":"google-reviews-server.d.ts","sourceRoot":"","sources":["../src/google-reviews-server.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAEpB,OAAO,EAOL,KAAK,8BAA8B,EACnC,KAAK,sBAAsB,EAC3B,KAAK,2BAA2B,EAEjC,MAAM,4CAA4C,CAAA;AAEnD,eAAO,MAAM,0CAA0C,EACrD,uCAAgD,CAAA;AAElD,eAAO,MAAM,yCAAyC;;CAE5C,CAAA;AAEV,eAAO,MAAM,wCAAwC;;;;CAI3C,CAAA;AAEV,eAAO,MAAM,sCAAsC;;;;CACT,CAAA;AAE1C,MAAM,WAAW,8BAA+B,SAAQ,8BAA8B;IACpF,YAAY,CAAC,EAAE,YAAY,CAAA;CAC5B;AA2CD,uEAAuE;AACvE,wBAAsB,qCAAqC,CACzD,OAAO,EAAE,8BAA8B,GACtC,OAAO,CAAC,QAAQ,CAAC,CAYnB;AAED,MAAM,WAAW,kDAAkD;IACjE,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,sBAAsB,CAAA;IAC5B,qGAAqG;IACrG,SAAS,CAAC,EAAE,2BAA2B,CAAA;IACvC,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;IACzD,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAA;CACvB;AAED,MAAM,WAAW,2CAA2C;IAC1D,WAAW,EAAE,OAAO,0CAA0C,CAAA;IAC9D,IAAI,EAAE,sBAAsB,CAAA;IAC5B;;;;;;OAMG;IACH,YAAY,EAAE,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CACvD;AAoCD;;;;;;GAMG;AACH,wBAAgB,iDAAiD,CAC/D,OAAO,EAAE,kDAAkD,GAC1D,2CAA2C,CA4C7C"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import 'server-only';
|
|
2
|
-
import { GOOGLE_PLACE_REVIEWS_MAX_REVIEWS, fetchGooglePlaceReviewsSnapshot, normalizeGooglePlaceId, normalizeGooglePlaceReviewsMode, normalizeGooglePlaceReviewsSelection, } from '@brandlab-digital/site-core/google-reviews';
|
|
2
|
+
import { GOOGLE_PLACE_REVIEWS_MAX_REVIEWS, createGooglePlaceReviewsUnavailableSnapshot, fetchGooglePlaceReviewsSnapshot, normalizeGooglePlaceId, normalizeGooglePlaceReviewsMode, normalizeGooglePlaceReviewsSelection, } from '@brandlab-digital/site-core/google-reviews';
|
|
3
3
|
export const BRANDLAB_GOOGLE_REVIEWS_PRODUCTION_RUNTIME = 'brandlab-google-reviews-production-v2';
|
|
4
4
|
export const BRANDLAB_GOOGLE_REVIEWS_ENVIRONMENT_NAMES = {
|
|
5
5
|
apiKey: 'GOOGLE_MAPS_PLATFORM_API_KEY',
|
|
@@ -51,6 +51,31 @@ export async function createGooglePlaceReviewsRouteResponse(options) {
|
|
|
51
51
|
headers,
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
|
+
/** The errors Next throws to control rendering (dynamic usage, redirects, not-found, postpone) carry a string digest. */
|
|
55
|
+
function isNextControlError(error) {
|
|
56
|
+
return typeof error === 'object' && error !== null && typeof error.digest === 'string';
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Whether the request comes from one of the site's own pages: every current
|
|
60
|
+
* browser sends `Sec-Fetch-Site: same-origin` on the page's own fetch, while
|
|
61
|
+
* crawlers, curl and scripted direct hits normally send nothing. Anything that
|
|
62
|
+
* cannot be read counts as no.
|
|
63
|
+
*/
|
|
64
|
+
async function requestedByOwnPage(request) {
|
|
65
|
+
const sameOrigin = (value) => value?.trim().toLowerCase() === 'same-origin';
|
|
66
|
+
if (request instanceof Request)
|
|
67
|
+
return sameOrigin(request.headers.get('sec-fetch-site'));
|
|
68
|
+
try {
|
|
69
|
+
const { headers } = await import('next/headers');
|
|
70
|
+
return sameOrigin((await headers()).get('sec-fetch-site'));
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
// Next's own control flow (for example a dynamic-usage signal) must reach Next.
|
|
74
|
+
if (isNextControlError(error))
|
|
75
|
+
throw error;
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
54
79
|
function optionalEnvironment(environment, name) {
|
|
55
80
|
const value = environment[name];
|
|
56
81
|
if (typeof value !== 'string' || !value.trim())
|
|
@@ -62,7 +87,9 @@ function optionalEnvironment(environment, name) {
|
|
|
62
87
|
/**
|
|
63
88
|
* Creates the approved production route. Neither the public response nor the
|
|
64
89
|
* server-only Places response is stored. Concurrent identical requests are
|
|
65
|
-
* coalesced only until the active request completes.
|
|
90
|
+
* coalesced only until the active request completes. With a Places key set, a
|
|
91
|
+
* request that is not from the site's own page answers the unavailable snapshot
|
|
92
|
+
* (`not-same-origin`) without calling Places.
|
|
66
93
|
*/
|
|
67
94
|
export function createBrandlabGooglePlaceReviewsProductionRuntime(options) {
|
|
68
95
|
if (options.fetcher && options.environment.NODE_ENV !== 'test') {
|
|
@@ -77,22 +104,29 @@ export function createBrandlabGooglePlaceReviewsProductionRuntime(options) {
|
|
|
77
104
|
return {
|
|
78
105
|
runtimeKind: BRANDLAB_GOOGLE_REVIEWS_PRODUCTION_RUNTIME,
|
|
79
106
|
mode,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
107
|
+
// Next calls GET(request, context); a GET written as GET() passes nothing.
|
|
108
|
+
routeHandler: async (request) => {
|
|
109
|
+
// Without a key no lookup can run, and the route answers as before (missing-api-key).
|
|
110
|
+
if (apiKey && !(await requestedByOwnPage(request))) {
|
|
111
|
+
return Response.json(createGooglePlaceReviewsUnavailableSnapshot('not-same-origin', options.fallbackGoogleMapsUri, mode), { headers: GOOGLE_PLACE_REVIEWS_UNAVAILABLE_HEADERS });
|
|
112
|
+
}
|
|
113
|
+
return createGooglePlaceReviewsRouteResponse({
|
|
114
|
+
apiKey,
|
|
115
|
+
placeId: options.placeId,
|
|
116
|
+
mode,
|
|
117
|
+
...(mode === 'full' && (selection === 'five-star-featured' || options.maxReviews !== undefined)
|
|
118
|
+
? {
|
|
119
|
+
maxReviews: selection === 'five-star-featured'
|
|
120
|
+
? GOOGLE_PLACE_REVIEWS_MAX_REVIEWS
|
|
121
|
+
: options.maxReviews,
|
|
122
|
+
}
|
|
123
|
+
: {}),
|
|
124
|
+
...(options.fallbackGoogleMapsUri
|
|
125
|
+
? { fallbackGoogleMapsUri: options.fallbackGoogleMapsUri }
|
|
126
|
+
: {}),
|
|
127
|
+
fetcher: options.fetcher ?? fetch,
|
|
128
|
+
});
|
|
129
|
+
},
|
|
96
130
|
};
|
|
97
131
|
}
|
|
98
132
|
//# sourceMappingURL=google-reviews-server.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-reviews-server.js","sourceRoot":"","sources":["../src/google-reviews-server.ts"],"names":[],"mappings":"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","sourcesContent":["import 'server-only'\n\nimport {\n GOOGLE_PLACE_REVIEWS_MAX_REVIEWS,\n fetchGooglePlaceReviewsSnapshot,\n normalizeGooglePlaceId,\n normalizeGooglePlaceReviewsMode,\n normalizeGooglePlaceReviewsSelection,\n type FetchGooglePlaceReviewsOptions,\n type GooglePlaceReviewsMode,\n type GooglePlaceReviewsSelection,\n type GooglePlaceReviewsSnapshot,\n} from '@brandlab-digital/site-core/google-reviews'\n\nexport const BRANDLAB_GOOGLE_REVIEWS_PRODUCTION_RUNTIME =\n 'brandlab-google-reviews-production-v2' as const\n\nexport const BRANDLAB_GOOGLE_REVIEWS_ENVIRONMENT_NAMES = {\n apiKey: 'GOOGLE_MAPS_PLATFORM_API_KEY',\n} as const\n\nexport const GOOGLE_PLACE_REVIEWS_UNAVAILABLE_HEADERS = {\n 'Cache-Control': 'private, no-store, max-age=0',\n Pragma: 'no-cache',\n 'X-Content-Type-Options': 'nosniff',\n} as const\n\nexport const GOOGLE_PLACE_REVIEWS_AVAILABLE_HEADERS =\n GOOGLE_PLACE_REVIEWS_UNAVAILABLE_HEADERS\n\nexport interface GooglePlaceReviewsRouteOptions extends FetchGooglePlaceReviewsOptions {\n responseInit?: ResponseInit\n}\n\ninterface GooglePlaceReviewsRequestResult {\n snapshot: GooglePlaceReviewsSnapshot\n}\n\nconst inFlightByFetcher = new WeakMap<\n typeof fetch,\n Map<string, Promise<GooglePlaceReviewsRequestResult>>\n>()\n\nfunction coalescingKey(options: GooglePlaceReviewsRouteOptions) {\n return JSON.stringify([\n normalizeGooglePlaceId(options.placeId) ?? '',\n normalizeGooglePlaceReviewsMode(options.mode),\n options.maxReviews ?? null,\n options.fallbackGoogleMapsUri ?? '',\n ])\n}\n\nasync function loadGooglePlaceReviewsSnapshot(\n options: GooglePlaceReviewsRouteOptions,\n): Promise<GooglePlaceReviewsRequestResult> {\n return { snapshot: await fetchGooglePlaceReviewsSnapshot(options) }\n}\n\nfunction getGooglePlaceReviewsRequest(\n options: GooglePlaceReviewsRouteOptions,\n): Promise<GooglePlaceReviewsRequestResult> {\n const fetcher = options.fetcher ?? fetch\n const requests = inFlightByFetcher.get(fetcher) ?? new Map()\n inFlightByFetcher.set(fetcher, requests)\n const key = coalescingKey(options)\n const existing = requests.get(key)\n if (existing) return existing\n const request = loadGooglePlaceReviewsSnapshot(options).finally(() => {\n requests.delete(key)\n if (requests.size === 0) inFlightByFetcher.delete(fetcher)\n })\n requests.set(key, request)\n return request\n}\n\n/** Low-level route response for tests and approved custom adapters. */\nexport async function createGooglePlaceReviewsRouteResponse(\n options: GooglePlaceReviewsRouteOptions,\n): Promise<Response> {\n const result = await getGooglePlaceReviewsRequest(options)\n const headers = new Headers(options.responseInit?.headers)\n Object.entries(GOOGLE_PLACE_REVIEWS_AVAILABLE_HEADERS).forEach(([name, value]) => {\n headers.set(name, value)\n })\n\n return Response.json(result.snapshot, {\n ...options.responseInit,\n status: options.responseInit?.status,\n headers,\n })\n}\n\nexport interface BrandlabGooglePlaceReviewsProductionRuntimeOptions {\n placeId: string\n mode: GooglePlaceReviewsMode\n /** Full-mode presentation policy. Featured mode always requests Google's full five-review window. */\n selection?: GooglePlaceReviewsSelection\n /** Full mode only. Google currently returns at most five reviews. */\n maxReviews?: number\n fallbackGoogleMapsUri?: string\n environment: Readonly<Record<string, string | undefined>>\n /** Test-only transport override. */\n fetcher?: typeof fetch\n}\n\nexport interface BrandlabGooglePlaceReviewsProductionRuntime {\n runtimeKind: typeof BRANDLAB_GOOGLE_REVIEWS_PRODUCTION_RUNTIME\n mode: GooglePlaceReviewsMode\n routeHandler: () => Promise<Response>\n}\n\nfunction optionalEnvironment(\n environment: Readonly<Record<string, string | undefined>>,\n name: string,\n) {\n const value = environment[name]\n if (typeof value !== 'string' || !value.trim()) return undefined\n if (value.length > 8_192 || /[\\u0000-\\u001f\\u007f]/.test(value)) return undefined\n return value.trim()\n}\n\n/**\n * Creates the approved production route. Neither the public response nor the\n * server-only Places response is stored. Concurrent identical requests are\n * coalesced only until the active request completes.\n */\nexport function createBrandlabGooglePlaceReviewsProductionRuntime(\n options: BrandlabGooglePlaceReviewsProductionRuntimeOptions,\n): BrandlabGooglePlaceReviewsProductionRuntime {\n if (options.fetcher && options.environment.NODE_ENV !== 'test') {\n throw new Error('Google reviews transport overrides are available only in test mode.')\n }\n const apiKey = optionalEnvironment(\n options.environment,\n BRANDLAB_GOOGLE_REVIEWS_ENVIRONMENT_NAMES.apiKey,\n )\n const mode = normalizeGooglePlaceReviewsMode(options.mode)\n const selection = normalizeGooglePlaceReviewsSelection(options.selection)\n if (selection === 'five-star-featured' && mode !== 'full') {\n throw new Error('Five-star featured reviews require full mode.')\n }\n\n return {\n runtimeKind: BRANDLAB_GOOGLE_REVIEWS_PRODUCTION_RUNTIME,\n mode,\n routeHandler: () => createGooglePlaceReviewsRouteResponse({\n apiKey,\n placeId: options.placeId,\n mode,\n ...(mode === 'full' && (selection === 'five-star-featured' || options.maxReviews !== undefined)\n ? {\n maxReviews: selection === 'five-star-featured'\n ? GOOGLE_PLACE_REVIEWS_MAX_REVIEWS\n : options.maxReviews,\n }\n : {}),\n ...(options.fallbackGoogleMapsUri\n ? { fallbackGoogleMapsUri: options.fallbackGoogleMapsUri }\n : {}),\n fetcher: options.fetcher ?? fetch,\n }),\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"google-reviews-server.js","sourceRoot":"","sources":["../src/google-reviews-server.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAEpB,OAAO,EACL,gCAAgC,EAChC,2CAA2C,EAC3C,+BAA+B,EAC/B,sBAAsB,EACtB,+BAA+B,EAC/B,oCAAoC,GAKrC,MAAM,4CAA4C,CAAA;AAEnD,MAAM,CAAC,MAAM,0CAA0C,GACrD,uCAAgD,CAAA;AAElD,MAAM,CAAC,MAAM,yCAAyC,GAAG;IACvD,MAAM,EAAE,8BAA8B;CAC9B,CAAA;AAEV,MAAM,CAAC,MAAM,wCAAwC,GAAG;IACtD,eAAe,EAAE,8BAA8B;IAC/C,MAAM,EAAE,UAAU;IAClB,wBAAwB,EAAE,SAAS;CAC3B,CAAA;AAEV,MAAM,CAAC,MAAM,sCAAsC,GACjD,wCAAwC,CAAA;AAU1C,MAAM,iBAAiB,GAAG,IAAI,OAAO,EAGlC,CAAA;AAEH,SAAS,aAAa,CAAC,OAAuC;IAC5D,OAAO,IAAI,CAAC,SAAS,CAAC;QACpB,sBAAsB,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE;QAC7C,+BAA+B,CAAC,OAAO,CAAC,IAAI,CAAC;QAC7C,OAAO,CAAC,UAAU,IAAI,IAAI;QAC1B,OAAO,CAAC,qBAAqB,IAAI,EAAE;KACpC,CAAC,CAAA;AACJ,CAAC;AAED,KAAK,UAAU,8BAA8B,CAC3C,OAAuC;IAEvC,OAAO,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC,OAAO,CAAC,EAAE,CAAA;AACrE,CAAC;AAED,SAAS,4BAA4B,CACnC,OAAuC;IAEvC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,KAAK,CAAA;IACxC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,GAAG,EAAE,CAAA;IAC5D,iBAAiB,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IACxC,MAAM,GAAG,GAAG,aAAa,CAAC,OAAO,CAAC,CAAA;IAClC,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAClC,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAA;IAC7B,MAAM,OAAO,GAAG,8BAA8B,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE;QACnE,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACpB,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC;YAAE,iBAAiB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAC5D,CAAC,CAAC,CAAA;IACF,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;IAC1B,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,uEAAuE;AACvE,MAAM,CAAC,KAAK,UAAU,qCAAqC,CACzD,OAAuC;IAEvC,MAAM,MAAM,GAAG,MAAM,4BAA4B,CAAC,OAAO,CAAC,CAAA;IAC1D,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,CAAA;IAC1D,MAAM,CAAC,OAAO,CAAC,sCAAsC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE;QAC/E,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IAC1B,CAAC,CAAC,CAAA;IAEF,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE;QACpC,GAAG,OAAO,CAAC,YAAY;QACvB,MAAM,EAAE,OAAO,CAAC,YAAY,EAAE,MAAM;QACpC,OAAO;KACR,CAAC,CAAA;AACJ,CAAC;AA4BD,yHAAyH;AACzH,SAAS,kBAAkB,CAAC,KAAc;IACxC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,OAAQ,KAA8B,CAAC,MAAM,KAAK,QAAQ,CAAA;AAClH,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,kBAAkB,CAAC,OAA4B;IAC5D,MAAM,UAAU,GAAG,CAAC,KAAgC,EAAE,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,aAAa,CAAA;IACtG,IAAI,OAAO,YAAY,OAAO;QAAE,OAAO,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAA;IACxF,IAAI,CAAC;QACH,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAA;QAChD,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAA;IAC5D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,gFAAgF;QAChF,IAAI,kBAAkB,CAAC,KAAK,CAAC;YAAE,MAAM,KAAK,CAAA;QAC1C,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED,SAAS,mBAAmB,CAC1B,WAAyD,EACzD,IAAY;IAEZ,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,CAAA;IAC/B,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;QAAE,OAAO,SAAS,CAAA;IAChE,IAAI,KAAK,CAAC,MAAM,GAAG,KAAK,IAAI,uBAAuB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IACjF,OAAO,KAAK,CAAC,IAAI,EAAE,CAAA;AACrB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iDAAiD,CAC/D,OAA2D;IAE3D,IAAI,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,WAAW,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;QAC/D,MAAM,IAAI,KAAK,CAAC,qEAAqE,CAAC,CAAA;IACxF,CAAC;IACD,MAAM,MAAM,GAAG,mBAAmB,CAChC,OAAO,CAAC,WAAW,EACnB,yCAAyC,CAAC,MAAM,CACjD,CAAA;IACD,MAAM,IAAI,GAAG,+BAA+B,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1D,MAAM,SAAS,GAAG,oCAAoC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;IACzE,IAAI,SAAS,KAAK,oBAAoB,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QAC1D,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAA;IAClE,CAAC;IAED,OAAO;QACL,WAAW,EAAE,0CAA0C;QACvD,IAAI;QACJ,2EAA2E;QAC3E,YAAY,EAAE,KAAK,EAAE,OAAiB,EAAE,EAAE;YACxC,sFAAsF;YACtF,IAAI,MAAM,IAAI,CAAC,CAAC,MAAM,kBAAkB,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;gBACnD,OAAO,QAAQ,CAAC,IAAI,CAClB,2CAA2C,CAAC,iBAAiB,EAAE,OAAO,CAAC,qBAAqB,EAAE,IAAI,CAAC,EACnG,EAAE,OAAO,EAAE,wCAAwC,EAAE,CACtD,CAAA;YACH,CAAC;YACD,OAAO,qCAAqC,CAAC;gBAC3C,MAAM;gBACN,OAAO,EAAE,OAAO,CAAC,OAAO;gBACxB,IAAI;gBACJ,GAAG,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,SAAS,KAAK,oBAAoB,IAAI,OAAO,CAAC,UAAU,KAAK,SAAS,CAAC;oBAC7F,CAAC,CAAC;wBACE,UAAU,EAAE,SAAS,KAAK,oBAAoB;4BAC5C,CAAC,CAAC,gCAAgC;4BAClC,CAAC,CAAC,OAAO,CAAC,UAAU;qBACvB;oBACH,CAAC,CAAC,EAAE,CAAC;gBACP,GAAG,CAAC,OAAO,CAAC,qBAAqB;oBAC/B,CAAC,CAAC,EAAE,qBAAqB,EAAE,OAAO,CAAC,qBAAqB,EAAE;oBAC1D,CAAC,CAAC,EAAE,CAAC;gBACP,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,KAAK;aAClC,CAAC,CAAA;QACJ,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["import 'server-only'\n\nimport {\n GOOGLE_PLACE_REVIEWS_MAX_REVIEWS,\n createGooglePlaceReviewsUnavailableSnapshot,\n fetchGooglePlaceReviewsSnapshot,\n normalizeGooglePlaceId,\n normalizeGooglePlaceReviewsMode,\n normalizeGooglePlaceReviewsSelection,\n type FetchGooglePlaceReviewsOptions,\n type GooglePlaceReviewsMode,\n type GooglePlaceReviewsSelection,\n type GooglePlaceReviewsSnapshot,\n} from '@brandlab-digital/site-core/google-reviews'\n\nexport const BRANDLAB_GOOGLE_REVIEWS_PRODUCTION_RUNTIME =\n 'brandlab-google-reviews-production-v2' as const\n\nexport const BRANDLAB_GOOGLE_REVIEWS_ENVIRONMENT_NAMES = {\n apiKey: 'GOOGLE_MAPS_PLATFORM_API_KEY',\n} as const\n\nexport const GOOGLE_PLACE_REVIEWS_UNAVAILABLE_HEADERS = {\n 'Cache-Control': 'private, no-store, max-age=0',\n Pragma: 'no-cache',\n 'X-Content-Type-Options': 'nosniff',\n} as const\n\nexport const GOOGLE_PLACE_REVIEWS_AVAILABLE_HEADERS =\n GOOGLE_PLACE_REVIEWS_UNAVAILABLE_HEADERS\n\nexport interface GooglePlaceReviewsRouteOptions extends FetchGooglePlaceReviewsOptions {\n responseInit?: ResponseInit\n}\n\ninterface GooglePlaceReviewsRequestResult {\n snapshot: GooglePlaceReviewsSnapshot\n}\n\nconst inFlightByFetcher = new WeakMap<\n typeof fetch,\n Map<string, Promise<GooglePlaceReviewsRequestResult>>\n>()\n\nfunction coalescingKey(options: GooglePlaceReviewsRouteOptions) {\n return JSON.stringify([\n normalizeGooglePlaceId(options.placeId) ?? '',\n normalizeGooglePlaceReviewsMode(options.mode),\n options.maxReviews ?? null,\n options.fallbackGoogleMapsUri ?? '',\n ])\n}\n\nasync function loadGooglePlaceReviewsSnapshot(\n options: GooglePlaceReviewsRouteOptions,\n): Promise<GooglePlaceReviewsRequestResult> {\n return { snapshot: await fetchGooglePlaceReviewsSnapshot(options) }\n}\n\nfunction getGooglePlaceReviewsRequest(\n options: GooglePlaceReviewsRouteOptions,\n): Promise<GooglePlaceReviewsRequestResult> {\n const fetcher = options.fetcher ?? fetch\n const requests = inFlightByFetcher.get(fetcher) ?? new Map()\n inFlightByFetcher.set(fetcher, requests)\n const key = coalescingKey(options)\n const existing = requests.get(key)\n if (existing) return existing\n const request = loadGooglePlaceReviewsSnapshot(options).finally(() => {\n requests.delete(key)\n if (requests.size === 0) inFlightByFetcher.delete(fetcher)\n })\n requests.set(key, request)\n return request\n}\n\n/** Low-level route response for tests and approved custom adapters. */\nexport async function createGooglePlaceReviewsRouteResponse(\n options: GooglePlaceReviewsRouteOptions,\n): Promise<Response> {\n const result = await getGooglePlaceReviewsRequest(options)\n const headers = new Headers(options.responseInit?.headers)\n Object.entries(GOOGLE_PLACE_REVIEWS_AVAILABLE_HEADERS).forEach(([name, value]) => {\n headers.set(name, value)\n })\n\n return Response.json(result.snapshot, {\n ...options.responseInit,\n status: options.responseInit?.status,\n headers,\n })\n}\n\nexport interface BrandlabGooglePlaceReviewsProductionRuntimeOptions {\n placeId: string\n mode: GooglePlaceReviewsMode\n /** Full-mode presentation policy. Featured mode always requests Google's full five-review window. */\n selection?: GooglePlaceReviewsSelection\n /** Full mode only. Google currently returns at most five reviews. */\n maxReviews?: number\n fallbackGoogleMapsUri?: string\n environment: Readonly<Record<string, string | undefined>>\n /** Test-only transport override. */\n fetcher?: typeof fetch\n}\n\nexport interface BrandlabGooglePlaceReviewsProductionRuntime {\n runtimeKind: typeof BRANDLAB_GOOGLE_REVIEWS_PRODUCTION_RUNTIME\n mode: GooglePlaceReviewsMode\n /**\n * The route's GET. A paid Places lookup runs only for a request from the site's\n * own page (`Sec-Fetch-Site: same-origin`); anything else gets the unavailable\n * snapshot. It reads the request it is given (`export const GET =\n * runtime.routeHandler`, which Next calls with the request), and otherwise Next's\n * request scope (`export function GET() { return runtime.routeHandler() }`).\n */\n routeHandler: (request?: Request) => Promise<Response>\n}\n\n/** The errors Next throws to control rendering (dynamic usage, redirects, not-found, postpone) carry a string digest. */\nfunction isNextControlError(error: unknown) {\n return typeof error === 'object' && error !== null && typeof (error as { digest?: unknown }).digest === 'string'\n}\n\n/**\n * Whether the request comes from one of the site's own pages: every current\n * browser sends `Sec-Fetch-Site: same-origin` on the page's own fetch, while\n * crawlers, curl and scripted direct hits normally send nothing. Anything that\n * cannot be read counts as no.\n */\nasync function requestedByOwnPage(request: Request | undefined): Promise<boolean> {\n const sameOrigin = (value: string | null | undefined) => value?.trim().toLowerCase() === 'same-origin'\n if (request instanceof Request) return sameOrigin(request.headers.get('sec-fetch-site'))\n try {\n const { headers } = await import('next/headers')\n return sameOrigin((await headers()).get('sec-fetch-site'))\n } catch (error) {\n // Next's own control flow (for example a dynamic-usage signal) must reach Next.\n if (isNextControlError(error)) throw error\n return false\n }\n}\n\nfunction optionalEnvironment(\n environment: Readonly<Record<string, string | undefined>>,\n name: string,\n) {\n const value = environment[name]\n if (typeof value !== 'string' || !value.trim()) return undefined\n if (value.length > 8_192 || /[\\u0000-\\u001f\\u007f]/.test(value)) return undefined\n return value.trim()\n}\n\n/**\n * Creates the approved production route. Neither the public response nor the\n * server-only Places response is stored. Concurrent identical requests are\n * coalesced only until the active request completes. With a Places key set, a\n * request that is not from the site's own page answers the unavailable snapshot\n * (`not-same-origin`) without calling Places.\n */\nexport function createBrandlabGooglePlaceReviewsProductionRuntime(\n options: BrandlabGooglePlaceReviewsProductionRuntimeOptions,\n): BrandlabGooglePlaceReviewsProductionRuntime {\n if (options.fetcher && options.environment.NODE_ENV !== 'test') {\n throw new Error('Google reviews transport overrides are available only in test mode.')\n }\n const apiKey = optionalEnvironment(\n options.environment,\n BRANDLAB_GOOGLE_REVIEWS_ENVIRONMENT_NAMES.apiKey,\n )\n const mode = normalizeGooglePlaceReviewsMode(options.mode)\n const selection = normalizeGooglePlaceReviewsSelection(options.selection)\n if (selection === 'five-star-featured' && mode !== 'full') {\n throw new Error('Five-star featured reviews require full mode.')\n }\n\n return {\n runtimeKind: BRANDLAB_GOOGLE_REVIEWS_PRODUCTION_RUNTIME,\n mode,\n // Next calls GET(request, context); a GET written as GET() passes nothing.\n routeHandler: async (request?: Request) => {\n // Without a key no lookup can run, and the route answers as before (missing-api-key).\n if (apiKey && !(await requestedByOwnPage(request))) {\n return Response.json(\n createGooglePlaceReviewsUnavailableSnapshot('not-same-origin', options.fallbackGoogleMapsUri, mode),\n { headers: GOOGLE_PLACE_REVIEWS_UNAVAILABLE_HEADERS },\n )\n }\n return createGooglePlaceReviewsRouteResponse({\n apiKey,\n placeId: options.placeId,\n mode,\n ...(mode === 'full' && (selection === 'five-star-featured' || options.maxReviews !== undefined)\n ? {\n maxReviews: selection === 'five-star-featured'\n ? GOOGLE_PLACE_REVIEWS_MAX_REVIEWS\n : options.maxReviews,\n }\n : {}),\n ...(options.fallbackGoogleMapsUri\n ? { fallbackGoogleMapsUri: options.fallbackGoogleMapsUri }\n : {}),\n fetcher: options.fetcher ?? fetch,\n })\n },\n }\n}\n"]}
|
|
@@ -4,6 +4,11 @@ export interface BrandlabGoogleTagConfig {
|
|
|
4
4
|
measurementId?: string | null;
|
|
5
5
|
googleAdsConversionId?: string | null;
|
|
6
6
|
googleAdsWebsiteCallConversionLabel?: string | null;
|
|
7
|
+
/**
|
|
8
|
+
* `granted` only for a disclosed Enhanced Conversions contract. Everything
|
|
9
|
+
* else keeps the ad_user_data default denied. ad_personalization stays denied.
|
|
10
|
+
*/
|
|
11
|
+
adUserData?: 'granted' | 'denied';
|
|
7
12
|
}
|
|
8
13
|
export interface BrandlabGoogleTagPlan {
|
|
9
14
|
loaderId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-tag-plan.d.ts","sourceRoot":"","sources":["../src/google-tag-plan.ts"],"names":[],"mappings":"AAEA,4DAA4D;AAC5D,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,mCAAmC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"google-tag-plan.d.ts","sourceRoot":"","sources":["../src/google-tag-plan.ts"],"names":[],"mappings":"AAEA,4DAA4D;AAC5D,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,mCAAmC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACnD;;;OAGG;IACH,UAAU,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAA;CAClC;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;IAC/B,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAA;CACtC;AAiBD,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,uBAAuB,EAC/B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAC1B,qBAAqB,GAAG,IAAI,CA+C9B"}
|
package/dist/google-tag-plan.js
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { getGoogleAdsWebsiteCallSendTo } from '@brandlab-digital/site-core/analytics';
|
|
2
|
+
/**
|
|
3
|
+
* Where Google's EU user consent policy needs a person's own consent for
|
|
4
|
+
* ad_user_data: the EEA, the United Kingdom and Switzerland. A disclosed
|
|
5
|
+
* contract's granted default never applies there.
|
|
6
|
+
*/
|
|
7
|
+
const CONSENT_REQUIRED_REGIONS = Object.freeze([
|
|
8
|
+
'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU',
|
|
9
|
+
'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'IS', 'LI', 'NO', 'GB', 'CH',
|
|
10
|
+
// EU outermost regions and Åland, which Google geolocates under their own codes.
|
|
11
|
+
'GF', 'GP', 'MQ', 'RE', 'YT', 'MF', 'AX',
|
|
12
|
+
]);
|
|
2
13
|
const GA4_MEASUREMENT_ID_PATTERN = /^G-[A-Z0-9]{4,20}$/i;
|
|
3
14
|
const GOOGLE_ADS_CONVERSION_ID_PATTERN = /^(?:AW-)?\d{6,20}$/;
|
|
4
15
|
export function createBrandlabGoogleTagPlan(config, phoneNumber) {
|
|
@@ -32,7 +43,11 @@ export function createBrandlabGoogleTagPlan(config, phoneNumber) {
|
|
|
32
43
|
inlineScript: [
|
|
33
44
|
'window.dataLayer = window.dataLayer || [];',
|
|
34
45
|
'window.gtag = window.gtag || function(){window.dataLayer.push(arguments);};',
|
|
35
|
-
|
|
46
|
+
// A region-specific default takes precedence over the global one for visitors in those regions.
|
|
47
|
+
...(config.adUserData === 'granted'
|
|
48
|
+
? [`window.gtag('consent', 'default', ${safeJson({ ad_user_data: 'denied', ad_personalization: 'denied', region: CONSENT_REQUIRED_REGIONS })});`]
|
|
49
|
+
: []),
|
|
50
|
+
`window.gtag('consent', 'default', {ad_user_data:'${config.adUserData === 'granted' ? 'granted' : 'denied'}',ad_personalization:'denied'});`,
|
|
36
51
|
"window.gtag('set', 'user_data', {});",
|
|
37
52
|
"window.gtag('js', new Date());",
|
|
38
53
|
...configCommands,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-tag-plan.js","sourceRoot":"","sources":["../src/google-tag-plan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,uCAAuC,CAAA;
|
|
1
|
+
{"version":3,"file":"google-tag-plan.js","sourceRoot":"","sources":["../src/google-tag-plan.ts"],"names":[],"mappings":"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","sourcesContent":["import { getGoogleAdsWebsiteCallSendTo } from '@brandlab-digital/site-core/analytics'\n\n/** Supplied by `defineBrandlabAnalytics(...).googleTag`. */\nexport interface BrandlabGoogleTagConfig {\n enabled: boolean\n measurementId?: string | null\n googleAdsConversionId?: string | null\n googleAdsWebsiteCallConversionLabel?: string | null\n /**\n * `granted` only for a disclosed Enhanced Conversions contract. Everything\n * else keeps the ad_user_data default denied. ad_personalization stays denied.\n */\n adUserData?: 'granted' | 'denied'\n}\n\nexport interface BrandlabGoogleTagPlan {\n loaderId: string\n source: string\n inlineScript: string\n destinations: readonly string[]\n websiteCallDestination: string | null\n}\n\n/**\n * Where Google's EU user consent policy needs a person's own consent for\n * ad_user_data: the EEA, the United Kingdom and Switzerland. A disclosed\n * contract's granted default never applies there.\n */\nconst CONSENT_REQUIRED_REGIONS = Object.freeze([\n 'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU',\n 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'IS', 'LI', 'NO', 'GB', 'CH',\n // EU outermost regions and Åland, which Google geolocates under their own codes.\n 'GF', 'GP', 'MQ', 'RE', 'YT', 'MF', 'AX',\n] as const)\n\nconst GA4_MEASUREMENT_ID_PATTERN = /^G-[A-Z0-9]{4,20}$/i\nconst GOOGLE_ADS_CONVERSION_ID_PATTERN = /^(?:AW-)?\\d{6,20}$/\n\nexport function createBrandlabGoogleTagPlan(\n config: BrandlabGoogleTagConfig,\n phoneNumber?: string | null,\n): BrandlabGoogleTagPlan | null {\n if (!config.enabled) return null\n const measurementId = validMeasurementId(config.measurementId)\n const googleAdsConversionId = validGoogleAdsConversionId(config.googleAdsConversionId)\n const loaderId = measurementId ?? googleAdsConversionId\n if (!loaderId) return null\n\n const destinations = [...new Set([\n ...(measurementId ? [measurementId] : []),\n ...(googleAdsConversionId ? [googleAdsConversionId] : []),\n ])]\n const displayPhone = safeDisplayedBusinessPhone(phoneNumber)\n const websiteCallDestination = displayPhone\n ? getGoogleAdsWebsiteCallSendTo({\n conversionId: googleAdsConversionId,\n conversionLabel: config.googleAdsWebsiteCallConversionLabel,\n }) ?? null\n : null\n const configCommands = destinations.map(\n (destination) => `window.gtag('config', ${safeJson(destination)});`,\n )\n if (websiteCallDestination && displayPhone) {\n configCommands.push(\n `window.gtag('config', ${safeJson(websiteCallDestination)}, ${safeJson({\n phone_conversion_number: displayPhone,\n })});`,\n )\n }\n\n return {\n loaderId,\n source: `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(loaderId)}`,\n inlineScript: [\n 'window.dataLayer = window.dataLayer || [];',\n 'window.gtag = window.gtag || function(){window.dataLayer.push(arguments);};',\n // A region-specific default takes precedence over the global one for visitors in those regions.\n ...(config.adUserData === 'granted'\n ? [`window.gtag('consent', 'default', ${safeJson({ ad_user_data: 'denied', ad_personalization: 'denied', region: CONSENT_REQUIRED_REGIONS })});`]\n : []),\n `window.gtag('consent', 'default', {ad_user_data:'${config.adUserData === 'granted' ? 'granted' : 'denied'}',ad_personalization:'denied'});`,\n \"window.gtag('set', 'user_data', {});\",\n \"window.gtag('js', new Date());\",\n ...configCommands,\n ].join('\\n'),\n destinations,\n websiteCallDestination,\n }\n}\n\nfunction validMeasurementId(value: string | null | undefined) {\n const normalized = value?.trim()\n return normalized && GA4_MEASUREMENT_ID_PATTERN.test(normalized)\n ? normalized.toUpperCase()\n : undefined\n}\n\nfunction validGoogleAdsConversionId(value: string | null | undefined) {\n const normalized = value?.trim()\n if (!normalized || !GOOGLE_ADS_CONVERSION_ID_PATTERN.test(normalized)) return undefined\n return normalized.startsWith('AW-') ? normalized : `AW-${normalized}`\n}\n\nfunction safeDisplayedBusinessPhone(value: string | null | undefined) {\n const normalized = value?.trim()\n const digits = normalized?.replace(/\\D/g, '') ?? ''\n return normalized &&\n normalized.length <= 32 &&\n digits.length >= 6 &&\n /^[0-9+().\\s-]+$/.test(normalized)\n ? normalized\n : undefined\n}\n\nfunction safeJson(value: unknown) {\n return JSON.stringify(value)\n .replace(/</g, '\\\\u003c')\n .replace(/>/g, '\\\\u003e')\n .replace(/&/g, '\\\\u0026')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029')\n}\n"]}
|
package/dist/headers.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* `public, max-age=31536000, immutable` by production builds; Previews and
|
|
17
17
|
* `next dev` never cache them.
|
|
18
18
|
*/
|
|
19
|
-
import type
|
|
19
|
+
import { type BrandlabDeployment } from '@brandlab-digital/site-core/site';
|
|
20
20
|
export interface BrandlabHeader {
|
|
21
21
|
key: string;
|
|
22
22
|
value: string;
|
|
@@ -24,9 +24,25 @@ export interface BrandlabHeader {
|
|
|
24
24
|
export interface BrandlabHeaderRule {
|
|
25
25
|
source: string;
|
|
26
26
|
headers: BrandlabHeader[];
|
|
27
|
+
/** The rule applies only when none of these match (Next `missing`: host values are anchored regular expressions). */
|
|
28
|
+
missing?: {
|
|
29
|
+
type: 'host';
|
|
30
|
+
value: string;
|
|
31
|
+
}[];
|
|
27
32
|
}
|
|
28
33
|
export declare const BRANDLAB_DEFAULT_PERMISSIONS_POLICY = "camera=(), microphone=(), geolocation=()";
|
|
29
34
|
export declare const BRANDLAB_DEFAULT_X_ROBOTS_TAG = "max-snippet:-1, max-image-preview:large, max-video-preview:-1";
|
|
35
|
+
/**
|
|
36
|
+
* What a production deployment answers on any host but its canonical one (3.2.1): the `<project>.vercel.app` alias
|
|
37
|
+
* and the deployment's own URLs serve the full site, so they tell crawlers not to index it. Alias hosts redirect.
|
|
38
|
+
*/
|
|
39
|
+
export declare const BRANDLAB_NON_CANONICAL_HOST_X_ROBOTS_TAG = "noindex";
|
|
40
|
+
/**
|
|
41
|
+
* Production deployments answer every host but the canonical one (and a local server's) with
|
|
42
|
+
* `X-Robots-Tag: noindex`: the `*.vercel.app` production alias and deployment URLs serve the full site. The rule
|
|
43
|
+
* comes last, so it overrides the default X-Robots-Tag there; the canonical host keeps the default.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildNonCanonicalHostRule(canonicalHost: string): BrandlabHeaderRule;
|
|
30
46
|
export declare const BRANDLAB_PREVIEW_X_ROBOTS_TAG = "noindex, nofollow, noarchive";
|
|
31
47
|
export declare const BRANDLAB_PREVIEW_CACHE_CONTROL = "private, no-store, max-age=0";
|
|
32
48
|
export declare const BRANDLAB_HSTS_VALUE = "max-age=31536000";
|
|
@@ -86,6 +102,11 @@ export interface BrandlabHeaderRuleOptions extends BrandlabSecurityHeaderOptions
|
|
|
86
102
|
* cached in browsers for up to a year, so give a changed file a new name.
|
|
87
103
|
*/
|
|
88
104
|
staticAssetCache?: readonly string[];
|
|
105
|
+
/**
|
|
106
|
+
* The canonical host (3.2.1): a production deployment answers every other host (the `*.vercel.app` alias, the
|
|
107
|
+
* deployment URLs) with `X-Robots-Tag: noindex`.
|
|
108
|
+
*/
|
|
109
|
+
canonicalHost?: string;
|
|
89
110
|
}
|
|
90
111
|
export declare function buildBrandlabHeaderRules(options: BrandlabHeaderRuleOptions): BrandlabHeaderRule[];
|
|
91
112
|
//# sourceMappingURL=headers.d.ts.map
|
package/dist/headers.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAEL,KAAK,kBAAkB,EAExB,MAAM,kCAAkC,CAAA;AAEzC,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB,qHAAqH;IACrH,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC5C;AAED,eAAO,MAAM,mCAAmC,6CACJ,CAAA;AAE5C,eAAO,MAAM,6BAA6B,kEACuB,CAAA;AAEjE;;;GAGG;AACH,eAAO,MAAM,wCAAwC,YAAY,CAAA;AAUjE;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,aAAa,EAAE,MAAM,GAAG,kBAAkB,CAYnF;AAED,eAAO,MAAM,6BAA6B,iCAAiC,CAAA;AAE3E,eAAO,MAAM,8BAA8B,iCAAiC,CAAA;AAE5E,eAAO,MAAM,mBAAmB,qBACZ,CAAA;AAEpB,iFAAiF;AACjF,eAAO,MAAM,mCAAmC,wCAAwC,CAAA;AAExF,MAAM,WAAW,qBAAqB;IACpC,iBAAiB,EAAE,IAAI,CAAA;IACvB,OAAO,CAAC,EAAE,IAAI,CAAA;IACd,mEAAmE;IACnE,iBAAiB,EAAE,MAAM,CAAA;CAC1B;AAoBD,MAAM,WAAW,uCAAuC;IACtD,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC7B,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC7B;AAED,MAAM,WAAW,6BAA6B;IAC5C,mDAAmD;IACnD,UAAU,EAAE,kBAAkB,CAAA;IAC9B,IAAI,CAAC,EAAE,qBAAqB,CAAA;IAC5B,wEAAwE;IACxE,+BAA+B,CAAC,EAAE,uCAAuC,CAAA;IACzE,2FAA2F;IAC3F,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,wEAAwE;IACxE,YAAY,CAAC,EAAE,SAAS,cAAc,EAAE,CAAA;CACzC;AAcD,wBAAgB,kCAAkC,CAAC,OAAO,EAAE;IAC1D,UAAU,EAAE,kBAAkB,CAAA;IAC9B,+BAA+B,CAAC,EAAE,uCAAuC,CAAA;CAC1E,UAsDA;AAiDD,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,6BAA6B,GACrC,cAAc,EAAE,CAmDlB;AAOD,MAAM,WAAW,yBAAyB;IACxC,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAA;IACd,mEAAmE;IACnE,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;IACzB;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,OAAO,GAAG,yBAAyB,EAAE,CA4B3F;AAED,MAAM,WAAW,yBAA0B,SAAQ,6BAA6B;IAC9E;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,kBAAkB,EAAE,CAyBjG"}
|
package/dist/headers.js
CHANGED
|
@@ -16,8 +16,38 @@
|
|
|
16
16
|
* `public, max-age=31536000, immutable` by production builds; Previews and
|
|
17
17
|
* `next dev` never cache them.
|
|
18
18
|
*/
|
|
19
|
+
import { isBrandlabHostname, } from '@brandlab-digital/site-core/site';
|
|
19
20
|
export const BRANDLAB_DEFAULT_PERMISSIONS_POLICY = 'camera=(), microphone=(), geolocation=()';
|
|
20
21
|
export const BRANDLAB_DEFAULT_X_ROBOTS_TAG = 'max-snippet:-1, max-image-preview:large, max-video-preview:-1';
|
|
22
|
+
/**
|
|
23
|
+
* What a production deployment answers on any host but its canonical one (3.2.1): the `<project>.vercel.app` alias
|
|
24
|
+
* and the deployment's own URLs serve the full site, so they tell crawlers not to index it. Alias hosts redirect.
|
|
25
|
+
*/
|
|
26
|
+
export const BRANDLAB_NON_CANONICAL_HOST_X_ROBOTS_TAG = 'noindex';
|
|
27
|
+
/** Host patterns a production build answers normally besides its canonical host: a local `next start` (CI, the harness). */
|
|
28
|
+
const LOCAL_HOST_PATTERNS = ['localhost', '127\\.0\\.0\\.1'];
|
|
29
|
+
/** A host as an exact pattern (Next anchors `has`/`missing` values as regular expressions). */
|
|
30
|
+
function exactHostPattern(host) {
|
|
31
|
+
return host.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Production deployments answer every host but the canonical one (and a local server's) with
|
|
35
|
+
* `X-Robots-Tag: noindex`: the `*.vercel.app` production alias and deployment URLs serve the full site. The rule
|
|
36
|
+
* comes last, so it overrides the default X-Robots-Tag there; the canonical host keeps the default.
|
|
37
|
+
*/
|
|
38
|
+
export function buildNonCanonicalHostRule(canonicalHost) {
|
|
39
|
+
// Next compares a request's host lowercased and without its port: a host in any other form would match no request,
|
|
40
|
+
// and every host, the canonical one included, would be told noindex.
|
|
41
|
+
const host = typeof canonicalHost === 'string' ? canonicalHost.toLowerCase() : canonicalHost;
|
|
42
|
+
if (!isBrandlabHostname(host)) {
|
|
43
|
+
throw new Error(`The canonical host must be a bare hostname (no scheme, port, path or trailing dot): "${String(canonicalHost)}".`);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
source: '/:path*',
|
|
47
|
+
missing: [exactHostPattern(host), ...LOCAL_HOST_PATTERNS].map((value) => ({ type: 'host', value })),
|
|
48
|
+
headers: [{ key: 'X-Robots-Tag', value: BRANDLAB_NON_CANONICAL_HOST_X_ROBOTS_TAG }],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
21
51
|
export const BRANDLAB_PREVIEW_X_ROBOTS_TAG = 'noindex, nofollow, noarchive';
|
|
22
52
|
export const BRANDLAB_PREVIEW_CACHE_CONTROL = 'private, no-store, max-age=0';
|
|
23
53
|
export const BRANDLAB_HSTS_VALUE = 'max-age=31536000';
|
|
@@ -231,7 +261,7 @@ export function buildBrandlabHeaderRules(options) {
|
|
|
231
261
|
if (!options || typeof options !== 'object') {
|
|
232
262
|
throw new TypeError('Header options are required.');
|
|
233
263
|
}
|
|
234
|
-
const { staticAssetCache, ...security } = options;
|
|
264
|
+
const { staticAssetCache, canonicalHost, ...security } = options;
|
|
235
265
|
const folders = staticAssetCache === undefined ? [] : parseBrandlabStaticAssetCache(staticAssetCache);
|
|
236
266
|
const rules = [
|
|
237
267
|
{
|
|
@@ -249,6 +279,9 @@ export function buildBrandlabHeaderRules(options) {
|
|
|
249
279
|
});
|
|
250
280
|
}
|
|
251
281
|
}
|
|
282
|
+
// Only a real production deployment: Previews already send noindex everywhere, and development has no crawlers.
|
|
283
|
+
if (environment === 'production' && canonicalHost)
|
|
284
|
+
rules.push(buildNonCanonicalHostRule(canonicalHost));
|
|
252
285
|
return rules;
|
|
253
286
|
}
|
|
254
287
|
//# sourceMappingURL=headers.js.map
|