@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.
Files changed (54) hide show
  1. package/dist/analytics.d.ts +2 -0
  2. package/dist/analytics.d.ts.map +1 -1
  3. package/dist/analytics.js +5 -1
  4. package/dist/analytics.js.map +1 -1
  5. package/dist/contact/form-client.d.ts +1 -1
  6. package/dist/contact/form-client.d.ts.map +1 -1
  7. package/dist/contact/form-client.js +1 -1
  8. package/dist/contact/form-client.js.map +1 -1
  9. package/dist/contact/resend.d.ts +25 -3
  10. package/dist/contact/resend.d.ts.map +1 -1
  11. package/dist/contact/resend.js +54 -8
  12. package/dist/contact/resend.js.map +1 -1
  13. package/dist/contact/server.d.ts +11 -1
  14. package/dist/contact/server.d.ts.map +1 -1
  15. package/dist/contact/server.js +50 -2
  16. package/dist/contact/server.js.map +1 -1
  17. package/dist/contact/testing.js +1 -1
  18. package/dist/contact/testing.js.map +1 -1
  19. package/dist/google-reviews-client.d.ts +9 -4
  20. package/dist/google-reviews-client.d.ts.map +1 -1
  21. package/dist/google-reviews-client.js +60 -13
  22. package/dist/google-reviews-client.js.map +1 -1
  23. package/dist/google-reviews-server.d.ts +11 -2
  24. package/dist/google-reviews-server.d.ts.map +1 -1
  25. package/dist/google-reviews-server.js +52 -18
  26. package/dist/google-reviews-server.js.map +1 -1
  27. package/dist/google-tag-plan.d.ts +5 -0
  28. package/dist/google-tag-plan.d.ts.map +1 -1
  29. package/dist/google-tag-plan.js +16 -1
  30. package/dist/google-tag-plan.js.map +1 -1
  31. package/dist/headers.d.ts +22 -1
  32. package/dist/headers.d.ts.map +1 -1
  33. package/dist/headers.js +34 -1
  34. package/dist/headers.js.map +1 -1
  35. package/dist/lead-registry.d.ts +2 -0
  36. package/dist/lead-registry.d.ts.map +1 -1
  37. package/dist/lead-registry.js.map +1 -1
  38. package/dist/lead-reporting.d.ts +7 -3
  39. package/dist/lead-reporting.d.ts.map +1 -1
  40. package/dist/lead-reporting.js +5 -2
  41. package/dist/lead-reporting.js.map +1 -1
  42. package/dist/next-config.d.ts +15 -0
  43. package/dist/next-config.d.ts.map +1 -1
  44. package/dist/next-config.js +29 -4
  45. package/dist/next-config.js.map +1 -1
  46. package/dist/robots.d.ts +4 -2
  47. package/dist/robots.d.ts.map +1 -1
  48. package/dist/robots.js +12 -2
  49. package/dist/robots.js.map +1 -1
  50. package/dist/user-interaction.d.ts +14 -0
  51. package/dist/user-interaction.d.ts.map +1 -0
  52. package/dist/user-interaction.js +53 -0
  53. package/dist/user-interaction.js.map +1 -0
  54. 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
- if (typeof IntersectionObserver !== 'function') {
92
- store.activate();
105
+ const unitTest = unitTestEnvironment();
106
+ if (!unitTest && isAutomatedBrowser())
93
107
  return;
94
- }
95
- const observer = new IntersectionObserver((entries) => {
96
- if (!entries.some((entry) => entry.isIntersecting))
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
- observer.disconnect();
100
- }, { rootMargin: '320px 0px' });
101
- observer.observe(element);
102
- return () => observer.disconnect();
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. The caller owns fallback
109
- * content and must display GooglePlaceReviewsAttribution beside live data.
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 when the
129
- * approved placement approaches the viewport and always retains a stable link.
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
- routeHandler: () => Promise<Response>;
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,EAML,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,YAAY,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAA;CACtC;AAYD;;;;GAIG;AACH,wBAAgB,iDAAiD,CAC/D,OAAO,EAAE,kDAAkD,GAC1D,2CAA2C,CAkC7C"}
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
- routeHandler: () => createGooglePlaceReviewsRouteResponse({
81
- apiKey,
82
- placeId: options.placeId,
83
- mode,
84
- ...(mode === 'full' && (selection === 'five-star-featured' || options.maxReviews !== undefined)
85
- ? {
86
- maxReviews: selection === 'five-star-featured'
87
- ? GOOGLE_PLACE_REVIEWS_MAX_REVIEWS
88
- : options.maxReviews,
89
- }
90
- : {}),
91
- ...(options.fallbackGoogleMapsUri
92
- ? { fallbackGoogleMapsUri: options.fallbackGoogleMapsUri }
93
- : {}),
94
- fetcher: options.fetcher ?? fetch,
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":"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","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;CACpD;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;AAKD,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,uBAAuB,EAC/B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAC1B,qBAAqB,GAAG,IAAI,CA2C9B"}
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"}
@@ -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
- "window.gtag('consent', 'default', {ad_user_data:'denied',ad_personalization:'denied'});",
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":"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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\nexport interface BrandlabGoogleTagPlan {\n loaderId: string\n source: string\n inlineScript: string\n destinations: readonly string[]\n websiteCallDestination: string | null\n}\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 \"window.gtag('consent', 'default', {ad_user_data:'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"]}
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 { BrandlabDeployment } from '@brandlab-digital/site-core/site';
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
@@ -1 +1 @@
1
- {"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAEnB,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;CAC1B;AAED,eAAO,MAAM,mCAAmC,6CACJ,CAAA;AAE5C,eAAO,MAAM,6BAA6B,kEACuB,CAAA;AAEjE,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;CACrC;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,kBAAkB,EAAE,CAuBjG"}
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